Skip to content

Commit 4f0f668

Browse files
committed
[feat] add templated jdict types, uuid, date, bytes
1 parent b329efd commit 4f0f668

File tree

5 files changed

+579
-32
lines changed

5 files changed

+579
-32
lines changed

jdataencode.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
if (iscell(item))
126126
newitem = cell2jd(item, opt);
127127
elseif (isa(item, 'jdict'))
128-
newitem = obj2jd(item.v(), opt);
128+
newitem = obj2jd(item(), opt);
129129
elseif (isstruct(item))
130130
newitem = struct2jd(item, opt);
131131
elseif (isnumeric(item) || islogical(item) || isa(item, 'timeseries'))

jdict.m

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
obj.schema = [];
179179
obj.currentpath__ = char(36);
180180
obj.root__ = obj;
181+
kindval = '';
181182
if (nargin >= 1)
182183
if (~isempty(varargin))
183184
allflags = [varargin(1:2:end); varargin(2:2:end)];
@@ -188,6 +189,9 @@
188189
if (isfield(obj.flags__, 'schema'))
189190
obj.schema = obj.flags__.schema;
190191
end
192+
if (isfield(obj.flags__, 'kind'))
193+
kindval = obj.flags__.kind;
194+
end
191195
end
192196
if (ischar(val) && ~isempty(regexpi(val, '^https*://', 'once')))
193197
try
@@ -207,6 +211,16 @@
207211
obj.data = val;
208212
end
209213
end
214+
% apply kind schema
215+
if ~isempty(kindval)
216+
kindschema = getkindschema_(kindval);
217+
if ~isempty(kindschema)
218+
obj.setschema(kindschema);
219+
elseif isempty(obj.schema)
220+
error('Unknown kind "%s" and no schema defined. Use: uuid, date, time, datetime, ipv4, ipv6, email, uri, posint, nonnegative', kindval);
221+
end
222+
obj.setattr(char(36), 'kind', kindval);
223+
end
210224
end
211225

212226
% overloaded numel to prevent subsref from outputting many outputs
@@ -237,9 +251,18 @@
237251
trackpath = obj.currentpath__;
238252

239253
if (oplen == 1 && strcmp(idxkey(1).type, '()') && isempty(idxkey(1).subs))
254+
kindval = obj.getattr(char(36), 'kind');
255+
if ~isempty(kindval) && isstruct(val)
256+
formatted = formatkind_(kindval, val);
257+
if ~isempty(formatted)
258+
varargout{1} = formatted;
259+
return
260+
end
261+
end
240262
varargout{1} = val;
241263
return
242264
end
265+
243266
i = 1;
244267
while i <= oplen
245268
idx = idxkey(i);
@@ -509,6 +532,15 @@
509532
fieldname = idxkey(1).subs;
510533
% Skip if JSONPath
511534
if (isempty(fieldname) || fieldname(1) ~= char(36))
535+
% validate if kind is set
536+
kindval = obj.getattr(char(36), 'kind');
537+
if ~isempty(obj.schema) && ~isempty(kindval)
538+
targetpath = [obj.currentpath__ '.' esckey_(fieldname)];
539+
tempobj = jdict();
540+
tempobj.schema = obj.schema;
541+
tempobj.currentpath__ = targetpath;
542+
le(tempobj, otherobj);
543+
end
512544
if (isempty(obj.data))
513545
obj.data = struct();
514546
end
@@ -536,6 +568,15 @@
536568

537569
% Fast path: single numeric index like jd.(1) = value
538570
if (oplen == 1 && strcmp(idxkey(1).type, '.') && isnumeric(idxkey(1).subs))
571+
% validate if kind is set
572+
kindval = obj.getattr(char(36), 'kind');
573+
if ~isempty(obj.schema) && ~isempty(kindval)
574+
targetpath = [obj.currentpath__ '[' num2str(idxkey(1).subs - 1) ']'];
575+
tempobj = jdict();
576+
tempobj.schema = obj.schema;
577+
tempobj.currentpath__ = targetpath;
578+
le(tempobj, otherobj);
579+
end
539580
newidx = idxkey(1).subs;
540581
if isstruct(obj.data) && isstruct(otherobj)
541582
fnames = fieldnames(obj.data);
@@ -658,8 +699,18 @@
658699
end
659700
end
660701

