Skip to content

Commit 58a97cd

Browse files
committed
[feat] support jdict - a portable hierarchical data container
1 parent 288dc92 commit 58a97cd

File tree

6 files changed

+245
-2
lines changed

6 files changed

+245
-2
lines changed

Contents.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
% jdatadecode - newdata=jdatadecode(data,opt,...)
1616
% jdataencode - jdata=jdataencode(data)
1717
% jdatahash.m - key = jdatahash(data, algorithm)
18+
% jdict - jd = jdict(data)
1819
% jdlink - data = jdlink(uripath)
1920
% jload - jload
2021
% jsave - jsave

INDEX

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ JData Specification
2020
Interface
2121
loadjd
2222
savejd
23+
Key Access
24+
jdict
2325
JSON Mmap
2426
jsonget
2527
jsonset

jdataencode.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101

102102
if (iscell(item))
103103
newitem = cell2jd(item, varargin{:});
104+
elseif (isa(item, 'jdict'))
105+
newitem = obj2jd(item(), varargin{:});
104106
elseif (isstruct(item))
105107
newitem = struct2jd(item, varargin{:});
106108
elseif (isnumeric(item) || islogical(item) || isa(item, 'timeseries'))

jdict.m

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
classdef jdict < handle
2+
%
3+
% jd = jdict(data)
4+
%
5+
% A universal dictionary-like interface that enables fast multi-level subkey access and
6+
% JSONPath-based element indexing, such as jd.('key1').('key2') and jd.('$.key1.key2'),
7+
% for hierachical data structures embedding struct, containers.Map or dictionary objects
8+
%
9+
% author: Qianqian Fang (q.fang <at> neu.edu)
10+
%
11+
% input:
12+
% data: a hierachical data structure made of struct, containers.Map, dictionary, or cell arrays
13+
% if data is a string starting with http:// or https://,
14+
% loadjson(data) will be used to dynamically load the data
15+
%
16+
% indexing:
17+
% jd.('key1').('subkey1')... can retrieve values that are recursively index keys that are
18+
% jd.('key1').('subkey1').v(1) if the subkey key1 is an array, this can retrieve the first element
19+
% jd.('key1').('subkey1').v(1).('subsubkey1') the indexing can be further applied for deeper objects
20+
% jd.('$.key1.subkey1') if the indexing starts with '$' this allows a JSONPath based index
21+
% jd.('$.key1.subkey1[0]') using a JSONPath can also read array-based subkey element
22+
% jd.('$.key1.subkey1[0].subsubkey1') JSONPath can also apply further indexing over objects of diverse types
23+
% jd.('$.key1..subkey') JSONPath can use '..' deep-search operator to find and retrieve subkey appearing at any level below
24+
%
25+
% member functions:
26+
% jd() or jd.v() returns the underlying hierachical data
27+
% jd.('cell1').v(i) or jd.('array1').v(2:3) returns specified elements if the element is a cell or array
28+
% jd.('key1'),('subkey1').v() returns the underlying hierachical data at the specified subkeys
29+
% jd.tojson() convers the underlying data to a JSON string
30+
% jd.tojson('compression', 'zlib', ...) convers the data to a JSON string with savejson() options
31+
% 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
32+
% jd.len() return the length of the sub-keys
33+
%
34+
% if using matlab, the .v(...) method can be replaced by bare
35+
% brackets .(...), but in octave, one must use .v(...)
36+
%
37+
% examples:
38+
% obj = struct('key1', struct('subkey1',1, 'subkey2',[1,2,3]), 'subkey2', 'str');
39+
% obj.key1.subkey3 = {8,'test',containers.Map('subsubkey1',0)}
40+
%
41+
% jd = jdict(obj);
42+
%
43+
% % getting values
44+
% jd.('key1').('subkey1') % return jdict(1)
45+
% jd.('key1').('subkey3') % return jdict(obj.key1.subkey3)
46+
% jd.('key1').('subkey3')() % return obj.key1.subkey3
47+
% jd.('key1').('subkey3').v(1) % return jdict({8})
48+
% jd.('key1').('subkey3').('subsubkey1') % return jdict(obj.key1.subkey3(2))
49+
% jd.('key1').('subkey3').v(2).v() % return {'test'}
50+
% jd.('$.key1.subkey1') % return jdict(1)
51+
% jd.('$.key1.subkey2')() % return 'str'
52+
% jd.('$.key1.subkey2').v().v(1) % return jdict(1)
53+
% jd.('$.key1.subkey2')().v(1).v() % return 1
54+
% jd.('$.key1.subkey3[2].subsubkey1') % return jdict(0)
55+
% jd.('$..subkey2') % jsonpath '..' operator runs a deep scan, return jdict({'str', [1 2 3]})
56+
% jd.('$..subkey2').v(2) % return jdict([1,2,3])
57+
%
58+
% % setting values
59+
% jd.('subkey2') = 'newstr' % setting obj.subkey2 to 'newstr'
60+
%
61+
% license:
62+
% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
63+
%
64+
% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
65+
%
66+
67+
properties (Access = private)
68+
data
69+
end
70+
methods
71+
72+
function obj = jdict(val)
73+
if (nargin == 1)
74+
if (ischar(val) && ~isempty(regexp(val, '^https*://', 'once')))
75+
try
76+
obj.data = loadjson(val);
77+
catch
78+
obj.data = val;
79+
end
80+
end
81+
if (isa(val, 'jdict'))
82+
obj = val;
83+
else
84+
obj.data = val;
85+
end
86+
end
87+
end
88+
89+
% overloading the getter function
90+
function val = subsref(obj, idxkey)
91+
oplen = length(idxkey);
92+
val = obj.data;
93+
if (oplen == 1 && strcmp(idxkey.type, '()') && isempty(idxkey.subs))
94+
return
95+
end
96+
i = 1;
97+
while i <= oplen
98+
idx = idxkey(i);
99+
% disp({i, savejson(idx)});
100+
if (isempty(idx.subs))
101+
i = i + 1;
102+
continue
103+
end
104+
if (idx.type == '.' && isnumeric(idx.subs))
105+
val = val(idx.subs);
106+
elseif ((strcmp(idx.type, '()') || idx.type == '.') && ismember(idx.subs, {'tojson', 'fromjson', 'v', 'keys', 'len'}) && i < oplen)
107+
if (strcmp(idx.subs, 'v'))
108+
if (iscell(val) && strcmp(idxkey(i + 1).type, '()'))
109+
idxkey(i + 1).type = '{}';
110+
end
111+
if (~isempty(idxkey(i + 1).subs))
112+
val = v(jdict(val), idxkey(i + 1));
113+
end
114+
else
115+
fhandle = str2func(idx.subs);
116+
val = fhandle(jdict(val), idxkey(i + 1).subs{:});
117+
end
118+
i = i + 1;
119+
if (i < oplen)
120+
val = jdict(val);
121+
end
122+
elseif ((idx.type == '.' && ischar(idx.subs)) || (iscell(idx.subs) && ~isempty(idx.subs{1})))
123+
onekey = idx.subs;
124+
if (iscell(onekey))
125+
onekey = onekey{1};
126+
end
127+
if (isa(val, 'jdict'))
128+
val = val.data;
129+
end
130+
if (ischar(onekey) && ~isempty(onekey) && onekey(1) == '$')
131+
val = jsonpath(val, onekey);
132+
elseif (isstruct(val))
133+
val = val.(onekey);
134+
elseif (isa(val, 'containers.Map') || isa(val, 'dictionary'))
135+
val = val(onekey);
136+
else
137+
error('key name "%s" not found', onekey);
138+
end
139+
else
140+
error('method not supported');
141+
end
142+
i = i + 1;
143+
end
144+
if (~(isempty(idxkey(end).subs) && strcmp(idxkey(end).type, '()')))
145+
val = jdict(val);
146+
end
147+
end
148+
149+
% overloading the setter function, obj.('idxkey')=otherobj
150+
function obj = subsasgn(obj, idxkey, otherobj)
151+
oplen = length(idxkey);
152+
val = obj.data;
153+
i = 1;
154+
while i <= oplen
155+
if (i > 1)
156+
error('multi-level assignment is not yet supported');
157+
end
158+
idx = idxkey(i);
159+
if ((idx.type == '.' && ischar(idx.subs)) || (iscell(idx.subs) && ~isempty(idx.subs{1})))
160+
onekey = idx.subs;
161+
if (iscell(onekey))
162+
onekey = onekey{1};
163+
end
164+
if (ischar(onekey) && ~isempty(onekey) && onekey(1) == '$')
165+
% jsonset(val, onekey) = otherobj;
166+
error('setting value via JSONPath is not supported');
167+
elseif (isstruct(val))
168+
obj.data.(onekey) = otherobj;
169+
elseif (isa(val, 'containers.Map') || isa(val, 'dictionary'))
170+
obj.data(onekey) = otherobj;
171+
end
172+
end
173+
i = i + 1;
174+
end
175+
end
176+
177+
function val = tojson(obj, varargin)
178+
val = savejson('', obj.data, 'compact', 1, varargin{:});
179+
end
180+
181+
function obj = fromjson(obj, fname, varargin)
182+
obj.data = loadjd(fname, varargin{:});
183+
end
184+
185+
function val = keys(obj)
186+
if (isstruct(obj.data))
187+
val = fieldnames(obj.data);
188+
elseif (isa(obj.data, 'containers.Map') || isa(obj.data, 'dictionary'))
189+
val = keys(obj.data);
190+
else
191+
val = 1:length(obj.data);
192+
end
193+
end
194+
195+
function val = len(obj)
196+
val = length(obj.data);
197+
end
198+
199+
function val = v(obj, varargin)
200+
if (~isempty(varargin))
201+
val = subsref(obj.data, varargin{:});
202+
else
203+
val = obj.data;
204+
end
205+
end
206+
207+
end
208+
end

