fxf 1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []