From d042a60c2576d221dd6652a1c351aa8ecca130d1 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Fri, 28 Aug 2026 03:07:53 +0200 Subject: [PATCH 1/2] feat: read documents written for any known openMINDS namespace A document written before openMINDS v4 uses the openminds.ebrains.eu namespace, and one written from v4 uses openminds.om-i.org. Reading either one required the active model to match the document, so a v3 file failed under a v4 model with an assertion about the expected prefix, and a v4 file failed under a v3 model. Types.fromAtType now accepts both namespaces. The type name is the last segment of the @type in either form, and the types that share a name are the same type, so the document maps onto the active model regardless of which version wrote it. A namespace that belongs to neither is still rejected, now with an error naming both accepted prefixes and the value that was given rather than an assertion. A type name that does not exist in the active model was previously an eval error naming a MATLAB enumeration. It now reports the type name and the model version that does not have it. jsonld2struct stripped the vocabulary prefix by replacing a hardcoded openminds.ebrains.eu vocabulary IRI. Documents in expanded form written under v4 use a different vocabulary IRI and were left with property names that no type class has. It now strips every known openMINDS vocabulary, listed in one place. Cross-namespace reads are not warned about. A property that a version does not have fails when it is assigned, and the deserializer already reports every node it could not read, so genuine incompatibilities surface on their own. A warning on every legacy document would fire on correct use. The fixture test for legacy documents changes from asserting a clear rejection to asserting the document loads, which is what its own comment anticipated. A test that a foreign namespace is still rejected takes its place. Co-Authored-By: Claude Opus 5 --- .../+openminds/+base/TypesEnumerationBase.m | 31 +++++++++++--- .../+serializer/+jsonld/getVocabularyIRIs.m | 14 +++++++ .../+internal/+serializer/jsonld2struct.m | 21 ++++++++-- tools/tests/unitTests/FixtureTest.m | 41 +++++++++++++------ 4 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 code/internal/+openminds/+internal/+serializer/+jsonld/getVocabularyIRIs.m 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 From 15d80fde6d71996afb2f7f86d3511d3e46df48cd Mon Sep 17 00:00:00 2001 From: Run tests by ehennestad Date: Fri, 28 Aug 2026 13:06:27 +0000 Subject: [PATCH 2/2] Update code issues and tests badges --- .github/badges/tests.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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