savejson.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@
291291

292292
if (iscell(item) || isa(item, 'string'))
293293
txt = cell2json(name, item, level, varargin{:});
294+
elseif (isa(item, 'jdict'))
295+
txt = obj2json(name, item, level, varargin{:});
294296
elseif (isstruct(item))
295297
txt = struct2json(name, item, level, varargin{:});
296298
elseif (isnumeric(item) || islogical(item) || isa(item, 'timeseries'))

test/run_jsonlab_test.m

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ function run_jsonlab_test(tests)
33
% run_jsonlab_test
44
% or
55
% run_jsonlab_test(tests)
6-
% run_jsonlab_test({'js','jso','bj','bjo','jmap','bmap','jpath','bugs'})
6+
% run_jsonlab_test({'js','jso','bj','bjo','jmap','bmap','jpath','jdict','bugs'})
77
%
88
% Unit testing for JSONLab JSON, BJData/UBJSON encoders and decoders
99
%
@@ -19,6 +19,7 @@ function run_jsonlab_test(tests)
1919
% 'jmap': test jsonmmap features in loadjson
2020
% 'bmap': test jsonmmap features in loadbj
2121
% 'jpath': test jsonpath
22+
% 'jdict': test jdict
2223
% 'bugs': test specific bug fixes
2324
%
2425
% license:
@@ -28,7 +29,7 @@ function run_jsonlab_test(tests)
2829
%
2930

