Skip to content

Commit

Permalink
Refactor: Add arguments block to main MatNWB api functions
Browse files Browse the repository at this point in the history
- Move some functions io.spec namespace
- Add validation functions in matnwb.common namespace
  • Loading branch information
ehennestad committed Nov 7, 2024
1 parent 5aa27d9 commit 4a305d0
Show file tree
Hide file tree
Showing 16 changed files with 390 additions and 282 deletions.
2 changes: 1 addition & 1 deletion +file/writeNamespace.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function writeNamespace(namespaceName, saveDir)

classFileDir = fullfile(saveDir, '+types', ['+' misc.str2validName(Namespace.name)]);

if 7 ~= exist(classFileDir, 'dir')
if ~isfolder(classFileDir)
mkdir(classFileDir);
end

Expand Down
18 changes: 18 additions & 0 deletions +io/+spec/+internal/readEmbeddedSpecLocation.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function specLocation = readEmbeddedSpecLocation(fid, specLocAttributeName)
arguments
fid (1,1) H5ML.id
specLocAttributeName (1,1) string = '.specloc'
end

specLocation = '';
try % Check .specloc
attributeId = H5A.open(fid, specLocAttributeName);
attributeCleanup = onCleanup(@(id) H5A.close(attributeId));
referenceRawData = H5A.read(attributeId);
specLocation = H5R.get_name(attributeId, 'H5R_OBJECT', referenceRawData);
catch ME
if ~strcmp(ME.identifier, 'MATLAB:imagesci:hdf5lib:libraryError')
rethrow(ME);

Check warning on line 15 in +io/+spec/+internal/readEmbeddedSpecLocation.m

View check run for this annotation

Codecov / codecov/patch

+io/+spec/+internal/readEmbeddedSpecLocation.m#L15

Added line #L15 was not covered by tests
end % don't error if the attribute doesn't exist.
end
end
16 changes: 16 additions & 0 deletions +io/+spec/getEmbeddedSpecLocation.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function specLocation = getEmbeddedSpecLocation(filename, options)
% getEmbeddedSpecLocation - Get location of embedded specs in NWB file
%
% Note: Returns an empty string if the spec location does not exist
%
% See also io.spec.internal.readEmbeddedSpecLocation

arguments
filename (1,1) string {matnwb.common.mustBeNwbFile}
options.SpecLocAttributeName (1,1) string = '.specloc'
end

fid = H5F.open(filename);
fileCleanup = onCleanup(@(id) H5F.close(fid) );
specLocation = io.spec.internal.readEmbeddedSpecLocation(fid, options.SpecLocAttributeName);
end
60 changes: 60 additions & 0 deletions +io/+spec/readEmbeddedSpecifications.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
function specs = readEmbeddedSpecifications(filename, specLocation)
% readEmbeddedSpecifications - Read embedded specs from an NWB file
%
% specs = io.spec.readEmbeddedSpecifications(filename, specLocation) read
% embedded specs from the specLocation in an NWB file
%
% Inputs:
% filename (string) : Absolute path of an nwb file
% specLocation (string) : h5 path for the location of specs inside the NWB file
%
% Outputs
% specs cell: A cell array of structs with one element for each embedded
% specification. Each struct has two fields:
%
% - namespaceName (char) : Name of the namespace for a specification
% - namespaceText (char) : The namespace declaration for a specification
% - schemaMap (containers.Map): A set of schema specifications for the namespace

arguments
filename (1,1) string {matnwb.common.mustBeNwbFile}
specLocation (1,1) string
end

specInfo = h5info(filename, specLocation);
specs = deal( cell(size(specInfo.Groups)) );

fid = H5F.open(filename);
fileCleanup = onCleanup(@(id) H5F.close(fid) );

for iGroup = 1:length(specInfo.Groups)
location = specInfo.Groups(iGroup).Groups(1);

namespaceName = split(specInfo.Groups(iGroup).Name, '/');
namespaceName = namespaceName{end};

filenames = {location.Datasets.Name};
if ~any(strcmp('namespace', filenames))
warning('NWB:Read:GenerateSpec:CacheInvalid',...
'Couldn''t find a `namespace` in namespace `%s`. Skipping cache generation.',...
namespaceName);
return;

