Skip to content

Commit 43eb70b

Browse files
nbeliyBeliy Nikitapre-commit-ci[bot]Remi-Gau
authored
Incorporating json into bids.File (issues #596 #371) (#597)
* 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]>
1 parent 10baa5d commit 43eb70b

File tree

4 files changed

+344
-1
lines changed

4 files changed

+344
-1
lines changed

+bids/+util/update_struct.m

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
function js = update_struct(js, varargin)
2+
%
3+
% Updates structure with new values.
4+
% Can add new fields, replace field values, remove fields,
5+
% and append new values to a cellarray.
6+
%
7+
% Designed for manipulating json structures and will not work
8+
% on structarrays.
9+
%
10+
% USAGE::
11+
%
12+
% js = update_struct(key1, value1, key2, value2);
13+
% js = update_struct(struct(key1, value1, ...
14+
% key2, value2));
15+
%
16+
% Examples:
17+
% ---------
18+
% Adding and replacing existing fields:
19+
% update_struct(struct('a', 'val_a'),...
20+
% 'a', 'new_val', 'b', 'val_b')
21+
% struct with fields:
22+
% a: 'new_val'
23+
% b: 'val_b'
24+
% Removing field from structure:
25+
% update_struct(struct('a', 'val_a', 'b', 'val_b'),
26+
% 'b', [])
27+
% struct with fields:
28+
% a: 'val_a'
29+
% Appending values to existing field:
30+
% update_struct(struct('a', 'val_a', 'b', 'val_b'),
31+
% 'b-add', 'val_b2')
32+
% struct with fields:
33+
% a: 'val_a'
34+
% b: {'val_b'; 'val_b2'}
35+
%
36+
37+
% (C) Copyright 2023 BIDS-MATLAB developers
38+
39+
if numel(varargin) == 0
40+
% Nothing to do
41+
return
42+
end
43+
44+
if numel(varargin) > 1
45+
key_list = varargin(1:2:end);
46+
val_list = varargin(2:2:end);
47+
elseif isstruct(varargin{1})
48+
key_list = fieldnames(varargin{1});
49+
val_list = cell(size(key_list));
50+
for i = 1:numel(key_list)
51+
val_list{i} = varargin{1}.(key_list{i});
52+
end
53+
else
54+
id = bids.internal.camel_case('invalidInput');
55+
msg = 'Not list of parameters or structure';
56+
bids.internal.error_handling(mfilename(), id, msg, false, true);
57+
end
58+
59+
for ii = 1:numel(key_list)
60+
par_key = key_list{ii};
61+
try
62+
par_value = val_list{ii};
63+
64+
% Removing field from json structure
65+
% Should use only empty double ([]) or any empth object?
66+
if isempty(par_value) && isnumeric(par_value)
67+
if isfield(js, par_key)
68+
js = rmfield(js, par_key);
69+
end
70+
continue
71+
end
72+
73+
if bids.internal.ends_with(par_key, '-add')
74+
par_key = par_key(1:end - 4);
75+
if isfield(js, par_key)
76+
if ischar(js.(par_key))
77+
par_value = {js.(par_key); par_value}; %#ok<AGROW>
78+
else
79+
par_value = [js.(par_key); par_value]; %#ok<AGROW>
80+
end
81+
end
82+
end
83+
js(1).(par_key) = par_value;
84+
85+
catch ME
86+
id = bids.internal.camel_case('structError');
87+
msg = sprintf('''%s'' (%d) -- %s', par_key, ii, ME.message);
88+
bids.internal.error_handling(mfilename(), id, msg, false, true);
89+
end
90+
end
91+
end

+bids/File.m

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,65 @@
6565
%
6666
% file = bids.File(name_spec, 'use_schema', true);
6767
%
68+
% Load metadata (supporting inheritance).
69+
%
70+
% .. code-block:: matlab
71+
%
72+
% f = bids.File('tests/data/synthetic/sub-01/anat/sub-01_T1w.nii.gz');
73+
%
74+
% Access metadata
75+
%
76+
% .. code-block:: matlab
77+
%
78+
% f.metadata()
79+
% struct with fields:
80+
% Manufacturer: 'Siemens'
81+
% FlipAngle: 10
82+
%
83+
% Modify metadata
84+
%
85+
% .. code-block:: matlab
86+
% % Adding new value
87+
% f = f.metadata_add('NewField', 'new value');
88+
% f.metadata()
89+
% struct with fields:
90+
% manufacturer: 'siemens'
91+
% flipangle: 10
92+
% NewField: 'new value'
93+
%
94+
% % Appending to existing value
95+
% f = f.metadata_append('NewField', 'new value 1');
96+
% f.metadata()
97+
% struct with fields:
98+
% manufacturer: 'siemens'
99+
% flipangle: 10
100+
% NewField: {'new value'; 'new value 1'}
101+
%
102+
% % Removing value
103+
% f = f.metadata_remove('NewField');
104+
% f.metadata()
105+
% struct with fields:
106+
% manufacturer: 'siemens'
107+
% flipangle: 10
108+
%
109+
% Modify several fields of metadata
110+
%
111+
% .. code-block:: matlab
112+
%
113+
% f = f.metadata_update('Description', 'source file', ...
114+
% 'NewField', 'new value', ...
115+
% 'manufacturer', []);
116+
% f.metadata()
117+
% struct with fields:
118+
% flipangle: 10
119+
% description: 'source file'
120+
% NewField: 'new value'
121+
%
122+
% Export metadata as json:
123+
%
124+
% .. code-block:: matlab
125+
%
126+
% f.metadata_write()
68127

69128
% (C) Copyright 2021 BIDS-MATLAB developers
70129

@@ -90,7 +149,7 @@
90149

91150
metadata_files = {} % list of metadata files related
92151

93-
metadata % list of metadata for this file
152+
metadata = [] % list of metadata for this file
94153

95154
entity_required = {} % Required entities
96155

@@ -707,6 +766,73 @@ function check_required_entities(obj)
707766

708767
end
709768

769+
% Functions related to metadata manipulation
770+
771+
function obj = metadata_update(obj, varargin)
772+
% Update stored metadata with new values passed in varargin,
773+
% which can be either a structure, or pairs of key-values.
774+
%
775+
% See also
776+
% bids.util.update_struct
777+
%
778+
% USAGE::
779+
%
780+
% f = f.metadata_update(key1, value1, key2, value2);
781+
% f = f.metadata_update(struct(key1, value1, ...
782+
% key2, value2));
783+
obj.metadata = bids.util.update_struct(obj.metadata, varargin{:});
784+
end
785+
786+
function obj = metadata_add(obj, field, value)
787+
% Add a new field (or replace existing) to the metadata structure
788+
obj.metadata.(field) = value;
789+
end
790+
791+
function obj = metadata_append(obj, field, value)
792+
% Append new value to a metadata.(field)
793+
% If metadata.(field) is a chararray, it will be first
794+
% transformed into cellarray.
795+
if isfield(obj.metadata, field)
796+
if ischar(obj.metadata.(field))
797+
value = {obj.metadata.(field); value};
798+
else
799+
value = [obj.metadata.(field); value];
800+
end
801+
end
802+
obj.metadata(1).(field) = value;
803+
end
804+
805+
function obj = metadata_remove(obj, field)
806+
% Removes field from metadata
807+
if isfield(obj.metadata, field)
808+
obj.metadata = rmfield(obj.metadata, field);
809+
end
810+
end
811+
812+
function out_file = metadata_write(obj, varargin)
813+
% Export current content of metadata to sidecar json with
814+
% same name as current file. Metadata fields can be modified
815+
% with new values passed in varargin, which can be either a structure,
816+
% or pairs of key-values. These modifications do not affect
817+
% current File object, and only exported into file. Use
818+
% bids.File.metadata_update to update current metadata.
819+
% Returns full path to the exported sidecar json file.
820+
%
821+
% See also
822+
% bids.util.update_struct
823+
%
824+
% USAGE::
825+
%
826+
% f.metadata_write(key1, value1, key2, value2);
827+
% f.metadata_write(struct(key1, value1, ...
828+
% key2, value2));
829+
[path, ~, ~] = fileparts(obj.path);
830+
out_file = fullfile(path, obj.json_filename);
831+
832+
der_json = bids.util.update_struct(obj.metadata, varargin{:});
833+
bids.util.jsonencode(out_file, der_json, 'indent', ' ');
834+
end
835+
710836
%% Things that might go private
711837

712838
function bids_file_error(obj, id, msg)

tests/test_bids_file.m

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,83 @@ function test_get_metadata_suffixes_basic()
6969

7070
end
7171

72+
function test_metadata_update()
73+
% Testung updating metadata with metadata_update function
74+
data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data');
75+
file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii');
76+
bf = bids.File(file);
77+
78+
% Adding
79+
bf = bf.metadata_update('Testing', 'adding field');
80+
assertTrue(isfield(bf.metadata, 'Testing'));
81+
assertEqual(bf.metadata.Testing, 'adding field');
82+
83+
% Modifying
84+
bf = bf.metadata_update('Testing', 'modifying field');
85+
assertEqual(bf.metadata.Testing, 'modifying field');
86+
87+
% Removing
88+
bf = bf.metadata_update('Testing', []);
89+
assertFalse(isfield(bf.metadata, 'Testing'));
90+
end
91+
92+
function test_metadata_add()
93+
data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data');
94+
file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii');
95+
bf = bids.File(file);
96+
bf = bf.metadata_add('Testing', 'adding field');
97+
assertTrue(isfield(bf.metadata, 'Testing'));
98+
assertEqual(bf.metadata.Testing, 'adding field');
99+
end
100+
101+
function test_metadata_append()
102+
data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data');
103+
file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii');
104+
bf = bids.File(file);
105+
bf = bf.metadata_append('Testing', 'adding field1');
106+
bf = bf.metadata_append('Testing', 'adding field2');
107+
assertEqual(bf.metadata.Testing, ...
108+
{'adding field1'; 'adding field2'});
109+
% testing char to cell conversion
110+
bf = bf.metadata_add('Testing2', 1);
111+
bf = bf.metadata_append('Testing2', 2);
112+
assertEqual(bf.metadata.Testing2, [1; 2]);
113+
end
114+
115+
function test_metadata_remove()
116+
data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data');
117+
file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii');
118+
bf = bids.File(file);
119+
bf = bf.metadata_add('Testing', 'adding field');
120+
assertTrue(isfield(bf.metadata, 'Testing'));
121+
bf = bf.metadata_remove('Testing');
122+
assertFalse(isfield(bf.metadata, 'Testing'));
123+
end
124+
125+
function test_metadata_write()
126+
data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data');
127+
file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii');
128+
bf = bids.File(file);
129+
130+
% Writing metadata
131+
bf.prefix = 'test_';
132+
out_file = bf.metadata_write();
133+
assertTrue(exist(out_file, 'file') > 0);
134+
exported_metadata = bids.util.jsondecode(out_file);
135+
assertEqual(bf.metadata, exported_metadata);
136+
teardown(out_file);
137+
138+
% Writing modified metadata
139+
out_file = bf.metadata_write('Testing', 'exporting');
140+
exported_metadata = bids.util.jsondecode(out_file);
141+
teardown(out_file);
142+
assertTrue(isfield(exported_metadata, 'Testing'));
143+
assertFalse(isfield(bf.metadata, 'Testing'));
144+
145+
bf = bf.metadata_add('Testing', 'exporting');
146+
assertEqual(bf.metadata, exported_metadata);
147+
end
148+
72149
function test_rename()
73150

74151
input_filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
function test_suite = test_update_struct %#ok<*STOUT>
2+
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
3+
test_functions = localfunctions(); %#ok<*NASGU>
4+
catch % no problem; early Matlab versions can use initTestSuite fine
5+
end
6+
initTestSuite;
7+
8+
end
9+
10+
function test_main_func()
11+
12+
% testing with parameters
13+
js = struct([]);
14+
js = bids.util.update_struct(js, 'key_a', 'val_a', 'key_b', 'val_b');
15+
assertTrue(isfield(js, 'key_a'));
16+
assertTrue(isfield(js, 'key_b'));
17+
assertEqual(js.key_a, 'val_a');
18+
assertEqual(js.key_b, 'val_b');
19+
20+
% testing with struct
21+
test_struct.key_c = 'val_c';
22+
23+
js = bids.util.update_struct(js, test_struct);
24+
assertTrue(isfield(js, 'key_c'));
25+
assertEqual(js.key_c, 'val_c');
26+
27+
% testing update and removal of field
28+
js = bids.util.update_struct(js, 'key_c', [], 'key_a', 'val_a2');
29+
assertFalse(isfield(js, 'key_c'));
30+
assertEqual(js.key_a, 'val_a2');
31+
32+
% testing concatenating as string cell
33+
js = bids.util.update_struct(js, 'key_b-add', 'val_b2');
34+
assertEqual(js.key_b, {'val_b'; 'val_b2'});
35+
36+
% testing concatenating numericals
37+
js = bids.util.update_struct(js, 'key_b-add', 3);
38+
assertEqual(js.key_b, {'val_b'; 'val_b2'; 3});
39+
end
40+
41+
function test_exceptions()
42+
% Invalid input
43+
assertExceptionThrown(@() bids.util.update_struct(struct([]), 'key_b-add'), ...
44+
'update_struct:invalidInput');
45+
assertExceptionThrown(@() bids.util.update_struct(struct([]), ...
46+
'key_b-add', [], ...
47+
'key_c'), ...
48+
'update_struct:structError');
49+
end

0 commit comments

Comments
 (0)