Skip to content

Commit

Permalink
Incorporating json into bids.File (issues #596 #371) (#597)
Browse files Browse the repository at this point in the history
* First attempt to incorporate json into bids.File

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* lint

* Added util function to update structures

* Style and tests for util/update_struct.m

* Using metadata field instead of json_struct

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Removed JSONFile subclass

* Added tests for metadata for File class

* Fixed spelling in doc

* File: Added functions that act on individual fields

* File: fixed style

* Splitted File.metadada tests into smaller ones

---------

Co-authored-by: Beliy Nikita <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Remi Gau <[email protected]>
  • Loading branch information
4 people authored Aug 8, 2023
1 parent 10baa5d commit 43eb70b
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 1 deletion.
91 changes: 91 additions & 0 deletions +bids/+util/update_struct.m
Original file line number Diff line number Diff line change
@@ -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<AGROW>
else
par_value = [js.(par_key); par_value]; %#ok<AGROW>
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
128 changes: 127 additions & 1 deletion +bids/File.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
77 changes: 77 additions & 0 deletions tests/test_bids_file.m
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
49 changes: 49 additions & 0 deletions tests/tests_utils/test_update_struct.m
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 43eb70b

Please sign in to comment.