Check warning on line 41 in +io/+spec/readEmbeddedSpecifications.m

View check run for this annotation

Codecov / codecov/patch

+io/+spec/readEmbeddedSpecifications.m#L38-L41

Added lines #L38 - L41 were not covered by tests
end
sourceNames = {location.Datasets.Name};
fileLocation = strcat(location.Name, '/', sourceNames);
schemaMap = containers.Map;
for iFileLocation = 1:length(fileLocation)
did = H5D.open(fid, fileLocation{iFileLocation});
if strcmp('namespace', sourceNames{iFileLocation})
namespaceText = H5D.read(did);
else
schemaMap(sourceNames{iFileLocation}) = H5D.read(did);
end
H5D.close(did);
end

specs{iGroup}.namespaceName = namespaceName;
specs{iGroup}.namespaceText = namespaceText;
specs{iGroup}.schemaMap = schemaMap;
end
end
45 changes: 45 additions & 0 deletions +io/+spec/writeEmbeddedSpecifications.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function writeEmbeddedSpecifications(fid, jsonSpecs)
specLocation = io.spec.internal.readEmbeddedSpecLocation(fid);

if isempty(specLocation)
specLocation = '/specifications';
io.writeGroup(fid, specLocation);
specView = types.untyped.ObjectView(specLocation);
io.writeAttribute(fid, '/.specloc', specView);
end

for iJson = 1:length(jsonSpecs)
JsonDatum = jsonSpecs(iJson);
schemaNamespaceLocation = strjoin({specLocation, JsonDatum.name}, '/');
namespaceExists = io.writeGroup(fid, schemaNamespaceLocation);
if namespaceExists
namespaceGroupId = H5G.open(fid, schemaNamespaceLocation);
names = getVersionNames(namespaceGroupId);
H5G.close(namespaceGroupId);
for iNames = 1:length(names)
H5L.delete(fid, [schemaNamespaceLocation '/' names{iNames}],...
'H5P_DEFAULT');
end
end
schemaLocation =...
strjoin({schemaNamespaceLocation, JsonDatum.version}, '/');
io.writeGroup(fid, schemaLocation);
Json = JsonDatum.json;
schemeNames = keys(Json);
for iScheme = 1:length(schemeNames)
name = schemeNames{iScheme};
path = [schemaLocation '/' name];
io.writeDataset(fid, path, Json(name));
end
end
end

function versionNames = getVersionNames(namespaceGroupId)
[~, ~, versionNames] = H5L.iterate(namespaceGroupId,...
'H5_INDEX_NAME', 'H5_ITER_NATIVE',...
0, @removeGroups, {});
function [status, versionNames] = removeGroups(~, name, versionNames)
versionNames{end+1} = name;
status = 0;
end
end
20 changes: 20 additions & 0 deletions +matnwb/+common/findLatestSchemaVersion.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function latestVersion = findLatestSchemaVersion()
% findLatestSchemaVersion - Find latest available schema version.

schemaListing = dir(fullfile(misc.getMatnwbDir(), 'nwb-schema'));
schemaVersionNumbers = setdiff({schemaListing.name}, {'.', '..'});

% Split each version number into major, minor, and patch components
versionComponents = cellfun(@(v) sscanf(v, '%d.%d.%d'), ...
schemaVersionNumbers, 'UniformOutput', false);

% Convert the components into an array for easy comparison
versionMatrix = cat(2, versionComponents{:})';

% Find the row with the highest version number, weighting major
% and minor with factors of 6 and 3 respectively
[~, latestIndex] = max(versionMatrix * [1e6; 1e3; 1]); % Weight major, minor, patch

% Return the latest version
latestVersion = schemaVersionNumbers{latestIndex};
end
7 changes: 7 additions & 0 deletions +matnwb/+common/mustBeNwbFile.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function mustBeNwbFile(filePath)
% mustBeNwbFile - Check that file path points to existing file with .nwb extension
arguments
filePath (1,1) string {mustBeFile}
end
assert(endsWith(filePath, ".nwb", "IgnoreCase", true))
end
30 changes: 30 additions & 0 deletions +matnwb/+common/mustBeValidSchemaVersion.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function mustBeValidSchemaVersion(versionNumber)
% mustBeValidSchemaVersion - Validate version number against available schemas
arguments
versionNumber (1,1) string
end

