diff --git a/+bids/+util/update_struct.m b/+bids/+util/update_struct.m new file mode 100644 index 00000000..9af2e358 --- /dev/null +++ b/+bids/+util/update_struct.m @@ -0,0 +1,91 @@ +function js = update_struct(js, varargin) + % + % Updates structure with new values. + % Can add new fields, replace field values, remove fields, + % and append new values to a cellarray. + % + % Designed for manipulating json structures and will not work + % on structarrays. + % + % USAGE:: + % + % js = update_struct(key1, value1, key2, value2); + % js = update_struct(struct(key1, value1, ... + % key2, value2)); + % + % Examples: + % --------- + % Adding and replacing existing fields: + % update_struct(struct('a', 'val_a'),... + % 'a', 'new_val', 'b', 'val_b') + % struct with fields: + % a: 'new_val' + % b: 'val_b' + % Removing field from structure: + % update_struct(struct('a', 'val_a', 'b', 'val_b'), + % 'b', []) + % struct with fields: + % a: 'val_a' + % Appending values to existing field: + % update_struct(struct('a', 'val_a', 'b', 'val_b'), + % 'b-add', 'val_b2') + % struct with fields: + % a: 'val_a' + % b: {'val_b'; 'val_b2'} + % + + % (C) Copyright 2023 BIDS-MATLAB developers + + if numel(varargin) == 0 + % Nothing to do + return + end + + if numel(varargin) > 1 + key_list = varargin(1:2:end); + val_list = varargin(2:2:end); + elseif isstruct(varargin{1}) + key_list = fieldnames(varargin{1}); + val_list = cell(size(key_list)); + for i = 1:numel(key_list) + val_list{i} = varargin{1}.(key_list{i}); + end + else + id = bids.internal.camel_case('invalidInput'); + msg = 'Not list of parameters or structure'; + bids.internal.error_handling(mfilename(), id, msg, false, true); + end + + for ii = 1:numel(key_list) + par_key = key_list{ii}; + try + par_value = val_list{ii}; + + % Removing field from json structure + % Should use only empty double ([]) or any empth object? + if isempty(par_value) && isnumeric(par_value) + if isfield(js, par_key) + js = rmfield(js, par_key); + end + continue + end + + if bids.internal.ends_with(par_key, '-add') + par_key = par_key(1:end - 4); + if isfield(js, par_key) + if ischar(js.(par_key)) + par_value = {js.(par_key); par_value}; %#ok + else + par_value = [js.(par_key); par_value]; %#ok + end + end + end + js(1).(par_key) = par_value; + + catch ME + id = bids.internal.camel_case('structError'); + msg = sprintf('''%s'' (%d) -- %s', par_key, ii, ME.message); + bids.internal.error_handling(mfilename(), id, msg, false, true); + end + end +end diff --git a/+bids/File.m b/+bids/File.m index 77cf3d01..0dd8e5fd 100644 --- a/+bids/File.m +++ b/+bids/File.m @@ -65,6 +65,65 @@ % % file = bids.File(name_spec, 'use_schema', true); % + % Load metadata (supporting inheritance). + % + % .. code-block:: matlab + % + % f = bids.File('tests/data/synthetic/sub-01/anat/sub-01_T1w.nii.gz'); + % + % Access metadata + % + % .. code-block:: matlab + % + % f.metadata() + % struct with fields: + % Manufacturer: 'Siemens' + % FlipAngle: 10 + % + % Modify metadata + % + % .. code-block:: matlab + % % Adding new value + % f = f.metadata_add('NewField', 'new value'); + % f.metadata() + % struct with fields: + % manufacturer: 'siemens' + % flipangle: 10 + % NewField: 'new value' + % + % % Appending to existing value + % f = f.metadata_append('NewField', 'new value 1'); + % f.metadata() + % struct with fields: + % manufacturer: 'siemens' + % flipangle: 10 + % NewField: {'new value'; 'new value 1'} + % + % % Removing value + % f = f.metadata_remove('NewField'); + % f.metadata() + % struct with fields: + % manufacturer: 'siemens' + % flipangle: 10 + % + % Modify several fields of metadata + % + % .. code-block:: matlab + % + % f = f.metadata_update('Description', 'source file', ... + % 'NewField', 'new value', ... + % 'manufacturer', []); + % f.metadata() + % struct with fields: + % flipangle: 10 + % description: 'source file' + % NewField: 'new value' + % + % Export metadata as json: + % + % .. code-block:: matlab + % + % f.metadata_write() % (C) Copyright 2021 BIDS-MATLAB developers @@ -90,7 +149,7 @@ metadata_files = {} % list of metadata files related - metadata % list of metadata for this file + metadata = [] % list of metadata for this file entity_required = {} % Required entities @@ -707,6 +766,73 @@ function check_required_entities(obj) end + % Functions related to metadata manipulation + + function obj = metadata_update(obj, varargin) + % Update stored metadata with new values passed in varargin, + % which can be either a structure, or pairs of key-values. + % + % See also + % bids.util.update_struct + % + % USAGE:: + % + % f = f.metadata_update(key1, value1, key2, value2); + % f = f.metadata_update(struct(key1, value1, ... + % key2, value2)); + obj.metadata = bids.util.update_struct(obj.metadata, varargin{:}); + end + + function obj = metadata_add(obj, field, value) + % Add a new field (or replace existing) to the metadata structure + obj.metadata.(field) = value; + end + + function obj = metadata_append(obj, field, value) + % Append new value to a metadata.(field) + % If metadata.(field) is a chararray, it will be first + % transformed into cellarray. + if isfield(obj.metadata, field) + if ischar(obj.metadata.(field)) + value = {obj.metadata.(field); value}; + else + value = [obj.metadata.(field); value]; + end + end + obj.metadata(1).(field) = value; + end + + function obj = metadata_remove(obj, field) + % Removes field from metadata + if isfield(obj.metadata, field) + obj.metadata = rmfield(obj.metadata, field); + end + end + + function out_file = metadata_write(obj, varargin) + % Export current content of metadata to sidecar json with + % same name as current file. Metadata fields can be modified + % with new values passed in varargin, which can be either a structure, + % or pairs of key-values. These modifications do not affect + % current File object, and only exported into file. Use + % bids.File.metadata_update to update current metadata. + % Returns full path to the exported sidecar json file. + % + % See also + % bids.util.update_struct + % + % USAGE:: + % + % f.metadata_write(key1, value1, key2, value2); + % f.metadata_write(struct(key1, value1, ... + % key2, value2)); + [path, ~, ~] = fileparts(obj.path); + out_file = fullfile(path, obj.json_filename); + + der_json = bids.util.update_struct(obj.metadata, varargin{:}); + bids.util.jsonencode(out_file, der_json, 'indent', ' '); + end + %% Things that might go private function bids_file_error(obj, id, msg) diff --git a/tests/test_bids_file.m b/tests/test_bids_file.m index 179ee4b0..adfadb3d 100644 --- a/tests/test_bids_file.m +++ b/tests/test_bids_file.m @@ -69,6 +69,83 @@ function test_get_metadata_suffixes_basic() end +function test_metadata_update() + % Testung updating metadata with metadata_update function + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + bf = bids.File(file); + + % Adding + bf = bf.metadata_update('Testing', 'adding field'); + assertTrue(isfield(bf.metadata, 'Testing')); + assertEqual(bf.metadata.Testing, 'adding field'); + + % Modifying + bf = bf.metadata_update('Testing', 'modifying field'); + assertEqual(bf.metadata.Testing, 'modifying field'); + + % Removing + bf = bf.metadata_update('Testing', []); + assertFalse(isfield(bf.metadata, 'Testing')); +end + +function test_metadata_add() + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + bf = bids.File(file); + bf = bf.metadata_add('Testing', 'adding field'); + assertTrue(isfield(bf.metadata, 'Testing')); + assertEqual(bf.metadata.Testing, 'adding field'); +end + +function test_metadata_append() + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + bf = bids.File(file); + bf = bf.metadata_append('Testing', 'adding field1'); + bf = bf.metadata_append('Testing', 'adding field2'); + assertEqual(bf.metadata.Testing, ... + {'adding field1'; 'adding field2'}); + % testing char to cell conversion + bf = bf.metadata_add('Testing2', 1); + bf = bf.metadata_append('Testing2', 2); + assertEqual(bf.metadata.Testing2, [1; 2]); +end + +function test_metadata_remove() + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + bf = bids.File(file); + bf = bf.metadata_add('Testing', 'adding field'); + assertTrue(isfield(bf.metadata, 'Testing')); + bf = bf.metadata_remove('Testing'); + assertFalse(isfield(bf.metadata, 'Testing')); +end + +function test_metadata_write() + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + bf = bids.File(file); + + % Writing metadata + bf.prefix = 'test_'; + out_file = bf.metadata_write(); + assertTrue(exist(out_file, 'file') > 0); + exported_metadata = bids.util.jsondecode(out_file); + assertEqual(bf.metadata, exported_metadata); + teardown(out_file); + + % Writing modified metadata + out_file = bf.metadata_write('Testing', 'exporting'); + exported_metadata = bids.util.jsondecode(out_file); + teardown(out_file); + assertTrue(isfield(exported_metadata, 'Testing')); + assertFalse(isfield(bf.metadata, 'Testing')); + + bf = bf.metadata_add('Testing', 'exporting'); + assertEqual(bf.metadata, exported_metadata); +end + function test_rename() input_filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; diff --git a/tests/tests_utils/test_update_struct.m b/tests/tests_utils/test_update_struct.m new file mode 100644 index 00000000..b6bf2216 --- /dev/null +++ b/tests/tests_utils/test_update_struct.m @@ -0,0 +1,49 @@ +function test_suite = test_update_struct %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; + +end + +function test_main_func() + + % testing with parameters + js = struct([]); + js = bids.util.update_struct(js, 'key_a', 'val_a', 'key_b', 'val_b'); + assertTrue(isfield(js, 'key_a')); + assertTrue(isfield(js, 'key_b')); + assertEqual(js.key_a, 'val_a'); + assertEqual(js.key_b, 'val_b'); + + % testing with struct + test_struct.key_c = 'val_c'; + + js = bids.util.update_struct(js, test_struct); + assertTrue(isfield(js, 'key_c')); + assertEqual(js.key_c, 'val_c'); + + % testing update and removal of field + js = bids.util.update_struct(js, 'key_c', [], 'key_a', 'val_a2'); + assertFalse(isfield(js, 'key_c')); + assertEqual(js.key_a, 'val_a2'); + + % testing concatenating as string cell + js = bids.util.update_struct(js, 'key_b-add', 'val_b2'); + assertEqual(js.key_b, {'val_b'; 'val_b2'}); + + % testing concatenating numericals + js = bids.util.update_struct(js, 'key_b-add', 3); + assertEqual(js.key_b, {'val_b'; 'val_b2'; 3}); +end + +function test_exceptions() + % Invalid input + assertExceptionThrown(@() bids.util.update_struct(struct([]), 'key_b-add'), ... + 'update_struct:invalidInput'); + assertExceptionThrown(@() bids.util.update_struct(struct([]), ... + 'key_b-add', [], ... + 'key_c'), ... + 'update_struct:structError'); +end