Skip to content

Commit

Permalink
[feat] support multi-level subkey assignment
Browse files Browse the repository at this point in the history
  • Loading branch information
fangq committed Sep 28, 2024
1 parent 011eae1 commit afce1de
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 95 deletions.
223 changes: 129 additions & 94 deletions jdict.m
Original file line number Diff line number Diff line change
@@ -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 <at> 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 <at> 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
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion test/run_jsonlab_test.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit afce1de

Please sign in to comment.