Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions +file/fillFunctionSignature.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
function entryText = fillFunctionSignature(name, props, namespace, superClassProps)
% fillFunctionSignature - Create the function signature entry for a constructor.
%
% entryText = file.fillFunctionSignature(name, props, namespace, superClassProps)
% returns the text of one entry for a functionSignatures.json file, in
% the form "types.<namespace>.<name>": {...}. The entry lists the
% name-value arguments of the class constructor, so MATLAB can suggest
% them while the user types.
%
% The arguments are the properties of the class and its superclasses, as
% listed in the constructor help text. Read-only properties are left out,
% because the constructor does not set them. Properties that hold a set
% of typed members are left out too: their members are passed under the
% member's own name, which the schema does not fix.

allProps = file.internal.mergeProps(props, superClassProps);
propNames = allProps.keys();

inputs = struct("name", {}, "kind", {}, "purpose", {});
for iProp = 1:numel(propNames)
prop = allProps(propNames{iProp});
if isReadOnly(prop) || holdsTypedSet(prop)
continue
end
argument = struct( ...
"name", file.internal.getMatlabPropertyName(propNames{iProp}), ...
"kind", "namevalue", ...
"purpose", getPurpose(prop));
inputs(end+1) = argument; %#ok<AGROW>
end

fullClassName = namespace.getFullClassName(name);
% A cell array encodes as a JSON array also when it has one element.
signatureJson = jsonencode(struct("inputs", {num2cell(inputs)}), "PrettyPrint", true);
entryText = sprintf('"%s": %s', fullClassName, signatureJson);
end

function tf = isReadOnly(prop)
tf = (isa(prop, "file.Attribute") || isa(prop, "file.Dataset")) && prop.readonly;
end

function tf = holdsTypedSet(prop)
% holdsTypedSet - True for a property that the constructor initializes as a Set.
%
% Matches the cases for which file.fillConstructor uses a Set as the default.

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;
tf = isPluralSet || isGroupSet || isDataSet;
end

function purpose = getPurpose(prop)
% getPurpose - Get the schema doc of a property as a single line.
purpose = "";
if isprop(prop, "doc") && ~isempty(prop(1).doc)
purpose = strjoin(split(strtrim(string(prop(1).doc))), " ");
end
end
18 changes: 18 additions & 0 deletions +file/writeNamespace.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function writeNamespace(namespaceName, saveDir)

classes = keys(Namespace.registry);
pregenerated = containers.Map; %generated nodes and props for faster dependency resolution
signatureEntries = cell(1, 0);
for i=1:length(classes)
className = classes{i};
[processed, classprops, inherited] = file.processClass(className, Namespace, pregenerated);
Expand All @@ -26,10 +27,27 @@ function writeNamespace(namespaceName, saveDir)
fileCleanupObj = onCleanup(@(id) fclose(fid));
fwrite(fid, file.fillClass(className, Namespace, processed, ...
classprops, inherited, superClassProps), 'char');
signatureEntries{end+1} = file.fillFunctionSignature( ...
className, classprops, Namespace, superClassProps); %#ok<AGROW>
else
% pass
end
end
writeFunctionSignatureFragment(classFileDir, signatureEntries)
end

function writeFunctionSignatureFragment(classFileDir, signatureEntries)
% writeFunctionSignatureFragment - Write the signature entries of a namespace.
%
% The fragment is a JSON object with one entry per class. It is combined
% with the fragments of the other generated namespaces into the
% functionSignatures.json file that MATLAB reads (see
% matnwb.internal.typecache.writeFunctionSignatures).

fragmentText = sprintf('{\n%s\n}\n', strjoin(signatureEntries, sprintf(',\n')));
fid = fopen(fullfile(classFileDir, matnwb.common.constant.SIGNATUREFRAGMENTFILE), 'w', 'n', 'UTF-8');
fileCleanupObj = onCleanup(@() fclose(fid));
fprintf(fid, '%s', fragmentText);
end

function writeNamespaceVersion(classFileDir, version)
Expand Down
4 changes: 4 additions & 0 deletions +matnwb/+common/+constant/SIGNATUREFRAGMENTFILE.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function constant = SIGNATUREFRAGMENTFILE()
% SIGNATUREFRAGMENTFILE - Name of the file with a namespace's function signature entries.
constant = 'functionSignatures.fragment.json';
end
1 change: 1 addition & 0 deletions +matnwb/+internal/+typecache/generateNamespaces.m
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function generateNamespaces(namespaceInfoList, saveDir)
removeLeastRecentlyUsed(versionFolder, maxEntriesPerVersion)
end
end
matnwb.internal.typecache.writeFunctionSignatures(saveDir)
rehash()
end

