diff --git a/code/internal/+openminds/+internal/+meta/Type.m b/code/internal/+openminds/+internal/+meta/Type.m index 725174cd..ad1dbefc 100644 --- a/code/internal/+openminds/+internal/+meta/Type.m +++ b/code/internal/+openminds/+internal/+meta/Type.m @@ -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) @@ -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) @@ -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) diff --git a/tools/tests/+ommtest/+helper/PropertyDeclarationFixture.m b/tools/tests/+ommtest/+helper/PropertyDeclarationFixture.m new file mode 100644 index 00000000..45001f23 --- /dev/null +++ b/tools/tests/+ommtest/+helper/PropertyDeclarationFixture.m @@ -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 diff --git a/tools/tests/+ommtest/+helper/synthesizeInstance.m b/tools/tests/+ommtest/+helper/synthesizeInstance.m index 3b1cf5db..5031e9a1 100644 --- a/tools/tests/+ommtest/+helper/synthesizeInstance.m +++ b/tools/tests/+ommtest/+helper/synthesizeInstance.m @@ -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 diff --git a/tools/tests/+ommtest/+internal/MetaTypeTest.m b/tools/tests/+ommtest/+internal/MetaTypeTest.m index d4e94403..7530aaa8 100644 --- a/tools/tests/+ommtest/+internal/MetaTypeTest.m +++ b/tools/tests/+ommtest/+internal/MetaTypeTest.m @@ -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);