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
95 changes: 71 additions & 24 deletions code/internal/+openminds/+internal/+meta/Type.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,53 @@
end
end

methods (Static, Access = private)

function tf = hasScalarValidator(metaProperty)
% hasScalarValidator - Check for a mustBeScalarOrEmpty validator

tf = false;
if isempty(metaProperty.Validation)
return
end

validatorFunctions = metaProperty.Validation.ValidatorFunctions;
tf = any( cellfun(@(c) contains(func2str(c), 'mustBeScalarOrEmpty'), ...
validatorFunctions) );
end

function tf = hasScalarSizeDeclaration(metaProperty)
% hasScalarSizeDeclaration - Check for a size declaration of (1,1)

tf = false;
if isempty(metaProperty.Validation) || isempty(metaProperty.Validation.Size)
return
end

columnDimension = metaProperty.Validation.Size(2);

% Any dimension that is not fixed leaves the property
% unrestricted in size, so it is not scalar.
if isa(columnDimension, 'meta.FixedDimension')
tf = columnDimension.Length == 1;
end
end

function className = getDeclaredClassName(metaProperty)
% getDeclaredClassName - Class a property is restricted to, if any
%
% Returns an empty string for a property declared without a
% class, such as one whose type could not be resolved when the
% type classes were generated.

className = "";
if isempty(metaProperty.Validation) || isempty(metaProperty.Validation.Class)
return
end
className = string(metaProperty.Validation.Class.Name);
end
end

methods (Access = private)

function metaProperty = getMetaProperty(obj, propertyName)
Expand All @@ -83,23 +130,16 @@
function tf = isPropertyValueScalar(obj, propertyName)
% isPropertyValueScalar - Check if property value must be scalar

mp = obj.getMetaProperty(propertyName);

if obj.isPropertyWithLinkedType(propertyName) || ...
obj.isPropertyWithEmbeddedType(propertyName)
validationFcn = mp.Validation.ValidatorFunctions;

isScalar = @(str) contains(str, 'mustBeScalarOrEmpty');
tf = any( cellfun(@(c) isScalar(func2str(c)), validationFcn) );
else
if isa( mp.Validation.Size(2), 'meta.UnrestrictedDimension')
tf = false;
elseif isa( mp.Validation.Size(2), 'meta.FixedDimension')
tf = mp.Validation.Size(2).Length == 1;
else
error('Not implemented.') % Is this ever going to happen?
end
end
metaProperty = obj.getMetaProperty(propertyName);

% A property can be restricted to a scalar in two ways: a
% mustBeScalarOrEmpty validator, or a fixed size declaration.
% Linked and embedded properties are declared (1,:) and always
% use the validator, but a property holding a primitive value
% may use either, so both have to be checked for every
% property.
tf = obj.hasScalarValidator(metaProperty) || ...
obj.hasScalarSizeDeclaration(metaProperty);
end

function tf = isPropertyWithLinkedType(obj, propertyName)
Expand Down Expand Up @@ -143,17 +183,24 @@

function tf = isPropertyMixedType(obj, propertyName)
% isPropertyMixedType - Check if property has linked or embedded MixedTypeSets.
mp = obj.getMetaProperty(propertyName);
className = mp.Validation.Class.Name;
tf = startsWith(className, 'openminds.internal.mixedtype');
metaProperty = obj.getMetaProperty(propertyName);
declaredClass = obj.getDeclaredClassName(metaProperty);

% A property without a declared class cannot be a mixed type.
tf = declaredClass ~= "" && ...
startsWith(declaredClass, 'openminds.internal.mixedtype');
end

function className = getMixedTypeForProperty(obj, propertyName)
% getMixedTypeForProperty - Get class name of MixedTypeSet for given property
mp = obj.getMetaProperty(propertyName);
className = mp.Validation.Class.Name;
assert( startsWith(className, 'openminds.internal.mixedtype'), ...
'Property is not a mixed type' );
metaProperty = obj.getMetaProperty(propertyName);
className = obj.getDeclaredClassName(metaProperty);