Expand Down
72 changes: 72 additions & 0 deletions +matnwb/+internal/+typecache/writeFunctionSignatures.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
function writeFunctionSignatures(saveDir)
% writeFunctionSignatures - Write the functionSignatures.json file for generated classes.
%
% matnwb.internal.typecache.writeFunctionSignatures(saveDir) writes
% saveDir/resources/functionSignatures.json with an entry for the
% constructor of every class in saveDir/+types. MATLAB reads the file to
% suggest name-value arguments while the user types a constructor call.
%
% MATLAB reads functionSignatures.json only from the resources folder next
% to the outermost namespace folder, +types. When saveDir is the matnwb
% root directory, that folder also holds the signatures of the matnwb
% functions, so the file combines the hand-written entries in
% resources/function_templates/functionSignatures.json with the entries
% of the generated classes.
%
% Each generated namespace folder has a fragment with the entries of its
% classes, written by file.writeNamespace. The file is rebuilt from the
% fragments, so it always matches the namespaces in saveDir/+types.

arguments
saveDir (1,1) string
end

matnwbDir = string(misc.getMatnwbDir());
if isSameFolder(saveDir, matnwbDir)
templatePath = fullfile(matnwbDir, "resources", "function_templates", "functionSignatures.json");
entryBlocks = getObjectContent(fileread(templatePath, "Encoding", "UTF-8"));
else
entryBlocks = """_schemaVersion"": ""1.0.0""";
end

fragmentList = dir(fullfile(saveDir, "+types", "+*", matnwb.common.constant.SIGNATUREFRAGMENTFILE));
[~, sortIndex] = sort(string({fragmentList.folder}));
fragmentList = fragmentList(sortIndex);
for iFragment = 1:numel(fragmentList)
fragmentPath = fullfile(fragmentList(iFragment).folder, fragmentList(iFragment).name);
fragmentContent = getObjectContent(fileread(fragmentPath, "Encoding", "UTF-8"));
if strlength(fragmentContent) > 0
entryBlocks(end+1) = fragmentContent; %#ok<AGROW>
end
end

resourcesFolder = fullfile(saveDir, "resources");
if ~isfolder(resourcesFolder)
mkdir(resourcesFolder)
end
fileText = "{" + newline + strjoin(entryBlocks, "," + newline) + newline + "}" + newline;
fileId = fopen(fullfile(resourcesFolder, "functionSignatures.json"), "w", "n", "UTF-8");
fileCleanup = onCleanup(@() fclose(fileId));
fprintf(fileId, "%s", fileText);
end

function content = getObjectContent(jsonText)
% getObjectContent - Get the text between the outer braces of a JSON object.
%
% The text is combined as text, not decoded, because the hand-written file
% repeats a key to declare several signatures for one function, which
% jsondecode does not keep.

jsonText = string(jsonText);
openIndex = strfind(jsonText, "{");
closeIndex = strfind(jsonText, "}");
content = strip(extractBetween(jsonText, openIndex(1) + 1, closeIndex(end) - 1));
end

function tf = isSameFolder(folderA, folderB)
tf = strcmp(getCanonicalPath(folderA), getCanonicalPath(folderB));
end

function canonicalPath = getCanonicalPath(folderPath)
canonicalPath = string(java.io.File(folderPath).getCanonicalPath());
end
1 change: 1 addition & 0 deletions +tests/+abstract/NwbTestCase.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function clearExtension(testCase, extensionName)
typesOutputFolder = testCase.getTypesOutputFolder();
rmdir(fullfile(typesOutputFolder, '+types', ['+', namespaceFolderName]), 's')
delete(fullfile(typesOutputFolder, 'namespaces', [extensionName '.mat']))
matnwb.internal.typecache.writeFunctionSignatures(typesOutputFolder)
end
end

Expand Down
1 change: 1 addition & 0 deletions +tests/+fixtures/ExtensionGenerationFixture.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function clearGenerated(fixture)
if isfile(cacheFile)
delete(cacheFile)
end
matnwb.internal.typecache.writeFunctionSignatures(fixture.TypesOutputFolder)
end
end
end
64 changes: 63 additions & 1 deletion +tests/+unit/TypeCacheTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,47 @@ function testGenerateExtensionCopiesFromCache(testCase)
"Expected generateExtension to copy the class from the cache.")
end

