Skip to content

Commit

Permalink
[feat] support jdict - a portable hierarchical data container
Browse files Browse the repository at this point in the history
  • Loading branch information
fangq committed Sep 27, 2024
1 parent 288dc92 commit 58a97cd
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 2 deletions.
1 change: 1 addition & 0 deletions Contents.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
% jdatadecode - newdata=jdatadecode(data,opt,...)
% jdataencode - jdata=jdataencode(data)
% jdatahash.m - key = jdatahash(data, algorithm)
% jdict - jd = jdict(data)
% jdlink - data = jdlink(uripath)
% jload - jload
% jsave - jsave
Expand Down
2 changes: 2 additions & 0 deletions INDEX
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ JData Specification
Interface
loadjd
savejd
Key Access
jdict
JSON Mmap
jsonget
jsonset
Expand Down
2 changes: 2 additions & 0 deletions jdataencode.m
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@

if (iscell(item))
newitem = cell2jd(item, varargin{:});
elseif (isa(item, 'jdict'))
newitem = obj2jd(item(), varargin{:});
elseif (isstruct(item))
newitem = struct2jd(item, varargin{:});
elseif (isnumeric(item) || islogical(item) || isa(item, 'timeseries'))
Expand Down
208 changes: 208 additions & 0 deletions jdict.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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').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.('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'
%
% 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)
data
end
methods

function obj = jdict(val)
if (nargin == 1)
if (ischar(val) && ~isempty(regexp(val, '^https*://', 'once')))
try
obj.data = loadjson(val);
catch
obj.data = val;
end
end
if (isa(val, 'jdict'))
obj = val;
else
obj.data = val;
end
end
end

% overloading the getter function
function val = subsref(obj, idxkey)
oplen = length(idxkey);
val = obj.data;
if (oplen == 1 && strcmp(idxkey.type, '()') && isempty(idxkey.subs))
return
end
i = 1;
while i <= oplen
idx = idxkey(i);
% disp({i, savejson(idx)});
if (isempty(idx.subs))
i = i + 1;
continue
end
if (idx.type == '.' && isnumeric(idx.subs))
val = val(idx.subs);
elseif ((strcmp(idx.type, '()') || idx.type == '.') && ismember(idx.subs, {'tojson', 'fromjson', 'v', 'keys', 'len'}) && i < oplen)
if (strcmp(idx.subs, 'v'))
if (iscell(val) && strcmp(idxkey(i + 1).type, '()'))
idxkey(i + 1).type = '{}';
end
if (~isempty(idxkey(i + 1).subs))
val = v(jdict(val), idxkey(i + 1));
end
else
fhandle = str2func(idx.subs);
val = fhandle(jdict(val), idxkey(i + 1).subs{:});
end
i = i + 1;
if (i < oplen)
val = jdict(val);
end
elseif ((idx.type == '.' && ischar(idx.subs)) || (iscell(idx.subs) && ~isempty(idx.subs{1})))
onekey = idx.subs;
if (iscell(onekey))
onekey = onekey{1};
end
if (isa(val, 'jdict'))
val = val.data;
end
if (ischar(onekey) && ~isempty(onekey) && onekey(1) == '$')
val = jsonpath(val, onekey);
elseif (isstruct(val))
val = val.(onekey);
elseif (isa(val, 'containers.Map') || isa(val, 'dictionary'))
val = val(onekey);
else
error('key name "%s" not found', onekey);
end
else
error('method not supported');
end
i = i + 1;
end
if (~(isempty(idxkey(end).subs) && strcmp(idxkey(end).type, '()')))
val = jdict(val);
end
end

% overloading the setter function, obj.('idxkey')=otherobj
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
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};
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;
end
end
i = i + 1;
end
end

function val = tojson(obj, varargin)
val = savejson('', obj.data, 'compact', 1, varargin{:});
end

function obj = fromjson(obj, fname, varargin)
obj.data = loadjd(fname, varargin{:});
end

function val = keys(obj)
if (isstruct(obj.data))
val = fieldnames(obj.data);
elseif (isa(obj.data, 'containers.Map') || isa(obj.data, 'dictionary'))
val = keys(obj.data);
else
val = 1:length(obj.data);
end
end

function val = len(obj)
val = length(obj.data);
end

function val = v(obj, varargin)
if (~isempty(varargin))
val = subsref(obj.data, varargin{:});
else
val = obj.data;
end
end

end
end
2 changes: 2 additions & 0 deletions savejson.m
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@

if (iscell(item) || isa(item, 'string'))
txt = cell2json(name, item, level, varargin{:});
elseif (isa(item, 'jdict'))
txt = obj2json(name, item, level, varargin{:});
elseif (isstruct(item))
txt = struct2json(name, item, level, varargin{:});
elseif (isnumeric(item) || islogical(item) || isa(item, 'timeseries'))
Expand Down
32 changes: 30 additions & 2 deletions test/run_jsonlab_test.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function run_jsonlab_test(tests)
% run_jsonlab_test
% or
% run_jsonlab_test(tests)
% run_jsonlab_test({'js','jso','bj','bjo','jmap','bmap','jpath','bugs'})
% run_jsonlab_test({'js','jso','bj','bjo','jmap','bmap','jpath','jdict','bugs'})
%
% Unit testing for JSONLab JSON, BJData/UBJSON encoders and decoders
%
Expand All @@ -19,6 +19,7 @@ function run_jsonlab_test(tests)
% 'jmap': test jsonmmap features in loadjson
% 'bmap': test jsonmmap features in loadbj
% 'jpath': test jsonpath
% 'jdict': test jdict
% 'bugs': test specific bug fixes
%
% license:
Expand All @@ -28,7 +29,7 @@ function run_jsonlab_test(tests)
%

if (nargin == 0)
tests = {'js', 'jso', 'bj', 'bjo', 'jmap', 'bmap', 'jpath', 'bugs'};
tests = {'js', 'jso', 'bj', 'bjo', 'jmap', 'bmap', 'jpath', 'jdict', 'bugs'};
end

%%
Expand Down Expand Up @@ -420,6 +421,33 @@ function run_jsonlab_test(tests)
clear testdata;
end

%%
if (ismember('jdict', tests))
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));
fprintf('Test jdict\n');
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));

testdata = struct('key1', struct('subkey1', 1, 'subkey2', [1, 2, 3]), 'subkey2', 'str');
testdata.key1.subkey3 = {8, 'test', struct('subsubkey1', 0)};
jd = jdict(testdata);

test_jsonlab('jd.(''key1'').(''subkey1'')', @savejson, jd.('key1').('subkey1'), '[1]', 'compact', 1);
test_jsonlab('jd.(''key1'').(''subkey3'')', @savejson, jd.('key1').('subkey3'), '[8,"test",{"subsubkey1":0}]', 'compact', 1);
test_jsonlab('jd.(''key1'').(''subkey3'')()', @savejson, jd.('key1').('subkey3')(), '[8,"test",{"subsubkey1":0}]', 'compact', 1);
test_jsonlab('jd.(''key1'').(''subkey3'').v(1)', @savejson, jd.('key1').('subkey3').v(1), '[8]', 'compact', 1);
test_jsonlab('jd.(''key1'').(''subkey3'').v(3).(''subsubkey1'')', @savejson, jd.('key1').('subkey3').v(3).('subsubkey1'), '[0]', 'compact', 1);
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(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);

clear testdata jd;
end

%%
if (ismember('bugs', tests))
fprintf(sprintf('%s\n', char(ones(1, 79) * 61)));
Expand Down

0 comments on commit 58a97cd

Please sign in to comment.