diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index a3155a35..8d599001 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -teststests700 passed700 passed \ No newline at end of file +teststests759 passed759 passed \ No newline at end of file diff --git a/code/internal/+openminds/+base/TypesEnumerationBase.m b/code/internal/+openminds/+base/TypesEnumerationBase.m index 48988608..2bbf55fa 100644 --- a/code/internal/+openminds/+base/TypesEnumerationBase.m +++ b/code/internal/+openminds/+base/TypesEnumerationBase.m @@ -137,18 +137,37 @@ arguments typeName (1,:) string end - - assert(all(startsWith(typeName, openminds.constant.BaseURI)), ... - 'OPENMINDS_MATLAB:Types:InvalidAtType', ... - 'Expected @type to start with "%s"', openminds.constant.BaseURI) - + + % Documents written for an older model use a different + % namespace. Both are accepted, because the type name is the + % last segment either way and the types themselves are stable + % across the versions that share a name. + knownBaseURIs = openminds.constant.BaseURI("v1") + "/" | ... + openminds.constant.BaseURI("v4") + "/"; + + isKnownNamespace = startsWith(typeName, knownBaseURIs); + if ~all(isKnownNamespace) + error('OPENMINDS_MATLAB:Types:InvalidAtType', ... + 'Expected @type to start with "%s" or "%s". Got "%s".', ... + openminds.constant.BaseURI("v1"), ... + openminds.constant.BaseURI("v4"), ... + typeName(find(~isKnownNamespace, 1))) + end + if numel(typeName) > 1 typeEnum = arrayfun(@(str) openminds.enum.Types.fromAtType(str), typeName); return end splitName = strsplit(typeName, '/'); - typeEnum = eval(sprintf('openminds.enum.Types.%s', splitName{end})); + + try + typeEnum = openminds.enum.Types(splitName{end}); + catch + error('OPENMINDS_MATLAB:Types:UnknownAtType', ... + ['"%s" does not name a type in version "%s" of the ', ... + 'openMINDS model.'], splitName{end}, openminds.getModelVersion()) + end end end end diff --git a/code/internal/+openminds/+internal/+serializer/+jsonld/getVocabularyIRIs.m b/code/internal/+openminds/+internal/+serializer/+jsonld/getVocabularyIRIs.m new file mode 100644 index 00000000..f2f08757 --- /dev/null +++ b/code/internal/+openminds/+internal/+serializer/+jsonld/getVocabularyIRIs.m @@ -0,0 +1,14 @@ +function vocabularyIRIs = getVocabularyIRIs() +%getVocabularyIRIs Vocabulary IRIs used by any openMINDS model version +% +% vocabularyIRIs = getVocabularyIRIs() returns every vocabulary IRI an +% openMINDS document may use for property names in expanded form. +% +% Documents are read regardless of which model version wrote them, so +% all known vocabularies are needed, not only the one belonging to the +% active version. + + vocabularyIRIs = [ ... + openminds.constant.BaseURI("v1") + "/vocab/", ... + openminds.constant.BaseURI("v4") + "/props/"]; +end diff --git a/code/internal/+openminds/+internal/+serializer/jsonld2struct.m b/code/internal/+openminds/+internal/+serializer/jsonld2struct.m index e8811a29..45bcd0a9 100644 --- a/code/internal/+openminds/+internal/+serializer/jsonld2struct.m +++ b/code/internal/+openminds/+internal/+serializer/jsonld2struct.m @@ -1,9 +1,24 @@ function structInstance = jsonld2struct(jsonInstance) -%Convert metadata instance(s) from JSON-LD text strings to struct arrays +%jsonld2struct Convert JSON-LD text into struct form +% +% structInstance = jsonld2struct(jsonInstance) decodes one JSON-LD +% document. A collection document is returned as its @graph, so the +% result is always the nodes rather than the wrapper. +% +% Property names written in expanded form carry a vocabulary IRI. That +% prefix is removed so the field names match the property names of the +% generated type classes. Every openMINDS vocabulary is removed, not +% just the one belonging to the active model version, so a document +% written for an older model can still be read. - vocabBaseUri = "https://openminds.ebrains.eu/vocab/"; + arguments + jsonInstance (1,1) string + end + + for vocabularyIRI = openminds.internal.serializer.jsonld.getVocabularyIRIs() + jsonInstance = strrep(jsonInstance, vocabularyIRI, ""); + end - jsonInstance = strrep(jsonInstance, vocabBaseUri, ''); structInstance = openminds.internal.utility.json.decode(jsonInstance); if isfield(structInstance, 'at_graph') diff --git a/tools/tests/unitTests/FixtureTest.m b/tools/tests/unitTests/FixtureTest.m index eb016a32..9ad2d036 100644 --- a/tools/tests/unitTests/FixtureTest.m +++ b/tools/tests/unitTests/FixtureTest.m @@ -98,14 +98,10 @@ function testGoldenFixtureLoadsWithValuesIntact(testCase) "https://openminds.om-i.org/instances/species/homoSapiens") end - function testLegacyNamespaceDocumentIsRejectedClearly(testCase) - % A document written with the pre-v4 EBRAINS namespace cannot be - % loaded while a v4 model is active. - % - % This pins current behaviour: the failure is a clear, identified - % error rather than silent data loss. Supporting cross-namespace - % loading would be an improvement, and this test must then be - % changed to assert that the document loads. + function testLegacyNamespaceDocumentIsRead(testCase) + % A document written with the pre-v4 EBRAINS namespace must load + % under a v4 model. The type name is the last segment of the @type + % either way, and the types that share a name are the same type. legacyPath = fullfile(ommtest.helper.fixturePath(), ... "collection_ebrains_legacy.jsonld"); @@ -113,10 +109,31 @@ function testLegacyNamespaceDocumentIsRejectedClearly(testCase) testCase.assumeEqual(ommtest.helper.fixtureNamespaceTag(), "omi", ... 'This test only applies while a v4 or later model is active.') - testCase.verifyError(@() openminds.Collection(legacyPath), ... - 'OPENMINDS_MATLAB:Types:InvalidAtType', ... - ['Loading a legacy namespace document should fail with a ', ... - 'clear error identifying the namespace mismatch.']) + collection = openminds.Collection(legacyPath); + + person = collection.list(openminds.enum.Types("Person")); + testCase.assertNumElements(person, 1, ... + 'The legacy document should produce one Person.') + testCase.verifyEqual(person.givenName, "Ada") + testCase.verifyEqual(person.familyName, "Lovelace") + end + + function testUnknownNamespaceIsRejectedClearly(testCase) + % A document from something that is not openMINDS at all must fail + % with an identified error rather than being partly read. + + documentPath = fullfile(testCase.TemporaryFolder, "foreign.jsonld"); + document = [ ... + '{"@context": {"@vocab": "https://example.org/props/"},' ... + ' "@graph": [{"@id": "_:x",' ... + ' "@type": "https://example.org/types/Person",' ... + ' "givenName": "Ada"}]}']; + fileIdentifier = fopen(documentPath, 'w'); + fwrite(fileIdentifier, document); + fclose(fileIdentifier); + + testCase.verifyError(@() openminds.Collection(documentPath), ... + 'OPENMINDS_MATLAB:Types:InvalidAtType') end end