3031
if (nargin == 0)
31-
tests = {'js', 'jso', 'bj', 'bjo', 'jmap', 'bmap', 'jpath', 'bugs'};
32+
tests = {'js', 'jso', 'bj', 'bjo', 'jmap', 'bmap', 'jpath', 'jdict', 'bugs'};
3233
end
3334

3435
%%
@@ -420,6 +421,33 @@ function run_jsonlab_test(tests)
420421
clear testdata;
421422
end
422423

424+
%%
425+
if (ismember('jdict', tests))
426+
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));
427+
fprintf('Test jdict\n');
428+
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));
429+
430+
testdata = struct('key1', struct('subkey1', 1, 'subkey2', [1, 2, 3]), 'subkey2', 'str');
431+
testdata.key1.subkey3 = {8, 'test', struct('subsubkey1', 0)};
432+
jd = jdict(testdata);
433+
434+
test_jsonlab('jd.(''key1'').(''subkey1'')', @savejson, jd.('key1').('subkey1'), '[1]', 'compact', 1);
435+
test_jsonlab('jd.(''key1'').(''subkey3'')', @savejson, jd.('key1').('subkey3'), '[8,"test",{"subsubkey1":0}]', 'compact', 1);
436+
test_jsonlab('jd.(''key1'').(''subkey3'')()', @savejson, jd.('key1').('subkey3')(), '[8,"test",{"subsubkey1":0}]', 'compact', 1);
437+
test_jsonlab('jd.(''key1'').(''subkey3'').v(1)', @savejson, jd.('key1').('subkey3').v(1), '[8]', 'compact', 1);
438+
test_jsonlab('jd.(''key1'').(''subkey3'').v(3).(''subsubkey1'')', @savejson, jd.('key1').('subkey3').v(3).('subsubkey1'), '[0]', 'compact', 1);
439+
test_jsonlab('jd.(''key1'').(''subkey3'').v(2).v()', @savejson, jd.('key1').('subkey3').v(2).v(), '"test"', 'compact', 1);
440+
test_jsonlab('jd.(''$.key1.subkey1'')', @savejson, jd.('$.key1.subkey1'), '[1]', 'compact', 1);
441+
test_jsonlab('jd.(''$.key1.subkey2'')()', @savejson, jd.('$.key1.subkey2')(), '[1,2,3]', 'compact', 1);
442+
test_jsonlab('jd.(''$.key1.subkey2'').v()', @savejson, jd.('$.key1.subkey2').v().v(1), '[1]', 'compact', 1);
443+
test_jsonlab('jd.(''$.key1.subkey2'')().v(1)', @savejson, jd.('$.key1.subkey2')().v(1), '[1]', 'compact', 1);
444+
test_jsonlab('jd.(''$.key1.subkey3[2].subsubkey1', @savejson, jd.('$.key1.subkey3[2].subsubkey1'), '[0]', 'compact', 1);
445+
test_jsonlab('jd.(''$..subkey2'')', @savejson, jd.('$..subkey2'), '["str",[1,2,3]]', 'compact', 1);
446+
test_jsonlab('jd.(''$..subkey2'').v(2)', @savejson, jd.('$..subkey2').v(2), '[1,2,3]', 'compact', 1);
447+
448+
clear testdata jd;
449+
end
450+
423451
%%
424452
if (ismember('bugs', tests))
425453
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));

0 commit comments

Comments
 (0)