702+
% check if kind-validation is needed
703+
kindval = obj.getattr(char(36), 'kind');
704+
needvalidate = ~isempty(obj.schema) && ~isempty(kindval);
705+
661706
if (oplen >= 2 && ischar(idxkey(oplen - 1).subs) && strcmp(idxkey(oplen - 1).subs, 'v') && strcmp(idxkey(oplen).type, '()'))
662707
% Handle .v(index) = value at any depth
708+
if needvalidate
709+
tempobj = jdict();
710+
tempobj.schema = obj.schema;
711+
tempobj.currentpath__ = buildpath_(obj.currentpath__, idxkey, oplen);
712+
le(tempobj, otherobj);
713+
end
663714
nextsubs = idxkey(oplen).subs;
664715
if iscell(nextsubs)
665716
nextsubs = nextsubs{1};
@@ -674,9 +725,21 @@
674725
end
675726
opcell{oplen + 1} = opcell{oplen};
676727
elseif (obj.flags__.isoctave_) && (ismap_(obj.flags__, opcell{oplen}))
728+
if needvalidate
729+
tempobj = jdict();
730+
tempobj.schema = obj.schema;
731+
tempobj.currentpath__ = buildpath_(obj.currentpath__, idxkey, oplen);
732+
le(tempobj, otherobj);
733+
end
677734
opcell{oplen}(idx.subs) = otherobj;
678735
opcell{oplen + 1} = opcell{oplen};
679736
else
737+
if needvalidate
738+
tempobj = jdict();
739+
tempobj.schema = obj.schema;
740+
tempobj.currentpath__ = buildpath_(obj.currentpath__, idxkey, oplen);
741+
le(tempobj, otherobj);
742+
end
680743
if (ischar(idx.subs) && ~isempty(idx.subs) && idx.subs(1) == char(36))
681744
opcell{oplen + 1} = obj.call_('jsonpath', opcell{oplen}, idx.subs, otherobj);
682745
else
@@ -1208,3 +1271,114 @@
12081271
function escaped = esckey_(key)
12091272
escaped = regexprep(key, '(?<=[^\\]|^)\.', '\\.');
12101273
end
1274+
1275+
% predefined schemas for known kinds
1276+
function schema = getkindschema_(kind)
1277+
switch lower(kind)
1278+
case 'bytes'
1279+
schema = struct('type', 'array', 'items', struct('type', 'integer', 'minimum', 0, 'maximum', 255));
1280+
case 'uuid'
1281+
schema = struct('type', 'object', ...
1282+
'properties', struct( ...
1283+
'time_low', struct('type', 'integer', 'minimum', 0, 'maximum', 4294967295), ... % 32-bit
1284+
'time_mid', struct('type', 'integer', 'minimum', 0, 'maximum', 65535), ... % 16-bit
1285+
'time_high', struct('type', 'integer', 'minimum', 0, 'maximum', 65535), ... % 16-bit
1286+
'clock_seq', struct('type', 'integer', 'minimum', 0, 'maximum', 65535), ... % 16-bit
1287+
'node', struct('type', 'integer', 'minimum', 0, 'maximum', 281474976710655)), ... % 48-bit
1288+
'required', {{'time_low', 'time_mid', 'time_high', 'clock_seq', 'node'}});
1289+
case 'date'
1290+
schema = struct('type', 'object', ...
1291+
'properties', struct( ...
1292+
'year', struct('type', 'integer', 'minimum', 1, 'maximum', 9999), ...
1293+
'month', struct('type', 'integer', 'minimum', 1, 'maximum', 12), ...
1294+
'day', struct('type', 'integer', 'minimum', 1, 'maximum', 31)), ...
1295+
'required', {{'year', 'month', 'day'}});
1296+
case 'time'
1297+
schema = struct('type', 'object', ...
1298+
'properties', struct( ...
1299+
'hour', struct('type', 'integer', 'minimum', 0, 'maximum', 23), ...
1300+
'min', struct('type', 'integer', 'minimum', 0, 'maximum', 59), ...
1301+
'sec', struct('type', 'number', 'minimum', 0, 'exclusiveMaximum', 60)), ...
1302+
'required', {{'hour', 'min', 'sec'}});
1303+
case 'datetime'
1304+
schema = struct('type', 'object', ...
1305+
'properties', struct( ...
1306+
'year', struct('type', 'integer', 'minimum', 1, 'maximum', 9999), ...
1307+
'month', struct('type', 'integer', 'minimum', 1, 'maximum', 12), ...
1308+
'day', struct('type', 'integer', 'minimum', 1, 'maximum', 31), ...
1309+
'hour', struct('type', 'integer', 'minimum', 0, 'maximum', 23), ...
1310+
'min', struct('type', 'integer', 'minimum', 0, 'maximum', 59), ...
1311+
'sec', struct('type', 'number', 'minimum', 0, 'exclusiveMaximum', 60)), ...
1312+
'required', {{'year', 'month', 'day', 'hour', 'min', 'sec'}});
1313+
case 'email'
1314+
schema = struct('type', 'object', ...
1315+
'properties', struct( ...
1316+
'user', struct('type', 'string', 'minLength', 1), ...
1317+
'domain', struct('type', 'string', 'pattern', '^[^@\s]+\.[^@\s]+$')), ...
1318+
'required', {{'user', 'domain'}});
1319+
case 'uri'
1320+
schema = struct('type', 'object', ...
1321+
'properties', struct( ...
1322+
'scheme', struct('type', 'string', 'pattern', '^[a-zA-Z][a-zA-Z0-9+.-]*$'), ...
1323+
'host', struct('type', 'string', 'minLength', 1), ...
1324+
'port', struct('type', 'integer', 'minimum', 0, 'maximum', 65535), ...
1325+
'path', struct('type', 'string'), ...
1326+
'query', struct('type', 'string'), ...
1327+
'fragment', struct('type', 'string')), ...
1328+
'required', {{'scheme', 'host'}});
1329+
otherwise
1330+
schema = [];
1331+
end
1332+
end
1333+
1334+
% format kind data as string
1335+
function str = formatkind_(kind, data)
1336+
str = [];
1337+
if ~isstruct(data)
1338+
return
1339+
end
1340+
try
1341+
switch lower(kind)
1342+
case 'bytes'
1343+
str = char(data);
1344+
case 'uuid'
1345+
str = sprintf('%08x-%04x-%04x-%04x-%012x', data.time_low, data.time_mid, data.time_high, data.clock_seq, data.node);
1346+
case 'date'
1347+
str = sprintf('%04d-%02d-%02d', data.year, data.month, data.day);
1348+
case 'time'
1349+
str = sprintf('%02d:%02d:%0*.*f', data.hour, data.min, 2 + (data.sec ~= floor(data.sec)) * 4, (data.sec ~= floor(data.sec)) * 3, data.sec);
1350+
case 'datetime'
1351+
str = sprintf('%04d-%02d-%02dT%02d:%02d:%0*.*f', data.year, data.month, data.day, data.hour, data.min, 2 + (data.sec ~= floor(data.sec)) * 4, (data.sec ~= floor(data.sec)) * 3, data.sec);
1352+
case 'email'
1353+
str = sprintf('%s@%s', data.user, data.domain);
1354+
case 'uri'
1355+
str = sprintf('%s://%s', data.scheme, data.host);
1356+
if isfield(data, 'port') && ~isempty(data.port)
1357+
str = [str sprintf(':%d', data.port)];
1358+
end
1359+
if isfield(data, 'path')
1360+
str = [str data.path];
1361+
end
1362+
if isfield(data, 'query') && ~isempty(data.query)
1363+
str = [str '?' data.query];
1364+
end
1365+
if isfield(data, 'fragment') && ~isempty(data.fragment)
1366+
str = [str '#' data.fragment];
1367+
end
1368+
end
1369+
catch
1370+
end
1371+
end
1372+
1373+
% build JSONPath from idxkey array
1374+
function targetpath = buildpath_(basepath, idxkey, oplen)
1375+
targetpath = basepath;
1376+
for ii = 1:oplen
1377+
idx = idxkey(ii);
1378+
if strcmp(idx.type, '.') && ischar(idx.subs) && ~strcmp(idx.subs, 'v')
1379+
targetpath = [targetpath '.' esckey_(idx.subs)];
1380+
elseif strcmp(idx.type, '()') && ~isempty(idx.subs) && isnumeric(idx.subs{1})
1381+
targetpath = [targetpath '[' num2str(idx.subs{1} - 1) ']'];
1382+
end
1383+
end
1384+
end

