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/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions code/internal/+openminds/+internal/+serializer/BaseDeserializer.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
classdef (Abstract) BaseDeserializer < handle
% BaseDeserializer - Turns serialized data into linked openMINDS instances
%
% The counterpart to BaseSerializer. Subclasses implement parseToStructs
% for one serialization format; this class provides the parts that do
% not depend on the format: dispatching each node to its type, building
% the instances, and wiring the references between them.
%
% USAGE:
% ------
% Subclasses implement one method:
%
% classdef MyDeserializer < openminds.internal.serializer.BaseDeserializer
% methods (Access = protected)
% function rawStructs = parseToStructs(obj, data)
% ...
% end
% end
% end
%
% See also openminds.internal.serializer.BaseSerializer,
% openminds.internal.serializer.LinkWiringVisitor

properties (Access = protected)
% What to do with a node that cannot be turned into an instance.
% Warning reports them together once; error raises on the first.
UnreadableNodePolicy (1,1) string ...
{mustBeMember(UnreadableNodePolicy, ["warning", "error"])} = "warning"
end

methods
function instances = deserialize(obj, data)
% deserialize - Build linked instances from serialized data
%
% Returns a cell array of openMINDS instances. Nodes that cannot
% be read are left out and reported.

rawStructs = obj.parseToStructs(data);
[instances, unreadable] = obj.instantiateAll(rawStructs);

obj.reportUnreadableNodes(unreadable);

if isempty(instances)
return
end

visitor = openminds.internal.serializer.LinkWiringVisitor(instances);
for i = 1:numel(instances)
visitor.visit(instances{i});
end
end
end

methods (Abstract, Access = protected)
rawStructs = parseToStructs(obj, data)
% parseToStructs - Format-specific parse into a cell array of structs
end

methods (Access = protected)
function [instances, unreadable] = instantiateAll(~, rawStructs)
% instantiateAll - Build one instance per node of the document

instances = cell(1, numel(rawStructs));
unreadable = struct('Identifier', {}, 'Reason', {});

for i = 1:numel(rawStructs)
node = rawStructs{i};

if ~isfield(node, 'at_type')
unreadable(end+1) = struct( ...
'Identifier', nodeIdentifier(node), ...
'Reason', "the node has no @type"); %#ok<AGROW>
continue
end

% Resolving the type is deliberately outside the try. A
% type in an unknown namespace means the whole document
% was written for a different model version, which is a
% document-level problem rather than one bad node, and
% reporting it as a skipped node would leave the caller
% with an empty result and a warning.
typeEnum = openminds.enum.Types.fromAtType(node.at_type);

try
instances{i} = feval(typeEnum.ClassName, node);
catch ME
unreadable(end+1) = struct( ...
'Identifier', nodeIdentifier(node), ...
'Reason', string(ME.message)); %#ok<AGROW>
end
end

instances = instances(~cellfun(@isempty, instances));
end

function reportUnreadableNodes(obj, unreadable)
% reportUnreadableNodes - Report every node that had to be skipped
%
% Reporting once, with the identifier of each node, makes it
% possible to tell how much of a document was lost. Reporting
% each node separately as it failed buried that.

if isempty(unreadable)
return
end

details = arrayfun( ...
@(entry) sprintf(' %s: %s', entry.Identifier, entry.Reason), ...
unreadable, 'UniformOutput', false);

message = sprintf('%d of the nodes in the data could not be read:\n%s', ...
numel(unreadable), strjoin(details, newline));

if obj.UnreadableNodePolicy == "error"
error('openMINDS:Deserializer:UnreadableNodes', '%s', message)
else
warning('openMINDS:Deserializer:UnreadableNodes', '%s', message)
end
end
end
end

function identifier = nodeIdentifier(node)
% Best available name for a node that could not be read.

if isfield(node, 'at_id')
identifier = string(node.at_id);
elseif isfield(node, 'x_id')
identifier = string(node.x_id);
else
identifier = "<node without an identifier>";
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
classdef JsonLdDeserializer < openminds.internal.serializer.BaseDeserializer
% JsonLdDeserializer - Reads openMINDS instances from JSON-LD
%
% The counterpart to JsonLdSerializer. Accepts either a single document
% or several, and both a document holding one instance and a collection
% document holding an @graph.
%
% USAGE:
% ------
% deserializer = openminds.internal.serializer.JsonLdDeserializer();
% instances = deserializer.deserialize(jsonText);
%
% See also openminds.internal.serializer.JsonLdSerializer

properties (Constant)
DefaultFileExtension = ".jsonld"
end

methods
function obj = JsonLdDeserializer(options)
arguments
options.UnreadableNodePolicy (1,1) string ...
{mustBeMember(options.UnreadableNodePolicy, ["warning", "error"])} = "warning"
end
obj.UnreadableNodePolicy = options.UnreadableNodePolicy;
end
end

methods (Access = protected)
function rawStructs = parseToStructs(~, data)
% parseToStructs - Decode one or more JSON-LD documents

arguments
~
data (1,:) string
end

rawStructs = {};

for i = 1:numel(data)
decoded = openminds.internal.serializer.jsonld2struct(data(i));

if ~iscell(decoded)
decoded = num2cell(decoded);
end

rawStructs = [rawStructs, reshape(decoded, 1, [])]; %#ok<AGROW>
end
end
end
end
76 changes: 76 additions & 0 deletions code/internal/+openminds/+internal/+serializer/LinkWiringVisitor.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
classdef LinkWiringVisitor < openminds.abstract.BaseVisitor
% LinkWiringVisitor - Replaces reference stubs with the instances they name
%
% After a document is parsed, each node is built on its own and its
% linked properties hold stubs carrying an identifier and nothing else.
% This visitor walks the instances and swaps each stub for the instance
% with that identifier, so the separate nodes of a document become one
% connected graph.
%
% A stub naming a controlled instance that is not part of the document
% is resolved from the local instance library instead.
%
% See also openminds.internal.serializer.BaseDeserializer

properties (Access = private)
% Identifier to instance, for everything in the document
InstancesById containers.Map
end

methods
function obj = LinkWiringVisitor(instances)
arguments
instances cell
end

obj.InstancesById = containers.Map( ...
'KeyType', 'char', 'ValueType', 'any');

for i = 1:numel(instances)
obj.InstancesById(char(instances{i}.id)) = instances{i};
end
end
end

methods (Access = protected)
function children = doForLinkedEdge(obj, ~, ~, children)
for i = 1:numel(children)
children{i} = obj.wireChild(children{i});
obj.visit(children{i});
end
end

function children = doForEmbeddedEdge(obj, ~, ~, children)
% An embedded instance is written inline, so it is never a stub.
% It can still hold stubs of its own.

for i = 1:numel(children)
obj.visit(children{i});
end
end
end

methods (Access = private)
function child = wireChild(obj, child)
% wireChild - Swap a stub for the instance it names

if ~child.isUnresolved()
return
end

identifier = char(child.id);

if obj.InstancesById.isKey(identifier)
child = obj.InstancesById(identifier);
return
end

% Not part of the document. A controlled instance can still be
% resolved from the local library; anything else stays a stub
% for the caller to resolve.
if openminds.utility.isInstanceIRI(child.id)
child = openminds.instanceFromIRI(child.id);
end
end
end
end
Loading