if className == "" || ~startsWith(className, 'openminds.internal.mixedtype')
error('OPENMINDS_MATLAB:MetaType:NotAMixedType', ...
'Property "%s" of "%s" is not a mixed type.', ...
propertyName, obj.Name)
end
end

function tf = isLinkedTypeOfAnyProperty(obj, type)
Expand Down
25 changes: 25 additions & 0 deletions tools/tests/+ommtest/+helper/PropertyDeclarationFixture.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
classdef PropertyDeclarationFixture
%PropertyDeclarationFixture Property declarations for meta.Type tests
%
% Exists only to be introspected. It carries one property of each
% declaration shape that openminds.internal.meta.Type has to interpret,
% so those tests do not depend on a particular openMINDS model version
% happening to contain an example of each shape.

properties
% Restricted to a scalar by a validator rather than by its size,
% which is how the generated type classes declare scalar
% properties.
scalarByValidator (1,:) string {mustBeScalarOrEmpty(scalarByValidator)}

% Restricted to a scalar by its size declaration.
scalarBySize (1,1) string

% Not restricted to a scalar.
unrestrictedList (1,:) string

% Declared without a class. The generated type classes contain
% such properties where the model does not resolve to a type.
withoutDeclaredClass (1,:)
end
end
9 changes: 1 addition & 8 deletions tools/tests/+ommtest/+helper/synthesizeInstance.m
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,7 @@
% that array handling is exercised, unless a min or max length validator
% requires otherwise.

% Inspect the validators directly rather than relying on
% metaType.isPropertyValueScalar, which only consults
% mustBeScalarOrEmpty for linked and embedded properties and reports
% unrestricted-size primitive properties as non-scalar.
isScalarProperty = any(contains(validatorText, "mustBeScalarOrEmpty")) ...
|| metaType.isPropertyValueScalar(propertyName);

if isScalarProperty
if metaType.isPropertyValueScalar(propertyName)
numItems = 1;
return
end
Expand Down
40 changes: 40 additions & 0 deletions tools/tests/+ommtest/+internal/MetaTypeTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,46 @@ function testGetMetatypeFromRegistry(testCase)
testCase.verifyEqual( string(registry('Person').Name), "Person")
end

function testScalarConstraintFromValidator(testCase)
% A property restricted to a scalar by mustBeScalarOrEmpty must be
% reported as scalar even though its size declaration is (1,:).
% Both the generated type classes and openMINDS itself rely on
% this, because every linked and embedded property is declared
% that way.

metaType = openminds.internal.meta.Type( ...
'ommtest.helper.PropertyDeclarationFixture');

testCase.verifyTrue( metaType.isPropertyValueScalar('scalarByValidator') )
testCase.verifyTrue( metaType.isPropertyValueScalar('scalarBySize') )
testCase.verifyFalse( metaType.isPropertyValueScalar('unrestrictedList') )
end

function testScalarConstraintOnGeneratedType(testCase)
% The same check against a generated type, so the behaviour is
% pinned for a real property and not only for the fixture.

metaType = openminds.internal.meta.fromClassName( ...
'openminds.core.miscellaneous.Membership');

testCase.verifyTrue( metaType.isPropertyValueScalar('startDate') )
end

function testPropertyWithoutDeclaredClassIsNotMixedType(testCase)
% Checking a property that has no declared class must answer the
% question rather than error, and asking for its mixed type must
% fail with an identified error.

metaType = openminds.internal.meta.Type( ...
'ommtest.helper.PropertyDeclarationFixture');

testCase.verifyFalse( metaType.isPropertyMixedType('withoutDeclaredClass') )

testCase.verifyError( ...
@() metaType.getMixedTypeForProperty('withoutDeclaredClass'), ...
'OPENMINDS_MATLAB:MetaType:NotAMixedType')
end

function testMetaType(testCase)
previousVersion = openminds.version();
openminds.version(5);
Expand Down