1## Copyright (C) 2019-2021 Ketan M. Patel
2##
3## This program is free software: you can redistribute it and/or modify it
4## under the terms of the GNU General Public License as published by
5## the Free Software Foundation, either version 3 of the License, or
6## (at your option) any later version.
7##
8## This program is distributed in the hope that it will be useful, but
9## WITHOUT ANY WARRANTY; without even the implied warranty of
10## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11## GNU General Public License for more details.
12##
13## You should have received a copy of the GNU General Public License
14## along with this program.  If not, see
15## <https://www.gnu.org/licenses/>.
16
17## -*- texinfo -*-
18## @deftypefn {} {@var{obj} =} fromJSON (@var{str})
19## @deftypefnx {} {@var{obj} =} fromJSON (@var{str}, @var{sarray})
20## Convert a JSON string into a native Octave object.
21##
22## fromJSON especially strives to convert numerical JSON arrays into Octave
23## vector, matrix or ND array.  Special provisions are made to recognize
24## +/-Inf, NaN and complex numbers, which are conventionally not permited in
25## JSON strings.
26##
27## Input arguments:
28##
29## @itemize
30## @item
31## @var{str} is a JSON string.
32## @end item
33##
34## @item
35## @var{sarray}, (default TRUE) logical value that determines how JSON member
36## arrays are parsed.  Setting it to FALSE is recommended for safer parsing of
37## non-numerical, mixed-class or mixed-size arrays JSON payloads.
38##
39## Leave @var{sarray} set to TRUE for fast parsing of numerical JSON arrays and
40## objects.  Octave vectors, matrices, ND arrays and struct arrays are returned
41## as much as possible, otherwise returned as a combination of vectors/matrices
42## and cell arrays.
43##
44## Set @var{sarray} to FALSE if, with fast parsing, output does not match
45## expections, particularly if @var{str} mainly comprises JSON objects/arrays
46## with strings.   All JSON member arrays are returned as (nested) Octave cell
47## arrays.
48## @end item
49## @end itemize
50##
51## Output:
52##
53## @itemize
54## @item
55## @var{obj} is a native Octave object.  JSON number, logical, array, and
56## object strings are converted to Octave numbers, logicals, vectors and
57## structs, respectively.  Quoted or unrecognizable JSON fragments are returned
58## as NaN values.
59## @end itemize
60##
61## Special numbers:
62##
63## The specification for JSON does not allow +/-Inf, NaN or complex numbers;
64## nevertheless, provisions are made here to enable these important numbers:
65##
66## @itemize
67## @item
68## JSON number strings '+/-1e308' are rounded to +/-Inf.
69## @end item
70##
71## @item
72## Unquoted JSON string 'null' is converted to NaN.
73## @end item
74##
75## @item
76## JSON objects, or arrays thereof, with exclusive members "real" and "imag"
77## (or "re" and "im") will be converted to Octave complex numbers, vectors or
78## matrices, respectively.
79##
80## @example
81## @w{e.g. '@{"re":3,"im":1@}'     => 3 + 1i               }
82## @w{     '[1,@{"re":3,"im":1@}]' => [ 1 + 0i, 3 + 1i ]   }
83## @end example
84## @end item
85## @end itemize
86##
87## @seealso{toJSON}
88## @end deftypefn
89
90## Author: Ketan M. Patel <kmpatel@roc-photonics.com>
91## Created: 2019-06-01
92
93function [obj] = fromJSON ( str=' ', SARRAY=[] );
94
95  if ~( isnumeric(SARRAY) || isbool(SARRAY) );
96    _warn_("invalid SARRAY");
97    SARRAY = [];
98  endif
99
100  ## set defaults
101  ischar(str) && str || (str=' ');
102  ~isempty(SARRAY)   || (SARRAY=true);
103
104  str(1) > 126 && (str = str( find(str<127,1) : end ));   # drop leading encoding chars
105
106  wstate = warning('off',"Octave:num-to-str");
107  obj    = _fromjson_(str,SARRAY);
108  warning(wstate);
109
110endfunction
111
112function [obj, remain] = _fromjson_ ( str, SARRAY );
113
114    [btag, block, remain] = _get_block_(str);
115
116    switch( btag );
117      case '[';  obj = _to_array_  (block, SARRAY);
118      case '{';  obj = _to_struct_ (block, SARRAY);
119      case '"';  obj = _to_string_ (block, SARRAY);
120      otherwise; obj = _to_number_ (block, SARRAY);
121    endswitch
122
123    remain = regexprep(remain,'^[\s,]*',''); #!!! keep this HERE, not in _get_block_
124
125endfunction
126
127## gets block quoted-string/array/object block from string
128##   this will work with single-quoted strings, but only double-quoted strings are safe
129function [btag, block, remain] = _get_block_ (str);
130
131    [open, btag] = regexp(str,'[^\s,]','once','start','match');
132
133    switch( btag );     # find ALL bracket tag (open/close) positions
134
135      case '[';  idx = [strfind(str,'[') -strfind(str,']')];    # strfind is 100x faster than regexp
136      case '{';  idx = [strfind(str,'{') -strfind(str,'}')];
137
138      case {'"',"'"}; idx   = -strfind(str,btag); idx(1) *= -1; # all but first are closers
139                      btag  = '"';      # regard single-quote as double-quote (lazy JSON)
140
141      otherwise;  [block,remain] = strtok(str(open:end),',');
142                  return;
143    endswitch
144
145    idx( '\' == str([1 abs(idx(2:end))-1]) ) = [];      # exclude escaped-tags (e.g.'\[', '\{', '\"', etc)
146
147    ## filter out double-quoted (exclusively) block tags
148    ( btag ~= '"' && (q = strfind(str,'"')  )           # find all double-quote marks
149                  && (q = q(str(q-1) ~= '\')) > 1       # discard \", stop if only single " remains
150                  && (idx = _bfilter_(idx, q))    );    # filter out quoted btags
151
152    if ~( numel(idx) > 1 && ( close=-idx(2) ) > 0 );    # unless simple (non-nested) block...
153
154        idx   = idx([~,i] = sort(abs(idx)));  # sort idx by abs(idx)
155
156        if( ( ci=find(0 == cumsum(sign(idx)), 1) )  );
157          close = -idx(ci);
158        else
159          close = numel(str)+1;
160          _warn_(btag,str);
161        endif
162    end
163
164    block  = str(  open+1 : close-1 );
165    remain = str( close+1 : end     );
166
167endfunction
168
169function [arr] = _to_array_ ( str, SARRAY );
170
171    ok = SARRAY && ~(    regexp(str,'\[\s*\[','once')    # no ND array
172                      || regexp(str,':\s*[''"]','once')  # no struct array with strings
173                      || strfind(str,'=') );             # no apparent assignment op
174    if( ok );
175      arr = regexprep( _rep_(str, Inf), ',(?=\s*\[)',';');  # JSON to Octave array syntax
176
177      if( strfind(str,'{') );   # JSON object
178          arr = [ _lazyJSON_ "; arr=_deep_complex_([" _rep_(arr, struct) "])" ];
179      else                      # JSON numerical? array
180          arr = [ "arr=[" arr "]" ];
181      endif
182
183      eval([ arr "; ok = isnumeric(arr) || isstruct(arr);" ], "ok=false;");
184
185    endif
186
187    if( ~ok );   # this can be slow, avoid getting here if possible
188
189      arr = {};
190
191      while( str );  # grind thru string, one array element at a time
192          [arr{end+1} str] = _fromjson_(str,SARRAY);
193      endwhile;
194
195      if( SARRAY );
196          switch numel(arr);
197            case 0;     arr = [];
198            case 1;     arr = arr{1};
199            otherwise;  arr = _ndarray_(arr);
200          endswitch
201      endif
202
203    endif
204
205endfunction
206
207function [obj] = _to_struct_ ( str, SARRAY );
208
209    obj = struct();
210
211    ## we're making this harder than needed in order to permit lazy JSON
212    ## i.e. unquoted and single quote keys (but! start/end quote chars must match)
213
214    while( ( q=regexp(str, '[^\s,:]', 'once','match') )       # detect key quote-char or empty str
215       &&  ( m=regexp(str, _qrgx_(q), 'once','names') ).k );  # get key,val or fail
216
217      [obj.(m.k) str] = _fromjson_(m.v, SARRAY);
218
219    endwhile
220
221    regexp(str,'[^\s]','once') && _warn_('}', str);
222    obj = _complex_( obj );    # _deep_complex_ not necessary here
223
224endfunction
225
226function [str] = _to_string_ ( str, SARRAY );
227
228    if( SARRAY && (numel(str) > 2 && str(1:2) == '@') );
229        try    str = str2func(str(2:end));
230        catch  _warn_('@', str);
231        end_try_catch;
232    endif
233
234endfunction
235
236function [num] = _to_number_( str, SARRAY );
237
238    if( strfind(str,'=') );   # guard against assignment expr
239        num = 'error()';
240    else
241        num = ['num=[' _rep_(str,Inf) '];'];
242    endif
243
244    eval(num, ['_warn_("invalid frag",str); num=NaN;']);
245
246endfunction
247
248##========================= helpers =======================
249
250## filter out quoted block tags
251function [idx] = _bfilter_(idx,q);
252
253  qo = q(1:2:end-1)'; # open quote (ignore unclosed quote)
254  qc = q(2:2:end)';   # close quote
255
256  ai = abs(idx);
257  idx( any(qo < ai & ai < qc,1) ) = [];  # purge quoted block tags
258
259endfunction
260
261## replace string frags according to set pattern
262function [str] = _rep_ ( str, pat );
263
264  if isstruct(pat);
265      pat = {'{','struct(',':',',','}',')'};
266  else
267      pat = {'null','NaN ','e308','e999','Infinity','Inf     ',"\n",' '};  # keep empty spaces
268  endif
269
270  do
271    str = strrep(str,pat{1:2},'overlaps',false);
272    pat(1:2) = [];
273  until isempty(pat);
274
275endfunction
276
277## try to make ND array (either mat or cell)
278function [c] = _ndarray_ ( c );
279
280  sz = size(e = c{1}); t = class(e);
281
282  if ~( ~ischar(e) && sz && cellfun(@(v)isa(v,t)&&size(v)==sz, c) );
283      return;
284  elseif( isscalar(e) );     # try matrix conversion, ok to fail if not uniform class
285      try c = [c{:}]; end;
286  elseif( isvector(e) );     # cell array of mat or cell array
287      c = reshape([c{:}], numel(e), numel(c)).';
288  else                       # make ND array
289      index.type = "()";
290      index.subs = repmat({':'},1,ndims(e)+1);
291      i = numel(c);
292
293      do
294        index.subs{end} = i;
295        e = subsasgn(e,index,c{i});
296      until (--i) == 1;
297      c = e;
298  endif
299
300endfunction
301
302## convert complex number struct DEEP WITHIN structure to complex number
303function [s] = _deep_complex_(s);
304
305  if( isstruct(s) && isstruct( s=_complex_(s) ) );
306     vals = cellfun(@_deep_complex_, struct2cell(s), 'uniformoutput', false);
307     s    = cell2struct(vals, __fieldnames__(s));
308  endif
309
310endfunction
311
312## convert complex number struct to complex number
313function [s] = _complex_ ( s );
314
315  if( numel( keys=__fieldnames__(s) ) == 2
316  &&  ( ismember( k={'real','imag'}, keys) || ismember( k={'re','im'}, keys) )
317  &&  isnumeric( re=[s.(k{1})] )
318  &&  isnumeric( im=[s.(k{2})] )
319  &&  size_equal(re,im) );
320
321        s = reshape( complex(re,im), ifelse(isscalar(s), size(re), size(s)) );
322
323  endif
324
325endfunction
326
327## for max conversion speeed, gaurd against common lazy JSON (unquoted keys)
328function [str] = _lazyJSON_ ( );
329
330  str = 'x="x";y="y";z="z";real=re="re";imag=im="im";r="r";rho="rho";theta="theta";phi="phi";';
331
332endfunction
333
334## compute regexp pattern for JSON object key (quoted or unquoted)
335function [rgx] = _qrgx_(q);
336
337  sum(q=="\"'") || (q='');
338  rgx = ['[\s,]*' q '(?<k>.+?)' q '\s*:\s*(?<v>[^\s].*)' ];
339
340endfunction
341
342
343function [out] = _warn_ ( msg, frag='' );
344
345  if( numel(msg) == 1 );
346    switch(msg);
347       case '{'; msg = "unclosed object";
348       case '['; msg = "unclosed array";
349       case '"'; msg = "unclosed quote";
350       case '}'; msg = "malformed structure";
351                 regexp(lastwarn,'unclosed') || ( frag=[frag '}'] );
352       case '@'; msg = "invalid inline func string";
353     endswitch
354  endif
355
356  frag && ( frag=["`" frag "`"] );
357  numel(frag)>40 && (frag = [frag(1:27) ' ... ' frag(end-27:end)]);
358  out = warning("fromJSON: %s %s\n",msg,frag);
359
360endfunction
361
362
363################################################################################
364###############################   BIST   #######################################
365################################################################################
366
367%!test	##	input validation
368%! assert( fromJSON(),[]); % ok, reference
369%!warning <invalid SARRAY> fromJSON('',struct);
370%!
371
372%!test	##	invalid input validation
373%! bad = {4,{},@sin,struct(),'',false,0,[1,2,3]};
374%! assert(fromJSON(4),      []);
375%! assert(fromJSON({}),     []);
376%! assert(fromJSON(@sin),   []);
377%! assert(fromJSON(struct), []);
378%! assert(fromJSON(false),  []);
379%! assert(fromJSON([1,2,3]),[]);
380
381############### JSON number BIST #####################
382
383%!test	##	number
384%! assert( fromJSON('4'),4)
385
386%!test	##	number string
387%! assert( fromJSON('"string"'),"string")
388
389%!test	##	bool
390%! assert( fromJSON('true'),true)
391
392%!test	##	round up to Inf
393%! assert( fromJSON('1e308'),      Inf)
394%! assert( fromJSON('-1e308'),    -Inf)
395
396%!test  ## do NOT round up to Inf
397%! assert( fromJSON('1e307'),    1e307)
398%! assert( fromJSON('-1e307'),  -1e307)
399
400%!test  ## automatically parse as Inf
401%! assert( fromJSON('1e309'),      Inf)
402%! assert( fromJSON('-1e309')     -Inf)
403%! assert( fromJSON('1e999'),      Inf)
404%! assert( fromJSON('-1e999'),    -Inf)
405
406%!test	##	null
407%! assert( fromJSON( 'null' ),NaN)
408
409%!test  ## in case JSON spec commitee gets head out of ass
410%! assert( fromJSON('Inf'),        Inf)
411%! assert( fromJSON('-Inf'),      -Inf)
412%! assert( fromJSON('Infinity'),   Inf)
413%! assert( fromJSON('-Infinity'), -Inf)
414%! assert( fromJSON('NaN'),        NaN)
415%! assert( fromJSON('nan'),        NaN)
416
417%!test	##	quoted null,Inf,etc (guard against false-positive, leave alone)
418%! assert( fromJSON('"null"'),"null")
419%! assert( fromJSON('"Inf"'),"Inf")
420%! assert( fromJSON('"1e308"'),"1e308")
421
422%!test  ## all of the above in array form
423%! assert( fromJSON('[1,1e308,Inf,-Infinity,NaN,nan,null]'),
424%!         [1 Inf Inf -Inf NaN NaN,NaN])
425
426%!test	##	garbage (non-quoted, meaningless string)
427%!warning <invalid> assert( fromJSON('garbage'),   NaN)
428
429############### JSON aray BIST #####################
430
431%!test	##	empty array
432%! assert( fromJSON('[]',true),[]);
433
434%!test	##	empty cell array
435%! assert( fromJSON('[]',false),{});
436
437%!test	##	single element array
438%! assert( fromJSON('[1]',true),1);
439%! assert( fromJSON('[1]',false),{1});
440
441%!test	##	JSON array to Octave vector
442%! assert( fromJSON('[1,2,3,4]', true),
443%!         [1 2 3 4])
444
445%!test	##	JSON array to Octave vector
446%! assert( fromJSON('[[1],[2],[3],[4]]', true),
447%!         [1;2;3;4])
448
449%!test	##	JSON array to Octave cell array
450%! assert( fromJSON('[1,2,3,4]', false),
451%!         {1 2 3 4})
452
453%!test	##	JSON array to Octave vector
454%! assert( fromJSON('[[1],[2],[3],[4]]', false),
455%!         {{1},{2},{3},{4}})
456
457%!test	##	JSON nested array to Octave matrix
458%! assert( fromJSON('[[1,2],[3,4]]', true),
459%!         [1 2;3 4])
460
461%!test	##	JSON nested array to nested Octave cell array
462%! assert( fromJSON('[[1,2],[3,4]]', false),
463%!         {{1 2},{3 4}})
464
465%!test	##	JSON nested array to Octave ND array
466%! assert( fromJSON('[[[1,3,5],[2,4,6]],[[7,9,11],[8,10,12]]]', true),
467%!         reshape(1:12,2,3,2));
468
469%!test	##	numerical ND cell array
470%! assert( fromJSON('[[[1,3,5],[2,4,6]],[[7,9,11],[8,10,12]]]', false),
471%!         {{{1,3,5},{2,4,6}},{{7,9,11},{8,10,12}}});
472
473%!test	##	test default SARRAY
474%! assert( fromJSON('[[1,2],[3,4]]'),
475%!         fromJSON('[[1,2],[3,4]]',true))
476
477%!test	##	bool array
478%! assert( fromJSON('[true,false,false,true]'),
479%!         !![1 0 0 1])
480
481%!test	##	mixed bool and number array
482%! assert( fromJSON("[[true,3],[false,1]]"),
483%!         [1 3;0 1])
484
485%!test	##	more N numerical ND array
486%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
487%! assert( fromJSON(json),
488%!         reshape([1:8 11:18 21:28 31:38],2,2,2,2,2));
489
490%!test	##	mismatch nested array (mix of scalar and cell array)
491%! assert( fromJSON('[[1,2,3,4,5],[1,2]]', true),
492%!         {[1 2 3 4 5] [1 2]})
493
494%!test	##	more mismatched nested array
495%! assert( fromJSON('[1,2,3,[2,3]]'),
496%!         {1,2,3,[2,3]})
497
498%!test	##	array of numerical array and mixed-class array
499%! assert( fromJSON('[[1,2,3,"a"],[2,3,4,4]]', true),
500%!         {{1 2 3 "a"},[2 3 4 4]})
501
502%!test	##	mixed-class array
503%! assert( fromJSON('[["a",2,3],[4,"b",5]]', true),
504%!         {'a' 2 3; 4 'b' 5});
505
506%!test	##	mismatch nested array, safe parsing to cell array
507%! assert( fromJSON('[[1,2,3,4,5],[1,2]]', false),
508%!         {{1 2 3 4 5},{1 2}})
509
510%!test	##	mixed-class array, safe parsing to cell array
511%! assert( fromJSON('[["a",2,3],[4,"b",5]]', false),
512%!         {{'a' 2 3},{4 'b' 5}} );
513
514%!test	##	more N numerical ND array
515%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
516%! json = regexprep(json,'(\d+)','"$1"'); % turn it input JSON array of strings
517%! c    = cellfun(@num2str,num2cell(reshape([1:8 11:18 21:28 31:38],2,2,2,2,2)), 'uniformoutput', false);
518%! assert( fromJSON(json),c);
519
520%!test	##	JSON-like: with non-JSON, Octave, numerical notation (bonus feature)
521%! assert( fromJSON('[Inf,-Inf,NaN,2i,pi,e]'),
522%!         [Inf,-Inf,NaN,2*i,pi,e],1e-15);
523
524
525%!test	##	beautified JSON vector
526%! assert( fromJSON("\n[1,\n2\n]\n",true),
527%!         [1,2])
528
529%! assert( fromJSON("\n[1,\n2\n]\n",false),
530%!         {1,2})
531
532%!test	##	beautified JSON array
533%! assert( fromJSON("[\n  [\n    1,\n    2\n  ],\n  [\n    3,\n    4\n  ]\n]",true),
534%!         [[1 2];[3 4]])
535
536%!test	##	beautified JSON array
537%! assert( fromJSON("[\n  [\n    1,\n    2\n  ],\n  [\n    3,\n    4\n  ]\n]",false),
538%!         {{1,2},{3,4}})
539
540%!test	##	incomplete array
541%! warning('off','all');
542%! assert( fromJSON("[1,2,3  "),
543%!         [1,2,3]);
544
545%!test	##	more incomplete array
546%! warning('off','all');
547%! assert( fromJSON("[[1,2,3],[3"),
548%!         {[1,2,3],[3]});
549
550%!test	##	string with whitespaces
551%! assert( fromJSON("\"te\nss      df\t t\""),
552%!         "te\nss      df\t t")
553
554%!test	##	char array
555%! assert( fromJSON('["a","b","c"]'),
556%!         {'a','b','c'})
557
558%!test	##	array of string
559%! assert( fromJSON('["test","list","more"]'),
560%!         {'test',"list","more"})
561
562%!test	##	escaped quote
563%! assert( fromJSON ('"tes\"t"'),
564%!         'tes\"t');
565
566%!test	##	escaped quote in array
567%! assert( fromJSON('["te\"t","list","more"]'),
568%!         {'te\"t' 'list' 'more'})
569
570%!test	##	garbage in array
571%! warning('off','all');
572%! assert( fromJSON('[1,garbage]'),     [1,NaN])
573
574############### JSON object BIST #####################
575
576%!test	##	empty object to empty struct
577%! assert( fromJSON('{}',true), struct())
578%! assert( fromJSON('{}',false),struct())
579%! assert( fromJSON('{ }',false),struct())
580
581%!test	##	struct to object
582%! assert( fromJSON('{"a":3,"b":5}',true),
583%!         struct("a",3,"b",5))
584
585%!test	##	unclosed object
586%!warning <unclosed>    assert(fromJSON('{a:3,b:4'),struct("a",3,"b",4));
587
588%!test	##	string with colons (guards against JSON object false postive)
589%! assert( fromJSON('["2012-11-12T10:35:32Z","2012-11-13T08:35:12Z"]'),
590%!        {"2012-11-12T10:35:32Z", "2012-11-13T08:35:12Z"});
591
592%!test	##	string with {} (guards against JSON object false postive)
593%! assert( fromJSON('["some text {hello:3}","more text \"{hello:3}\""]'),
594%!        {'some text {hello:3}', 'more text \"{hello:3}\"'});
595
596%!test	##	lazy JSON object (unquoted and single-quote keys)
597%! assert( fromJSON("{a:3,'b':5}",true),
598%!         struct("a",3,"b",5))
599
600%!test	##	bad (but passable) lazy JSON object, unquoted-key with quote char at end
601%! assert( fromJSON("{a':3,b\":5}",true),
602%!         struct("a'",3,'b"',5))
603
604%!test	## bad (failing) JSON object (mismatched or unclosed quoted-keys)
605%!warning <malformed> assert( fromJSON("{'a  :3}"), struct());
606%!warning <malformed> assert( fromJSON("{'a\":3}"), struct());
607%!warning <malformed> assert( fromJSON("{\"a :3}"), struct());
608%!warning <malformed> assert( fromJSON("{\"a':3}"), struct());
609
610%!test	##	keys (quoted and unquoted) with whitespace
611%! assert( fromJSON('{"a":3,"   key with space  ": 4,  more white space  : 40}'),
612%!         struct("a",3,"   key with space  ",4,"more white space",40))
613
614%!test	##	object with duplicate key
615%! assert( fromJSON('{"a":3,"a":5}'),
616%!         struct("a",5))
617
618%!test	##	empty object key-val
619%! assert( fromJSON('{a:3,,,    ,     b   :5}'),
620%!         struct("a",3,"b",5))
621
622%!test	##	object with object
623%! assert( fromJSON('{a:{b:2}}', true),
624%!         struct('a',struct('b',2)));
625
626%!test	##	object with object with object
627%! assert( fromJSON('{a:{b:{c:3,d:4},e:5}}', true),
628%!         struct('a',struct('b',struct('c',3,'d',4),'e',5)));
629
630%!test	##	object with array to struct with array
631%! assert( fromJSON('{a:[1,2,3,4]}', true),
632%!         struct('a',[1 2 3 4]));
633
634%!test	##	object with array to struct with cell array
635%! assert( fromJSON('{a:[1,2,3,4]}', false),
636%!         struct('a',{{1 2 3 4}}));
637
638%!test	##	array of objects to struct array
639%! assert( fromJSON('[{a:1},{a:2},{a:3},{a:4}]',true),
640%!         struct('a',{1 2 3 4}));
641
642%!test	##	array of objects to cell array
643%! assert( fromJSON('[{a:1},{a:2},{a:3},{a:4}]',false),
644%!         num2cell(struct('a',{1 2 3 4})));
645
646%!test	##	object with 2x2 array to struct with 2x2 array
647%! assert( fromJSON('{a:[[1,3],[2,4]]}', true),
648%!         struct('a',[1 3;2 4]));
649
650%!test	##	object with 2x2 array to struct with cell array
651%! assert( fromJSON('{a:[[1,3],[2,4]]}', false),
652%!         struct('a',{{{1,3},{2,4}}}));
653
654%!test	##	2x2 array of object to struct array
655%! assert( fromJSON('[[{a:1},{a:2}],[{a:3},{a:4}]]',true),
656%!         struct('a',{1 2;3 4}));
657
658%!test	##	2x2 array of object to cell array
659%! assert( fromJSON('[[{a:1},{a:2}],[{a:3},{a:4}]]',false),
660%!         { num2cell(struct('a',{1 2})), num2cell(struct('a',{3 4}))} );
661
662%!test	##	**nested** object with array to struct array
663%! assert( fromJSON('{"a":{"b":[1,2,3]}}',true ),
664%!         struct("a",num2cell(struct("b",[1,2,3]))))  % <== struct array
665
666%!test	##	**nested** object with array to cell array
667%! assert( fromJSON('{"a":{"b":[1,2,3]}}',false),
668%!         struct("a",struct("b",{{1,2,3}})))          % <== struct with array b
669
670%!test	##	object with mixed-size arrays (will not honor SARRAY=true)
671%! assert( fromJSON('{"a":[1,2],"b":[3,4,5]}',true),
672%!         struct("a",[1,2],"b",[3,4,5]))
673
674%!test	##	object with number and string (guard against turing string into char array)
675%! assert( fromJSON('{"a":1,"b":"hello"}',true),
676%!         struct("a",1,"b","hello"))
677
678%!test	##	object with empty array  (gaurd against returning empty struct array)
679%! assert( fromJSON('{"a":3,"b":[]}',true),
680%!         struct("a",3,"b",[]))
681
682%!test	##	object with colon value
683%! assert( fromJSON('{"time": "10:35:00"}'),
684%!         struct("time", "10:35:00"));
685
686%!test	##	object with colon key
687%! assert( fromJSON('{"ti:me": "10:35:00", ":tricky:" : 4}'),
688%!         struct("ti:me", "10:35:00",":tricky:",4));
689
690%!test	##	array of structure with value with colons
691%! assert( fromJSON('[{"time": "10:35"},{"time": "10:54"}]', true),
692%!         struct("time", {"10:35","10:54"}));
693
694%!test	##	array of structure with value with  keys
695%! assert( fromJSON('[{"ti:me": "10:35"},{"ti:me": "10:54"}]', true),
696%!         struct("ti:me", {"10:35","10:54"}));
697
698%!test	##	incomplete struct
699%!warning <malformed> assert( fromJSON('{a:3,b   }', true),
700%!                            struct('a',3));
701
702%!test	##	incomplete struct
703%!warning <malformed> assert( fromJSON('{a:3,b:   }', true),
704%!                            struct('a',3));
705
706
707%!test	##	struct with mixed class array SARRAY=true
708%! assert( fromJSON('{"b":[1,2,{c:4}]}',true),
709%!         struct('b',{{1,2,struct('c',4)}}));
710
711%!test	##	struct with mixed class array SARRAY=false
712%! assert( fromJSON('{"b":[1,2,{c:4}]}',false),
713%!         struct('b',{{1,2,struct('c',4)}}));
714
715%!test	##	2x2 array of struct
716%! assert( fromJSON('[[{a:1},{a:3}],[{a:2},{a:4}]]'),
717%!         struct('a',{1 3;2 4}));
718
719%!test	##	ND array of struct to ND struct array
720%! assert( fromJSON('[[[{a:1},{a:3}],[{a:2},{a:4}]],[[{a:11},{a:13}],[{a:12},{a:14}]]]'),
721%!         struct('a',num2cell(reshape([1:4 11:14],2,2,2))));
722
723%!test	##	mixed array with struct
724%! assert( fromJSON('[2,{a:3,"b":5}]'),
725%!         {2,struct("a",3,"b",5)})
726
727%!test	##	more mixed array with struct
728%! assert( fromJSON('[{a:3,"b":5},{a:3}]'),
729%!         {struct("a",3,"b",5),struct("a",3)})
730
731%!test	##	garbage in struct
732%! warning('off','all');
733%! assert( fromJSON('{a:garbage}'),  struct('a', NaN));
734
735######### complex number JSON object BIST ################
736
737%!test	##	complex number object
738%! assert( fromJSON('{"re":3,"im":5}'),     3+5i);
739%! assert( fromJSON('{"real":3,"imag":5}'), 3+5i);
740
741%!test	##	complex number lazy notation object
742%! assert( fromJSON('{re:3,im:5}'), 3+5i);
743%! assert( fromJSON('{real:3,imag:5}'), 3+5i);
744
745%!test	##	fake complex number object
746%! assert( fromJSON('{"re": "hello","im":5}'),
747%!         struct('re',"hello",'im',5));
748
749%!test	##	complex number in reverse order
750%! assert( fromJSON('{im:3,re:5}'), 5+3i);
751
752%!test	##	complex with excess members
753%! assert( fromJSON('{im:3,re:5,re:7,im:10}'),7+10i);
754
755%!test	##	complex number object with mismatch re/im and real/imag
756%! assert( fromJSON('{real:3,im:5}'),
757%!         struct('real',3,'im',5));
758
759%!test	##	apparent complex number object with extra member
760%! assert( fromJSON('{re:3,im:5,t:3}'),
761%!         struct('re',3,'im',5,'t',3));
762
763%!test	##	array of complex number object  to scalar array of complex number
764%! assert( fromJSON('[{re:4,im:6},{re:5,im:7}]}',true),
765%!         [4+6i,5+7i] );
766
767%!test	##	array of complex number object  to cell array of complex number
768%! assert( fromJSON('[{re:4,im:6},{re:5,im:7}]}',false),
769%!         {4+6i,5+7i} );
770
771%!test	##	complex number object with array to scalar array of complex number
772%! assert( fromJSON('{re:[4,5],im:[6,7]}',true),
773%!         [4+6i,5+7i] );
774
775%!test	##	complex number object with array to struct with cell array
776%! assert( fromJSON('{re:[4,5],im:[6,7]}',false),
777%!         struct("re",{{4,5}},"im",{{6,7}}));
778
779%!test	##	complex number object with mismatch array sizes
780%! assert( fromJSON('{re:[4,5],im:[6]}',true),
781%!         struct('re',[4,5],'im',6));
782
783%!test	##	complex number object with mismatch array sizes
784%! assert( fromJSON('{re:[4,5],im:[6]}',true),
785%!         struct('re',[4,5],'im',6));
786
787%!test	##	bogus complex number object with mixed classed array
788%! assert( fromJSON('{re:[4,5],im:[6,"a"]}',true),
789%!         struct('re',[4,5],'im',{{6,'a'}}));
790
791%!test	##	array of bogus complex number object
792%! assert( fromJSON('[{re:4,im:6},{re:5,im:"a"}]',true),
793%!         {4+6i,struct('re',5,'im','a')});
794
795%!test	##	complex number object with matrix
796%! assert( fromJSON('[{re:[4,5;0 2],im:[6,7;3 0]}]'),
797%!         [4+6i,5+7i;3i 2]);
798
799%!test	##	array of complex number object
800%! assert( fromJSON('[[{re:4,im:6},{re:1,im:0}],[{re:0,im:2}, {re:2,im:4}]]'),
801%!         [4+6i 1;2i 2+4i]);
802
803%!test	##	complex number object with ND array
804%! assert( fromJSON('{re:[[[1,1],[2,2]],[[3,3],[4,4]]],im:[[[1,1],[2,2]],[[3,3],[4,4]]]}', true),
805%!        reshape( (1+i)*[1 2 1 2 3 4 3 4], 2,2,2));
806
807%!test  ##  ND array of complex number objects
808%! assert( fromJSON('[[[{"re":1,"im":1},{"re":1,"im":1}],[{"re":2,"im":2},{"re":2,"im":2}]],[[{"re":3,"im":3},{"re":3,"im":3}],[{"re":4,"im":4},{"re":4,"im":4}]]]'),
809%!        reshape( (1+i)*[1 2 1 2 3 4 3 4], 2,2,2));
810
811%!test	##	array of objects with complex number member
812%! assert( fromJSON('[{"a": {re:4, im:6}},{"a": {re:5,im:7}}]'),
813%!         struct('a',{4+6i,5+7i}) );
814
815%!test	##	ND array objects with complex number members
816%! obj = struct('a',{4+6i,5+7i;4+6i,5+7i});
817%! obj(:,:,2) = obj;
818%! assert( fromJSON('[[[{"a": {re:4, im:6}},{"a": {re:5,im:7}}],[{"a": {re:4, im:6}},{"a": {re:5,im:7}}]],[[{"a": {re:4, im:6}},{"a": {re:5,im:7}}],[{"a": {re:4, im:6}},{"a": {re:5,im:7}}]]]'),
819%!         obj );
820
821%!test	##	array of objects with DEEP complex number member
822%! assert( fromJSON('[{"a": {"b": {re:4, im:6}}},{"a": {re:5,im:7}}]'),
823%!         [struct('a',struct('b',4+6i)),struct('a',5+7i)] );
824
825%!test	##	mismatch array (to cell array) of objects with DEEP complex number member
826%! assert( fromJSON('[{"a":{"b":{re:4, im:6}}}, {"c":{"d":{"e":{re:5,im:7}}}}]'),
827%!         {struct('a',struct('b',4+6i)),struct('c',struct('d',struct('e',5+7i)))} );
828
829%!test  # array of complex number in algebraic format (extracircular parsing)
830%! assert( fromJSON('[[4+6i, 1+0i], [2i, 4i + 2]]'),
831%!         [4+6i 1;2i 2+4i]);
832
833%!test	##	complex number object mixed with number within array (test array parsing)
834%! assert( fromJSON('[[4,{re:1,im:0}],[{re:0,im:2}, {re:4,im:6}]]'),
835%!         [4 1;2i 4+6i]);
836
837%!test	##	complex number object within a structure (nested)
838%! assert( fromJSON('{"a": {re:1,im:5}}'),
839%!         struct('a',1+5i));
840
841%!test	##	complex number object with structure array (nested)
842%! assert( fromJSON('[{"a":{re:1,im:5}},{"a":{re:2,im:5}}]'),
843%!         struct('a',{1+5i,2+5i}));
844
845%!test	##	complex number object deeply nested in object
846%! assert( fromJSON('{a:1,b:{c:2,d:{e:3,f:{re:3,im:5}}}}'),
847%!         struct('a',1,'b',struct('c',2,'d',struct('e',3,'f',3+5i))));
848
849############## file encoding error ######################
850###### simulate parsing with non-ASCII character#########
851
852%!test  ##  leading file-encoding bytes (simulated with emoji)
853%! assert( fromJSON('��4'), 4);    %  just ignore it, likely some file encoding taken in by fread
854
855%!test  ##  inside arrray
856%!warning <invalid> assert( fromJSON('[��,4]'), [NaN, 4]); % convert to NaN
857
858%!test  ##  inside struct
859%!warning <invalid> assert( fromJSON('{"a":��4}'), struct('a',NaN));  % convert to NaN
860
861%!test  ##  inside quotes
862%! assert( fromJSON('"��"'), '��');     % SMILE! not invalid
863
864############### exotic JSON BIST #####################
865
866%!test  ## test octave inline fn  (convert to inline)
867%! assert(fromJSON('"@@sin"'),@sin);
868%! assert(func2str(fromJSON('"@@(x)3*x"')),func2str(@(x)3*x));
869
870%!test  ## test apparent octave inline fn  (do NOT convert to inline)
871%! assert(fromJSON('"@sin"'), '@sin');
872%! assert(fromJSON('"@(x)3*x"'),'@(x)3*x');
873
874%!test  ## test octave inline fn with SARRAY=false (do NOT convert otherwise valid inline)
875%! assert(fromJSON('"@@sin"', false), '@@sin');
876%! assert(fromJSON('"@@(x)3*x"', false),'@@(x)3*x');
877
878%!test	##	exotic object in structure
879%! assert( fromJSON('{"a":"[java.math.BigDecimal]"}'),struct('a','[java.math.BigDecimal]'));
880
881%!test	##	exotic object (placeholder of class name)
882%! assert( fromJSON('"[java.math.BigDecimal]"'),
883%!         '[java.math.BigDecimal]');
884
885############### beautified or confusing JSON BIST #####################
886
887%!test	##	JSON with confusing '[]{},' AND missing quotes (hard string parse test)
888%! warning('off','all');
889%! obj=fromJSON('[{a:"tes, {}: [ ] t"},"lkj{} sdf",im mi{}ing quotes]');
890%! assert(obj,{struct('a',"tes, {}: [ ] t"), 'lkj{} sdf',NaN})
891
892%!test	##	beautified JSON object (extraneous whitespace/newline)
893%! assert( fromJSON("\n  {  \n\t\"a\"\t\n\n\n  :\t\n\n\n  3\n\t} \n \n"),
894%!         struct("a",3))
895
896%!test	##	beautified JSON string with array (test parse with extraneous whitespace)
897%! pretty_json ="{\n  \"data\": [\n    {\n      \"vendor\": \"0x10de\",\n      \"details\": {\n        \"created\": \"2012-11-12T10:35:32Z\"\n      },\n      \"_status\": \"synced\"\n    }\n  ]\n}";
898%!
899%! obj = struct('data', ...
900%!          struct('vendor','0x10de', ...
901%!                 'details', struct('created','2012-11-12T10:35:32Z'), ...
902%!                 '_status', 'synced'
903%!           )
904%!       );
905%!
906%! assert( fromJSON(pretty_json),obj,true);
907
908%!test	##	garbage string with whitespace
909%!warning <invalid> assert( fromJSON("[1, gar bage]"),[1,NaN])
910
911%!test	##	garbage string with newline
912%!warning <invalid> assert( fromJSON("[1, \n gar \n bage]"),[1,NaN])
913
914%!test	##	garbage string with quotes
915%!warning <invalid> assert( fromJSON('[1,gar""bage]'),[1,NaN])
916
917%!test	##	garbage string with array brackets
918%!warning <invalid> assert( fromJSON('[1,gar[]bage]'),[1,NaN])
919
920%!test	##	garbage string with object brackets
921%!warning <invalid> assert( fromJSON('[1,{"a":gar{}bage}]'),{1,struct('a',NaN)})
922
923%!test  ## looks like fn, but is UNQUOTED string
924%!warning <invalid>   assert(fromJSON('@@nofunc'),NaN);
925
926########## complex (non-numerical) JSON BIST ################
927
928%!test  ## a JSON string stored inside JSON string object
929%! json = "[         \n  {                \n  \"plot\": [      \n      {            \n      \"line1\": { \n         \"x\": [  \n             1,     \n            3      \n          ],       \n         \"y\": [  \n            12,    \n            32     \n          ]        \n        }          \n      }            \n    ]              \n  }                \n]";
930%! assert( fromJSON(json,true),
931%!         struct('plot',struct('line1',struct('x',[1,3],'y',[12,32]))));
932
933%!test  ## jsondecode's Arrays with the same field names in the same order.
934%! json = '[{"x_id":"5ee28980fc9ab3","index":0,"guid":"b229d1de-f94a","latitude":-17.124067,"longitude":-61.161831,"friends":[{"id":0,"name":"Collins"},{"id":1,"name":"Hays"},{"id":2,"name":"Griffin"}]},{"x_id":"5ee28980dd7250","index":1,"guid":"39cee338-01fb","latitude":13.205994,"longitude":-37.276231,"friends":[{"id":0,"name":"Osborn"},{"id":1,"name":"Mcdowell"},{"id":2,"name":"Jewel"}]},{"x_id":"5ee289802422ac","index":2,"guid":"3db8d55a-663e","latitude":-35.453456,"longitude":14.080287,"friends":[{"id":0,"name":"Socorro"},{"id":1,"name":"Darla"},{"id":2,"name":"Leanne"}]}]';
935%!
936%! f1 = num2cell(struct ('id', {0 1 2}, 'name', {'Collins' 'Hays' 'Griffin'}));
937%! f2 = num2cell(struct ('id', {0 1 2}, 'name', {'Osborn' 'Mcdowell' 'Jewel'}));
938%! f3 = num2cell(struct ('id', {0 1 2}, 'name', {'Socorro' 'Darla' 'Leanne'}));
939%!
940%! keys = {'x_id','index','guid','latitude','longitude','friends'};
941%! val1 = {'5ee28980fc9ab3', 0, 'b229d1de-f94a', -17.124067, -61.161831, f1};
942%! val2 = {'5ee28980dd7250', 1, '39cee338-01fb', 13.205994,  -37.276231, f2};
943%! val3 = {'5ee289802422ac', 2, '3db8d55a-663e', -35.453456,  14.080287, f3};
944%!
945%! exp = cell2struct({val1{:};val2{:};val3{:}}',keys)';
946%! fromJSON(json);     # this may be confusing mix of array and non-cell array, but NO fail allowed
947%! assert (fromJSON(json, false), num2cell(exp));
948
949
950%!test  ## from firefox active-stream.discovery_stream.json (UGH!  url link as object key)
951%!
952%! json = '[{"feeds":{"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale&region=$region&count=30":{"lastUpdated":1640020188467,"data":{"settings":{"domainAffinityParameterSets":{"default":{"recencyFactor":0.5,"frequencyFactor":0.5,"combinedDomainFactor":0.5,"perfectFrequencyVisits":10,"perfectCombinedDomainScore":2,"multiDomainBoost":0,"itemScoreFactor":1}}}}}}}]';
953%! exp  = struct("feeds",
954%!          struct('https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale&region=$region&count=30',
955%!             struct("lastUpdated",1640020188467,
956%!                    "data",
957%!                     struct("settings",
958%!                             struct("domainAffinityParameterSets",
959%!                                struct("default",
960%!                                  struct("recencyFactor",0.5,
961%!                                      "frequencyFactor",0.5,
962%!                                      "combinedDomainFactor",0.5,
963%!                                      "perfectFrequencyVisits",10,
964%!                                      "perfectCombinedDomainScore",2,
965%!                                      "multiDomainBoost",0,
966%!                                      "itemScoreFactor",1
967%!                                  )
968%!                            )
969%!                    )
970%!              )
971%!            )
972%!          )
973%!        );
974%! assert (fromJSON(json, true),   exp );
975%! assert (fromJSON(json, false), {exp});
976
977