Skip to content

Commit

Permalink
Major refactor to checkDType
Browse files Browse the repository at this point in the history
- expand and clarify variable names
- Add order and field existence checks to compound data types
- convert empty types to their expected values if possible.
  • Loading branch information
lawrence-mbf committed Jul 28, 2023
1 parent 8524cde commit bbf83ef
Showing 1 changed file with 94 additions and 51 deletions.
145 changes: 94 additions & 51 deletions +types/+util/checkDtype.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function val = checkDtype(name, type, val)
function value = checkDtype(name, typeDescriptor, value)
%ref
%any, double, int/uint, char
persistent WHITELIST;
Expand All @@ -8,108 +8,151 @@
'types.untyped.SoftLink'...
};
end

%% compound type processing
if isstruct(type)
names = fieldnames(type);
assert(isstruct(val) || istable(val) || isa(val, 'containers.Map'), ...
'types.untyped.checkDtype: Compound Type must be a struct, table, or a containers.Map');
if (isstruct(val) && isscalar(val)) || isa(val, 'containers.Map')
%check for correct array shape
sizes = zeros(length(names),1);
for i=1:length(names)
if isstruct(val)
subv = val.(names{i});
if isstruct(typeDescriptor)
expectedFields = fieldnames(typeDescriptor);
assert(isstruct(value) || istable(value) || isa(value, 'containers.Map') ...
, 'NWB:CheckDType:InvalidValue' ...
, 'Compound Type must be a struct, table, or a containers.Map' ...
);

% assert field names and order of fields is correct.
if isstruct(value)
valueFields = fieldnames(value);
else % table
valueFields = value.Properties.VariableNames;
end
assert(isempty(setdiff(expectedFields, valueFields)) ...
, 'NWB:CheckDType:InvalidValue' ...
, 'Compound type must only contain fields (%s)', strjoin(expectedFields, ', ') ...
);
for iField = 1:length(expectedFields)
assert(strcmp(expectedFields{iField}, valueFields{iField}) ...
, 'NWB:CheckDType:InvalidValue' ...
, 'Compound fields are out of order.\nExpected (%s) Got (%s)' ...
, strjoin(expectedFields, ', '), strjoin(valueFields, ', '));
end

