diff --git a/code/+openminds/Node.m b/code/+openminds/Node.m index 1975c8dc0..f8809aefd 100644 --- a/code/+openminds/Node.m +++ b/code/+openminds/Node.m @@ -120,6 +120,14 @@ else if ~isscalar(instance) % Preallocate object array obj(1, numel(instance)) = feval( class(obj) ); + % Growing the array fills the elements in between with + % copies of one default object, so those copies share + % the id that object generated. Every element gets its + % own blank node id here; fromStruct replaces it when + % the struct carries an identifier. + for i = 1:numel(obj) + obj(i).id = obj(i).generateInstanceId(); + end end for i = 1:numel( instance ) % Todo: Separate method to handle reference structure. diff --git a/tools/tests/unitTests/CollectionTest.m b/tools/tests/unitTests/CollectionTest.m index 960eebb88..f87de75b7 100644 --- a/tools/tests/unitTests/CollectionTest.m +++ b/tools/tests/unitTests/CollectionTest.m @@ -269,6 +269,36 @@ function testSaveAndLoad(testCase) testCase.verifyTrue(newCollection.isKey(org.id)); end + function testEmbeddedArrayOfFourSurvivesRoundTrip(testCase) + % A multi-valued embedded property is saved without identifiers + % and read back as a struct array. Each value must come back as a + % node of its own, or the owning node fails the uniqueness check + % on that property and is dropped from the loaded collection. Four + % values, so that the array holds more than one element between + % the first and the last. + + hashes = openminds.core.data.Hash.empty; + for digest = ["1", "2", "3", "4"] + hashes(end+1) = openminds.core.data.Hash( ... + "algorithm", "SHA-1", "digest", digest); %#ok + end + file = openminds.core.data.File("name", "data.bin", ... + "IRI", "https://example.org/data.bin", "hash", hashes); + + filePath = 'embedded-array.jsonld'; + openminds.Collection(file).save(filePath); + reloaded = openminds.Collection(filePath); + + testCase.assertTrue(reloaded.isKey(file.id), ... + 'The node holding the embedded values was dropped on load.') + reloadedFile = reloaded.get(file.id); + testCase.assertNumElements(reloadedFile.hash, 4) + identifiers = arrayfun(@(h) string(h.id), reloadedFile.hash); + testCase.verifyEqual(numel(unique(identifiers)), 4, ... + 'The embedded values share an identifier after loading.') + testCase.verifyEqual([reloadedFile.hash.digest], ["1", "2", "3", "4"]) + end + function testSaveWithMetadataStoreOption(testCase) % A store passed to save by name-value must be the store used. % This went through obj.MetadataStore instead, which is empty diff --git a/tools/tests/unitTests/InstanceTest.m b/tools/tests/unitTests/InstanceTest.m index 020dd5397..d2f64764c 100644 --- a/tools/tests/unitTests/InstanceTest.m +++ b/tools/tests/unitTests/InstanceTest.m @@ -98,5 +98,26 @@ function testSaveLoadForTypedInstance(testCase, MetadataType) testCase.verifyEqual(S.itemPreSave, itemPreSave, ... sprintf('Loaded object does not match the original for %s', metadataType.ClassName)); end + + function testStructArrayWithoutIdsGetsDistinctIds(testCase) + % A multi-valued embedded property is read as a struct array with + % no identifiers, and each element must become a node of its own. + % Four elements, so that the array holds more than one element + % between the first and the last. + + structs = struct( ... + 'algorithm', {"SHA-1", "SHA-1", "SHA-1", "SHA-1"}, ... + 'digest', {"1", "2", "3", "4"}); + + hashes = openminds.core.data.Hash(structs); + + testCase.assertNumElements(hashes, 4) + identifiers = arrayfun(@(h) string(h.id), hashes); + testCase.verifyEqual(numel(unique(identifiers)), 4, ... + 'Elements built from one struct array share an identifier.') + testCase.verifyTrue(all(startsWith(identifiers, "_:")), ... + 'An element whose struct carries no identifier gets a blank node id.') + testCase.verifyEqual([hashes.digest], ["1", "2", "3", "4"]) + end end end