diff --git a/jdict.m b/jdict.m index 7d069a5..9102f67 100644 --- a/jdict.m +++ b/jdict.m @@ -1,80 +1,84 @@ -classdef jdict < handle - % - % jd = jdict(data) - % - % A universal dictionary-like interface that enables fast multi-level subkey access and - % JSONPath-based element indexing, such as jd.('key1').('key2') and jd.('$.key1.key2'), - % for hierachical data structures embedding struct, containers.Map or dictionary objects - % - % author: Qianqian Fang (q.fang neu.edu) - % - % input: - % data: a hierachical data structure made of struct, containers.Map, dictionary, or cell arrays - % if data is a string starting with http:// or https://, - % loadjson(data) will be used to dynamically load the data - % - % indexing: - % jd.('key1').('subkey1')... can retrieve values that are recursively index keys that are - % jd.key1.subkey1... can also retrieve the same data regardless - % if the underlying data is struct, containers.Map or dictionary - % jd.('key1').('subkey1').v(1) if the subkey key1 is an array, this can retrieve the first element - % jd.('key1').('subkey1').v(1).('subsubkey1') the indexing can be further applied for deeper objects - % jd.('$.key1.subkey1') if the indexing starts with '$' this allows a JSONPath based index - % jd.('$.key1.subkey1[0]') using a JSONPath can also read array-based subkey element - % jd.('$.key1.subkey1[0].subsubkey1') JSONPath can also apply further indexing over objects of diverse types - % jd.('$.key1..subkey') JSONPath can use '..' deep-search operator to find and retrieve subkey appearing at any level below - % - % member functions: - % jd() or jd.v() returns the underlying hierachical data - % jd.('cell1').v(i) or jd.('array1').v(2:3) returns specified elements if the element is a cell or array - % jd.('key1'),('subkey1').v() returns the underlying hierachical data at the specified subkeys - % jd.tojson() convers the underlying data to a JSON string - % jd.tojson('compression', 'zlib', ...) convers the data to a JSON string with savejson() options - % jd.keys() return the sub-key names of the object - if it a struct, dictionary or containers.Map - or 1:length(data) if it is an array - % jd.len() return the length of the sub-keys - % - % if using matlab, the .v(...) method can be replaced by bare - % brackets .(...), but in octave, one must use .v(...) - % - % examples: - % obj = struct('key1', struct('subkey1',1, 'subkey2',[1,2,3]), 'subkey2', 'str'); - % obj.key1.subkey3 = {8,'test',containers.Map('subsubkey1',0)} - % - % jd = jdict(obj); - % - % % getting values - % jd.('key1').('subkey1') % return jdict(1) - % jd.keys.subkey1 % return jdict(1) - % jd.('key1').('subkey3') % return jdict(obj.key1.subkey3) - % jd.('key1').('subkey3')() % return obj.key1.subkey3 - % jd.('key1').('subkey3').v(1) % return jdict({8}) - % jd.('key1').('subkey3').('subsubkey1') % return jdict(obj.key1.subkey3(2)) - % jd.('key1').('subkey3').v(2).v() % return {'test'} - % jd.('$.key1.subkey1') % return jdict(1) - % jd.('$.key1.subkey2')() % return 'str' - % jd.('$.key1.subkey2').v().v(1) % return jdict(1) - % jd.('$.key1.subkey2')().v(1).v() % return 1 - % jd.('$.key1.subkey3[2].subsubkey1') % return jdict(0) - % jd.('$..subkey2') % jsonpath '..' operator runs a deep scan, return jdict({'str', [1 2 3]}) - % jd.('$..subkey2').v(2) % return jdict([1,2,3]) - % - % % setting values - % jd.('subkey2') = 'newstr' % setting obj.subkey2 to 'newstr' - % - % % loading complex data from REST-API - % jd = jdict('https://neurojson.io:7777/cotilab/NeuroCaptain_2024'); - % - % jd.('Atlas_Age_19_0') - % jd.Atlas_Age_19_0.('Landmark_10_10').('$.._DataLink_') - % jd.Atlas_Age_19_0.Landmark_10_10.('$.._DataLink_')() - % - % license: - % BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details - % - % -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab) - % +% +% jd = jdict(data) +% +% A universal dictionary-like interface that enables fast multi-level subkey access and +% JSONPath-based element indexing, such as jd.('key1').('key2') and jd.('$.key1.key2'), +% for hierachical data structures embedding struct, containers.Map or dictionary objects +% +% author: Qianqian Fang (q.fang neu.edu) +% +% input: +% data: a hierachical data structure made of struct, containers.Map, dictionary, or cell arrays +% if data is a string starting with http:// or https://, +% loadjson(data) will be used to dynamically load the data +% +% indexing: +% jd.('key1').('subkey1')... can retrieve values that are recursively index keys that are +% jd.key1.subkey1... can also retrieve the same data regardless +% if the underlying data is struct, containers.Map or dictionary +% jd.('key1').('subkey1').v(1) if the subkey key1 is an array, this can retrieve the first element +% jd.('key1').('subkey1').v(1).('subsubkey1') the indexing can be further applied for deeper objects +% jd.('$.key1.subkey1') if the indexing starts with '$' this allows a JSONPath based index +% jd.('$.key1.subkey1[0]') using a JSONPath can also read array-based subkey element +% jd.('$.key1.subkey1[0].subsubkey1') JSONPath can also apply further indexing over objects of diverse types +% jd.('$.key1..subkey') JSONPath can use '..' deep-search operator to find and retrieve subkey appearing at any level below +% +% member functions: +% jd() or jd.v() returns the underlying hierachical data +% jd.('cell1').v(i) or jd.('array1').v(2:3) returns specified elements if the element is a cell or array +% jd.('key1'),('subkey1').v() returns the underlying hierachical data at the specified subkeys +% jd.tojson() convers the underlying data to a JSON string +% jd.tojson('compression', 'zlib', ...) convers the data to a JSON string with savejson() options +% jd.keys() return the sub-key names of the object - if it a struct, dictionary or containers.Map - or 1:length(data) if it is an array +% jd.len() return the length of the sub-keys +% +% if using matlab, the .v(...) method can be replaced by bare +% brackets .(...), but in octave, one must use .v(...) +% +% examples: +% obj = struct('key1', struct('subkey1',1, 'subkey2',[1,2,3]), 'subkey2', 'str'); +% obj.key1.subkey3 = {8,'test',containers.Map('subsubkey1',0)}; +% +% jd = jdict(obj); +% +% % getting values +% jd.('key1').('subkey1') % return jdict(1) +% jd.keys.subkey1 % return jdict(1) +% jd.('key1').('subkey3') % return jdict(obj.key1.subkey3) +% jd.('key1').('subkey3')() % return obj.key1.subkey3 +% jd.('key1').('subkey3').v(1) % return jdict({8}) +% jd.('key1').('subkey3').('subsubkey1') % return jdict(obj.key1.subkey3(2)) +% jd.('key1').('subkey3').v(2).v() % return {'test'} +% jd.('$.key1.subkey1') % return jdict(1) +% jd.('$.key1.subkey2')() % return 'str' +% jd.('$.key1.subkey2').v().v(1) % return jdict(1) +% jd.('$.key1.subkey2')().v(1).v() % return 1 +% jd.('$.key1.subkey3[2].subsubkey1') % return jdict(0) +% jd.('$..subkey2') % jsonpath '..' operator runs a deep scan, return jdict({'str', [1 2 3]}) +% jd.('$..subkey2').v(2) % return jdict([1,2,3]) +% +% % setting values +% jd.('subkey2') = 'newstr' % setting obj.subkey2 to 'newstr' +% jd.('key1').('subkey2').v(1) = 2; % modify indexed element +% jd.('key1').('subkey2').v([2, 3]) = [10, 11]; % modify multiple values +% jd.('key1').('subkey3').v(3).('subsubkey1') = 1; % modify keyed value +% jd.('key1').('subkey3').v(3).('subsubkey2') = 'new'; % add new key +% +% % loading complex data from REST-API +% jd = jdict('https://neurojson.io:7777/cotilab/NeuroCaptain_2024'); +% +% jd.('Atlas_Age_19_0') +% jd.Atlas_Age_19_0.('Landmark_10_10').('$.._DataLink_') +% jd.Atlas_Age_19_0.Landmark_10_10.('$.._DataLink_')() +% +% license: +% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details +% +% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab) +% - properties (Access = private) +classdef jdict < handle + properties data end methods @@ -158,31 +162,62 @@ end % overloading the setter function, obj.('idxkey')=otherobj + % expanded from rahnema1's sample at https://stackoverflow.com/a/79030223/4271392 function obj = subsasgn(obj, idxkey, otherobj) oplen = length(idxkey); - val = obj.data; - i = 1; - while i <= oplen - if (i > 1) - error('multi-level assignment is not yet supported'); - end + opcell = cell (1, oplen + 1); + if (isempty(obj.data)) + obj.data = containers.Map(); + end + opcell{1} = obj.data; + + for i = 1:oplen idx = idxkey(i); - if ((idx.type == '.' && ischar(idx.subs)) || (iscell(idx.subs) && ~isempty(idx.subs{1}))) - onekey = idx.subs; - if (iscell(onekey)) - onekey = onekey{1}; + if (strcmp(idx.type, '.')) + if (ischar(idx.subs) && strcmp(idx.subs, 'v')) + opcell{i + 1} = opcell{i}; + if (i < oplen && iscell(opcell{i})) + idxkey(i + 1).type = '{}'; + end + continue end - if (ischar(onekey) && ~isempty(onekey) && onekey(1) == '$') - % jsonset(val, onekey) = otherobj; - error('setting value via JSONPath is not supported'); - elseif (isstruct(val)) - obj.data.(onekey) = otherobj; - elseif (isa(val, 'containers.Map') || isa(val, 'dictionary')) - obj.data(onekey) = otherobj; + if (ischar(idx.subs) && ~isempty(idx.subs) && idx.subs(1) == '$') + error('setting values based on JSONPath indices is not yet supported'); + end + if (ischar(idx.subs)) + if (((isa(opcell{i}, 'containers.Map') || isa(opcell{i}, 'dictionary')) && ~isKey(opcell{i}, idx.subs))) + idx.type = '()'; + opcell{i} = subsasgn(opcell{i}, idx, containers.Map()); + elseif (isstruct(opcell{i}) && ~isfield(opcell{i}, idx.subs)) + opcell{i} = subsasgn(opcell{i}, idx, containers.Map()); + end end end - i = i + 1; + opcell{i + 1} = subsref(opcell{i}, idx); end + + opcell{end - 1} = subsasgn(opcell{i}, idx, otherobj); + + for i = oplen - 1:-1:1 + idx = idxkey(i); + if (ischar(idx.subs) && strcmp(idx.subs, 'v')) + opcell{i} = opcell{i + 1}; + continue + end + + if (i > 1 && ischar(idxkey(i - 1).subs) && strcmp(idxkey(i - 1).subs, 'v')) + if (iscell(opcell{i}) && ~isempty(idx.subs)) + opcell{i} = subsasgn(opcell{i}, idx, opcell{i + 1}); + else + opcell{i} = opcell{i + 1}; + end + i = i - 1; + else + opcell{i} = subsasgn(opcell{i}, idx, opcell{i + 1}); + end + end + + obj.data = opcell{1}; end function val = tojson(obj, varargin) diff --git a/test/run_jsonlab_test.m b/test/run_jsonlab_test.m index 00b45c4..333cb52 100644 --- a/test/run_jsonlab_test.m +++ b/test/run_jsonlab_test.m @@ -439,11 +439,18 @@ function run_jsonlab_test(tests) test_jsonlab('jd.(''key1'').(''subkey3'').v(2).v()', @savejson, jd.('key1').('subkey3').v(2).v(), '"test"', 'compact', 1); test_jsonlab('jd.(''$.key1.subkey1'')', @savejson, jd.('$.key1.subkey1'), '[1]', 'compact', 1); test_jsonlab('jd.(''$.key1.subkey2'')()', @savejson, jd.('$.key1.subkey2')(), '[1,2,3]', 'compact', 1); - test_jsonlab('jd.(''$.key1.subkey2'').v()', @savejson, jd.('$.key1.subkey2').v().v(1), '[1]', 'compact', 1); + test_jsonlab('jd.(''$.key1.subkey2'').v()', @savejson, jd.('$.key1.subkey2').v(), '[1,2,3]', 'compact', 1); test_jsonlab('jd.(''$.key1.subkey2'')().v(1)', @savejson, jd.('$.key1.subkey2')().v(1), '[1]', 'compact', 1); test_jsonlab('jd.(''$.key1.subkey3[2].subsubkey1', @savejson, jd.('$.key1.subkey3[2].subsubkey1'), '[0]', 'compact', 1); test_jsonlab('jd.(''$..subkey2'')', @savejson, jd.('$..subkey2'), '["str",[1,2,3]]', 'compact', 1); test_jsonlab('jd.(''$..subkey2'').v(2)', @savejson, jd.('$..subkey2').v(2), '[1,2,3]', 'compact', 1); + jd.('key1').('subkey2').v(1) = 2; + jd.('key1').('subkey2').v([2, 3]) = [10, 11]; + jd.('key1').('subkey3').v(2) = 'mod'; + jd.('key1').('subkey3').v(3).('subsubkey1') = 1; + % jd.('key1').('subkey3').v(3).('subsubkey2') = 'new'; + test_jsonlab('jd.(''key1'').(''subkey3'')', @savejson, jd.('key1').('subkey3'), '[8,"mod",{"subsubkey1":1}]', 'compact', 1); + test_jsonlab('jd.(''key1'').(''subkey2'')', @savejson, jd.('key1').('subkey2'), '[2,10,11]', 'compact', 1); clear testdata jd; end