if (isstruct(value) && isscalar(value)) || isa(value, 'containers.Map')
% check for correct array shape
fieldSizes = zeros(length(expectedFields),1);
for iField = 1:length(expectedFields)
if isstruct(value)
subValue = value.(expectedFields{iField});
else
subv = val(names{i});
subValue = value(expectedFields{iField});
end
assert(isvector(subv),...
assert(isvector(subValue),...
'NWB:CheckDType:InvalidShape',...
['struct of arrays as a compound type ',...
'cannot have multidimensional data in their fields. ',...
'Field data shape must be scalar or vector to be valid.']);
sizes(i) = length(subv);
fieldSizes(iField) = length(subValue);
end
sizes = unique(sizes);
assert(isscalar(sizes),...
fieldSizes = unique(fieldSizes);
assert(isscalar(fieldSizes),...
'NWB:CheckDType:InvalidShape',...
['struct of arrays as a compound type ',...
'contains mismatched number of elements with unique sizes: [%s]. ',...
'Number of elements for each struct field must match to be valid.'], ...
num2str(sizes));
num2str(fieldSizes));
end
for i=1:length(names)
pnm = names{i};
subnm = [name '.' pnm];
typenm = type.(pnm);

if (isstruct(val) && isscalar(val)) || istable(val)
val.(pnm) = types.util.checkDtype(subnm,typenm,val.(pnm));
elseif isstruct(val)
for j=1:length(val)
elem = val(j).(pnm);
for iField = 1:length(expectedFields)
% validate subfield types.
name = expectedFields{iField};
subName = [name '.' name];
subType = typeDescriptor.(name);

if (isstruct(value) && isscalar(value)) || istable(value)
% scalar struct or table with columns.
value.(name) = types.util.checkDtype(subName,subType,value.(name));
elseif isstruct(value)
% array of structs
for j=1:length(value)
elem = value(j).(name);
assert(~iscell(elem) && ...
(isempty(elem) || ...
(isscalar(elem) || (ischar(elem) && isvector(elem)))),...
'NWB:CheckDType:InvalidType',...
['Fields for an array of structs for '...
'compound types should have non-cell scalar values or char arrays.']);
val(j).(pnm) = types.util.checkDtype(subnm, typenm, elem);
value(j).(name) = types.util.checkDtype(subName, subType, elem);
end
else
val(names{i}) = types.util.checkDtype(subnm,typenm,val(names{i}));
value(expectedFields{iField}) = types.util.checkDtype( ...
subName, subType, value(expectedFields{iField}));
end
end
return;
end


%% primitives
if isempty(val) ... % MATLAB's "null" operator. Even if it's numeric, you can replace it with any class.
|| isa(val, 'types.untyped.SoftLink') % Softlinks cannot be validated at this level.

if isa(value, 'types.untyped.SoftLink')
% Softlinks cannot be validated at this level.
return;
end

if isempty(value)
% MATLAB's "null" operator. Even if it's numeric, you can replace it with any class.
% we can replace empty values with their equivalents, however.
replaceableNullTypes = {...
'char' ...
, 'logical' ...
, 'single', 'double' ...
, 'int8', 'uint8' ...
, 'int16', 'uint16' ...
, 'int32', 'uint32' ...
, 'int64', 'uint64' ...
};
if ischar(typeDescriptor) && any(strcmp(typeDescriptor, replaceableNullTypes))
value = cast(value, typeDescriptor);
end
return;
end

% retrieve sample of val
if isa(val, 'types.untyped.DataStub')
if isa(value, 'types.untyped.DataStub')
%grab first element and check
valueWrapper = val;
if any(val.dims == 0)
val = [];
valueWrapper = value;
if any(value.dims == 0)
value = [];
else
val = val.load(1);
value = value.load(1);
end
elseif isa(val, 'types.untyped.Anon')
valueWrapper = val;
val = val.value;
elseif isa(val, 'types.untyped.ExternalLink') &&...
~strcmp(type, 'types.untyped.ExternalLink')
valueWrapper = val;
val = val.deref();
elseif isa(val, 'types.untyped.DataPipe')
valueWrapper = val;
val = cast([], val.dataType);
elseif isa(value, 'types.untyped.Anon')
valueWrapper = value;
value = value.value;
elseif isa(value, 'types.untyped.ExternalLink') &&...
~strcmp(typeDescriptor, 'types.untyped.ExternalLink')
valueWrapper = value;
value = value.deref();
elseif isa(value, 'types.untyped.DataPipe')
valueWrapper = value;
value = cast([], value.dataType);
else
valueWrapper = [];
end

correctedValue = types.util.correctType(val, type);
correctedValue = types.util.correctType(value, typeDescriptor);
% this specific conversion is fine as HDF5 doesn't have a representative
% datetime type. Thus we suppress the warning for this case.
isDatetimeConversion = isa(correctedValue, 'datetime')...
&& (ischar(val) || isstring(val) || iscellstr(val));
&& (ischar(value) || isstring(value) || iscellstr(value));
if ~isempty(valueWrapper) ...
&& ~strcmp(class(correctedValue), class(val)) ...
&& ~strcmp(class(correctedValue), class(value)) ...
&& ~isDatetimeConversion
warning('NWB:CheckDataType:NeedsManualConversion',...
'Property `%s` is not of type `%s` and should be corrected by the user.', ...
name, class(correctedValue));
else
val = correctedValue;
value = correctedValue;
end

% re-wrap value
if ~isempty(valueWrapper)
val = valueWrapper;
value = valueWrapper;
end
end

0 comments on commit bbf83ef

Please sign in to comment.