diff --git a/+matnwb/+extension/+internal/buildRepoDownloadUrl.m b/+matnwb/+extension/+internal/buildRepoDownloadUrl.m new file mode 100644 index 00000000..59751f78 --- /dev/null +++ b/+matnwb/+extension/+internal/buildRepoDownloadUrl.m @@ -0,0 +1,25 @@ +function downloadUrl = buildRepoDownloadUrl(repositoryUrl, branchName) +% buildRepoDownloadUrl - Build a download URL for a given repository and branch + arguments + repositoryUrl (1,1) string + branchName (1,1) string + end + + if endsWith(repositoryUrl, '/') + repositoryUrl = extractBefore(repositoryUrl, strlength(repositoryUrl)); + end + + if contains(repositoryUrl, 'github.com') + downloadUrl = sprintf( '%s/archive/refs/heads/%s.zip', repositoryUrl, branchName ); + + elseif contains(repositoryUrl, 'gitlab.com') + repoPathSegments = strsplit(repositoryUrl, '/'); + repoName = repoPathSegments{end}; + downloadUrl = sprintf( '%s/-/archive/%s/%s-%s.zip', ... + repositoryUrl, branchName, repoName, branchName); + + else + error('NWB:BuildRepoDownloadUrl:UnsupportedRepository', ... + 'Expected repository URL to point to a GitHub or a GitLab repository') + end +end diff --git a/+matnwb/+extension/+internal/downloadExtensionRepository.m b/+matnwb/+extension/+internal/downloadExtensionRepository.m new file mode 100644 index 00000000..4c47dd18 --- /dev/null +++ b/+matnwb/+extension/+internal/downloadExtensionRepository.m @@ -0,0 +1,46 @@ +function [wasDownloaded, repoTargetFolder] = downloadExtensionRepository(... + repositoryUrl, repoTargetFolder, extensionName) +% downloadExtensionRepository - Download the repository (source) for an extension +% +% The metadata for a neurodata extension only provides the url to the +% repository containing the extension, not the full download url. This +% function tries to download a zipped version of the repository from +% either the "main" or the "master" branch. +% +% Works for repositories located on GitHub or GitLab +% +% As of Dec. 2024, this approach works for all registered extensions + + arguments + repositoryUrl (1,1) string + repoTargetFolder (1,1) string + extensionName (1,1) string + end + + import matnwb.extension.internal.downloadZippedRepo + import matnwb.extension.internal.buildRepoDownloadUrl + + defaultBranchNames = ["main", "master"]; + + wasDownloaded = false; + for i = 1:2 + try + branchName = defaultBranchNames(i); + downloadUrl = buildRepoDownloadUrl(repositoryUrl, branchName); + repoTargetFolder = downloadZippedRepo(downloadUrl, repoTargetFolder); + wasDownloaded = true; + break + catch ME + if strcmp(ME.identifier, 'MATLAB:webservices:HTTP404StatusCodeError') + continue + elseif strcmp(ME.identifier, 'NWB:BuildRepoDownloadUrl:UnsupportedRepository') + error('NWB:InstallExtension:UnsupportedRepository', ... + ['Extension "%s" is located in an unsupported repository ', ... + '/ source location. \nPlease create an issue on MatNWB''s ', ... + 'github page'], extensionName) + else + rethrow(ME) + end + end + end +end diff --git a/+matnwb/+extension/+internal/downloadZippedRepo.m b/+matnwb/+extension/+internal/downloadZippedRepo.m new file mode 100644 index 00000000..1d3acd94 --- /dev/null +++ b/+matnwb/+extension/+internal/downloadZippedRepo.m @@ -0,0 +1,37 @@ +function repoFolder = downloadZippedRepo(githubUrl, targetFolder) +%downloadZippedRepo - Download a zipped repository + + % Create a temporary path for storing the downloaded file. + [~, ~, fileType] = fileparts(githubUrl); + tempFilepath = [tempname, fileType]; + + % Download the file containing the zipped repository + tempFilepath = websave(tempFilepath, githubUrl); + fileCleanupObj = onCleanup( @(fname) delete(tempFilepath) ); + + unzippedFiles = unzip(tempFilepath, tempdir); + unzippedFolder = unzippedFiles{1}; + if endsWith(unzippedFolder, filesep) + unzippedFolder = unzippedFolder(1:end-1); + end + + [~, repoFolderName] = fileparts(unzippedFolder); + targetFolder = fullfile(targetFolder, repoFolderName); + + if isfolder(targetFolder) + try + rmdir(targetFolder, 's') + catch + error('Could not delete previously downloaded extension which is located at:\n"%s"', targetFolder) + end + else + % pass + end + + movefile(unzippedFolder, targetFolder); + + % Delete the temp zip file + clear fileCleanupObj + + repoFolder = targetFolder; +end diff --git a/+matnwb/+extension/getExtensionInfo.m b/+matnwb/+extension/getExtensionInfo.m new file mode 100644 index 00000000..411978b0 --- /dev/null +++ b/+matnwb/+extension/getExtensionInfo.m @@ -0,0 +1,16 @@ +function metadata = getExtensionInfo(extensionName) +% getExtensionInfo - Get metadata for specified extension + + arguments + extensionName (1,1) string + end + + T = matnwb.extension.listExtensions(); + isMatch = T.name == extensionName; + extensionList = join( compose(" %s", [T.name]), newline ); + assert( ... + any(isMatch), ... + 'NWB:DisplayExtensionMetadata:ExtensionNotFound', ... + 'Extension "%s" was not found in the extension catalog:\n%s', extensionName, extensionList) + metadata = table2struct(T(isMatch, :)); +end diff --git a/+matnwb/+extension/installAll.m b/+matnwb/+extension/installAll.m new file mode 100644 index 00000000..a4fff669 --- /dev/null +++ b/+matnwb/+extension/installAll.m @@ -0,0 +1,6 @@ +function installAll() + T = matnwb.extension.listExtensions(); + for i = 1:height(T) + matnwb.extension.installExtension( T.name(i) ) + end +end diff --git a/+matnwb/+extension/installExtension.m b/+matnwb/+extension/installExtension.m new file mode 100644 index 00000000..b333f9d7 --- /dev/null +++ b/+matnwb/+extension/installExtension.m @@ -0,0 +1,49 @@ +function installExtension(extensionName, options) +% installExtension - Install NWB extension from Neurodata Extensions Catalog +% +% matnwb.extension.nwbInstallExtension(extensionName) installs a Neurodata +% Without Borders (NWB) extension from the Neurodata Extensions Catalog to +% extend the functionality of the core NWB schemas. + + arguments + extensionName (1,1) string + options.savedir (1,1) string = misc.getMatnwbDir() + end + + import matnwb.extension.internal.downloadExtensionRepository + + repoTargetFolder = fullfile(userpath, "NWB-Extension-Source"); + if ~isfolder(repoTargetFolder); mkdir(repoTargetFolder); end + + T = matnwb.extension.listExtensions(); + isMatch = T.name == extensionName; + + extensionList = join( compose(" %s", [T.name]), newline ); + assert( ... + any(isMatch), ... + 'NWB:InstallExtension:ExtensionNotFound', ... + 'Extension "%s" was not found in the extension catalog:\n', extensionList) + + repositoryUrl = T{isMatch, 'src'}; + + [wasDownloaded, repoTargetFolder] = ... + downloadExtensionRepository(repositoryUrl, repoTargetFolder, extensionName); + + if ~wasDownloaded + error('NWB:InstallExtension:DownloadFailed', ... + 'Failed to download spec for extension "%s"', extensionName) + end + L = dir(fullfile(repoTargetFolder, 'spec', '*namespace.yaml')); + assert(... + ~isempty(L), ... + 'NWB:InstallExtension:NamespaceNotFound', ... + 'No namespace file was found for extension "%s"', extensionName ... + ) + assert(... + numel(L)==1, ... + 'NWB:InstallExtension:MultipleNamespacesFound', ... + 'More than one namespace file was found for extension "%s"', extensionName ... + ) + generateExtension( fullfile(L.folder, L.name), 'savedir', options.savedir ); + fprintf("Installed extension ""%s"".\n", extensionName) +end diff --git a/+matnwb/+extension/listExtensions.m b/+matnwb/+extension/listExtensions.m new file mode 100644 index 00000000..e8ae59c1 --- /dev/null +++ b/+matnwb/+extension/listExtensions.m @@ -0,0 +1,53 @@ +function extensionTable = listExtensions(options) + arguments + options.Refresh (1,1) logical = false + end + + persistent extensionRecords + + if isempty(extensionRecords) || options.Refresh + catalogUrl = "https://raw.githubusercontent.com/nwb-extensions/nwb-extensions.github.io/refs/heads/main/data/records.json"; + extensionRecords = jsondecode(webread(catalogUrl)); + extensionRecords = consolidateStruct(extensionRecords); + + extensionRecords = struct2table(extensionRecords); + + fieldsKeep = ["name", "version", "last_updated", "src", "license", "maintainers", "readme"]; + extensionRecords = extensionRecords(:, fieldsKeep); + + for name = fieldsKeep + if ischar(extensionRecords.(name){1}) + extensionRecords.(name) = string(extensionRecords.(name)); + end + end + end + extensionTable = extensionRecords; +end + +function structArray = consolidateStruct(S) + % Get all field names of S + mainFields = fieldnames(S); + + % Initialize an empty struct array + structArray = struct(); + + % Iterate over each field of S + for i = 1:numel(mainFields) + subStruct = S.(mainFields{i}); % Extract sub-struct + + % Add all fields of the sub-struct to the struct array + fields = fieldnames(subStruct); + for j = 1:numel(fields) + structArray(i).(fields{j}) = subStruct.(fields{j}); + end + end + + % Ensure consistency by filling missing fields with [] + allFields = unique([fieldnames(structArray)]); + for i = 1:numel(structArray) + missingFields = setdiff(allFields, fieldnames(structArray(i))); + for j = 1:numel(missingFields) + structArray(i).(missingFields{j}) = []; + end + end +end diff --git a/+tests/+unit/InstallExtensionTest.m b/+tests/+unit/InstallExtensionTest.m new file mode 100644 index 00000000..db7e1826 --- /dev/null +++ b/+tests/+unit/InstallExtensionTest.m @@ -0,0 +1,87 @@ +classdef InstallExtensionTest < matlab.unittest.TestCase + + methods (TestClassSetup) + function setupClass(testCase) + % Get the root path of the matnwb repository + rootPath = misc.getMatnwbDir(); + + % Use a fixture to add the folder to the search path + testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); + + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + generateCore('savedir', '.'); + end + end + + methods (Test) + function testInstallExtensionFailsWithNoInputArgument(testCase) + testCase.verifyError(... + @(varargin) nwbInstallExtension(), ... + 'NWB:InstallExtension:MissingArgument') + end + + function testInstallExtension(testCase) + nwbInstallExtension("ndx-miniscope", 'savedir', '.') + + testCase.verifyTrue(isfolder('./+types/+ndx_miniscope'), ... + 'Folder with extension types does not exist') + end + + function testUseInstalledExtension(testCase) + nwbObject = testCase.initNwbFile(); + + miniscopeDevice = types.ndx_miniscope.Miniscope(... + 'deviceType', 'test_device', ... + 'compression', 'GREY', ... + 'frameRate', '30fps', ... + 'framesPerFile', int8(100) ); + + nwbObject.general_devices.set('TestMiniscope', miniscopeDevice); + + testCase.verifyClass(nwbObject.general_devices.get('TestMiniscope'), ... + 'types.ndx_miniscope.Miniscope') + end + + function testGetExtensionInfo(testCase) + extensionName = "ndx-miniscope"; + metadata = matnwb.extension.getExtensionInfo(extensionName); + testCase.verifyClass(metadata, 'struct') + testCase.verifyEqual(metadata.name, extensionName) + end + + function testDownloadUnknownRepository(testCase) + repositoryUrl = "https://www.unknown-repo.com/anon/my_nwb_extension"; + testCase.verifyError(... + @() matnwb.extension.internal.downloadExtensionRepository(repositoryUrl, "", "my_nwb_extension"), ... + 'NWB:InstallExtension:UnsupportedRepository'); + end + + function testBuildRepoDownloadUrl(testCase) + + import matnwb.extension.internal.buildRepoDownloadUrl + + repoUrl = buildRepoDownloadUrl('https://github.com/user/test', 'main'); + testCase.verifyEqual(repoUrl, 'https://github.com/user/test/archive/refs/heads/main.zip') + + repoUrl = buildRepoDownloadUrl('https://github.com/user/test/', 'main'); + testCase.verifyEqual(repoUrl, 'https://github.com/user/test/archive/refs/heads/main.zip') + + repoUrl = buildRepoDownloadUrl('https://gitlab.com/user/test', 'main'); + testCase.verifyEqual(repoUrl, 'https://gitlab.com/user/test/-/archive/main/test-main.zip') + + testCase.verifyError(... + @() buildRepoDownloadUrl('https://unsupported.com/user/test', 'main'), ... + 'NWB:BuildRepoDownloadUrl:UnsupportedRepository') + end + end + + methods (Static) + function nwb = initNwbFile() + nwb = NwbFile( ... + 'session_description', 'test file for nwb extension', ... + 'identifier', 'export_test', ... + 'session_start_time', datetime("now", 'TimeZone', 'local') ); + end + end +end diff --git a/.github/workflows/update_extension_list.yml b/.github/workflows/update_extension_list.yml new file mode 100644 index 00000000..f9dfb763 --- /dev/null +++ b/.github/workflows/update_extension_list.yml @@ -0,0 +1,49 @@ +name: Update extension list + +on: + schedule: + # Run at 8:15 on working days [Minute Hour Day Month Weekdays] + # Run this 15 minutes after source repo is updated + # https://github.com/nwb-extensions/nwb-extensions.github.io/blob/main/.github/workflows/data.yml + - cron: 15 8 * * 0-5 + workflow_dispatch: + +permissions: + contents: write + +jobs: + update_extension_list: + runs-on: ubuntu-latest + steps: + # Use deploy key to push back to protected branch + - name: Checkout repository using deploy key + uses: actions/checkout@v4 + with: + ref: refs/heads/main + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Install MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Update extension list in nwbInstallExtensions + uses: matlab-actions/run-command@v2 + with: + command: | + addpath(genpath("tools")); + matnwb_createNwbInstallExtension(); + + - name: Commit the updated nwbInstallExtension function + run: | + set -e # Exit script on error + git config user.name "${{ github.workflow }} by ${{ github.actor }}" + git config user.email "<>" + git pull --rebase # Ensure the branch is up-to-date + + if [[ -n $(git status --porcelain nwbInstallExtension.m) ]]; then + git add nwbInstallExtension.m + git commit -m "Update list of extensions in nwbInstallExtension" + git push + else + echo "Nothing to commit" + fi + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 14397dfc..0511f62e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,14 @@ workspace/ .DS_Store +tests/env.mat +# Ignore everything in the +types/ folder ++types/* + +# Explicitly include these subdirectories +!+types/+core/ +!+types/+hdmf_common/ +!+types/+hdmf_experimental/ +!+types/+untyped/ +!+types/+util/ + docs/build diff --git a/docs/source/index.rst b/docs/source/index.rst index 6e25d53e..e2e95244 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,7 @@ Contents pages/getting_started/installation_users pages/getting_started/important pages/getting_started/file_read + pages/getting_started/using_extenstions.rst pages/tutorials/index pages/getting_started/overview_citing @@ -38,4 +39,3 @@ Contents pages/developer/contributing pages/developer/documentation - diff --git a/docs/source/pages/functions/index.rst b/docs/source/pages/functions/index.rst index 35497918..1dd788fe 100644 --- a/docs/source/pages/functions/index.rst +++ b/docs/source/pages/functions/index.rst @@ -13,3 +13,4 @@ These are the main functions of the MatNWB API generateCore generateExtension nwbClearGenerated + nwbInstallExtension diff --git a/docs/source/pages/functions/nwbInstallExtension.rst b/docs/source/pages/functions/nwbInstallExtension.rst new file mode 100644 index 00000000..58b37cfe --- /dev/null +++ b/docs/source/pages/functions/nwbInstallExtension.rst @@ -0,0 +1,5 @@ +nwbInstallExtension +=================== + +.. mat:module:: . +.. autofunction:: nwbInstallExtension diff --git a/docs/source/pages/getting_started/using_extensions/generating_extension_api.rst b/docs/source/pages/getting_started/using_extensions/generating_extension_api.rst new file mode 100644 index 00000000..d2f5ca36 --- /dev/null +++ b/docs/source/pages/getting_started/using_extensions/generating_extension_api.rst @@ -0,0 +1,40 @@ +Generating Extension API +------------------------ + +If you have created a neurodata extension or have the files for a third-party +extension locally, you can use the MatNWB function :func:`generateExtension` to +create MATLAB classes for the extension (replace the path argument with the real +path name to the namespace.yaml file): + +.. code-block:: MATLAB + + generateExtension("path/to/extension/namespace.yaml") + +The class files will be generated under the ``+types/+`` namespace in +the matnwb root directory, and can be accessed via standard MATLAB class syntax. +For example, if we had an extension called ``ndx-example`` which defined a +``TetrodeSeries`` neurodata type, we would call: + +.. code-block:: MATLAB + + ts = types.ndx_example.TetrodeSeries(); + +.. important:: + Spaces are not allowed in Neurodata Extensions names, and ``-`` is used instead. + In MATLAB, any occurrence of ``-`` is converted to ``_``, and in general, MatNWB + will convert namespace names if they are not valid MATLAB identifiers. See + `Variable Names `_ + for more information. In most cases, the conversion conforms with MATLAB's approach + with `matlab.lang.makeValidName() `_ + +To generate MatNWB classes in a custom location, you can use the optional ``savedir`` argument: + +.. code-block:: MATLAB + + generateExtension("path/to/ndx-example/namespace.yaml", ... + "savedir", "my/temporary/folder") + +.. note:: + Generating extensions in a custom location is generally not needed, + but is useful in advanced use cases like running tests or in other situations + where you need to better control the MATLAB search path. diff --git a/docs/source/pages/getting_started/using_extensions/installing_extensions.rst b/docs/source/pages/getting_started/using_extensions/installing_extensions.rst new file mode 100644 index 00000000..b9ab274c --- /dev/null +++ b/docs/source/pages/getting_started/using_extensions/installing_extensions.rst @@ -0,0 +1,12 @@ +Installing Published Extensions +------------------------------- + +In MatNWB, use the function :func:`nwbInstallExtension` to download and generate classes +for published Neurodata Extensions: + +.. code-block:: MATLAB + + nwbInstallExtension("ndx-extension") + +Replace ``ndx-extension`` with the name of an actual extension. For a complete +list of published extensions, visit the `Neurodata Extension Catalog `_. diff --git a/docs/source/pages/getting_started/using_extenstions.rst b/docs/source/pages/getting_started/using_extenstions.rst new file mode 100644 index 00000000..c15ce343 --- /dev/null +++ b/docs/source/pages/getting_started/using_extenstions.rst @@ -0,0 +1,17 @@ +Using Neurodata Extensions +========================== + +The `NWB Specification Language `_ +can be used to create Neurodata Extensions (NDX), which extend the core NWB schemas +with modified or entirely new data types. This is useful if you work with data +that has specific metadata or data requirements not covered by the core NWB schemas. +To learn more about extending NWB, see the :nwb_overview:`NWB Overview Documentation`, +and for a list of published extensions, visit the `Neurodata Extension Catalog `_. + +The following sections describe how to use extensions in MatNWB: + +.. toctree:: + :maxdepth: 2 + + using_extensions/generating_extension_api + using_extensions/installing_extensions diff --git a/nwbInstallExtension.m b/nwbInstallExtension.m new file mode 100644 index 00000000..24f961f5 --- /dev/null +++ b/nwbInstallExtension.m @@ -0,0 +1,83 @@ +function nwbInstallExtension(extensionNames, options) +% NWBINSTALLEXTENSION - Installs a specified NWB extension. +% +% Syntax: +% NWBINSTALLEXTENSION(extensionNames) installs Neurodata Without Borders +% (NWB) extensions to extend the functionality of the core NWB schemas. +% extensionNames is a scalar string or a string array, containing the name +% of one or more extensions from the Neurodata Extensions Catalog +% +% Valid Extension Names (from https://nwb-extensions.github.io): +% - "ndx-miniscope" +% - "ndx-simulation-output" +% - "ndx-ecog" +% - "ndx-fret" +% - "ndx-icephys-meta" +% - "ndx-events" +% - "ndx-nirs" +% - "ndx-hierarchical-behavioral-data" +% - "ndx-sound" +% - "ndx-extract" +% - "ndx-photometry" +% - "ndx-acquisition-module" +% - "ndx-odor-metadata" +% - "ndx-whisk" +% - "ndx-ecg" +% - "ndx-franklab-novela" +% - "ndx-photostim" +% - "ndx-multichannel-volume" +% - "ndx-depth-moseq" +% - "ndx-probeinterface" +% - "ndx-dbs" +% - "ndx-hed" +% - "ndx-ophys-devices" +% +% Usage: +% Example 1 - Install "ndx-miniscope" extension:: +% +% nwbInstallExtension("ndx-miniscope") +% +% See also: +% matnwb.extension.listExtensions, matnwb.extension.installExtension + + arguments + extensionNames (1,:) string {mustBeMember(extensionNames, [... + "ndx-miniscope", ... + "ndx-simulation-output", ... + "ndx-ecog", ... + "ndx-fret", ... + "ndx-icephys-meta", ... + "ndx-events", ... + "ndx-nirs", ... + "ndx-hierarchical-behavioral-data", ... + "ndx-sound", ... + "ndx-extract", ... + "ndx-photometry", ... + "ndx-acquisition-module", ... + "ndx-odor-metadata", ... + "ndx-whisk", ... + "ndx-ecg", ... + "ndx-franklab-novela", ... + "ndx-photostim", ... + "ndx-multichannel-volume", ... + "ndx-depth-moseq", ... + "ndx-probeinterface", ... + "ndx-dbs", ... + "ndx-hed", ... + "ndx-ophys-devices" ... + ] ... + )} = [] + options.savedir (1,1) string = misc.getMatnwbDir() + end + if isempty(extensionNames) + T = matnwb.extension.listExtensions(); + extensionList = join( compose(" %s", [T.name]), newline ); + error('NWB:InstallExtension:MissingArgument', ... + 'Please specify the name of an extension. Available extensions:\n\n%s\n', extensionList) + else + for extensionName = extensionNames + matnwb.extension.installExtension(extensionName, 'savedir', options.savedir) + end + end +end + diff --git a/nwbtest.m b/nwbtest.m index a1f2ed15..c15c4ec0 100644 --- a/nwbtest.m +++ b/nwbtest.m @@ -64,7 +64,10 @@ [installDir, ~, ~] = fileparts(mfilename('fullpath')); ignoreFolders = {'tutorials', 'tools', '+contrib', '+util', 'external_packages', '+tests'}; - ignorePaths = {fullfile('+misc', 'generateDocs.m'), [mfilename '.m'], 'nwbClearGenerated.m'}; + ignorePaths = {... + fullfile('+matnwb', '+extension', 'installAll.m'), ... + [mfilename '.m'], ... + 'nwbClearGenerated.m'}; mfilePaths = getMfilePaths(installDir, ignoreFolders, ignorePaths); if ~verLessThan('matlab', '9.3') && ~isempty(mfilePaths) runner.addPlugin(CodeCoveragePlugin.forFile(mfilePaths,... diff --git a/resources/function_templates/nwbInstallExtension.txt b/resources/function_templates/nwbInstallExtension.txt new file mode 100644 index 00000000..1500b711 --- /dev/null +++ b/resources/function_templates/nwbInstallExtension.txt @@ -0,0 +1,36 @@ +function nwbInstallExtension(extensionNames, options) +% nwbInstallExtension - Installs a specified NWB extension. +% +% Usage: +% nwbInstallExtension(extensionNames) installs Neurodata Without Borders +% (NWB) extensions to extend the functionality of the core NWB schemas. +% extensionNames is a scalar string or a string array, containing the name +% of one or more extensions from the Neurodata Extensions Catalog +% +% Valid Extension Names (from https://nwb-extensions.github.io): +{{extensionNamesDoc}} +% +% Example: +% % Install the "ndx-miniscope" extension +% nwbInstallExtension("ndx-miniscope") +% +% See also: +% matnwb.extension.listExtensions, matnwb.extension.installExtension + + arguments + extensionNames (1,:) string {mustBeMember(extensionNames, [... +{{extensionNames}} ... + ] ... + )} = [] + options.savedir (1,1) string = misc.getMatnwbDir() + end + if isempty(extensionNames) + T = matnwb.extension.listExtensions(); + extensionList = join( compose(" %s", [T.name]), newline ); + error("Please specify the name of an extension. Available extensions:\n\n%s\n", extensionList) + else + for extensionName = extensionNames + matnwb.extension.installExtension(extensionName, 'savedir', options.savedir) + end + end +end diff --git a/tools/documentation/private/generateRstForNwbFunctions.m b/tools/documentation/private/generateRstForNwbFunctions.m index d6f1dd68..f0a1bfce 100644 --- a/tools/documentation/private/generateRstForNwbFunctions.m +++ b/tools/documentation/private/generateRstForNwbFunctions.m @@ -5,7 +5,7 @@ function generateRstForNwbFunctions() rootDir = misc.getMatnwbDir(); rootFiles = dir(rootDir); rootFileNames = {rootFiles.name}; - rootWhitelist = {'nwbRead.m', 'NwbFile.m', 'nwbExport.m', 'generateCore.m', 'generateExtension.m', 'nwbClearGenerated.m'};%, 'nwbInstallExtension.m'}; + rootWhitelist = {'nwbRead.m', 'NwbFile.m', 'nwbExport.m', 'generateCore.m', 'generateExtension.m', 'nwbClearGenerated.m', 'nwbInstallExtension.m'}; isWhitelisted = ismember(rootFileNames, rootWhitelist); rootFiles(~isWhitelisted) = []; diff --git a/tools/maintenance/matnwb_createNwbInstallExtension.m b/tools/maintenance/matnwb_createNwbInstallExtension.m new file mode 100644 index 00000000..b056d8bd --- /dev/null +++ b/tools/maintenance/matnwb_createNwbInstallExtension.m @@ -0,0 +1,30 @@ +function matnwb_createNwbInstallExtension() +% matnwb_createNwbInstallExtension - Create nwbInstallExtension from template +% +% Running this function will update the nwbInstallExtension function in +% the root directory of the matnwb package. It will update the list of +% available extension names in the nwbInstallExtension function's arguments +% block and docstring based on the available records in the neurodata +% extensions catalog + + matnwbRootDir = misc.getMatnwbDir(); + fcnTemplate = fileread(fullfile(matnwbRootDir, ... + 'resources', 'function_templates', 'nwbInstallExtension.txt')); + + extensionTable = matnwb.extension.listExtensions(); + extensionNames = extensionTable.name; + + indentStr = repmat(' ', 1, 12); + extensionNamesStr = compose("%s""%s""", indentStr, extensionNames); + extensionNamesStr = strjoin(extensionNamesStr, ", ..." + newline); + fcnStr = replace(fcnTemplate, "{{extensionNames}}", extensionNamesStr); + + extensionNamesStr = compose("%% - ""%s""", extensionNames); + extensionNamesStr = strjoin(extensionNamesStr, newline); + fcnStr = replace(fcnStr, "{{extensionNamesDoc}}", extensionNamesStr); + + + fid = fopen(fullfile(matnwbRootDir, 'nwbInstallExtension.m'), "wt"); + fwrite(fid, fcnStr); + fclose(fid); +end