ig-serialize 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +217 -13
  2. package/package.json +1 -1
  3. package/serialize.js +64 -20
  4. package/test.js +4 -9
package/README.md CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  This extends the default JSON serialization adding the following:
4
4
  - Recursive data structure serialization
5
+ - Sparse array serialization
5
6
  - `undefined`/`NaN` serialization
6
- - `Set`/`Map` serialization
7
+ - Serialization of `Infinity`, `BigInt`'s, `Set`'s, `Map`'s
7
8
  - Function serialization (off by default)
8
9
  - Deep and partial-deep cleen object copy
9
10
 
10
11
  Possible differences to JSON output:
11
- - Repeating long strings and BigInts can be referenced instead of
12
- reincluded in the output.
12
+ - Repeating long strings and BigInts are referenced by default instead of
13
+ being reincluded in the output.
14
+
13
15
 
14
16
 
15
17
  ## Motivation
@@ -23,6 +25,7 @@ and tooling design, basic parsing, among others.
23
25
 
24
26
  ## Installation
25
27
 
28
+ For basic use:
26
29
  ```shell
27
30
  $ npm install ig-serilaize
28
31
  ```
@@ -30,6 +33,10 @@ $ npm install ig-serilaize
30
33
  Or just download and drop [serialize.js](serialize.js) into your code.
31
34
 
32
35
 
36
+ ```javascript
37
+ serialize = require('ig-serialize')
38
+ ```
39
+
33
40
 
34
41
  ## Introduction
35
42
 
@@ -53,12 +60,106 @@ Thus, care must be taken when serializing structures containing function.
53
60
 
54
61
  ### `serialize(..)` / `eJSON.stringify(..)`
55
62
 
56
- ### `deserialize(..)` / 'eJSON.parse(..)'
63
+ Serialize a JavaScript value into a JSON/eJSON string.
64
+ ```
65
+ serialize(<value>)
66
+ eJSON.stringify(<value>)
67
+ -> <string>
68
+ ```
69
+
70
+ More control:
71
+ ```
72
+ serialize(obj, options){
73
+ serialize(obj, indent, depth=0, options){
74
+ -> <string>
75
+ ```
76
+
77
+ Options format:
78
+ ```
79
+ {
80
+ // pretty-printing indent...
81
+ // (default: undefined)
82
+ indent: undefined,
83
+
84
+ // outout root indent...
85
+ // (default: 0)
86
+ depth: 0,
87
+
88
+ // minimal referenced string/bigint length...
89
+ // (default: MIN_LENGTH_REF)
90
+ min_length_ref: MIN_LENGTH_REF,
91
+
92
+ // functions list...
93
+ // (default: undefined)
94
+ functions: undefined,
95
+ }
96
+ ```
97
+
98
+ Supported options:
99
+ - `indent` controls formatting and nested value indent, if set to a number
100
+ that number of spaces will be used to indent nested values if given a
101
+ string that string is used for indenting, note that only whitespace is
102
+ supported currently.
103
+ Default: `undefined` (disabled)
104
+ - `depth` if given is a number of `indent`'s, used to set top level indent
105
+ depth of the returned string, this can be useful when pretty-printing
106
+ or nesting the output.
107
+ Default: `0`
108
+ - `min_length_ref` sets the minimal length of a string or big-int value
109
+ for referencing when encountered repeatedly.
110
+ If set to `0` or `Infinity` referencing of strings and big-ints will
111
+ be is disabled.
112
+ Default: 'MIN_LENGTH_REF'
113
+ - `functions` if passed an array, encounterd functions will be pushed to
114
+ it and stored in the output by index.
115
+ Default: `undefined`
116
+
117
+
118
+ ### `deserialize(..)` / `eJSON.parse(..)`
119
+
120
+ Deserialize a JSON/eJSON into a value.
121
+ ```
122
+ deserialize(<string>)
123
+ eJSON.parse(<string>)
124
+ -> <value>
125
+ ```
126
+
127
+ Deserializing function is disabled by default as it can be a security
128
+ risk if the eJSON came from an untrusted source.
129
+
130
+ Enable function deserialization:
131
+ ```
132
+ deserialize(<string>, true)
133
+ eJSON.parse(<string>, true)
134
+ deserialize(<string>, {functions: true})
135
+ eJSON.parse(<string>, {functions: true})
136
+ -> <value>
137
+ ```
138
+
139
+ Passing a function list (generated by `serialize(<value>, {functions: <functions>})`)
140
+ for deserialization:
141
+ ```
142
+ deserialize(<string>, {functions: <functions>})
143
+ eJSON.parse(<string>, {functions: <functions>})
144
+ -> <value>
145
+ ```
146
+
57
147
 
