ig-serialize 1.2.0 → 1.3.0

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 -10
  4. package/test.js +4 -4
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.0",
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
@@ -94,6 +104,29 @@ module.MIN_LENGTH_REF = REFERENCE.length * 16
94
104
  // -> str
95
105
  //
96
106
  //
107
+ // options format:
108
+ // {
109
+ // // Indent to use for formatting output.
110
+ // // Can be:
111
+ // // <number> - number of spaces to use for indent
112
+ // // <string> - string to use for indent
113
+ // // NOTE: only whitespace characters are supported
114
+ // // currently.
115
+ // indent: undefined,
116
+ //
117
+ // // Top level indent.
118
+ // depth: 0,
119
+ //
120
+ // // Minimum length of string/bigint to reference.
121
+ // min_length_ref: MIN_LENGTH_REF,
122
+ //
123
+ // // Stored functions.
124
+ // // If set to an array functions will be pushed to it and stored
125
+ // // by index.
126
+ // functions: undefined,
127
+ // }
128
+ //
129
+ //
97
130
  // Paths
98
131
  // A path is a chain of indexes/keys leading to a specific object in
99
132
  // tree.
@@ -144,6 +177,8 @@ module.MIN_LENGTH_REF = REFERENCE.length * 16
144
177
  var _serialize =
145
178
  module._serialize =
146
179
  function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
180
+ if(typeof(indent) == 'number'){
181
+ indent = ' '.repeat(indent) }
147
182
  var min_length_ref =
148
183
  options.min_length_ref
149
184
  ?? module.MIN_LENGTH_REF
@@ -212,11 +247,13 @@ function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
212
247
  if(obj instanceof Array){
213
248
  pre = '['
214
249
  post = ']'
250
+ elems.length = obj.length
215
251
  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) }
252
+ if(i in obj){
253
+ elems[i] = _serialize(obj[i], [...path, i], seen, indent, depth+1, options) } }
254
+ // add trailing coma if last elem is empty...
255
+ if(!(elems.length-1 in elems)){
256
+ elems.length++ }
220
257
  } else if(obj instanceof Map){
221
258
  pre = 'Map(['
222
259
  post = '])'
@@ -260,6 +297,11 @@ function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
260
297
  var serialize =
261
298
  module.serialize =
262
299
  function(obj, indent, depth=0, options){
300
+ if(indent
301
+ && typeof(indent) == 'object'){
302
+ options = indent
303
+ indent = options.indent
304
+ depth = options.depth ?? 0 }
263
305
  return _serialize(obj, [], new Map(), indent, depth, options) }
264
306
 
265
307
 
@@ -340,8 +382,13 @@ module.eJSON = {
340
382
  && i == path.length-1){
341
383
  return [path.slice(0, -1), obj] }
342
384
 
343
- obj = obj instanceof Set ?
385
+ obj =
386
+ // special case: string key -> attribute...
387
+ typeof(k) == 'string' ?
388
+ obj[k]
389
+ : obj instanceof Set ?
344
390
  [...obj][k]
391
+ // takes two items off the path -- [intex, key/value]...
345
392
  : obj instanceof Map ?
346
393
  [...obj][k][path[++i]]
347
394
  : obj[k] }
@@ -365,6 +412,12 @@ module.eJSON = {
365
412
  setItem: function(obj, path, value){
366
413
  var [p, parent] = this._getItem(obj, path.slice(0, -1))
367
414
  var k = path.at(-1)
415
+
416
+ // special case: string kye -> attr...
417
+ if(typeof(k) == 'string'){
418
+ parent[k] = value
419
+ return value }
420
+
368
421
  if(parent instanceof Set){
369
422
  var elems = [...parent]
370
423
  elems[k] = value
@@ -695,7 +748,12 @@ module.eJSON = {
695
748
  return this[handler](state, path, match, str, i, line) },
696
749
 
697
750
 
698
- parse: function(str, options={}){
751
+ parse: function(str, options){
752
+ options =
753
+ options === true ?
754
+ {functions: true}
755
+ : (options
756
+ ?? {})
699
757
 
700
758
  // stage 1: build the object...
701
759
  var state = {functions: options.functions}
@@ -715,10 +773,6 @@ module.eJSON = {
715
773
  var deserialize =
716
774
  module.deserialize =
717
775
  function(str, options){
718
- options =
719
- options === true ?
720
- {functions: true}
721
- : options
722
776
  return eJSON.parse(str, options) }
723
777
 
724
778
 
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] },