diff --git a/+file/+interface/HasProps.m b/+file/+interface/HasProps.m new file mode 100644 index 00000000..5098d034 --- /dev/null +++ b/+file/+interface/HasProps.m @@ -0,0 +1,6 @@ +classdef HasProps < matlab.mixin.Heterogeneous + methods (Abstract) + props = getProps(obj); + end +end + diff --git a/+file/Dataset.m b/+file/Dataset.m index 70787b78..77a2bbda 100644 --- a/+file/Dataset.m +++ b/+file/Dataset.m @@ -1,4 +1,4 @@ -classdef Dataset +classdef Dataset < file.interface.HasProps properties name; doc; @@ -108,6 +108,7 @@ obj.linkable = ~isempty(obj.name) && hasNoAttributes; end + %% HasProps function props = getProps(obj) props = containers.Map; diff --git a/+file/Group.m b/+file/Group.m index a9d4df92..d9fababe 100644 --- a/+file/Group.m +++ b/+file/Group.m @@ -1,4 +1,4 @@ -classdef Group +classdef Group < file.interface.HasProps properties doc; name; @@ -139,8 +139,9 @@ isempty(obj.attributes); end - function Prop_Map = getProps(obj) - Prop_Map = containers.Map; + %% HasProps + function PropertyMap = getProps(obj) + PropertyMap = containers.Map; %typed + constrained %should never happen @@ -161,27 +162,27 @@ attr_names = strcat(SubData.name, '_', attr_names); Sub_Attribute_Map =... containers.Map(attr_names, num2cell(SubData.attributes)); - Prop_Map = [Prop_Map; Sub_Attribute_Map]; + PropertyMap = [PropertyMap; Sub_Attribute_Map]; end - Prop_Map(SubData.name) = SubData; + PropertyMap(SubData.name) = SubData; else if isempty(SubData.name) - Prop_Map(lower(SubData.type)) = SubData; + PropertyMap(lower(SubData.type)) = SubData; else - Prop_Map(SubData.name) = SubData; + PropertyMap(SubData.name) = SubData; end end end %attributes if ~isempty(obj.attributes) - Prop_Map = [Prop_Map;... + PropertyMap = [PropertyMap;... containers.Map({obj.attributes.name}, num2cell(obj.attributes))]; end %links if ~isempty(obj.links) - Prop_Map = [Prop_Map;... + PropertyMap = [PropertyMap;... containers.Map({obj.links.name}, num2cell(obj.links))]; end @@ -200,49 +201,46 @@ %if untyped, check if elided % if elided, add to prefix and check all subgroups, attributes and datasets. % otherwise, call getprops and assign to its name. - Sub_Group = obj.subgroups(i); - groupName = Sub_Group.name; - groupType = Sub_Group.type; + SubGroup = obj.subgroups(i); + groupName = SubGroup.name; + groupType = SubGroup.type; if ~isempty(groupType) if isempty(groupName) - Prop_Map(lower(groupType)) = Sub_Group; + PropertyMap(lower(groupType)) = SubGroup; else - Prop_Map(groupName) = Sub_Group; + PropertyMap(groupName) = SubGroup; end continue; end - if ~Sub_Group.elide - Prop_Map(groupName) = Sub_Group; + if ~SubGroup.elide + PropertyMap(groupName) = SubGroup; continue; end - - Descendant_Map = Sub_Group.getProps; - descendant_names = keys(Descendant_Map); - for iSubGroup = 1:length(descendant_names) - descendantName = descendant_names{iSubGroup}; - Descendant = Descendant_Map(descendantName); + + DescendantMap = SubGroup.getProps(); + descendantNames = keys(DescendantMap); + for iSubGroup = 1:length(descendantNames) + descendantName = descendantNames{iSubGroup}; + Descendant = DescendantMap(descendantName); % hoist constrained sets to the current % subname. isPossiblyConstrained =... isa(Descendant, 'file.Group')... || isa(Descendant, 'file.Dataset'); isConstrained = isPossiblyConstrained... - && strcmpi(descendantName, Descendant.type)... - && Descendant.isConstrainedSet; + && all(strcmpi(descendantName, {Descendant.type}))... + && all(Descendant.isConstrainedSet); if isConstrained - propName = groupName; + if isKey(PropertyMap, groupName) + SetType = PropertyMap(groupName); + else + SetType = []; + end + PropertyMap(groupName) = [SetType, Descendant]; else - propName = [groupName '_' descendantName]; - end - - if isKey(Prop_Map, propName) && ~isConstrained - warning(['Generic group `%s` is currently unsupported '... - 'in MatNwb and is ignored.'], propName); - continue; + PropertyMap([groupName, '_', descendantName]) = Descendant; end - - Prop_Map(propName) = Descendant_Map(descendantName); end end end diff --git a/+file/fillClass.m b/+file/fillClass.m index d373ab4c..79f3acb5 100644 --- a/+file/fillClass.m +++ b/+file/fillClass.m @@ -15,8 +15,18 @@ for i=1:length(allprops) pnm = allprops{i}; prop = classprops(pnm); + + isRequired = ischar(prop) || isa(prop, 'containers.Map') || isstruct(prop); + iIsPropertyRequired = false; + if isa(prop, 'file.interface.HasProps') + iIsPropertyRequired = false(size(prop)); + for iProp = 1:length(prop) + p = prop(iProp); + iIsPropertyRequired(iProp) = p.required; + end + end - if ischar(prop) || isa(prop, 'containers.Map') || isstruct(prop) || prop.required + if isRequired || all(iIsPropertyRequired) required = [required {pnm}]; else optional = [optional {pnm}]; diff --git a/+file/fillConstructor.m b/+file/fillConstructor.m index 4347f785..bbe4be70 100644 --- a/+file/fillConstructor.m +++ b/+file/fillConstructor.m @@ -62,22 +62,40 @@ nm = names{i}; prop = props(nm); - if isa(prop, 'file.Group') || isa(prop, 'file.Dataset') - dynamicConstrained(i) = prop.isConstrainedSet && strcmpi(nm, prop.type); - anon(i) = ~prop.isConstrainedSet && isempty(prop.name); + if isa(prop, 'file.Attribute') + isattr(i) = true; + continue; + end + + if isa(prop, 'file.interface.HasProps') + isDynamicConstrained = false(size(prop)); + isAnon = false(size(prop)); + hasType = false(size(prop)); + typeNames = cell(size(prop)); + for iProp = 1:length(prop) + p = prop(iProp); + isDynamicConstrained(iProp) = p.isConstrainedSet && strcmpi(nm, p.type); + isAnon(iProp) = ~p.isConstrainedSet && isempty(p.name); + hasType(iProp) = ~isempty(p.type); + typeNames{iProp} = p.type; + end + dynamicConstrained(i) = all(isDynamicConstrained); + anon(i) = all(isAnon); - if ~isempty(prop.type) + if all(hasType) varnames{i} = nm; + typeNameCell = cell(size(prop)); try - typenames{i} = namespace.getFullClassName(prop.type); + for iProp = 1:length(prop) + typeNameCell{iProp} = namespace.getFullClassName(prop(iProp).type); + end catch ME if ~strcmp(ME.identifier, 'NWB:Scheme:Namespace:NotFound') rethrow(ME); end end + typenames{i} = misc.cellPrettyPrint(typeNameCell); end - elseif isa(prop, 'file.Attribute') - isattr(i) = true; end end @@ -132,9 +150,14 @@ defaults = cell(size(names)); for i=1:length(names) prop = props(names{i}); - if (isa(prop, 'file.Group') &&... - (prop.hasAnonData || prop.hasAnonGroups || prop.isConstrainedSet)) ||... - (isa(prop, 'file.Dataset') && prop.isConstrainedSet) + isPluralSet = isa(prop, 'file.interface.HasProps') && ~isscalar(prop); + isGroupSet = ~isPluralSet ... + && isa(prop, 'file.Group') ... + && (prop.hasAnonData || prop.hasAnonGroups || prop.isConstrainedSet); + isDataSet = ~isPluralSet ... + && isa(prop, 'file.Dataset')... + && prop.isConstrainedSet; + if isPluralSet || isGroupSet || isDataSet defaults{i} = 'types.untyped.Set()'; else defaults{i} = '[]'; diff --git a/+file/fillProps.m b/+file/fillProps.m index f29b99c9..97b0fdfc 100644 --- a/+file/fillProps.m +++ b/+file/fillProps.m @@ -71,15 +71,30 @@ error('Invalid reftype found whilst filling Constructor prop docs.'); end typeStr = sprintf('%s Reference to %s', refTypeName, prop('target_type')); - elseif isa(prop, 'file.Dataset') && isempty(prop.type) - typeStr = getPropStr(prop.dtype); - elseif isempty(prop.type) - typeStr = 'types.untyped.Set'; + elseif isa(prop, 'file.interface.HasProps') + typeStrCell = cell(size(prop)); + for iProp = 1:length(typeStrCell) + anonProp = prop(iProp); + if isa(anonProp, 'file.Dataset') && isempty(anonProp.type) + typeStrCell{iProp} = getPropStr(anonProp.dtype); + elseif isempty(anonProp.type) + typeStrCell{iProp} = 'types.untyped.Set'; + else + typeStrCell{iProp} = anonProp.type; + end + end + typeStr = strjoin(typeStrCell, '|'); else typeStr = prop.type; end - if isa(prop, 'file.Dataset') || isa(prop, 'file.Attribute') || isa(prop, 'file.Group') + if isa(prop, 'file.interface.HasProps') + propStrCell = cell(size(prop)); + for iProp = 1:length(prop) + propStrCell{iProp} = prop(iProp).doc; + end + propStr = sprintf('(%s) %s', typeStr, strjoin(propStrCell, ' | ')); + elseif isa(prop, 'file.Attribute') propStr = sprintf('(%s) %s', typeStr, prop.doc); else propStr = typeStr; diff --git a/+file/fillValidators.m b/+file/fillValidators.m index b663a37a..214cb93d 100644 --- a/+file/fillValidators.m +++ b/+file/fillValidators.m @@ -1,264 +1,282 @@ -function fvstr = fillValidators(propnames, props, namespacereg) -fvstr = ''; -for i=1:length(propnames) - nm = propnames{i}; - prop = props(nm); - - %if readonly and value exists then ignore - if isa(prop, 'file.Attribute') && prop.readonly && ~isempty(prop.value) - continue; +function validationStr = fillValidators(propnames, props, namespacereg) + validationStr = ''; + for i=1:length(propnames) + nm = propnames{i}; + prop = props(nm); + + % if readonly and value exists then ignore + if isa(prop, 'file.Attribute') && prop.readonly && ~isempty(prop.value) + continue; + end + if startsWith(class(prop), 'file.') + validationBody = fillUnitValidation(nm, prop, namespacereg); + else % primitive type + validationBody = fillDtypeValidation(nm, prop); + end + headerStr = ['function val = validate_' nm '(obj, val)']; + if isempty(validationBody) + funcstionStr = [headerStr newline 'end']; + else + funcstionStr = strjoin({headerStr ... + file.addSpaces(strtrim(validationBody), 4) 'end'}, newline); + end + validationStr = [validationStr newline funcstionStr]; end - if startsWith(class(prop), 'file.') - validationBody = fillUnitValidation(nm, prop, namespacereg); - else %primitive type - validationBody = fillDtypeValidation(nm, prop); +end + +function unitValidationStr = fillUnitValidation(name, prop, namespaceReg) + unitValidationStr = ''; + if ~isscalar(prop) + constrained = cell(size(prop)); + for iProp = 1:length(prop) + p = prop(iProp); + assert(p.isConstrainedSet && ~isempty(p.type), 'Non-constrained anonymous set?'); + constrained{iProp} = ['''', namespaceReg.getFullClassName(p.type), '''']; + end + + unitValidationStr = strjoin({... + unitValidationStr, ... + ['constrained = {', strjoin(constrained, ', '), '};'], ... + ['types.util.checkSet(''', name, ''', struct(), constrained, val);'], ... + }, newline); + elseif isa(prop, 'file.Dataset') + unitValidationStr = fillDatasetValidation(name, prop, namespaceReg); + elseif isa(prop, 'file.Group') + unitValidationStr = fillGroupValidation(name, prop, namespaceReg); + elseif isa(prop, 'file.Attribute') + unitValidationStr = strjoin({unitValidationStr... + fillDtypeValidation(name, prop.dtype)... + fillDimensionValidation(prop.dtype, prop.shape)... + }, newline); + else % Link + fullname = namespaceReg.getFullClassName(prop.type); + unitValidationStr = fillDtypeValidation(name, fullname); end - hdrstr = ['function val = validate_' nm '(obj, val)']; - if isempty(validationBody) - fcnStr = [hdrstr newline 'end']; +end + +function unitValidationStr = fillGroupValidation(name, prop, namespaceReg) + if ~isempty(prop.type) && ~prop.isConstrainedSet + fulltypename = namespaceReg.getFullClassName(prop.type); + unitValidationStr = fillDtypeValidation(name, fulltypename); + return; + end + + namedprops = struct(); + constraints = {}; + if isempty(prop.type) + %% process datasets + % if type, check if constrained + % if constrained, add to constr + % otherwise, check type once + % otherwise, check dtype + for iDataset = 1:length(prop.datasets) + dataset = p.datasets(iDataset); + + if isempty(dataset.type) + namedprops.(dataset.name) = dataset.dtype; + else + type = namespaceReg.getFullClassName(dataset.type); + if dataset.isConstrainedSet + constraints{end+1} = type; + else + namedprops.(dataset.name) = type; + end + end + end + + %% process groups + % if type, check if constrained + % if constrained, add to constr + % otherwise, check type once + % otherwise, error. This shouldn't happen. + for iSubGroup = 1:length(prop.subgroups) + subGroup = prop.subgroups(iSubGroup); + subGroupFullName = namespaceReg.getFullClassName(subGroup.type); + assert(~isempty(subGroup.type), 'Weird case with two untyped groups'); + + if isempty(subGroup.name) + constraints{end+1} = subGroupFullName; + else + namedprops.(subGroup.name) = subGroupFullName; + end + end + + %% process attributes + for iAttribute = 1:length(prop.attributes) + Attribute = prop.attributes(iAttribute); + namedprops.(Attribute.name) = Attribute.dtype; + end + + %% process links + for iLink = 1:length(prop.links) + Link = prop.links(iLink); + namespace = namespaceReg.getNamespace(Link.type); + namedprops.(Link.name) = ['types.', namespace, '.', Link.type]; + end else - fcnStr = strjoin({hdrstr ... - file.addSpaces(strtrim(validationBody), 4) 'end'}, newline); + constraints{end+1} = namespaceReg.getFullClassName(prop.type); end - fvstr = [fvstr newline fcnStr]; -end + + %% create unit validation string + + propnames = fieldnames(namedprops); + unitValidationStr = 'namedprops = struct();'; + for i=1:length(propnames) + nm = propnames{i}; + unitValidationStr = strjoin({unitValidationStr... + ['namedprops.' nm ' = ''' namedprops.(nm) ''';']}, newline); + end + + for iConstraint = 1:length(constraints) + constraints{iConstraint} = ['''', constraints{iConstraint}, '''']; + end + + unitValidationStr = strjoin({... + unitValidationStr, ... + ['constrained = {', strjoin(constraints, ','), '};'], ... + ['types.util.checkSet(''', name, ''', namedprops, constrained, val);'], ... + }, newline); end -function fuvstr = fillUnitValidation(name, prop, namespacereg) -fuvstr = ''; -constr = {}; -if isa(prop, 'file.Dataset') +function unitValidationStr = fillDatasetValidation(name, prop, namespaceReg) + unitValidationStr = ''; if isempty(prop.type) - fuvstr = strjoin({fuvstr... + unitValidationStr = strjoin({unitValidationStr... fillDtypeValidation(name, prop.dtype)... fillDimensionValidation(prop.dtype, prop.shape)... }, newline); elseif prop.isConstrainedSet try - fullname = namespacereg.getFullClassName(prop.type); + fullname = namespaceReg.getFullClassName(prop.type); catch ME if ~endsWith(ME.identifier, 'Namespace:NotFound') rethrow(ME); end - + warning('NWB:Fill:Validators:NamespaceNotFound',... ['Namespace could not be found for type `%s`.' ... ' Skipping Validation for property `%s`.'], prop.type, name); return; end - fuvstr = strjoin({fuvstr... + unitValidationStr = strjoin({unitValidationStr... ['constrained = { ''' fullname ''' };']... ['types.util.checkSet(''' name ''', struct(), constrained, val);']... }, newline); else try - fullname = namespacereg.getFullClassName(prop.type); + fullname = namespaceReg.getFullClassName(prop.type); catch ME if ~endsWith(ME.identifier, 'Namespace:NotFound') rethrow(ME); end - + warning('NWB:Fill:Validators:NamespaceNotFound',... ['Namespace could not be found for type `%s`.' ... ' Skipping Validation for property `%s`.'], prop.type, name); return; end - fuvstr = [fuvstr newline ... - fillDtypeValidation(name, fullname)]; + unitValidationStr = [unitValidationStr newline fillDtypeValidation(name, fullname)]; end -elseif isa(prop, 'file.Group') - if isempty(prop.type) - namedprops = struct(); - - %process datasets - %if type, check if constrained - % if constrained, add to constr - % otherwise, check type once - %otherwise, check dtype - for i=1:length(prop.datasets) - ds = prop.datasets(i); - - if isempty(ds.type) - namedprops.(ds.name) = ds.dtype; - else - type = namespacereg.getFullClassName(ds.type); - if ds.isConstrainedSet - constr = [constr {type}]; - else - namedprops.(ds.name) = type; - end - end - end - - %process groups - %if type, check if constrained - % if constrained, add to constr - % otherwise, check type once - %otherwise, error. This shouldn't happen. - for i=1:length(prop.subgroups) - sg = prop.subgroups(i); - sgfullname = namespacereg.getFullClassName(sg.type); - if isempty(sg.type) - error('Weird case with two untyped groups'); - end - - if isempty(sg.name) - constr = [constr {sgfullname}]; - else - namedprops.(sg.name) = sgfullname; - end - end - - %process attributes - if ~isempty(prop.attributes) - namedprops = [namedprops;... - containers.Map({prop.attributes.name}, ... - {prop.attributes.dtype})]; - end - - %process links - if ~isempty(prop.links) - linktypes = {prop.links.type}; - linkNamespaces = cell(size(linktypes)); - for i=1:length(linktypes) - lt = linktypes{i}; - linkNamespaces{i} = namespacereg.getNamespace(lt); - end - linkTypenames = strcat('types.', linkNamespaces, '.', linktypes); - namedprops = [namedprops; ... - containers.Map({prop.links.name}, linkTypenames)]; - end - - propnames = fieldnames(namedprops); - fuvstr = 'namedprops = struct();'; - for i=1:length(propnames) - nm = propnames{i}; - fuvstr = strjoin({fuvstr... - ['namedprops.' nm ' = ''' namedprops.(nm) ''';']}, newline); - end - fuvstr = strjoin({fuvstr... - ['constrained = {' strtrim(evalc('disp(constr)')) '};']... - ['types.util.checkSet(''' name ''', namedprops, constrained, val);']... - }, newline); - elseif prop.isConstrainedSet - fullname = namespacereg.getFullClassName(prop.type); - fuvstr = strjoin({fuvstr... - sprintf('constrained = {''%s''};', fullname),... - ['types.util.checkSet(''' name ''', struct(), constrained, val);']... - }, newline); - else - fulltypename = namespacereg.getFullClassName(prop.type); - fuvstr = fillDtypeValidation(name, fulltypename); - end -elseif isa(prop, 'file.Attribute') - fuvstr = strjoin({fuvstr... - fillDtypeValidation(name, prop.dtype)... - fillDimensionValidation(prop.dtype, prop.shape)... - }, newline); -else %Link - fullname = namespacereg.getFullClassName(prop.type); - fuvstr = fillDtypeValidation(name, fullname); -end end function fdvstr = fillDimensionValidation(type, shape) -if strcmp(type, 'any') - fdvstr = ''; - return; -end + if strcmp(type, 'any') + fdvstr = ''; + return; + end -if iscell(shape) - if ~isempty(shape) && iscell(shape{1}) - for i = 1:length(shape) - for j = 1:length(shape{i}) - shape{i}{j} = num2str(shape{i}{j}); + if iscell(shape) + if ~isempty(shape) && iscell(shape{1}) + for i = 1:length(shape) + for j = 1:length(shape{i}) + shape{i}{j} = num2str(shape{i}{j}); + end + shape{i} = ['[' strjoin(shape{i}, ',') ']']; end - shape{i} = ['[' strjoin(shape{i}, ',') ']']; + shapeStr = ['{' strjoin(shape, ', ') '}']; + else + for i = 1:length(shape) + shape{i} = num2str(shape{i}); + end + shapeStr = ['{[' strjoin(shape, ',') ']}']; end - shapeStr = ['{' strjoin(shape, ', ') '}']; else - for i = 1:length(shape) - shape{i} = num2str(shape{i}); - end - shapeStr = ['{[' strjoin(shape, ',') ']}']; + shapeStr = ['{[' num2str(shape) ']}']; end -else - shapeStr = ['{[' num2str(shape) ']}']; -end -fdvstr = strjoin({... - 'if isa(val, ''types.untyped.DataStub'')' ... - ' if 1 == val.ndims' ... - ' valsz = [val.dims 1];' ... - ' else' ... - ' valsz = val.dims;' ... - ' end' ... - 'elseif istable(val)' ... - ' valsz = [height(val) 1];'... - 'elseif ischar(val)'... - ' valsz = [size(val, 1) 1];'... - 'else'... - ' valsz = size(val);'... - 'end' ... - ['validshapes = ' shapeStr ';']... - 'types.util.checkDims(valsz, validshapes);'}, newline); + fdvstr = strjoin({... + 'if isa(val, ''types.untyped.DataStub'')' ... + ' if 1 == val.ndims' ... + ' valsz = [val.dims 1];' ... + ' else' ... + ' valsz = val.dims;' ... + ' end' ... + 'elseif istable(val)' ... + ' valsz = [height(val) 1];'... + 'elseif ischar(val)'... + ' valsz = [size(val, 1) 1];'... + 'else'... + ' valsz = size(val);'... + 'end' ... + ['validshapes = ' shapeStr ';']... + 'types.util.checkDims(valsz, validshapes);'}, newline); end %NOTE: can return empty strings function fdvstr = fillDtypeValidation(name, type) -if isstruct(type) - fnames = fieldnames(type); - fdvstr = strjoin({... - 'if isempty(val) || isa(val, ''types.untyped.DataStub'')'... - ' return;'... - 'end'... - 'if ~istable(val) && ~isstruct(val) && ~isa(val, ''containers.Map'')'... - [' error(''Property `' name '` must be a table,struct, or containers.Map.'');']... - 'end'... - 'vprops = struct();'... - }, newline); - vprops = cell(length(fnames),1); - for i=1:length(fnames) - nm = fnames{i}; - if isa(type.(nm), 'containers.Map') + if isstruct(type) + fnames = fieldnames(type); + fdvstr = strjoin({... + 'if isempty(val) || isa(val, ''types.untyped.DataStub'')'... + ' return;'... + 'end'... + 'if ~istable(val) && ~isstruct(val) && ~isa(val, ''containers.Map'')'... + [' error(''Property `' name '` must be a table,struct, or containers.Map.'');']... + 'end'... + 'vprops = struct();'... + }, newline); + vprops = cell(length(fnames),1); + for i=1:length(fnames) + nm = fnames{i}; + if isa(type.(nm), 'containers.Map') + %ref + switch type.(nm)('reftype') + case 'region' + rt = 'RegionView'; + case 'object' + rt = 'ObjectView'; + end + typeval = ['types.untyped.' rt]; + else + typeval = type.(nm); + end + vprops{i} = ['vprops.' nm ' = ''' typeval ''';']; + end + fdvstr = [fdvstr, newline, strjoin(vprops, newline), newline, ... + 'val = types.util.checkDtype(''' name ''', vprops, val);']; + else + fdvstr = ''; + if isa(type, 'containers.Map') %ref - switch type.(nm)('reftype') + ref_t = type('reftype'); + switch ref_t case 'region' rt = 'RegionView'; case 'object' rt = 'ObjectView'; end - typeval = ['types.untyped.' rt]; + ts = ['types.untyped.' rt]; + %there is no objective way to guarantee a reference refers to the + %correct target type + tt = type('target_type'); + fdvstr = ['% Reference to type `' tt '`' newline]; + elseif strcmp(type, 'any') + fdvstr = ''; + return; else - typeval = type.(nm); + ts = strrep(type, '-', '_'); end - vprops{i} = ['vprops.' nm ' = ''' typeval ''';']; + fdvstr = [fdvstr ... + 'val = types.util.checkDtype(''' name ''', ''' ts ''', val);']; end - fdvstr = [fdvstr, newline, strjoin(vprops, newline), newline, ... - 'val = types.util.checkDtype(''' name ''', vprops, val);']; -else - fdvstr = ''; - if isa(type, 'containers.Map') - %ref - ref_t = type('reftype'); - switch ref_t - case 'region' - rt = 'RegionView'; - case 'object' - rt = 'ObjectView'; - end - ts = ['types.untyped.' rt]; - %there is no objective way to guarantee a reference refers to the - %correct target type - tt = type('target_type'); - fdvstr = ['% Reference to type `' tt '`' newline]; - elseif strcmp(type, 'any') - fdvstr = ''; - return; - else - ts = strrep(type, '-', '_'); - end - fdvstr = [fdvstr ... - 'val = types.util.checkDtype(''' name ''', ''' ts ''', val);']; -end end \ No newline at end of file diff --git a/+file/processClass.m b/+file/processClass.m index de54631b..6f288a0a 100644 --- a/+file/processClass.m +++ b/+file/processClass.m @@ -8,7 +8,7 @@ branchNames{i} = branch{i}(TYPEDEF_KEYS{hasTypeDefs}); end -for iAncestor=length(branch):-1:1 +for iAncestor = 1:length(branch) node = branch{iAncestor}; hasTypeDefs = isKey(node, TYPEDEF_KEYS); nodename = node(TYPEDEF_KEYS{hasTypeDefs}); diff --git a/+schemes/Namespace.m b/+schemes/Namespace.m index f3178888..271d77d3 100644 --- a/+schemes/Namespace.m +++ b/+schemes/Namespace.m @@ -90,14 +90,23 @@ %the returned value is a cell array of containers.Maps [parent -> root] function branch = getRootBranch(obj, classname) cursor = obj.getClass(classname); + typeNames = {}; branch = {}; - hasTypeDef = isKey(cursor, obj.TYPEDEF_KEYS); - parent = obj.getParent(cursor(obj.TYPEDEF_KEYS{hasTypeDef})); + iHasTypeDef = isKey(cursor, obj.TYPEDEF_KEYS); + typeName = cursor(obj.TYPEDEF_KEYS{iHasTypeDef}); + parent = obj.getParent(typeName); while ~isempty(parent) - branch = [branch {parent}]; + branch{end+1} = parent; + typeNames{end+1} = typeName; + assert(length(unique(typeNames)) == length(typeNames), ... + 'NWB:Namespace:GetRootBranch:InfiniteLoopDetected', ... + ['Failed to find root branch. A lower-level definition override might be causing ' ... + 'an infinite loop with parent name detection. If you are an extension developer, ' ... + 'this is usually due to overwriting a "core" type name.']); cursor = parent; - hasTypeDef = isKey(cursor, obj.TYPEDEF_KEYS); - parent = obj.getParent(cursor(obj.TYPEDEF_KEYS{hasTypeDef})); + iHasTypeDef = isKey(cursor, obj.TYPEDEF_KEYS); + typeName = cursor(obj.TYPEDEF_KEYS{iHasTypeDef}); + parent = obj.getParent(typeName); end end end diff --git a/+tests/+unit/multipleConstrainedSchema/mcs.manyset.yaml b/+tests/+unit/multipleConstrainedSchema/mcs.manyset.yaml new file mode 100644 index 00000000..9079f0c6 --- /dev/null +++ b/+tests/+unit/multipleConstrainedSchema/mcs.manyset.yaml @@ -0,0 +1,24 @@ +groups: +- neurodata_type_def: ArbitraryTypeA + neurodata_type_inc: NWBDataInterface +- neurodata_type_def: ArbitraryTypeB + neurodata_type_inc: NWBDataInterface +- neurodata_type_def: MultiSetContainer + neurodata_type_inc: NWBDataInterface + groups: + - name: something + groups: + - neurodata_type_inc: ArbitraryTypeA + doc: Group Type A + quantity: '*' + - neurodata_type_inc: ArbitraryTypeB + doc: Group Type B + quantity: '*' + datasets: + - neurodata_type_inc: DatasetType + doc: Dataset Type + quantity: '*' +datasets: +- neurodata_type_def: DatasetType + neurodata_type_inc: NWBData + doc: one-off dataset type diff --git a/+tests/+unit/multipleConstrainedSchema/mcs.namespace.yaml b/+tests/+unit/multipleConstrainedSchema/mcs.namespace.yaml new file mode 100644 index 00000000..3d1da75a --- /dev/null +++ b/+tests/+unit/multipleConstrainedSchema/mcs.namespace.yaml @@ -0,0 +1,6 @@ +namespaces: +- full_name: Combining multiple anonymous constrained set tests + name: mcs + schema: + - namespace: core + - source: mcs.manyset.yaml diff --git a/+tests/+unit/multipleConstrainedTest.m b/+tests/+unit/multipleConstrainedTest.m new file mode 100644 index 00000000..b9194445 --- /dev/null +++ b/+tests/+unit/multipleConstrainedTest.m @@ -0,0 +1,32 @@ +function tests = multipleConstrainedTest() + tests = functiontests(localfunctions); +end + +function setupOnce(testCase) + rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); + testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); +end + +function setup(testCase) + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + generateCore('savedir', '.'); + schemaPath = fullfile(misc.getMatnwbDir(),... + '+tests', '+unit', 'multipleConstrainedSchema', 'mcs.namespace.yaml'); + generateExtension(schemaPath, 'savedir', '.'); + rehash(); +end + +function testRoundabout(testCase) + MultiSet = types.mcs.MultiSetContainer(); + MultiSet.something.set('A', types.mcs.ArbitraryTypeA()); + MultiSet.something.set('B', types.mcs.ArbitraryTypeB()); + MultiSet.something.set('Data', types.mcs.DatasetType()); + nwbExpected = NwbFile(... + 'identifier', 'MCS', ... + 'session_description', 'multiple constrained schema testing', ... + 'session_start_time', datetime()); + nwbExpected.acquisition.set('multiset', MultiSet); + nwbExport(nwbExpected, 'testmcs.nwb'); + + tests.util.verifyContainerEqual(testCase, nwbRead('testmcs.nwb', 'ignorecache'), nwbExpected); +end \ No newline at end of file diff --git a/+types/+util/checkConstraint.m b/+types/+util/checkConstraint.m index 17b8ca75..92b58324 100644 --- a/+types/+util/checkConstraint.m +++ b/+types/+util/checkConstraint.m @@ -1,25 +1,30 @@ function checkConstraint(pname, name, namedprops, constrained, val) -if isempty(val) - return; -end + if isempty(val) + return; + end -names = fieldnames(namedprops); -if any(strcmp(name, names)) - types.util.checkDtype([pname '.' name], namedprops.(name), val); -else + names = fieldnames(namedprops); + if any(strcmp(name, names)) + types.util.checkDtype([pname '.' name], namedprops.(name), val); + return; + end + for i=1:length(constrained) allowedType = constrained{i}; try types.util.checkDtype([pname '.' name], allowedType, val); return; catch ME - if ~any(strcmp(ME.identifier, {'NWB:CheckDType:InvalidType', 'NWB:CheckDType:InvalidShape'})) + expectedErrorTypes = {... + 'NWB:CheckDType:InvalidType', ... + 'NWB:CheckDType:InvalidShape', ... + 'NWB:TypeCorrection:InvalidConversion'}; + if ~any(strcmp(ME.identifier, expectedErrorTypes)) rethrow(ME); end end end error('NWB:CheckConstraint:InvalidType',... - 'Property `%s.%s` should be one of type(s) {%s}.',... - pname, name, misc.cellPrettyPrint(constrained)); -end + 'Property `%s.%s` should be one of type(s) {%s}. Found type "%s"',... + pname, name, misc.cellPrettyPrint(constrained), class(val)); end \ No newline at end of file