diff --git a/code/+nansen/+config/+varmodel/VariableModel.m b/code/+nansen/+config/+varmodel/VariableModel.m index eabc1490..23c858ab 100644 --- a/code/+nansen/+config/+varmodel/VariableModel.m +++ b/code/+nansen/+config/+varmodel/VariableModel.m @@ -162,8 +162,6 @@ function updateDefaultValues(obj) dlModel = nansen.DataLocationModel(); end - fileAdapterList = nansen.dataio.listFileAdapters(); - for i = 1:numel(obj.Data) if isempty( obj.Data(i).FileAdapter ) || strcmp(obj.Data(i).FileAdapter, str) obj.Data(i).FileAdapter = 'Default'; @@ -179,15 +177,7 @@ function updateDefaultValues(obj) if ~isfield(obj.Data, 'DataType') % Infer from file adapter [obj.Data(:).DataType] = deal(''); for i = 1:numel(obj.Data) - if ~strcmp(obj.Data(i).FileAdapter, 'Default') - isMatch = strcmp({fileAdapterList.FileAdapterName}, obj.Data(i).FileAdapter); - if any(isMatch) - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); - obj.Data(i).DataType = fileAdapterFcn().DataType; - else - % pass - end - end + obj.Data(i) = obj.updateVariableDataType(obj.Data(i)); end else % Normalize existing DataType to char. Todo: support string @@ -659,15 +649,16 @@ function disableNotifications(obj) methods (Static) % Todo: Should be moved to a data variable class function variableItem = updateVariableDataType(variableItem) + % The list holds the data type of every file adapter, also of a + % function-based one, which is not a class that can be created fileAdapterList = nansen.dataio.listFileAdapters(); if ~strcmp(variableItem.FileAdapter, 'Default') isMatch = strcmp({fileAdapterList.FileAdapterName}, variableItem.FileAdapter); if any(isMatch) - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); - - variableItem.DataType = fileAdapterFcn().DataType; + variableItem.DataType = char(fileAdapterList(find(isMatch, 1)).DataType); else - % pass + % An adapter that is not in the current project keeps + % the data type the item has end end end diff --git a/code/+nansen/+dataio/DynamicFileAdapter.m b/code/+nansen/+dataio/DynamicFileAdapter.m index c02f3e09..bdb486a8 100644 --- a/code/+nansen/+dataio/DynamicFileAdapter.m +++ b/code/+nansen/+dataio/DynamicFileAdapter.m @@ -47,11 +47,15 @@ end methods (Access = protected) - function data = readData(obj) + function data = readData(obj, varargin) + % Session.loadData passes the variable name and name-value + % options to load; the read function takes the file only data = obj.ReadFunction(obj.Filename); end - function writeData(obj, data) + function writeData(obj, data, varargin) + % Session.saveData passes the variable name; the write + % function takes the file and the data only if ~isempty(obj.WriteFunction) obj.WriteFunction(obj.Filename, data) else diff --git a/code/+nansen/+metadata/+type/Session.m b/code/+nansen/+metadata/+type/Session.m index 4f104ba7..0e4b1832 100644 --- a/code/+nansen/+metadata/+type/Session.m +++ b/code/+nansen/+metadata/+type/Session.m @@ -717,7 +717,17 @@ function updateSessionFolder(obj, dataLocationName, folderPath) %#ok error('NANSEN:Session:MultipleFileAdapters', 'This is a bug. Please report') end - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); + fileAdapterInfo = fileAdapterList(isMatch); + if isfield(fileAdapterInfo, 'IsDynamic') && isequal(fileAdapterInfo.IsDynamic, true) + % A function-based file adapter is a folder with a + % fileadapter.json and read and write functions, not a + % class, and DynamicFileAdapter wraps it, as nansen.load does + functionName = fileAdapterInfo.FunctionName; + fileAdapterFcn = @(filePath, varargin) ... + nansen.dataio.DynamicFileAdapter(functionName, filePath); + else + fileAdapterFcn = str2func(fileAdapterInfo.FunctionName); + end end function viewDataVariable(obj, varName) diff --git a/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m b/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m new file mode 100644 index 00000000..5293f8c0 --- /dev/null +++ b/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m @@ -0,0 +1,65 @@ +classdef FunctionBasedFileAdapterTest < matlab.unittest.TestCase +%FunctionBasedFileAdapterTest - Data variables whose file adapter is function-based +% +% A function-based file adapter is a folder with a fileadapter.json and +% read and write functions, such as MatFile of the core module. The mock +% project gets the variable Values, with the adapter MatFile, and a .mat +% file for it in the first session folder. +% +% Run tests: +% runtests('nansen.unittest.session.FunctionBasedFileAdapterTest') + + properties + Project + Session + end + + methods (TestClassSetup) + function setupProject(testCase) + testCase.applyFixture(nansen.fixture.ProjectFixture) + testCase.Project = nansen.getCurrentProject(); + + % The new item copies the fields of the mock project's item, + % which has fields that a default item lacks + variableModel = testCase.Project.VariableModel; + item = variableModel.Data(1); + item.VariableName = 'Values'; + item.Uuid = nansen.util.getuuid(); + item.FilePathPattern = ''; + item.FileNameExpression = 'values'; + item.FileType = '.mat'; + item.FileAdapter = 'MatFile'; + item.DataType = ''; + variableModel.insertItem(item); + variableModel.save() + + testCase.Session = testCase.Project.getSessionObjects( ... + testCase.Project.MetaTableCatalog.getMasterMetaTable('session').entries.sessionID{1}); + Values = magic(3); + save(fullfile(testCase.Session.getSessionFolder('MockData'), 'values.mat'), 'Values') + end + end + + methods (Test) + function testInsertedVariableGetsDataTypeOfAdapter(testCase) + item = testCase.Project.VariableModel.getItem('Values'); + + testCase.verifyEqual(item.DataType, 'struct') + end + + function testLoadDataReadsWithFunctionBasedAdapter(testCase) + data = testCase.Session.loadData('Values'); + + testCase.verifyEqual(data.Values, magic(3)) + end + + function testSaveDataWritesWithFunctionBasedAdapter(testCase) + testCase.addTeardown(@() testCase.Session.saveData('Values', struct('Values', magic(3)))) + + testCase.Session.saveData('Values', struct('Values', magic(4))) + + data = testCase.Session.loadData('Values'); + testCase.verifyEqual(data.Values, magic(4)) + end + end +end