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 @@
-
\ No newline at end of file
+
\ 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