Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/badges/code_issues.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 16 additions & 3 deletions code/+openminds/@Collection/Collection.m
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ function remove(obj, instance)

function instances = getAll(obj)
% getAll - Get all instances of collection
if obj.NumNodes == 0
instances = {};
return
end

instances = obj.Nodes.values();

% For older MATLAB releases, the instances might be nested a
Expand Down Expand Up @@ -326,6 +331,10 @@ function remove(obj, instance)
end

function updateLinks(obj)
if obj.NumNodes == 0
return
end

allInstances = obj.Nodes.values;
if isa(obj.Nodes, 'containers.Map')
allInstances = [allInstances{:}];
Expand Down Expand Up @@ -385,7 +394,7 @@ function updateLinks(obj)
outputPaths = tempStore.save(instances);

elseif ~isempty(options.MetadataStore)
outputPaths = obj.MetadataStore.save(instances);
outputPaths = options.MetadataStore.save(instances);

elseif ~isempty(obj.MetadataStore)
% Use configured store
Expand Down Expand Up @@ -584,11 +593,15 @@ function initializeFromInstances(obj, instance)

% Initialize from file(s)
if all( cellfun(isFilePath, instance) )
obj.load(instance{:})
for i = 1:numel(instance)
obj.load(instance{i})
end

% Initialize from folder
elseif all( cellfun(isFolderPath, instance) )
obj.load(instance{:})
for i = 1:numel(instance)
obj.load(instance{i})
end

% Initialize from instance(s)
elseif all( cellfun(isMetadata, instance) )
Expand Down
11 changes: 9 additions & 2 deletions code/internal/+openminds/+internal/+serializer/jsonld2struct.m
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
function structInstance = jsonld2struct(jsonInstance)
%Convert metadata instance(s) from JSON-LD text strings to struct arrays

vocabBaseUri = "https://openminds.ebrains.eu/vocab/";
vocabBaseUri = [
"https://openminds.ebrains.eu/vocab/"
"https://openminds.om-i.org/props/"
];

jsonInstance = strrep(jsonInstance, vocabBaseUri, '');
for i = 1:numel(vocabBaseUri)
propertyKeyPattern = sprintf('"%s([^"]+)"\\s*:', ...
regexptranslate('escape', vocabBaseUri(i)));
jsonInstance = regexprep(jsonInstance, propertyKeyPattern, '"$1":');
end
structInstance = openminds.internal.utility.json.decode(jsonInstance);

if isfield(structInstance, 'at_graph')
Expand Down
16 changes: 15 additions & 1 deletion code/internal/+openminds/+internal/+store/loadInstances.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
% Produce a cell array of instances represented as structs
if isscalar(str)
structInstances = jsonld2struct(str);
if ~iscell(structInstances); structInstances={structInstances};end
else
structInstances = cellfun(@jsonld2struct, str, 'UniformOutput', false);
end
structInstances = normalizeStructInstances(structInstances);

% Create instance objects
instances = cell(size(structInstances));
Expand Down Expand Up @@ -75,6 +75,18 @@
end
end

function structInstances = normalizeStructInstances(structInstances)
%normalizeStructInstances Return one cell element per serialized instance.

if iscell(structInstances)
structInstances = cellfun(@normalizeStructInstances, ...
structInstances, 'UniformOutput', false);
structInstances = [structInstances{:}];
else
structInstances = num2cell(reshape(structInstances, 1, []));
end
end

function resolveLinks(instance, instanceIds, instanceCollection)
%resolveLinks Resolve linked types, i.e replace an @id with the actual
% instance object.
Expand Down Expand Up @@ -112,6 +124,8 @@ function resolveLinks(instance, instanceIds, instanceCollection)
% Check if instance is a controlled instance
if startsWith(instanceId, "https://openminds.ebrains.eu/instances/")
resolvedInstances{j} = openminds.instanceFromIRI(instanceId);
else
resolvedInstances{j} = linkedInstances(j);
end
end
end
Expand Down
38 changes: 27 additions & 11 deletions code/internal/+openminds/+internal/FolderMetadataStore.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,18 @@

% Serialize instances to individual documents
serializedDocuments = obj.Serializer.serialize(instances);
if ~iscell(serializedDocuments)
serializedDocuments = {serializedDocuments};
end

% Save each document to a separate file
outputPaths = cell(size(serializedDocuments));
for i = 1:numel(serializedDocuments)
instance = instances{i};
instance = openminds.internal.serializer.jsonld2struct( ...
serializedDocuments{i});

% Build file path using unified method
filePath = obj.buildFilepath(instance);
filePath = obj.buildFilepath(instance, i);