loadbj.m

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,9 +1226,14 @@
12261226
p = typecast(payload, 'double');
12271227
val = complex(p(1), p(2));
12281228
case 10 % uuid: 16 bytes Big-Endian
1229-
h = lower(reshape(dec2hex(payload, 2)', 1, []));
1230-
val = jdict([h(1:8) '-' h(9:12) '-' h(13:16) '-' h(17:20) '-' h(21:32)], ...
1231-
'schema', struct('type', 'string', 'format', 'uuid'));
1229+
p = uint64(payload);
1230+
uuidstruct = struct( ...
1231+
'time_low', sum(p(1:4) .* uint64([16777216, 65536, 256, 1])), ...
1232+
'time_mid', sum(p(5:6) .* uint64([256, 1])), ...
1233+
'time_high', sum(p(7:8) .* uint64([256, 1])), ...
1234+
'clock_seq', sum(p(9:10) .* uint64([256, 1])), ...
1235+
'node', sum(p(11:16) .* uint64([1099511627776, 4294967296, 16777216, 65536, 256, 1])));
1236+
val = jdict(uuidstruct, 'kind', 'uuid');
12321237
otherwise % unknown extension
12331238
val = jdict(payload, 'schema', struct('type', 'bytes', 'exttype', int32(typeid)));
12341239
end

savebj.m

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
txt = ext2ubjson(name, item, 'datetime', opt);
326326
elseif (isa(item, 'duration'))
327327
txt = ext2ubjson(name, item, 'duration', opt);
328-
elseif (isa(item, 'jdict'))
328+
elseif (isa(item, 'jdict') && ~isempty(item{'kind'}))
329329
txt = jdict2ubjson(name, item, level, opt);
330330
elseif (ischar(item))
331331
if (numel(item) >= opt.compressstringsize)
@@ -1803,7 +1803,7 @@
18031803
typeid = 4;
18041804
payload = [typecast(int16(year(val)), 'uint8'), uint8([month(val), day(val)])];
18051805
elseif mod(second(val), 1) ~= 0 || pt < 0 || pt > 4294967295
1806-
typeid = 6;
1806+
typeid = 2;
18071807
payload = typecast(int64(round(pt * 1e6)), 'uint8');
18081808
else
18091809
typeid = 1;
@@ -1847,26 +1847,26 @@
18471847
txt = struct2ubjson(name, item, level, opt);
18481848
return
18491849
end
1850-
s = item.schema;
1851-
if item.getattr('$', '') && strcmp(s.format, 'uuid')
1850+
datakind = item{'kind'};
1851+
if ~isempty(datakind) && strcmp(datakind, 'uuid')
18521852
% UUID string
18531853
if opt.messagepack || opt.ubjson
18541854
txt = str2ubjson(name, char(item), level, opt);
18551855
return
18561856
end
1857-
uuidstr = char(item);
1858-
hexstr = strrep(uuidstr, '-', '');
1859-
payload = uint8(zeros(1, 16));
1860-
for i = 1:16
1861-
payload(i) = hex2dec(hexstr(2 * i - 1:2 * i));
1862-
end
1857+
d = item.v();
1858+
shifts = uint64([24 16 8 0 8 0 8 0 8 0 40 32 24 16 8 0]);
1859+
vals = uint64([d.time_low d.time_low d.time_low d.time_low ...
1860+
d.time_mid d.time_mid d.time_high d.time_high ...
1861+
d.clock_seq d.clock_seq d.node d.node d.node d.node d.node d.node]);
1862+
payload = uint8(bitand(bitshift(vals, -shifts), 255));
18631863
txt = ['E' I_(uint8(10), opt) I_(uint8(16), opt) char(payload)];
18641864
if ~isempty(name)
18651865
txt = [N_(decodevarname(name, opt.unpackhex), opt) txt];
18661866
end
1867-
elseif isfield(s, 'type') && strcmp(s.type, 'bytes')
1867+
elseif ~isempty(datakind) && strcmp(datakind, 'bytes')
18681868
% Raw extension bytes
1869-
payload = uint8(item.data);
1869+
payload = uint8(item.v());
18701870
typeid = uint32(0);
18711871
if isfield(s, 'exttype')
18721872
typeid = uint32(s.exttype);
@@ -1876,6 +1876,6 @@
18761876
txt = [N_(decodevarname(name, opt.unpackhex), opt) txt];
18771877
end
18781878
else
1879-
% Generic jdict - encode as struct
1880-
txt = struct2ubjson(name, struct(item), level, opt);
1879+
% Generic jdict
1880+
txt = obj2ubjson(name, item.v(), level, opt);
18811881
end

0 commit comments

Comments
 (0)