From bbb95737aa6e9ed683e317cef777d43ee49112ec Mon Sep 17 00:00:00 2001 From: Lawrence Date: Thu, 19 Apr 2018 12:13:55 -0400 Subject: [PATCH] added types.untyped.Ref to handle references. Fixed gitignore --- +file/Dataset.m | 17 +++--- +file/fillClass.m | 2 +- +file/fillConstructor.m | 10 +++- +file/fillValidators.m | 5 -- +io/parseDataset.m | 21 +++---- +types/+untyped/DynamicClass.m | 98 +++++++++++++++++++++++++++++++++ +types/+untyped/Link.m | 92 ++++++++++++++++++++++--------- +types/+untyped/MetaClass.m | 59 -------------------- +types/+untyped/Ref.m | 74 +++++++++++++++++++++++++ +types/+util/checkConstrained.m | 28 ++++++++++ +types/+util/checkDims.m | 24 ++++++++ +types/+util/checkDtype.m | 34 ++++++++++++ .gitignore | 2 +- 13 files changed, 354 insertions(+), 112 deletions(-) create mode 100644 +types/+untyped/DynamicClass.m create mode 100644 +types/+untyped/Ref.m create mode 100644 +types/+util/checkConstrained.m create mode 100644 +types/+util/checkDims.m create mode 100644 +types/+util/checkDtype.m diff --git a/+file/Dataset.m b/+file/Dataset.m index 221ee62a..8460d087 100644 --- a/+file/Dataset.m +++ b/+file/Dataset.m @@ -113,16 +113,15 @@ % type. % therefore, we currently do not have a case for a regular typed % dataset (because there isn't any. - if isstruct(obj.dtype) - props('table') = obj.dtype; - elseif isa(obj.dtype, 'java.util.HashMap') - props('target') = obj.dtype; - rt = obj.dtype.get('reftype'); - if strcmp(rt, 'region') - props('region') = 'double'; + if ~isempty(obj.dtype) + if isstruct(obj.dtype) + props('table') = obj.dtype; + elseif isa(obj.dtype, 'java.util.HashMap') + props('ref') = obj.dtype; + elseif ~isempty(obj.type) + %regular dataset + props('data') = obj.dtype; end - else - %regular dataset. TODO end if ~isempty(obj.attributes) diff --git a/+file/fillClass.m b/+file/fillClass.m index c40c9535..c56741d3 100644 --- a/+file/fillClass.m +++ b/+file/fillClass.m @@ -25,7 +25,7 @@ for i=1:length(propertylist) propname = propertylist{i}; prop = classprops.named(propname); - + if isa(prop, 'file.Attribute') && prop.readonly ro_props.(propname) = prop.doc; elseif ischar(prop) diff --git a/+file/fillConstructor.m b/+file/fillConstructor.m index 3bf535ea..9d1e8851 100644 --- a/+file/fillConstructor.m +++ b/+file/fillConstructor.m @@ -48,7 +48,15 @@ elseif isa(prop, 'file.Attribute') fdfp = prop.dtype; elseif isa(prop, 'java.util.HashMap') - fdfp = ['ref to ' prop.get('target_type')]; + switch prop.get('reftype') + case 'region' + reftypenm = 'region'; + case 'object' + reftypenm = 'object'; + otherwise + error('Invalid reftype found whilst filling Constructor prop docs.'); + end + fdfp = ['ref to ' prop.get('target_type') ' ' reftypenm]; elseif isa(prop, 'file.Dataset') && isempty(prop.type) fdfp = fillDocFromProp(prop.dtype); elseif isempty(prop.type) diff --git a/+file/fillValidators.m b/+file/fillValidators.m index f238f897..c724cae5 100644 --- a/+file/fillValidators.m +++ b/+file/fillValidators.m @@ -135,10 +135,5 @@ ts = type; end fdvstr = ['types.util.checkDtype(''' name ''', ''' ts ''', val);']; - - % special case for region reftype - if strcmp(name, 'region') && strcmp(type, 'double') - fdvstr = [fdvstr newline 'types.util.checkRegion(obj, val);']; - end end end \ No newline at end of file diff --git a/+io/parseDataset.m b/+io/parseDataset.m index 81994c19..cde9468d 100644 --- a/+io/parseDataset.m +++ b/+io/parseDataset.m @@ -29,7 +29,6 @@ % compound data w/ reference (ElectrodeTable) % All other cases do not exist in this current schema. props = attrargs; -props('associated_nwbfile') = filename; fid = H5F.open(filename); did = H5D.open(fid, fullpath); @@ -38,16 +37,17 @@ datatype = info.Datatype; if strcmp(datatype.Class, 'H5T_REFERENCE') reftype = datatype.Type; - switch reftype - case 'H5R_OBJECT' - %TODO when included in schema - case 'H5R_DATASET_REGION' - sid = H5R.get_region(did, reftype, data); - [start, finish] = H5S.get_select_bounds(sid); - props('target') = H5R.get_name(did, reftype, data); - props('region') = [start+1 finish]; - H5S.close(sid); + path = H5R.get_name(did, reftype, data); + props('ref') = []; + if strcmp(reftype, 'H5R_DATASET_REGION') + sid = H5R.get_region(did, reftype, data); + [start, finish] = H5S.get_select_bounds(sid); + H5S.close(sid); + reg = [start finish]; + else + reg = []; end + refs([fullpath '/ref']) = struct('path', path, 'region', reg); elseif strcmp(datatype.Class, 'H5T_COMPOUND') t = table; compound = datatype.Type.Member; @@ -58,6 +58,7 @@ isref(j) = true; end end +else end kwargs = io.map2kwargs(props); parsed = eval([typename '(kwargs{:})']); diff --git a/+types/+untyped/DynamicClass.m b/+types/+untyped/DynamicClass.m new file mode 100644 index 00000000..19bdaf42 --- /dev/null +++ b/+types/+untyped/DynamicClass.m @@ -0,0 +1,98 @@ +classdef DynamicClass < handle & matlab.mixin.CustomDisplay + properties(Access=protected) + map; + validators; + allowAddProp; + end + + methods + function obj = DynamicClass + obj.map = containers.Map; + obj.schema = containers.Map; + obj.allowAddProp = false; + end + + function validate_property(obj, name, val) + if isKey(obj.schema, name) + feval(obj.schema(name), val); + elseif ~obj.allowAddProp + error('This class does not allow adding properties.'); + end + end + + function export(~, loc_id) + %write namespace and class name + [path, classname, ~] = fileparts(mfilename('fullpath')); + [~, namespacename, ~] = fileparts(path); + h5util.writeAttribute(loc_id, 'namespace', namespacename(2:end), 'string'); + h5util.writeAttribute(loc_id, 'neurodata_type', classname(2:end), 'string'); + end + end + + methods(Sealed, Access=protected) + %% Subsref/Subsasgn Overrides + + function varargout = subsref(obj, s) + if strcmp('{}', s(1).type) + error('subcont only supports ''.'' and ''()'' indexing'); + end + + if length(s) > 1 + [varargout{1:nargout}] = subsref(obj.map(s(1).subs), s(2:end)); + else + varargout{1} = obj.map(s.subs); + end + end + + function obj = subsasgn(obj, s, varargin) + if strcmp('{}', s(1).type) + error('subcont only supports ''.'' and ''()'' indexing'); + end + + if strcmp('()', s(1).type) + obj.validate_property(s.subs, varargin{1}); + obj.map(s.subs{:}) = varargin{1}; + else + if length(s) > 1 + obj.map(s(1).subs) = subsasgn(obj.map(s(1).subs), s(2:end), varargin); + else + obj.validate_property(s.subs, varargin{1}); + obj.map(s.subs) = varargin{1}; + end + end + end + + %% Custom Display Overrides + function displayScalarObject(obj) + if isempty(obj.map) + disp(matlab.mixin.CustomDisplay.getSimpleHeader(obj)); + else + disp([' ' matlab.mixin.CustomDisplay.getClassNameForHeader(obj)... + ' with properties:' newline]); + mk = keys(obj.map); + maxwordlen = 0; + for i=1:length(mk) + mklen = length(mk{i}); + if mklen > maxwordlen + maxwordlen = mklen; + end + end + + for i=1:length(mk) + mknm = mk{i}; + val = obj.map(mknm); + if ischar(val) + val = ['''' val '''']; + end + disp([repmat(' ', 1, (maxwordlen - length(mknm)) + 4)... + mknm ': ' strtrim(evalc('disp(val)'))]); + end + disp(' '); + end + end + + function displayNonScalarObject(obj) + disp([strjoin(size(obj), 'x') ' ' getClassNameForHeader(obj)]); + end + end +end \ No newline at end of file diff --git a/+types/+untyped/Link.m b/+types/+untyped/Link.m index 1fcfb7ae..152d6354 100644 --- a/+types/+untyped/Link.m +++ b/+types/+untyped/Link.m @@ -1,30 +1,70 @@ -classdef Link - properties - filename = ''; - path; - ref; - end - - methods - function obj = Link(path, filename, ref) - obj.path = path; - - if nargin > 1 - obj.filename = filename; - end - - if nargin > 2 - obj.ref = ref; - end +classdef Link < handle + + properties(SetAccess=immutable) + nwb; %nwbfile into which link should be searching + filename; + end + + properties + path; + end + + properties(Hidden, SetAccess=immutable) + type; %type constraint, used by file generation end - function export(obj, loc_id, nm) - plist = 'H5P_DEFAULT'; - if isempty(obj.filename) - H5L.create_soft(obj.path, loc_id, nm, plist, plist); - else - H5L.create_external(obj.filename, obj.path, loc_id, nm, plist, plist); - end + methods + function obj = Link(path, context, type) + obj.path = path; + + %if context is char, then it's an external link + %if context is nwbfile then it's a softlink + if ischar(context) + obj.filename = context; + obj.nwb = []; + elseif isa(context, 'nwbfile') + obj.filename = ''; + obj.nwb = context; + else + error('Argument `context` must either be a filename for external links, or a nwbfile for soft links'); + end + + if nargin >= 3 + if ~ischar(type) + error('Argument `type` must be a char array specifying type'); + end + obj.type = type; + end + obj.deref(); + end + + function set.path(obj, val) + if ~ischar(val) + error('Property `path` should be a char array'); + end + obj.path = val; + end + + function refobj = deref(obj) + if isempty(obj.filename) + refobj = io.resolvePath(obj.nwb, obj.path); + if ~isa(refobj, obj.type) + error('Expected link to point to a `%s`. Got `%s`.', obj.type, class(refobj)); + end + else + %there are no guarantees regarding external links so just + %resolve as HDF5 dataset. + refobj = h5read(obj.filename, obj.path); + end + end + + function export(obj, loc_id, nm) + plist = 'H5P_DEFAULT'; + if isempty(obj.filename) + H5L.create_soft(obj.path, loc_id, nm, plist, plist); + else + H5L.create_external(obj.filename, obj.path, loc_id, nm, plist, plist); + end + end end - end end \ No newline at end of file diff --git a/+types/+untyped/MetaClass.m b/+types/+untyped/MetaClass.m index 534af818..20bd4d9a 100644 --- a/+types/+untyped/MetaClass.m +++ b/+types/+untyped/MetaClass.m @@ -10,63 +10,4 @@ function export(~, loc_id) h5util.writeAttribute(loc_id, 'neurodata_type', namespace(2:end), 'string'); end end - - methods(Access=protected) - function res = validateDynamicProperty(obj, val) - res = []; - valmc = metaclass(val); - valparents = valmc.SuperclassList; - for i=1:length(valparents) - found = strcmp(obj.dynamic_constraints, valparents(i).Name); - if any(found) - res = obj.dynamic_constraints{found}; - return; - end - end - end - end - - methods(Sealed, Access=protected) - %% Subsref/Subsasgn Overrides - function varargout = subsref(obj, s) - if strcmp('{}', s(1).type) - error('This class only supports ''.'' and ''()'' indexing'); - end - - if isKey(obj.dynamic_properties, s(1).subs) - if length(s) > 1 - [varargout{1:nargout}] = subsref(obj.dynamic_properties(s(1).subs), s(2:end)); - else - varargout{1} = obj.dynamic_properties(s.subs); - end - else - [varargout{1:nargout}] = builtin('subsref', obj, s); - end - end - - function obj = subsasgn(obj, s, varargin) - if strcmp('{}', s(1).type) - error('This class only supports ''.'' and ''()'' indexing'); - end - - if strcmp('()', s(1).type) - if ~iscellstr(s.subs) - error('Invalid class index type. Must be char array index'); - end - obj.addDynamicProperty(s.subs{1}, varargin{1}); - elseif any(strcmp(properties(obj), s(1).subs)) - obj = builtin('subsasgn', obj, s, varargin); - else - if ~isKey(obj.dynamic_properties, s(1).subs) - error('No static or dynamic property %s', s(1).subs); - end - if length(s) > 1 - obj.dynamic_properties(s(1).subs) = subsasgn(obj.map(s(1).subs), s(2:end), varargin); - else - obj.validate_property(s.subs, varargin{1}); - obj.map(s.subs) = varargin{1}; - end - end - end - end end \ No newline at end of file diff --git a/+types/+untyped/Ref.m b/+types/+untyped/Ref.m new file mode 100644 index 00000000..746ee8c8 --- /dev/null +++ b/+types/+untyped/Ref.m @@ -0,0 +1,74 @@ +classdef Ref < handle + properties + path; + region = []; + end + + properties(Hidden, SetAccess=immutable) + nwb; + type = ''; %string requiring some type + end + + methods + function obj = Ref(path, nwb, type, region) + if ~isa(nwb, 'nwbfile') + error('Argument `nwb` must be a nwbfile type'); + end + obj.nwb = nwb; + + obj.path = path; %validations are in set.path fcn + if nargin >= 3 + if ~ischar(type) + error('Argument `type` should be a char array.'); + end + obj.type = type; + end + + + if nargin >= 4 + obj.region = region; %validations are in set.region fcn + end + obj.deref(); + end + + function set.path(obj, val) + if ~ischar(val) + error('Argument `path` should be a char array.'); + end + obj.path = val; + end + + function set.region(obj, val) + %region must be in form [start end] + if length(val) < 2 + error('Argument `region` should be in the form of [start end]'); + end + if ~isnumeric(val) + error('Argument `region` should be numeric indices.'); + end + obj.region = val(1:2); + end + + function refobj = deref(obj) + refobj = io.resolvePath(obj.nwb, obj.path); + + %schema-level type constraint + if ~isempty(obj.type) && ~isa(refobj, obj.type) + error('Reference object expected to be `%s`. Got %s', obj.type, class(refobj)); + end + + if ~isempty(obj.region) + if ~isa(refobj, 'types.core.NWBData') && ~isa(refobj, 'types.core.SpecFile') + error('Region reference points to a Non-dataset.'); + end + + if any(strcmp(properties(refobj), 'table')) + %return table subset + refobj = refobj.table(obj.region(1):obj.region(2), :); + else %regular dataset + refobj = refobj.data(obj.region(1):obj.region(2)); + end + end + end + end +end \ No newline at end of file diff --git a/+types/+util/checkConstrained.m b/+types/+util/checkConstrained.m new file mode 100644 index 00000000..c413d0eb --- /dev/null +++ b/+types/+util/checkConstrained.m @@ -0,0 +1,28 @@ +function checkConstrained(name, namedprops, constrained, val) +if ~isa(val, 'containers.Map') + error('Property `%s` must be a containers.Map.', name); +end + +pnames = fieldnames(namedprops); +for i=1:length(pnames) + nm = pnames{i}; + types.util.checkDtype([name '.' nm], namedprops.(nm), val(nm)); +end + +constrainedNames = setdiff(keys(val), pnames); +for i=1:length(constrainedNames) + nm = constrainedNames{i}; + fitsConstraint = false; + for j=1:length(constrained) + constr = constrained{j}; + if isa(val(nm), constr) + fitsConstraint = true; + break; + end + end + if ~fitsConstraint + error('Property `%s.%s` does not fit the proper struct constraints.',... + name, nm); + end +end +end \ No newline at end of file diff --git a/+types/+util/checkDims.m b/+types/+util/checkDims.m new file mode 100644 index 00000000..4f6e2411 --- /dev/null +++ b/+types/+util/checkDims.m @@ -0,0 +1,24 @@ +function checkDims(valsize, validSizes) + validSizesStrings = cell(size(validSizes)); + for i=1:length(validSizes) + vs = validSizes{i}; + if length(valsize) ~= length(vs) + continue; + end + + noninf = ~isinf(vs); + if ~any(noninf) || all(valsize(noninf) == vs(noninf)) + %has a valid size + return; + end + validSizesStrings{i} = ['[' sizeFormatStr(vs) ']']; + end + valsizef = ['[' sizeFormatStr(valsize) ']']; + validSizesf = ['{' strjoin(validSizesStrings, ' ') '}']; + error(['Values size ' valsizef ' is invalid. Must be one of ' validSizesf],... + valsize, validSizes{:}); +end + +function s = sizeFormatStr(sz) + s = strjoin(repmat({'%d'}, size(sz)), ' '); +end \ No newline at end of file diff --git a/+types/+util/checkDtype.m b/+types/+util/checkDtype.m new file mode 100644 index 00000000..6b9a7410 --- /dev/null +++ b/+types/+util/checkDtype.m @@ -0,0 +1,34 @@ +function checkDtype(name, type, val) +%ref +%any, double, int/uint, char +errmsg = ['Property `' name '` must be a ' type '.']; +switch type + case 'double' + if ~isnumeric(val) + error(errmsg); + end + case {'int64' 'uint64'} + if ~isinteger(val) + error(errmsg); + end + + if strcmp(types, 'uint64') && val < 0 + error('Property `%s` must be greater than zero.', name); + end + case 'char' + if ~ischar(val) + error(errmsg); + end + otherwise %class or ref to class + if ~iscell(val) + val = {val}; + end + for i=1:length(val) + subval = val{i}; + if ~isa(subval(1), type) + error(errmsg); + end + end +end + +end \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e50e851..acc8b046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -+types ++types/+core testResults.xml coverage.xml .ropeproject