% Write to file
openminds.internal.utility.filewrite(filePath, serializedDocuments{i});
Expand Down Expand Up @@ -158,7 +162,7 @@
end

methods (Access = private)
function instanceFilePath = buildFilepath(obj, instance)
function instanceFilePath = buildFilepath(obj, instance, documentIndex)
%buildFilepath Build complete filepath for an instance
%
% Creates the appropriate file path based on the store's Nested property.
Expand All @@ -180,14 +184,10 @@
% Flat: /root/Person_123.jsonld
% Nested: /root/person/123.jsonld

% Get instance type and ID information
className = class(instance);
classNameParts = strsplit(className, '.');
typeName = classNameParts{end};

% Get instance ID and make it filesystem-safe
instanceId = string(instance.id);
if startsWith(instanceId, "http")
[typeName, instanceId] = getTypeNameAndId(instance);
if ismissing(instanceId) || instanceId == ""
safeId = sprintf('%04d', documentIndex);
elseif startsWith(instanceId, "http")
idParts = strsplit(instanceId, '/');
safeId = idParts{end};
else
Expand All @@ -212,3 +212,19 @@
end
end
end

function [typeName, instanceId] = getTypeNameAndId(instance)
if isstruct(instance)
typeNameParts = strsplit(instance.at_type, '/');
typeName = typeNameParts{end};
if isfield(instance, 'at_id')
instanceId = string(instance.at_id);
else
instanceId = string(missing);
end
else
classNameParts = strsplit(class(instance), '.');
typeName = classNameParts{end};
instanceId = string(instance.id);
end
end
140 changes: 140 additions & 0 deletions tools/tests/unitTests/CollectionTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,24 @@ function testGet(testCase)
ommtest.oneoffs.organizationName(retrievedOrg), ...
ommtest.oneoffs.organizationName(org));
end

function testGetAllEmptyCollection(testCase)
collection = openminds.Collection();

instances = collection.getAll();

testCase.verifyEqual(instances, {});
end

function testSaveEmptyCollection(testCase)
collection = openminds.Collection();
filePath = "empty-collection.jsonld";

outputPath = collection.save(filePath);

testCase.verifyEqual(outputPath, filePath);
testCase.verifyTrue(isfile(filePath));
end

function testHasType(testCase)
% Test the hasType method
Expand Down Expand Up @@ -295,6 +313,73 @@ function testSaveToMultipleFiles(testCase)
testCase.verifyTrue(newCollection.isKey(person.id));
testCase.verifyTrue(newCollection.isKey(org.id));
end

function testFolderStoreSavesRecursiveLinkedDocuments(testCase)
identifier = openminds.core.ORCID( ...
"identifier", "https://orcid.org/0000-0000-0000-0000");
person = openminds.core.Person("digitalIdentifier", identifier);

folderPath = "recursive-folder-store";
metadataStore = openminds.internal.FolderMetadataStore( ...
folderPath, "RecursionDepth", 1);

outputPaths = metadataStore.save(person);

files = dir(fullfile(folderPath, "*.jsonld"));
testCase.verifyEqual(numel(outputPaths), 2);
testCase.verifyEqual(numel(files), 2);
testCase.verifyTrue(any(contains(string(outputPaths), "Person_")));
testCase.verifyTrue(any(contains(string(outputPaths), "ORCID_")));
end

function testFolderStoreSavesScalarInstance(testCase)
contact = openminds.core.ContactInformation( ...
"email", "contact@example.org");
folderPath = "scalar-folder-store";
metadataStore = openminds.internal.FolderMetadataStore(folderPath);

outputPaths = metadataStore.save(contact);

testCase.verifyEqual(numel(outputPaths), 1);
testCase.verifyTrue(isfile(outputPaths{1}));
testCase.verifyTrue(contains(string(outputPaths{1}), ...
"ContactInformation_"));
end

function testFolderStoreSavesInstanceWithoutIdentifier(testCase)
contact = openminds.core.ContactInformation( ...
"email", "contact@example.org");
folderPath = "identifier-free-folder-store";
metadataStore = openminds.internal.FolderMetadataStore( ...
folderPath, "IncludeIdentifier", false);

outputPaths = metadataStore.save(contact);
serializedDocument = fileread(outputPaths{1});

testCase.verifyEqual(numel(outputPaths), 1);
testCase.verifyTrue(isfile(outputPaths{1}));
testCase.verifyTrue(contains(string(outputPaths{1}), ...
"ContactInformation_0001"));
testCase.verifyFalse(contains(serializedDocument, '"@id"'));
end

