fxf 1.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. checksums.yaml +7 -0
  2. data/README.md +190 -0
  3. data/lib/fxf.rb +467 -0
  4. metadata +45 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 70f8912e081de135c77935749f2b329a76f3db905f0c19fdac9aee2b3c1aef30
4
+ data.tar.gz: e92108b05650908867a92ee460e604bf4664ce3fc3f5e3e4132b70da6b112dce
5
+ SHA512:
6
+ metadata.gz: 0401c245c79b49771cabb9bf9d4e21b64fd375972aac13236dd9f0540bcecbe5a2a393588a6cd29336d3835a98afec46f6dc53da3f731a68952b532e00ddf0be
7
+ data.tar.gz: becc4252056063906a4e06815cb9c2c36f82b9f4fbfb23e623ad45e74d09ead403770e10344478aff39af007cb007d151eeb1df9fda32f7a7be8708ee878b9d7
@@ -0,0 +1,190 @@
1
+ # FXF
2
+
3
+ FXF is a JSON structure for serializing interconnected data. It also allows for
4
+ serialization of objects of arbitrary classes.
5
+
6
+ Native JSON only allows for hierarchical data. For example, the following
7
+ structure serializes to JSON quite nicely.
8
+
9
+ ```text
10
+ hsh = {}
11
+ hsh['mary'] = {'name'=>'Mary'}
12
+ hsh['fred'] = {'name'=>'Fred'}
13
+ ```
14
+
15
+ That produces JSON like this:
16
+
17
+ ```text
18
+ {
19
+ "mary": {
20
+ "name": "Mary"
21
+ },
22
+ "fred": {
23
+ "name": "Fred"
24
+ }
25
+ }
26
+ ```
27
+
28
+ However, if you add interconnections to the data you start getting repeated data
29
+ in the JSON. For example, consider this structure in which the hash for Mary
30
+ includes a reference to the hash for Fred.
31
+
32
+ ```text
33
+ hsh = {}
34
+ hsh['mary'] = {'name'=>'Mary'}
35
+ hsh['fred'] = {'name'=>'Fred'}
36
+ hsh['mary']['friend'] = hsh['fred']
37
+ ```
38
+
39
+ In that case you get this structure with redundant information:
40
+
41
+ ```text
42
+ {
43
+ "mary": {
44
+ "name": "Mary",
45
+ "friend": {
46
+ "name": "Fred"
47
+ }
48
+ },
49
+ "fred": {
50
+ "name": "Fred"
51
+ }
52
+ }
53
+ ```
54
+
55
+ The situation gets worse if data references itself. For example, if the JSON
56
+ module tries to implement this structure, an error results.
57
+
58
+ ```text
59
+ hsh = {}
60
+ hsh['mary'] = {'name'=>'Mary'}
61
+ hsh['fred'] = {'name'=>'Fred'}
62
+ hsh['mary']['self'] = hsh['mary']
63
+ ```
64
+
65
+ That gives us this nesting error:
66
+
67
+ ```text
68
+ Traceback (most recent call last):
69
+ 2: from ./json.rb:39:in `<main>'
70
+ 1: from /usr/lib/ruby/2.5.0/json/common.rb:286:in `pretty_generate'
71
+ /usr/lib/ruby/2.5.0/json/common.rb:286:in `generate': nesting of 100 is too deep (JSON::NestingError)
72
+ ```
73
+
74
+ FXF preserves the original structure of the object without any difficulties with
75
+ redundant or self-nested objects. To generate an FXF string, simply call
76
+ FXF.generate. In these examples we add `'pretty'=>true` for readability.
77
+
78
+ ```text
79
+ fxf = FXF.generate(hsh, 'pretty'=>true)
80
+ ```
81
+
82
+ So the structure from the previous example would be serialized with this
83
+ (admittedly not very human readable) JSON structure:
84
+
85
+ ```text
86
+ {
87
+ "root": "47283390982220",
88
+ "objects": {
89
+ "47283390982220": {
90
+ "mary": "47283390982160",
91
+ "fred": "47283390982120"
92
+ },
93
+ "47283390982160": {
94
+ "name": "47283390982180",
95
+ "self": "47283390982160"
96
+ },
97
+ "47283390982180": "Mary",
98
+ "47283390982120": {
99
+ "name": "47283390982140"
100
+ },
101
+ "47283390982140": "Fred"
102
+ }
103
+ }
104
+ ```
105
+
106
+ To parse that string back into a structure, use `FXF.parse`.
107
+
108
+ ```text
109
+ parsed = FXF.parse(fxf)
110
+ ```
111
+
112
+ The resulting parsed data has the same structure as the original.
113
+
114
+ ```text
115
+ puts parsed['mary'] == parsed['mary']['self'] # true
116
+ ```
117
+
118
+ ## Custom classes
119
+
120
+ FXF can serialize data so that the class of the object is preserved. Classes
121
+ must be defined in such a way that their objects can be exported to FXF format,
122
+ then imported again from that format. Doing so requires implementing the
123
+ instance method `to_fxf` and the class method `from_fxf`. Consider this class.
124
+
125
+ ```ruby
126
+ class MyClass
127
+ attr_reader :pk
128
+
129
+ def initialize(pk)
130
+ @pk = pk
131
+ end
132
+
133
+ def to_fxf
134
+ return {'pk'=>@pk}
135
+ end
136
+
137
+ def self.from_fxf(details)
138
+ return self.new(details['pk'])
139
+ end
140
+ end
141
+ ```
142
+
143
+ The `to_fxf` method returns a hash with information necessary to recreate the
144
+ object. In this case just the `pk` property is required.
145
+
146
+ When the object is deserialized, that information is passed to the class'
147
+ `from_fxf` method. Note that that is a method of the class itself, so it needs
148
+ to be defined with `self.from_fxf`. The method is given a hash of the same
149
+ information that was exported from `to_fxf`. In this case, `from_fxf` uses that
150
+ hash to create a new `MyClass` object using the primary key.
151
+
152
+ ## Standard classes
153
+
154
+ FXF can export two standard classes without the need for any additional
155
+ modifications to them: DateTime and URI::HTTPS. More common classes will be
156
+ added as FXF is developed.
157
+
158
+ In this example, we create a `DateTime` object and a `URI::HTTPS` object.
159
+
160
+ ```ruby
161
+ hsh = {}
162
+ hsh['timestamp'] = DateTime.parse('Jan 5, 2017, 7:18 am')
163
+ hsh['uri'] = URI('https://www.example.com')
164
+ fxf = FXF.generate(hsh, 'pretty'=>true)
165
+ ```
166
+
167
+ Identical objects are created when the FXF string is parsed.
168
+
169
+ ```ruby
170
+ parsed = FXF.parse(fxf)
171
+ puts parsed['timestamp'].class # DateTime
172
+ puts parsed['uri'].class # URI::HTTPS
173
+ ```
174
+
175
+ ## Install
176
+
177
+ ```
178
+ gem install fxf
179
+ ```
180
+
181
+ ## Author
182
+
183
+ Mike O'Sullivan
184
+ mike@idocs.com
185
+
186
+ ## History
187
+
188
+ | version | date | notes |
189
+ |---------|--------------|-------------------------------|
190
+ | 1.0 | Jan 27, 2020 | Initial upload. |
@@ -0,0 +1,467 @@
1
+ require 'json'
2
+
3
+
4
+ #===============================================================================
5
+ # FXF
6
+ #
7
+ module FXF
8
+ # version 1.0
9
+ VERSION = '1.0'
10
+
11
+ #---------------------------------------------------------------------------
12
+ # generate
13
+ #
14
+
15
+ # Generates a FXF JSON string.
16
+
17
+ def self.generate(struct, opts={})
18
+ # $tm.hrm
19
+ rv = {}
20
+ self.generate_object(rv, struct)
21
+ rv = {'root'=>rv.keys[0], 'objects'=>rv}
22
+
23
+ # return JSON
24
+ if opts['pretty']
25
+ return JSON.pretty_generate(rv)
26
+ else
27
+ return JSON.generate(rv)
28
+ end
29
+ end
30
+ #
31
+ # generate
32
+ #---------------------------------------------------------------------------
33
+
34
+
35
+ #---------------------------------------------------------------------------
36
+ # parse
37
+ #
38
+
39
+ # Parses an FXF string and returns the structure it defines.
40
+
41
+ def self.parse(fxf)
42
+ parser = FXF::Parser.new(fxf)
43
+ return parser.parse()
44
+ end
45
+ #
46
+ # parse
47
+ #---------------------------------------------------------------------------
48
+
49
+
50
+ # private
51
+ private
52
+
53
+
54
+ #---------------------------------------------------------------------------
55
+ # generate_object
56
+ #
57
+
58
+ # Scalar object types. These are all the data types in JSON except for
59
+ # hashes and arrays.
60
+ SCALARS = [String, Integer, Float, TrueClass, FalseClass, NilClass]
61
+
62
+ def self.generate_object(rv, obj)
63
+ # $tm.hrm
64
+ key = obj.object_id.to_s
65
+
66
+ # add to rv
67
+ if not rv.has_key?(key)
68
+ # scalar
69
+ if SCALARS.include?(obj.class)
70
+ rv[key] = obj
71
+
72
+ # hash
73
+ elsif obj.is_a?(Hash)
74
+ hsh = rv[key] = {}
75
+
76
+ obj.each do |k, v|
77
+ hsh[k] = self.generate_object(rv, v)
78
+ end
79
+
80
+ # array
81
+ elsif obj.is_a?(Array)
82
+ arr = rv[key] = []
83
+
84
+ obj.each do |v|
85
+ arr.push self.generate_object(rv, v)
86
+ end
87
+
88
+ # to_fxf
89
+ elsif obj.respond_to?('to_fxf')
90
+ dfn = {}
91
+ dfn['class'] = obj.class.to_s
92
+ dfn['details'] = obj.to_fxf()
93
+ object_dfn rv, key, 'custom', dfn
94
+
95
+ # standard class
96
+ elsif translator = FXF::Standard::TO_FXF[obj.class.to_s]
97
+ dfn = translator.call(obj)
98
+ object_dfn rv, key, 'standard', dfn
99
+
100
+ # else unknown class
101
+ else
102
+ # build details
103
+ details = {}
104
+
105
+ # string
106
+ if obj.respond_to?('to_s')
107
+ details['str'] = obj.to_s
108
+ end
109
+
110
+ # buid definition
111
+ dfn = {}
112
+ dfn['scope'] = 'unrecognized'
113
+ dfn['class'] = obj.class.to_s
114
+ dfn['details'] = details
115
+
116
+ # store
117
+ rv[key] = [dfn]
118
+ end
119
+ end
120
+
121
+ # return
122
+ return key
123
+ end
124
+ #
125
+ # generate_object
126
+ #---------------------------------------------------------------------------
127
+
128
+
129
+ #---------------------------------------------------------------------------
130
+ # object_dfn
131
+ #
132
+ def self.object_dfn(rv, key, scope, dfn)
133
+ # $tm.hrm
134
+ export = {}
135
+ rv[key] = [export]
136
+ export['scope'] = scope
137
+ export['class'] = dfn['class']
138
+ export['details'] = {}
139
+
140
+ # add object references
141
+ if dfn['details']
142
+ if dfn['details'].is_a?(Hash)
143
+ dfn['details'].each do |k, v|
144
+ export['details'][k] = self.generate_object(rv, v)
145
+ end
146
+ else
147
+ raise 'custom-object-details-must-be-hash'
148
+ end
149
+ end
150
+ end
151
+ #
152
+ # object_dfn
153
+ #---------------------------------------------------------------------------
154
+ end
155
+ #
156
+ # FXF
157
+ #===============================================================================
158
+
159
+
160
+ #===============================================================================
161
+ # FXF::Standard
162
+ #
163
+
164
+ # This module defines procs for serializing and deserializing DateTime and
165
+ # URI::HTTPS objects.
166
+
167
+ module FXF::Standard
168
+ TO_FXF = {}
169
+ FROM_FXF = {}
170
+
171
+ #---------------------------------------------------------------------------
172
+ # DateTime
173
+ #
174
+ TO_FXF['DateTime'] = proc do |obj|
175
+ dfn = {}
176
+ dfn['class'] = 'datetime'
177
+ dfn['details'] = {'str'=>obj.to_s}
178
+ dfn
179
+ end
180
+
181
+ FROM_FXF['datetime'] = proc do |dfn|
182
+ # $tm.hrm
183
+ DateTime.parse(dfn.details['str'])
184
+ end
185
+ #
186
+ # DateTime
187
+ #---------------------------------------------------------------------------
188
+
189
+
190
+ #---------------------------------------------------------------------------
191
+ # URI
192
+ #
193
+ TO_FXF['URI::HTTPS'] = proc do |obj|
194
+ dfn = {}
195
+ dfn['class'] = 'uri'
196
+ dfn['details'] = {'str'=>obj.to_s}
197
+ dfn
198
+ end
199
+
200
+ FROM_FXF['uri'] = proc do |dfn|
201
+ URI(dfn.details['str'])
202
+ end
203
+ #
204
+ # URI
205
+ #---------------------------------------------------------------------------
206
+ end
207
+ #
208
+ # FXF::Standard
209
+ #===============================================================================
210
+
211
+
212
+ #===============================================================================
213
+ # FXF::Parser
214
+ #
215
+
216
+ # `FXF::Parser` isn't intended to be used directly. FXF.parse creates a
217
+ # `FXF::Parser` and calls its `parse` method.
218
+
219
+ class FXF::Parser
220
+ #---------------------------------------------------------------------------
221
+ # initialize
222
+ #
223
+ def initialize(raw)
224
+ @raw = raw
225
+ end
226
+ #
227
+ # initialize
228
+ #---------------------------------------------------------------------------
229
+
230
+
231
+ #---------------------------------------------------------------------------
232
+ # parse
233
+ #
234
+ def parse()
235
+ @full = JSON.parse(@raw)
236
+ @org = @full['objects']
237
+ @found = {}
238
+ @collections = []
239
+ rv = self.build_object(@full['root'])
240
+
241
+ # look for object place holders
242
+ placeholder_mod()
243
+
244
+ # return
245
+ return rv
246
+ end
247
+ #
248
+ # parse
249
+ #---------------------------------------------------------------------------
250
+
251
+
252
+ #---------------------------------------------------------------------------
253
+ # placeholder_mod
254
+ #
255
+ def placeholder_mod
256
+ # $tm.hrm
257
+ placeholders = []
258
+ phs_to_obj = {}
259
+
260
+ # build unique list of placeholders
261
+ @collections.each do |collection|
262
+ if collection.is_a?(Array)
263
+ collection.each do |el|
264
+ placeholder_add placeholders, el
265
+ end
266
+ elsif collection.is_a?(Hash)
267
+ collection.values.each do |el|
268
+ placeholder_add placeholders, el
269
+ end
270
+ end
271
+ end
272
+
273
+ # build a hash of placeholders to objects
274
+ placeholders.each do |ph|
275
+ # standard
276
+ if ph.scope == 'standard'
277
+ if translator = FXF::Standard::FROM_FXF[ph.clss]
278
+ phs_to_obj[ph] = translator.call(ph)
279
+ else
280
+ raise 'do-not-have-standard-translator: ' + ph.clss
281
+ end
282
+
283
+ # custom
284
+ elsif ph.scope == 'custom'
285
+ if Module.const_defined?(ph.clss)
286
+ clss = Module.const_get(ph.clss)
287
+
288
+ if clss.respond_to?('from_fxf')
289
+ phs_to_obj[ph] = clss.from_fxf(ph.details)
290
+ else
291
+ raise 'class-does-not-have-from-fxf: ' + ph.clss
292
+ end
293
+ else
294
+ raise 'unknown-custom-class: ' + ph.clss
295
+ end
296
+
297
+ # else don't know how to build this object
298
+ else
299
+ phs_to_obj[ph] = ph
300
+ end
301
+ end
302
+
303
+ # substitute placeholders to objects
304
+ @collections.each do |collection|
305
+ if collection.is_a?(Array)
306
+ collection.map! do |el|
307
+ rv = nil
308
+
309
+ if phs_to_obj.has_key?(el)
310
+ rv = phs_to_obj[el]
311
+ else
312
+ rv = el
313
+ end
314
+
315
+ rv
316
+ end
317
+
318
+ elsif collection.is_a?(Hash)
319
+ collection.each do |k, v|
320
+ if phs_to_obj.has_key?(v)
321
+ collection[k] = phs_to_obj[v]
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+ #
328
+ # placeholder_mod
329
+ #---------------------------------------------------------------------------
330
+
331
+
332
+ #---------------------------------------------------------------------------
333
+ # placeholder_add
334
+ #
335
+ def placeholder_add(placeholders, el)
336
+ # $tm.hrm
337
+
338
+ # if placeholder, and not in placeholders array, add to array
339
+ if el.is_a?(FXF::Parser::ObjectHolder) and (not placeholders.include?(el))
340
+ placeholders.push el
341
+ end
342
+ end
343
+ #
344
+ # placeholder_add
345
+ #---------------------------------------------------------------------------
346
+
347
+
348
+ #---------------------------------------------------------------------------
349
+ # build_object
350
+ #
351
+ def build_object(key)
352
+ # $tm.hrm
353
+ rv = nil
354
+
355
+ # if already found
356
+ if @found.has_key?(key)
357
+ return @found[key]
358
+ end
359
+
360
+ # get object definition
361
+ dfn = @org.delete(key)
362
+
363
+ # scalar
364
+ if FXF::SCALARS.include?(dfn.class)
365
+ @found[key] = dfn
366
+ return dfn
367
+
368
+ # array
369
+ elsif dfn.is_a?(Array)
370
+ # if object description
371
+ if dfn[0].is_a?(Hash)
372
+ details = {}
373
+ build_hash(dfn[0]['details'], details)
374
+ dfn = FXF::Parser::ObjectHolder.new dfn[0]['scope'], dfn[0]['class'], details
375
+ @found[key] = dfn
376
+ return dfn
377
+ else
378
+ @found[key] = []
379
+ @collections.push @found[key]
380
+ return build_array(dfn, @found[key])
381
+ end
382
+
383
+ # hash
384
+ elsif dfn.is_a?(Hash)
385
+ @found[key] = {}
386
+ @collections.push @found[key]
387
+ return build_hash(dfn, @found[key])
388
+
389
+ # else unknown
390
+ else
391
+ puts 'unknown: ' + dfn.class.to_s
392
+ exit
393
+ end
394
+
395
+ # return
396
+ return rv
397
+ end
398
+ #
399
+ # build_object
400
+ #---------------------------------------------------------------------------
401
+
402
+
403
+ #---------------------------------------------------------------------------
404
+ # build_hash
405
+ #
406
+ def build_hash(dfn, hsh)
407
+ # loop through elements
408
+ dfn.each do |key, ref|
409
+ hsh[key] = build_object(ref)
410
+ end
411
+
412
+ # return
413
+ return hsh
414
+ end
415
+ #
416
+ # build_hash
417
+ #---------------------------------------------------------------------------
418
+
419
+
420
+ #---------------------------------------------------------------------------
421
+ # build_array
422
+ #
423
+ def build_array(dfn, arr)
424
+ # loop through elements
425
+ dfn.each do |ref|
426
+ arr.push build_object(ref)
427
+ end
428
+
429
+ # return
430
+ return arr
431
+ end
432
+ #
433
+ # build_array
434
+ #---------------------------------------------------------------------------
435
+ end
436
+ #
437
+ # FXF::Parser
438
+ #===============================================================================
439
+
440
+
441
+ #===============================================================================
442
+ # FXF::Parser::ObjectHolder
443
+ #
444
+
445
+ # Objects of this class are placeholders for objects that have been serialized.
446
+ # This class is used by FXF::Parser and is not meant to be used directly.
447
+
448
+ class FXF::Parser::ObjectHolder
449
+ attr_reader :scope
450
+ attr_reader :clss
451
+ attr_reader :details
452
+
453
+ #---------------------------------------------------------------------------
454
+ # initialize
455
+ #
456
+ def initialize(scope, clss, details)
457
+ @scope = scope
458
+ @clss = clss
459
+ @details = details
460
+ end
461
+ #
462
+ # initialize
463
+ #---------------------------------------------------------------------------
464
+ end
465
+ #
466
+ # FXF::Parser::ObjectHolder
467
+ #===============================================================================
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fxf
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Mike O'Sullivan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A JSON implementation for non-hierarchical data and for arbitrary objects
14
+ email: mike@idocs.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/fxf.rb
21
+ homepage: https://rubygems.org/gems/fxf
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.7.6
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Serializer for tangled data and objects
45
+ test_files: []