58
148
  ### `deepCopy(..)`
59
149
 
150
+ ```
151
+ deepCopy(<value>)
152
+ -> <value>
153
+ ```
154
+
155
+
60
156
  ### `partialDeepCopy(..)`
61
157
 
158
+ ```
159
+ partialDeepCopy(<value>)
160
+ -> <value>
161
+ ```
162
+
62
163
 
63
164
  ### `MIN_LENGTH_REF` / `<options>.min_length_ref`
64
165
 
@@ -79,23 +180,108 @@ Default: 96
79
180
  The output of `.serialize(..)` is a strict superset of [standard JSON](https://www.json.org/json-en.html),
80
181
  while the input format is a bit more relaxed than in several details.
81
182
 
82
- Extensions to JSON:
83
- - Recursion
84
- - undefined / NaN
85
- - BigInt
86
- - Map / Set
87
- - Function
88
183
 
89
184
  ### Structural paths
90
185
 
91
- ### Recursion
186
+ Paths are used for internal references in cases when objects are
187
+ encountered multiple times, e.g. in recursion.
188
+
189
+ A path is an array of keys, the meaning of each key depends on the data
190
+ structure traversed:
191
+ - array -> number
192
+ - object -> string
193
+ - set -> number -- item order in set
194
+ - map -> pair of numbers -- the first indicates item order the second
195
+ if 0 selects the key, if 1 selects the value.
196
+
197
+ Note that string path items are unambiguous and are always treated as
198
+ attributes.
199
+
200
+ An empty path indicates the root object.
201
+
202
+
203
+ ### Referencing
204
+
205
+ If an object is encountered for a second time it will be serialized as
206
+ a reference by path to the first occurrence.
207
+
208
+ Format:
209
+ ```bnf
210
+ <ref> ::=
211
+ '<REF' <path> '>'
212
+
213
+ <path> ::=
214
+ '[' <path-items> ']'
215
+
216
+ <path-items> ::=
217
+ <item>
218
+ | <item> ',' <path-items>
219
+
220
+ <item> ::=
221
+ <number>
222
+ | <string>
223
+ ```
224
+
225
+ Example:
226
+ ```javascript
227
+ // a recursive array...
228
+ var o = []
229
+ o.o = o
230
+
231
+ // root object reference...
232
+ serialize(o) // -> '[<REF[]>]'
233
+
234
+ // array item...
235
+ serialize([o]) // -> '[[<REF[0]>]]'
236
+
237
+ // set item...
238
+ // NOTE: the path here is the same as in the above example -- since we
239
+ // use ordered topology for paths sets do not differ from arrays.
240
+ serialize(new Set([o])) // -> 'Set([[<REF[0]>]])'
241
+
242
+ // map key...
243
+ serialize(new Map([[o, 'value']])) // -> 'Map([[[<REF[0,0]>],"value"]])'
244
+
245
+ // map value...
246
+ serialize(new Map([['key', o]])) // -> 'Map([["key",[<REF[0,1]>]]])'
247
+ ```
92
248
 
93
- If an object is encountered
94
249
 
95
- ### null types
250
+ ### Null types
251
+
252
+ In addition to `null`, serialize.js adds support for `undefined` and
253
+ `NaN` which are stored as-is.
254
+
255
+ Example:
256
+ ```javascript
257
+ serialize([null, undefined, NaN]) // -> '[null,undefined,NaN]'
258
+ ```
259
+
260
+ ### Sparse arrays
261
+
262
+ Sparse arrays are represented in the same way JavaScript handles them
263
+ syntactically.
264
+
265
+ Example:
266
+ ```javascript
267
+ serialize([,]) // -> '[,]'
268
+ serialize([,,]) // -> '[,,]'
269
+ ```
270
+
271
+ Note that trailing commas are ignored in the same way JavaScript ignores
272
+ them:
273
+ ```javascript
274
+ // trailing comma...
275
+ serialize([1,]) // -> '[1]'
276
+
277
+ // sparse element...
278
+ serialize([1,,]) // -> '[1,,]'
279
+ ```
96
280
 
97
281
  ### BigInt
98
282
 
283
+ ### Infinity
284
+
99
285
  ### Map / Set
100
286
 
101
287
  ### Functions
@@ -103,6 +289,24 @@ If an object is encountered
103
289
 
104
290
 
105
291
 
292
+ ## Running tests
293
+
294
+ Get the development dependencies:
295
+ ```shell
296
+ $ npm install -D
297
+ ```
298
+
299
+ Run the tests:
300
+ ```shell
301
+ $ npm test
302
+ ```
303
+
304
+ To run the tests directly:
305
+ ```shell
306
+ $ node ./test.js
307
+ ```
308
+
106
309
 
107
310
 
108
311
 
312
+ <!-- vim:set nowrap : -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ig-serialize",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "experimental extended json serializaion...",
5
5
  "main": "serialize.js",
6
6
  "scripts": {
package/serialize.js CHANGED
@@ -8,6 +8,16 @@
8
8
  * - get/set object by path
9
9
  * Object.get(..) / Object.set(..)
10
10
  *
11
+ * XXX try removing paths from refs and use the same indexes, the same
12
+ * system as with functions...
13
+ * + this will make the refs shorter
14
+ * - the deserialization would require the same "seen" logic as
15
+ * serialization -- reuse a function
16
+ * - the "seen" logic can depend on settings, i.e. would either
17
+ * require a sable algorithm that would be settings-agnostic
18
+ * a-la paths (preferred), or a way to transfer serialization
19
+ * settings...
20
+ *
11
21
  * XXX the current path implementation is fully complient to the task at
12
22
  * hand but it will not suite the diff task as there is no way to know
13
23
  * the meaning of the path element without seeing the object (map/set
@@ -44,7 +54,6 @@
44
54
  /*********************************************************************/
45
55
 
46
56
 
47
- var EMPTY = '<empty>'
48
57
  var NULL = 'null'
49
58
  var UNDEFINED = 'undefined'
50
59
  var NAN = 'NaN'
@@ -94,6 +103,29 @@ module.MIN_LENGTH_REF = REFERENCE.length * 16
94
103
  // -> str
95
104
  //
96
105
  //
106
+ // options format:
107
+ // {
108
+ // // Indent to use for formatting output.
109
+ // // Can be:
110
+ // // <number> - number of spaces to use for indent
111
+ // // <string> - string to use for indent
112
+ // // NOTE: only whitespace characters are supported
113
+ // // currently.
114
+ // indent: undefined,
115
+ //
116
+ // // Top level indent.
117
+ // depth: 0,
118
+ //
119
+ // // Minimum length of string/bigint to reference.
120
+ // min_length_ref: MIN_LENGTH_REF,
121
+ //
122
+ // // Stored functions.
123
+ // // If set to an array functions will be pushed to it and stored
124
+ // // by index.
125
+ // functions: undefined,
126
+ // }
127
+ //
128
+ //
97
129
  // Paths
98
130
  // A path is a chain of indexes/keys leading to a specific object in
99
131
  // tree.
@@ -144,6 +176,8 @@ module.MIN_LENGTH_REF = REFERENCE.length * 16
144
176
  var _serialize =
145
177
  module._serialize =
146
178
  function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
179
+ if(typeof(indent) == 'number'){
180
+ indent = ' '.repeat(indent) }
147
181
  var min_length_ref =
148
182
  options.min_length_ref
149
183
  ?? module.MIN_LENGTH_REF
@@ -212,11 +246,13 @@ function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
212
246
  if(obj instanceof Array){
213
247
  pre = '['
214
248
  post = ']'
249
+ elems.length = obj.length
215
250
  for(var i=0; i < obj.length; i++){
216
- elems.push(
217
- i in obj ?
218
- _serialize(obj[i], [...path, i], seen, indent, depth+1, options)
219
- : EMPTY) }
251
+ if(i in obj){
252
+ elems[i] = _serialize(obj[i], [...path, i], seen, indent, depth+1, options) } }
253
+ // add trailing coma if last elem is empty...
254
+ if(!(elems.length-1 in elems)){
255
+ elems.length++ }
220
256
  } else if(obj instanceof Map){
221
257
  pre = 'Map(['
222
258
  post = '])'
@@ -260,6 +296,11 @@ function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
260
296
  var serialize =
261
297
  module.serialize =
262
298
  function(obj, indent, depth=0, options){
299
+ if(indent
300
+ && typeof(indent) == 'object'){
301
+ options = indent
302
+ indent = options.indent
303
+ depth = options.depth ?? 0 }
263
304
  return _serialize(obj, [], new Map(), indent, depth, options) }
264
305
 
265
306
 
@@ -340,8 +381,13 @@ module.eJSON = {
340
381
  && i == path.length-1){
341
382
  return [path.slice(0, -1), obj] }
342
383
 
343
- obj = obj instanceof Set ?
384
+ obj =
385
+ // special case: string key -> attribute...
386
+ typeof(k) == 'string' ?
387
+ obj[k]
388
+ : obj instanceof Set ?
344
389
  [...obj][k]
390
+ // takes two items off the path -- [intex, key/value]...
345
391
  : obj instanceof Map ?
346
392
  [...obj][k][path[++i]]
347
393
  : obj[k] }
@@ -365,6 +411,12 @@ module.eJSON = {
365
411
  setItem: function(obj, path, value){
366
412
  var [p, parent] = this._getItem(obj, path.slice(0, -1))
367
413
  var k = path.at(-1)
414
+
415
+ // special case: string kye -> attr...
416
+ if(typeof(k) == 'string'){
417
+ parent[k] = value
418
+ return value }
419
+
368
420
  if(parent instanceof Set){
369
421
  var elems = [...parent]
370
422
  elems[k] = value
@@ -505,15 +557,6 @@ module.eJSON = {
505
557
  initial instanceof Array
506
558
  && initial.length++
507
559
  continue }
508
- if(str.slice(i, i+EMPTY.length) == EMPTY){
509
- index++
510
- i += EMPTY.length
511
- if(str[i] == ','){
512
- i++ }
513
- // XXX this feels hackish -- can this be deligated to the handler???
514
- initial instanceof Array
515
- && initial.length++
516
- continue }
517
560
 
518
561
  // end of input...
519
562
  if(i >= str.length-1){
@@ -695,7 +738,12 @@ module.eJSON = {
695
738
  return this[handler](state, path, match, str, i, line) },
696
739
 
697
740
 
698
- parse: function(str, options={}){
741
+ parse: function(str, options){
742
+ options =
743
+ options === true ?
744
+ {functions: true}
745
+ : (options
746
+ ?? {})
699
747
 
700
748
  // stage 1: build the object...
701
749
  var state = {functions: options.functions}
@@ -715,10 +763,6 @@ module.eJSON = {
715
763
  var deserialize =
716
764
  module.deserialize =
717
765
  function(str, options){
718
- options =
719
- options === true ?
720
- {functions: true}
721
- : options
722
766
  return eJSON.parse(str, options) }
723
767
 
724
768
 
package/test.js CHANGED
@@ -82,13 +82,13 @@ var setups = test.Setups({
82
82
  'array-empty': function(assert){
83
83
  return ['[]', json] },
84
84
  'array-sparse-a': function(assert){
85
- return ['[<empty>]'] },
85
+ return ['[,]'] },
86
86
  'array-sparse-b': function(assert){
87
- return ['["a",<empty>]'] },
87
+ return ['["a",,]'] },
88
88
  'array-sparse-c': function(assert){
89
- return ['[<empty>,"a"]'] },
89
+ return ['[,"a"]'] },
90
90
  'array-sparse-d': function(assert){
91
- return ['[<empty>,1,<empty>,<empty>,2,<empty>]'] },
91
+ return ['[,1,,,2,,]'] },
92
92
 
93
93
  'object-empty': function(assert){
94
94
  return ['{}', json] },
@@ -228,11 +228,6 @@ test.Cases({
228
228
 
229
229
  // arrays...
230
230
  ['[1,2,]', '[1,2]'],
231
-
232
- // sparse arrays...
233
- ['[,]', '[<empty>]'],
234
- ['[1,2,,]', '[1,2,<empty>]'],
235
- ['[1,2,<empty>,]', '[1,2,<empty>]'],
236
231
  ],
237
232
  'syntax-simplifications': function(assert){
238
233
  var aa, bb