function testCreateCollectionFromMultipleFiles(testCase)
firstContact = openminds.core.ContactInformation( ...
"email", "first@example.org");
secondContact = openminds.core.ContactInformation( ...
"email", "second@example.org");

firstFilePath = "first-contact.jsonld";
secondFilePath = "second-contact.jsonld";
openminds.internal.FileMetadataStore(firstFilePath).save(firstContact);
openminds.internal.FileMetadataStore(secondFilePath).save(secondContact);

collection = openminds.Collection(firstFilePath, secondFilePath);

testCase.verifyEqual(length(collection), 2);
testCase.verifyTrue(collection.isKey(firstContact.id));
testCase.verifyTrue(collection.isKey(secondContact.id));
end

function testLoadInstances(testCase)
% Test the loadInstances static method
Expand All @@ -317,6 +402,50 @@ function testLoadInstances(testCase)
% Verify that instances are loaded
testCase.verifyEqual(length(instances), expectedNumDocuments);
end

function testLoadHomogeneousGraphAsSeparateInstances(testCase)
firstContact = openminds.core.ContactInformation( ...
"email", "first@example.org");
secondContact = openminds.core.ContactInformation( ...
"email", "second@example.org");

filePath = "homogeneous-graph.jsonld";
openminds.internal.FileMetadataStore(filePath).save( ...
[firstContact, secondContact]);

newCollection = openminds.Collection();
newCollection.load(filePath);

testCase.verifyEqual(length(newCollection), 2);
testCase.verifyTrue(newCollection.isKey(firstContact.id));
testCase.verifyTrue(newCollection.isKey(secondContact.id));
end

function testLoadPreservesPartiallyUnresolvedLinks(testCase)
firstIdentifier = openminds.core.ORCID( ...
"identifier", "https://orcid.org/0000-0000-0000-0001");
secondIdentifier = openminds.core.ORCID( ...
"identifier", "https://orcid.org/0000-0000-0000-0002");
person = openminds.core.Person( ...
"digitalIdentifier", [firstIdentifier, secondIdentifier]);

serializer = openminds.internal.serializer.JsonLdSerializer( ...
"OutputMode", "single", ...
"RecursionDepth", 0);
filePath = "partial-graph.jsonld";
openminds.internal.utility.filewrite( ...
filePath, serializer.serialize({person, firstIdentifier}));

instances = openminds.internal.store.loadInstances(filePath);
loadedPerson = instances{1};
loadedIdentifiers = loadedPerson.digitalIdentifier;

testCase.verifyEqual(numel(loadedIdentifiers), 2);
testCase.verifyEqual(loadedIdentifiers(1).Instance.id, ...
firstIdentifier.id);
testCase.verifyEqual(loadedIdentifiers(2).Instance.id, ...
secondIdentifier.id);
end

function testSaveInstances(testCase)
% Tests saving instances with MetadataStore
Expand All @@ -337,6 +466,17 @@ function testSaveInstances(testCase)
% Verify that instances are loaded
testCase.verifyEqual(length(instances), expectedNumDocuments);
end

function testSaveUsesMethodMetadataStoreOption(testCase)
collection = openminds.Collection(organizationWithOneId());
filePath = "method-store-option.jsonld";
metadataStore = openminds.internal.FileMetadataStore(filePath);

outputPath = collection.save("", "MetadataStore", metadataStore);

testCase.verifyEqual(outputPath, filePath);
testCase.verifyTrue(isfile(filePath));
end

% % function testGetBlankNodeIdentifier(testCase)
% % % Test the getBlankNodeIdentifier method
Expand Down
26 changes: 26 additions & 0 deletions tools/tests/unitTests/SerializationTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,31 @@ function testInstanceWithLinkedArray(testCase)
testCase.verifyLength(str, 3)
testCase.verifyClass(str{1}, 'char')
end

function testExpandedJsonLdFileRoundTrip(testCase)
contact = openminds.core.ContactInformation( ...
"email", "contact@example.org");
filePath = "expanded-contact.jsonld";
metadataStore = openminds.internal.FileMetadataStore( ...
filePath, ...
"PropertyNameSyntax", "expanded");

metadataStore.save(contact);
loadedInstances = metadataStore.load();

testCase.verifyEqual(loadedInstances{1}.email, contact.email);
end

function testJsonLdCompactionPreservesLiteralValues(testCase)
contact = openminds.core.ContactInformation( ...
"email", "https://openminds.om-i.org/props/contact@example.org");
filePath = string(tempname) + ".jsonld";
metadataStore = openminds.internal.FileMetadataStore(filePath);

metadataStore.save(contact);
loadedInstances = metadataStore.load();

testCase.verifyEqual(loadedInstances{1}.email, contact.email);
end
end
end