persistent schemaVersionNumbers

if versionNumber == "latest"
return % Should be resolved downstream.
end

versionPattern = "^\d+\.\d+\.\d+$"; % i.e 2.0.0
if isempty(regexp(versionNumber, versionPattern, 'once'))
error('NWB:VersionValidator:InvalidVersionNumber', ...
"Version number should formatted as <major>.<minor>.<patch>")

Check warning on line 16 in +matnwb/+common/mustBeValidSchemaVersion.m

View check run for this annotation

Codecov / codecov/patch

+matnwb/+common/mustBeValidSchemaVersion.m#L15-L16

Added lines #L15 - L16 were not covered by tests
end

% Validate supported schema version
if isempty(schemaVersionNumbers)
schemaListing = dir(fullfile(misc.getMatnwbDir(), 'nwb-schema'));
schemaVersionNumbers = setdiff({schemaListing.name}, {'.', '..'});
end

if ~any(strcmp(versionNumber, schemaVersionNumbers))
error('NWB:VersionValidator:UnsupportedSchemaVersion', ...
"The provided version number ('%s') is not supported by this version of MatNWB", ...
versionNumber)
end
end
6 changes: 2 additions & 4 deletions +spec/generate.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@

for iInfo = 1:length(Namespaces)
Namespaces(iInfo).namespace = namespace;
if ischar(schemaSource)
if ischar(schemaSource) || isstring(schemaSource)
schema = containers.Map;
Namespace = Namespaces(iInfo);
for iFilenames = 1:length(Namespace.filenames)
filenameStub = Namespace.filenames{iFilenames};
filename = [filenameStub '.yaml'];
fid = fopen(fullfile(schemaSource, filename));
schema(filenameStub) = fread(fid, '*char') .';
fclose(fid);
schema(filenameStub) = fileread(fullfile(schemaSource, filename));
end
schema = spec.getSourceInfo(schema);
else % map of schemas with their locations
Expand Down
25 changes: 23 additions & 2 deletions +tests/+system/NWBFileIOTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ function readFileWithoutSpecLoc(testCase)

testCase.deleteAttributeFromFile(fileName, '/', '.specloc')

nwbRead(fileName);
% When specloc is missing, the specifications are not added to
% the blacklist, so it will get passed as an input to NwbFile.
testCase.verifyError(@(fn) nwbRead(fileName), 'MATLAB:TooManyInputs');
end

function readFileWithUnsupportedVersion(testCase)
Expand All @@ -74,7 +76,26 @@ function readFileWithUnsupportedVersion(testCase)
io.writeAttribute(file_id, '/nwb_version', '1.0.0')
H5F.close(file_id);

nwbRead(fileName);
testCase.verifyWarning(@(fn) nwbRead(fileName), 'NWB:Read:UnsupportedSchema')
end

function readFileWithUnsupportedVersionAndNoSpecloc(testCase)
import matlab.unittest.fixtures.SuppressedWarningsFixture
testCase.applyFixture(SuppressedWarningsFixture('NWB:Read:UnsupportedSchema'))

fileName = ['MatNWB.' testCase.className() '.testReadFileWithUnsupportedVersionAndNoSpecloc.nwb'];
nwbExport(testCase.file, fileName)

testCase.deleteAttributeFromFile(fileName, '/', '.specloc')
testCase.deleteAttributeFromFile(fileName, '/', 'nwb_version')

file_id = H5F.open(fileName, 'H5F_ACC_RDWR', 'H5P_DEFAULT');
io.writeAttribute(file_id, '/nwb_version', '1.0.0')
H5F.close(file_id);

% When specloc is missing, the specifications are not added to
% the blacklist, so it will get passed as an input to NwbFile.
testCase.verifyError(@(fn) nwbRead(fileName), 'MATLAB:TooManyInputs');
end
end

Expand Down
Loading

0 comments on commit 4a305d0

Please sign in to comment.