function testSignaturesFileListsConstructorArguments(testCase)
parentInfo = testCase.parseParentNamespace("0.1.0", "Parent doc.");
testCase.generate([parentInfo, testCase.parseChildNamespace()])

signatureText = testCase.readSignaturesFile();
childEntry = extractAfter(signatureText, """types.test_cache_child.CacheChild""");
% Each entry ends with a closing brace at the start of a line.
childEntry = extractBefore(childEntry, newline + "}");

testCase.verifySubstring(childEntry, """name"": ""note""", ...
"Expected an inherited property that can be set.")
testCase.verifyFalse(contains(childEntry, """fixed_note"""), ...
"Expected a read-only property to be left out.")
testCase.verifyFalse(contains(childEntry, """cacheparent"""), ...
"Expected a property holding a set of typed members to be left out.")
end

function testSignaturesFileIsValidJson(testCase)
testCase.generate(testCase.parseParentNamespace("0.1.0", "Parent doc."))
testCase.verifyWarningFree(@() jsondecode(testCase.readSignaturesFile()))
end

function testSignaturesFileCopiedFromCacheMatchesGenerated(testCase)
parentInfo = testCase.parseParentNamespace("0.1.0", "Parent doc.");
testCase.generate(parentInfo)
generatedText = testCase.readSignaturesFile();
nwbClearGenerated(testCase.SaveFolder);

testCase.generate(parentInfo)

testCase.verifyEqual(testCase.readSignaturesFile(), generatedText)
end

function testClearingTypesRemovesSignatures(testCase)
testCase.generate(testCase.parseParentNamespace("0.1.0", "Parent doc."))

nwbClearGenerated(testCase.SaveFolder);

testCase.verifyFalse(contains(testCase.readSignaturesFile(), "types.test_cache.CacheParent"))
end

function testSpecHashIsIndependentOfSourceFormat(testCase)
yamlInfo = testCase.parseParentNamespace("0.1.0", "Parent doc.");

Expand All @@ -209,7 +250,11 @@ function testSpecHashIsIndependentOfSourceFormat(testCase)
"schema", {{struct("source", "test-cache.extensions")}})}}));
schemaJson = jsonencode(struct("groups", {{struct( ...
"neurodata_type_def", "CacheParent", ...
"doc", "Parent doc.")}}));
"doc", "Parent doc.", ...
"attributes", {{ ...
struct("name", "note", "dtype", "text", "doc", "A note that can be set."), ...
struct("name", "fixed_note", "dtype", "text", "value", "fixed", ...
"doc", "A note with a fixed value.")}})}}));
jsonInfo = spec.generate(namespaceJson, ...
containers.Map({'test-cache.extensions'}, {schemaJson}));

Expand All @@ -236,6 +281,14 @@ function generate(testCase, namespaceInfoList)
"groups:"
"- neurodata_type_def: CacheParent"
" doc: " + typeDoc
" attributes:"
" - name: note"
" dtype: text"
" doc: A note that can be set."
" - name: fixed_note"
" dtype: text"
" value: fixed"
" doc: A note with a fixed value."
];
if options.IncludeExtraType
schemaLines = [schemaLines; [
Expand All @@ -260,6 +313,10 @@ function generate(testCase, namespaceInfoList)
"- neurodata_type_def: CacheChild"
" neurodata_type_inc: CacheParent"
" doc: Child doc."
" groups:"
" - neurodata_type_inc: CacheParent"
" quantity: '*'"
" doc: Any number of parent objects."
];
namespaceLines = [
"namespaces:"
Expand Down Expand Up @@ -306,6 +363,11 @@ function addMarkerToCacheEntry(testCase, namespaceName, version, className)
writeText(classPath, newline + testCase.Marker + newline, "a")
end

function signatureText = readSignaturesFile(testCase)
signatureText = string(fileread( ...
fullfile(testCase.SaveFolder, "resources", "functionSignatures.json")));
end

function tf = hasMarker(testCase, namespaceName, className)
classText = fileread(testCase.getSavedClassPath(namespaceName, className));
tf = contains(classText, testCase.Marker);
Expand Down
Loading
Loading