Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions code/+openminds/@Collection/Collection.m
Original file line number Diff line number Diff line change
Expand Up @@ -522,12 +522,24 @@ function load(obj, loadPath, options)
|| startsWith(instance.id, "https://openminds.om-i.org/instances/");

if isControlledInstance
% A controlled instance is a reference into the instance
% library, which every reader has, so it can be a node of
% the collection. Whether it is one is a preference.
% A controlled instance is in the instance library, which
% every reader has, so it can be a node of the collection.
% Whether it is one is a preference.
if ~openminds.getpref('AddControlledInstanceToCollection')
return
end

if instance.isReference()
% A reference, typed or not, is replaced by the instance
% from the library, so that the node has the properties
% of the controlled instance and not only its
% identifier. A reference to an instance the library
% does not have stays a link.
instance = getLibraryInstance(instance.id);
if isempty(instance)
return
end
end
elseif instance.isReference()
% Any other reference stands for a node that is not here,
% whether its type is known or not. It is not a node
Expand Down Expand Up @@ -679,3 +691,27 @@ function refreshTypeKeys(obj, instanceType)
end
end
end

function instance = getLibraryInstance(identifier)
%getLibraryInstance - The controlled instance with an identifier, taken from the instance library
% Returns [] when the library does not have the instance. The lookup
% can fail in several ways, for example when the name is not in the
% library or the type has no library instances, and all of them mean
% that the link stays a link. A controlled term whose name the library
% does not have is created with the warning
% openMINDS:ControlledTerm:UnknownInstanceName rather than an error,
% because a user may define a term of their own, so that warning is
% raised as an error here.

unknownNameId = 'openMINDS:ControlledTerm:UnknownInstanceName';
warningState = warning('error', unknownNameId);
restoreWarning = onCleanup(@() warning(warningState));

try
instance = openminds.instanceFromIRI(identifier);
catch
% The instance is not in the library; the caller keeps the link
instance = [];
end
clear restoreWarning
end
118 changes: 116 additions & 2 deletions tools/tests/unitTests/CollectionTest.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
classdef CollectionTest < matlab.unittest.TestCase
% CollectionTest - Unit tests for the openminds metadata collection class


properties (Constant, Access = private)
HomoSapiensIRI = "https://openminds.om-i.org/instances/species/homoSapiens"
end

methods (TestMethodSetup)
function createTempDir(testCase)
import matlab.unittest.fixtures.WorkingFolderFixture
Expand Down Expand Up @@ -528,6 +532,101 @@ function testUnresolvedLinkIsNotANode(testCase)
testCase.verifyFalse(contains(document, "MixedTypeReference"));
end

function testUnresolvedControlledInstanceLinkIsNotANode(testCase)
% A link to a controlled instance that could not be taken from
% the instance library stays a reference. With controlled
% instances added to collections, it is still not a node, so
% saving writes it as a reference instead of failing on it.
original = openminds.getpref('AddControlledInstanceToCollection');
testCase.addTeardown(@() openminds.setpref( ...
'AddControlledInstanceToCollection', original));
openminds.setpref('AddControlledInstanceToCollection', true);

referenceIRI = "https://openminds.om-i.org/instances/species/notInTheLibrary";
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.internal.MixedTypeReference(referenceIRI);

collection = openminds.Collection(subject);
testCase.verifyEqual(length(collection), 1);

filePath = "unresolved-controlled-instance-collection.jsonld";
collection.save(filePath);

document = fileread(filePath);
testCase.verifyTrue(contains(document, referenceIRI));
testCase.verifyFalse(contains(document, "MixedTypeReference"));
end

function testTypedControlledReferenceBecomesLibraryInstance(testCase)
% With controlled instances added to collections, a typed
% reference to one is replaced by the instance from the
% library, so the saved node has the instance's properties.
testCase.setControlledInstancePreference(true)
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.controlledterms.Species( ...
"id", testCase.HomoSapiensIRI, "IsReference", true);

document = testCase.saveCollection(openminds.Collection(subject), "typed-reference.jsonld");

testCase.verifyTrue(contains(document, """name"": ""Homo sapiens"""));
end

function testUntypedControlledReferenceBecomesLibraryInstance(testCase)
testCase.setControlledInstancePreference(true)
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.internal.MixedTypeReference(testCase.HomoSapiensIRI);

collection = openminds.Collection(subject);
document = testCase.saveCollection(collection, "untyped-reference.jsonld");

testCase.verifyEqual(length(collection), 2);
testCase.verifyTrue(contains(document, """name"": ""Homo sapiens"""));
end

function testTypedControlledReferenceNotInLibraryStaysLink(testCase)
testCase.setControlledInstancePreference(true)
referenceIRI = "https://openminds.om-i.org/instances/species/notInTheLibrary";
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.controlledterms.Species( ...
"id", referenceIRI, "IsReference", true);

collection = openminds.Collection(subject);
document = testCase.saveCollection(collection, "typed-reference-not-in-library.jsonld");

testCase.verifyEqual(length(collection), 1);
testCase.verifyTrue(contains(document, referenceIRI));
end

function testControlledReferenceStaysLinkWhenPreferenceIsOff(testCase)
testCase.setControlledInstancePreference(false)
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.controlledterms.Species( ...
"id", testCase.HomoSapiensIRI, "IsReference", true);

collection = openminds.Collection(subject);
document = testCase.saveCollection(collection, "reference-preference-off.jsonld");

testCase.verifyEqual(length(collection), 1);
testCase.verifyTrue(contains(document, testCase.HomoSapiensIRI));
testCase.verifyFalse(contains(document, "Homo sapiens"));
end

function testControlledReferenceRoundTripIsStable(testCase)
% Saving writes the library instance; loading that file and
% saving again writes the same document.
testCase.setControlledInstancePreference(true)
subject = openminds.core.Subject("lookupLabel", "S");
subject.species = openminds.controlledterms.Species( ...
"id", testCase.HomoSapiensIRI, "IsReference", true);

firstDocument = testCase.saveCollection(openminds.Collection(subject), "first.jsonld");
reloaded = openminds.Collection("first.jsonld");
secondDocument = testCase.saveCollection(reloaded, "second.jsonld");

testCase.verifyEqual(sort(splitlines(string(secondDocument))), ...
sort(splitlines(string(firstDocument))));
end

function testTypedReferenceSurvivesRoundTrip(testCase)
% A reference whose type is known is still a reference, not a
% node with no properties. It gets no file of its own, so
Expand Down Expand Up @@ -695,7 +794,22 @@ function testSaveInstances(testCase)
% % testCase.verifyTrue(startsWith(char(identifier), '_:'));
% % end
end


methods (Access = private)
function setControlledInstancePreference(testCase, value)
% Sets AddControlledInstanceToCollection for one test
original = openminds.getpref('AddControlledInstanceToCollection');
testCase.addTeardown(@() openminds.setpref( ...
'AddControlledInstanceToCollection', original));
openminds.setpref('AddControlledInstanceToCollection', value);
end

function document = saveCollection(~, collection, filePath)
collection.save(filePath);
document = fileread(filePath);
end
end

methods (Static, Access = private)
function [dataset, affiliation] = datasetWithOneContributorAffiliation()
[person, affiliation] = personWithOneAffiliation();
Expand Down
Loading