firm 0.9.7 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +9 -8
- data/lib/firm/serialize/core.rb +10 -2
- data/lib/firm/serializer/json.rb +142 -78
- data/lib/firm/serializer/xml.rb +90 -71
- data/lib/firm/serializer/yaml.rb +7 -2
- data/lib/firm/version.rb +1 -1
- data/tests/serializer_tests.rb +104 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ebb1f6594b3ecc4e56863a0f04ce3eb44fc5bfad0218a183298b44e56e9cf8e
|
4
|
+
data.tar.gz: 4e2349b45ae0479c968406fcd2eafb7ac4e76c8317cbbeeea20ecb784e998115
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 247ca7e7c9eb24fc5ca31c8cfb3f06ec9e4c3779627f3e35722d2c51a630c255cc4d182d434d35da7207f112b0905635a5717fd767f3c904a251b7ab84873cbd
|
7
|
+
data.tar.gz: 2953c510c88ed9078f777a0c8e23399f48339ef5f6c85fcf264fe025de795ecc187407d7d7bddf77b89dbb182eb158123e62aad909c91fbfffc04f613cf8280f
|
data/README.md
CHANGED
@@ -8,11 +8,11 @@
|
|
8
8
|
|
9
9
|
## Introduction
|
10
10
|
|
11
|
-
FIRM is a pure Ruby library that works across different Ruby implementations like MRI Ruby and JRuby providing
|
12
|
-
|
11
|
+
FIRM is a pure Ruby library that works across different Ruby implementations like MRI Ruby and JRuby providing format
|
12
|
+
independent object (de-)serialization support.
|
13
13
|
|
14
14
|
FIRM is explicitly **NOT** intended as a non-discriminative marshaling library (dumping any object's attributes)
|
15
|
-
but rather as structured and safe serialization library requiring users to think about what state they want
|
15
|
+
but rather as a structured and safe serialization library requiring users to think about what state they want
|
16
16
|
persisted (and possibly in what form) and what not.
|
17
17
|
Straightforward attribute serialization is simple with minimal intrusion on user code.
|
18
18
|
In addition various customization options are available to tweak (de-)serialization for a perfect fit if needed.
|
@@ -39,20 +39,21 @@ FIRM supports (de-)serializing many core Ruby objects out of the box including:
|
|
39
39
|
- `Time`
|
40
40
|
- `Struct`
|
41
41
|
- `Set`
|
42
|
-
- `OpenStruct`
|
42
|
+
- `OpenStruct` (optional starting from Ruby 3.5 which removes this class from the standard library)
|
43
43
|
- `Date`
|
44
44
|
- `DateTime`
|
45
45
|
|
46
|
-
For security reasons FIRM does **not** support direct (de-)serializing of `Class` objects but will rather
|
46
|
+
For simplicity and security reasons FIRM does **not** support direct (de-)serializing of `Class` objects but will rather
|
47
47
|
serialize (and deserialize) these as their scoped string names. Customized property setters can be used to
|
48
48
|
resolve Class objects from these names if really needed.
|
49
49
|
|
50
|
-
|
50
|
+
Serialization support for user defined classes is available through a simple DSL scheme.
|
51
51
|
|
52
52
|
FIRM provides object aliasing support for JSON and XML in a similar fashion as the standard support provided
|
53
|
-
by YAML
|
53
|
+
by YAML.<br>
|
54
|
+
In addition FIRM automatically recognizes and handles cyclic references of aliasable objects.
|
54
55
|
|
55
|
-
FIRM also
|
56
|
+
FIRM serialization is also thread safe and supports re-entrancy (i.e. nested serialization).
|
56
57
|
|
57
58
|
## Installing FIRM
|
58
59
|
|
data/lib/firm/serialize/core.rb
CHANGED
@@ -32,12 +32,20 @@ module FIRM
|
|
32
32
|
end
|
33
33
|
|
34
34
|
require 'set'
|
35
|
-
|
35
|
+
# from Ruby 3.5.0 OpenStruct will not be available by default anymore
|
36
|
+
begin
|
37
|
+
require 'ostruct'
|
38
|
+
rescue LoadError
|
39
|
+
end
|
36
40
|
|
37
|
-
[::Array, ::Hash, ::Struct, ::Range, ::Rational, ::Complex, ::Regexp, ::Set, ::
|
41
|
+
[::Array, ::Hash, ::Struct, ::Range, ::Rational, ::Complex, ::Regexp, ::Set, ::Time, ::Date, ::DateTime].each do |c|
|
38
42
|
c.include FIRM::Serializable::CoreExt
|
39
43
|
end
|
40
44
|
|
45
|
+
if ::Object.const_defined?(:OpenStruct)
|
46
|
+
::OpenStruct.include FIRM::Serializable::CoreExt
|
47
|
+
end
|
48
|
+
|
41
49
|
if ::Object.const_defined?(:BigDecimal)
|
42
50
|
::BigDecimal.include FIRM::Serializable::CoreExt
|
43
51
|
end
|
data/lib/firm/serializer/json.rb
CHANGED
@@ -14,7 +14,12 @@ require 'json/add/bigdecimal' if ::Object.const_defined?(:BigDecimal)
|
|
14
14
|
require 'json/add/rational'
|
15
15
|
require 'json/add/complex'
|
16
16
|
require 'json/add/set'
|
17
|
-
|
17
|
+
# from Ruby 3.5.0 OpenStruct will not be available by default anymore
|
18
|
+
begin
|
19
|
+
require 'ostruct'
|
20
|
+
require 'json/add/ostruct'
|
21
|
+
rescue LoadError
|
22
|
+
end
|
18
23
|
|
19
24
|
module FIRM
|
20
25
|
|
@@ -22,6 +27,8 @@ module FIRM
|
|
22
27
|
|
23
28
|
module JSON
|
24
29
|
|
30
|
+
CREATE_ID = 'rbklass'.freeze
|
31
|
+
|
25
32
|
# Derived Hash class to use for deserialized JSON object data which
|
26
33
|
# supports using Symbol keys.
|
27
34
|
class ObjectHash < ::Hash
|
@@ -44,21 +51,71 @@ module FIRM
|
|
44
51
|
alias key? include?
|
45
52
|
end
|
46
53
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
module ContainerPatch
|
55
|
+
|
56
|
+
def self.included(base)
|
57
|
+
class << base
|
58
|
+
def json_new(object, &block)
|
59
|
+
# deserializing (anchor) object or alias
|
60
|
+
if object.has_key?('*id')
|
61
|
+
if FIRM::Serializable::Aliasing.restored?(self, object['*id'])
|
62
|
+
# resolving an already restored anchor for this alias
|
63
|
+
FIRM::Serializable::Aliasing.resolve_anchor(self, object['*id'])
|
64
|
+
else
|
65
|
+
# in case of cyclic references JSON will restore aliases before the anchors
|
66
|
+
# so in this case we instantiate an instance here and register it as
|
67
|
+
# the anchor; when the anchor is restored it will replace the contents of this
|
68
|
+
# instance with the restored elements
|
69
|
+
FIRM::Serializable::Aliasing.restore_anchor(object['*id'], self.new)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
instance = if object.has_key?('&id')
|
73
|
+
anchor_id = object['&id'] # extract anchor id
|
74
|
+
if FIRM::Serializable::Aliasing.restored?(self, anchor_id)
|
75
|
+
# in case of cyclic references an alias will already have restored the anchor instance
|
76
|
+
# (default constructed); retrieve that instance here for deserialization of properties
|
77
|
+
FIRM::Serializable::Aliasing.resolve_anchor(self, anchor_id)
|
78
|
+
else
|
79
|
+
# restore the anchor here with a newly instantiated instance
|
80
|
+
FIRM::Serializable::Aliasing.restore_anchor(anchor_id, self.new)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
self.new
|
84
|
+
end
|
85
|
+
block.call(instance)
|
86
|
+
instance
|
87
|
+
end
|
88
|
+
end
|
89
|
+
private :json_new
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_json(&block)
|
94
|
+
json_data = {
|
95
|
+
::JSON.create_id => self.class.name
|
96
|
+
}
|
97
|
+
if (anchor = FIRM::Serializable::Aliasing.get_anchor(self))
|
98
|
+
anchor_data = FIRM::Serializable::Aliasing.get_anchor_data(self)
|
99
|
+
# retroactively insert the anchor in the anchored instance's serialization data
|
100
|
+
anchor_data['&id'] = anchor unless anchor_data.has_key?('&id')
|
101
|
+
json_data['*id'] = anchor
|
102
|
+
else
|
103
|
+
# register anchor object **before** serializing properties to properly handle cycling (bidirectional
|
104
|
+
# references)
|
105
|
+
FIRM::Serializable::Aliasing.register_anchor_object(self, json_data)
|
106
|
+
block.call(json_data)
|
107
|
+
end
|
108
|
+
json_data
|
55
109
|
end
|
110
|
+
private :build_json
|
111
|
+
|
56
112
|
end
|
57
113
|
|
58
114
|
class << self
|
59
115
|
def serializables
|
60
116
|
set = ::Set.new( [::NilClass, ::TrueClass, ::FalseClass, ::Integer, ::Float, ::String, ::Array, ::Hash,
|
61
|
-
::Date, ::DateTime, ::Range, ::Rational, ::Complex, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set
|
117
|
+
::Date, ::DateTime, ::Range, ::Rational, ::Complex, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set])
|
118
|
+
set << ::OpenStruct if ::Object.const_defined?(:OpenStruct)
|
62
119
|
set << ::BigDecimal if ::Object.const_defined?(:BigDecimal)
|
63
120
|
set
|
64
121
|
end
|
@@ -107,6 +164,8 @@ module FIRM
|
|
107
164
|
begin
|
108
165
|
# initialize anchor registry
|
109
166
|
Serializable::Aliasing.start_anchor_object_registry
|
167
|
+
# set custom (more compact) create_id
|
168
|
+
::JSON.create_id = Serializable::JSON::CREATE_ID
|
110
169
|
for_json = obj.respond_to?(:as_json) ? obj.as_json : obj
|
111
170
|
if pretty
|
112
171
|
if io || io.respond_to?(:write)
|
@@ -130,9 +189,11 @@ module FIRM
|
|
130
189
|
Serializable::Aliasing.start_anchor_references
|
131
190
|
# enable safe deserializing
|
132
191
|
self.start_safe_deserialize
|
192
|
+
# set custom (more compact) create_id
|
193
|
+
::JSON.create_id = Serializable::JSON::CREATE_ID
|
133
194
|
::JSON.parse!(source,
|
134
|
-
|
135
|
-
|
195
|
+
create_additions: true,
|
196
|
+
object_class: Serializable::JSON::ObjectHash)
|
136
197
|
ensure
|
137
198
|
# reset safe deserializing
|
138
199
|
self.end_safe_deserialize
|
@@ -279,100 +340,103 @@ class ::Class
|
|
279
340
|
end
|
280
341
|
|
281
342
|
class ::Array
|
343
|
+
include FIRM::Serializable::JSON::ContainerPatch
|
344
|
+
|
345
|
+
class << self
|
346
|
+
# Create a new Array instance from deserialized JSON data.
|
347
|
+
# @param [Hash] object deserialized JSON object
|
348
|
+
# @return [Array] restored Array instance
|
349
|
+
def json_create(object)
|
350
|
+
json_new(object) { |instance| instance.replace(object['data']) }
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
282
354
|
def as_json(*)
|
283
|
-
|
355
|
+
build_json do |json_data|
|
356
|
+
json_data['data'] = collect { |e| e.respond_to?(:as_json) ? e.as_json : e }
|
357
|
+
end
|
284
358
|
end
|
285
359
|
end
|
286
360
|
|
287
361
|
class ::Hash
|
362
|
+
include FIRM::Serializable::JSON::ContainerPatch
|
363
|
+
|
288
364
|
class << self
|
289
|
-
|
365
|
+
# Create a new Hash instance from deserialized JSON data.
|
366
|
+
# @param [Hash] object deserialized JSON object
|
367
|
+
# @return [Hash] restored Hash instance
|
368
|
+
def json_create(object)
|
369
|
+
json_new(object) { |instance| instance.replace(object['data'].to_h) }
|
370
|
+
end
|
290
371
|
end
|
372
|
+
|
291
373
|
def as_json(*)
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
}
|
374
|
+
build_json do |json_data|
|
375
|
+
json_data['data'] = collect { |k,v| [k.respond_to?(:as_json) ? k.as_json : k, v.respond_to?(:as_json) ? v.as_json : v] }
|
376
|
+
end
|
296
377
|
end
|
297
378
|
end
|
298
379
|
|
299
380
|
class ::Set
|
381
|
+
include FIRM::Serializable::JSON::ContainerPatch
|
382
|
+
|
383
|
+
class << self
|
384
|
+
# Create a new Set instance from deserialized JSON data.
|
385
|
+
# @param [Hash] object deserialized JSON object
|
386
|
+
# @return [Set] restored Set instance
|
387
|
+
def json_create(object)
|
388
|
+
json_new(object) { |instance| instance.replace(object['a']) }
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
300
392
|
def as_json(*)
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
}
|
393
|
+
build_json do |json_data|
|
394
|
+
json_data['a'] = to_a.collect { |e| e.respond_to?(:as_json) ? e.as_json : e }
|
395
|
+
end
|
305
396
|
end
|
306
397
|
end
|
307
398
|
|
308
399
|
class ::Struct
|
400
|
+
include FIRM::Serializable::JSON::ContainerPatch
|
401
|
+
|
309
402
|
class << self
|
403
|
+
# Create a new Struct instance from deserialized JSON data.
|
404
|
+
# @param [Hash] object deserialized JSON object
|
405
|
+
# @return [Struct] restored Set instance
|
310
406
|
def json_create(object)
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
# resolving an already restored anchor for this alias
|
315
|
-
FIRM::Serializable::Aliasing.resolve_anchor(self, object['*id'])
|
316
|
-
else
|
317
|
-
# in case of cyclic references JSON will restore aliases before the anchors
|
318
|
-
# so in this case we allocate an instance here and register it as
|
319
|
-
# the anchor; when the anchor is restored it will re-use this instance to restore
|
320
|
-
# the properties
|
321
|
-
FIRM::Serializable::Aliasing.restore_anchor(object['*id'], self.allocate)
|
407
|
+
json_new(object) do |instance|
|
408
|
+
values = object['v']
|
409
|
+
instance.members.each_with_index { |n, i| instance[n] = values[i] }
|
322
410
|
end
|
323
|
-
else
|
324
|
-
if object.has_key?('&id')
|
325
|
-
anchor_id = object['&id'] # extract anchor id
|
326
|
-
instance = if FIRM::Serializable::Aliasing.restored?(self, anchor_id)
|
327
|
-
# in case of cyclic references an alias will already have restored the anchor instance
|
328
|
-
# (default constructed); retrieve that instance here for deserialization of properties
|
329
|
-
FIRM::Serializable::Aliasing.resolve_anchor(self, anchor_id)
|
330
|
-
else
|
331
|
-
# restore the anchor here with a newly instantiated instance
|
332
|
-
FIRM::Serializable::Aliasing.restore_anchor(anchor_id, self.allocate)
|
333
|
-
end
|
334
|
-
instance.__send__(:initialize, *object['v'])
|
335
|
-
instance
|
336
|
-
else
|
337
|
-
self.new(*object['v'])
|
338
|
-
end
|
339
|
-
end
|
340
411
|
end
|
341
412
|
end
|
342
413
|
|
343
414
|
def as_json(*)
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
# JSON.create_id => klass,
|
348
|
-
# 'v' => values.as_json,
|
349
|
-
# }
|
350
|
-
json_data = {
|
351
|
-
::JSON.create_id => klass
|
352
|
-
}
|
353
|
-
if (anchor = FIRM::Serializable::Aliasing.get_anchor(self))
|
354
|
-
anchor_data = FIRM::Serializable::Aliasing.get_anchor_data(self)
|
355
|
-
# retroactively insert the anchor in the anchored instance's serialization data
|
356
|
-
anchor_data['&id'] = anchor unless anchor_data.has_key?('&id')
|
357
|
-
json_data['*id'] = anchor
|
358
|
-
else
|
359
|
-
# register anchor object **before** serializing properties to properly handle cycling (bidirectional
|
360
|
-
# references)
|
361
|
-
FIRM::Serializable::Aliasing.register_anchor_object(self, json_data)
|
362
|
-
json_data['v'] = values.as_json
|
415
|
+
self.class.name.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
|
416
|
+
build_json do |json_data|
|
417
|
+
json_data['v'] = values.collect { |e| e.respond_to?(:as_json) ? e.as_json : e }
|
363
418
|
end
|
364
|
-
json_data
|
365
419
|
end
|
366
420
|
end
|
367
421
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
JSON.
|
374
|
-
|
375
|
-
|
422
|
+
if ::Object.const_defined?(:OpenStruct)
|
423
|
+
class ::OpenStruct
|
424
|
+
include FIRM::Serializable::JSON::ContainerPatch
|
425
|
+
|
426
|
+
class << self
|
427
|
+
# Create a new OpenStruct instance from deserialized JSON data.
|
428
|
+
# @param [Hash] object deserialized JSON object
|
429
|
+
# @return [OpenStruct] restored OpenStruct instance
|
430
|
+
def json_create(object)
|
431
|
+
json_new(object) { |instance| object['t'].each { |k,v| instance[k] = v } }
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def as_json(*)
|
436
|
+
build_json do |json_data|
|
437
|
+
json_data['t'] = table.collect { |k,v| [k.respond_to?(:as_json) ? k.as_json : k, v.respond_to?(:as_json) ? v.as_json : v] }
|
438
|
+
end
|
439
|
+
end
|
376
440
|
end
|
377
441
|
end
|
378
442
|
|
data/lib/firm/serializer/xml.rb
CHANGED
@@ -3,7 +3,11 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
require 'set'
|
6
|
-
|
6
|
+
# from Ruby 3.5.0 OpenStruct will not be available by default anymore
|
7
|
+
begin
|
8
|
+
require 'ostruct'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
7
11
|
require 'date'
|
8
12
|
|
9
13
|
module FIRM
|
@@ -56,15 +60,52 @@ module FIRM
|
|
56
60
|
def create_type_node(xml)
|
57
61
|
xml.add_child(Nokogiri::XML::Node.new(tag.to_s, xml.document))
|
58
62
|
end
|
63
|
+
private :create_type_node
|
59
64
|
def to_xml(_, _)
|
60
65
|
raise Serializable::Exception, "Missing serialization method for #{klass} XML handler"
|
61
66
|
end
|
62
|
-
def from_xml(
|
67
|
+
def from_xml(_xml)
|
63
68
|
raise Serializable::Exception, "Missing serialization method for #{klass} XML handler"
|
64
69
|
end
|
65
70
|
end
|
66
71
|
private_constant :HandlerMethods
|
67
72
|
|
73
|
+
module AliasableHandler
|
74
|
+
def build_xml(xml, value, &block)
|
75
|
+
node = create_type_node(xml)
|
76
|
+
node['class'] = value.class.name
|
77
|
+
if (anchor = Serializable::Aliasing.get_anchor(value))
|
78
|
+
anchor_data = Serializable::Aliasing.get_anchor_data(value)
|
79
|
+
# retroactively insert the anchor in the anchored instance's serialization data
|
80
|
+
anchor_data['anchor'] = anchor unless anchor_data.has_attribute?('anchor')
|
81
|
+
node['alias'] = "#{anchor}"
|
82
|
+
else
|
83
|
+
# register anchor object **before** serializing properties to properly handle cycling (bidirectional
|
84
|
+
# references)
|
85
|
+
Serializable::Aliasing.register_anchor_object(value, node)
|
86
|
+
block.call(node)
|
87
|
+
end
|
88
|
+
xml
|
89
|
+
end
|
90
|
+
private :build_xml
|
91
|
+
def create_from_xml(xml, &block)
|
92
|
+
klass = ::Object.const_get(xml['class'])
|
93
|
+
if xml.has_attribute?('alias')
|
94
|
+
# deserializing alias
|
95
|
+
Serializable::Aliasing.resolve_anchor(klass, xml['alias'].to_i)
|
96
|
+
else
|
97
|
+
instance = klass.new
|
98
|
+
# in case this is an anchor restore the anchor instance before restoring the member values
|
99
|
+
# and afterwards initialize the instance with the restored member values
|
100
|
+
Serializable::Aliasing.restore_anchor(xml['anchor'].to_i, instance) if xml.has_attribute?('anchor')
|
101
|
+
block.call(instance)
|
102
|
+
instance
|
103
|
+
end
|
104
|
+
end
|
105
|
+
private :create_from_xml
|
106
|
+
end
|
107
|
+
private_constant :AliasableHandler
|
108
|
+
|
68
109
|
def xml_handlers
|
69
110
|
@xml_handlers ||= {}
|
70
111
|
end
|
@@ -86,9 +127,10 @@ module FIRM
|
|
86
127
|
end
|
87
128
|
private :get_xml_handler
|
88
129
|
|
89
|
-
def define_xml_handler(klass, tag=nil, &block)
|
130
|
+
def define_xml_handler(klass, tag=nil, aliasable: false, &block)
|
90
131
|
hnd_klass = Class.new
|
91
132
|
hnd_klass.singleton_class.include(HandlerMethods)
|
133
|
+
hnd_klass.singleton_class.include(AliasableHandler) if aliasable
|
92
134
|
tag_code = if tag
|
93
135
|
::Symbol === tag ? ":#{tag}" : "'#{tag.to_s}'"
|
94
136
|
else
|
@@ -143,7 +185,7 @@ module FIRM
|
|
143
185
|
create_type_node(xml).add_child(Nokogiri::XML::CDATA.new(xml.document, value.name))
|
144
186
|
xml
|
145
187
|
end
|
146
|
-
def from_xml(
|
188
|
+
def from_xml(_xml)
|
147
189
|
# should never be called
|
148
190
|
raise Serializable::Exception, 'Unsupported Class deserialization'
|
149
191
|
end
|
@@ -179,16 +221,14 @@ module FIRM
|
|
179
221
|
end
|
180
222
|
end
|
181
223
|
|
182
|
-
define_xml_handler(::Array) do
|
224
|
+
define_xml_handler(::Array, aliasable: true) do
|
183
225
|
def to_xml(xml, value)
|
184
|
-
node
|
185
|
-
value.each do |v|
|
186
|
-
Serializable::XML.to_xml(node, v)
|
187
|
-
end
|
188
|
-
xml
|
226
|
+
build_xml(xml, value) { |node| value.each { |v| Serializable::XML.to_xml(node, v) } }
|
189
227
|
end
|
190
228
|
def from_xml(xml)
|
191
|
-
xml
|
229
|
+
create_from_xml(xml) do |instance|
|
230
|
+
instance.replace(xml.elements.collect { |child| Serializable::XML.from_xml(child) })
|
231
|
+
end
|
192
232
|
end
|
193
233
|
end
|
194
234
|
|
@@ -243,56 +283,34 @@ module FIRM
|
|
243
283
|
end
|
244
284
|
end
|
245
285
|
|
246
|
-
define_xml_handler(::Hash) do
|
286
|
+
define_xml_handler(::Hash, aliasable: true) do
|
247
287
|
def to_xml(xml, value)
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
288
|
+
build_xml(xml, value) do |node|
|
289
|
+
value.each_pair do |k,v|
|
290
|
+
pair = node.add_child(Nokogiri::XML::Node.new('P', node.document))
|
291
|
+
Serializable::XML.to_xml(pair, k)
|
292
|
+
Serializable::XML.to_xml(pair, v)
|
293
|
+
end
|
253
294
|
end
|
254
|
-
xml
|
255
295
|
end
|
256
296
|
def from_xml(xml)
|
257
|
-
xml
|
258
|
-
|
259
|
-
|
260
|
-
|
297
|
+
create_from_xml(xml) do |instance|
|
298
|
+
xml.elements.each do |pair|
|
299
|
+
k, v = pair.elements
|
300
|
+
instance[Serializable::XML.from_xml(k)] = Serializable::XML.from_xml(v)
|
301
|
+
end
|
261
302
|
end
|
262
303
|
end
|
263
304
|
end
|
264
305
|
|
265
|
-
define_xml_handler(::Struct) do
|
306
|
+
define_xml_handler(::Struct, aliasable: true) do
|
266
307
|
def to_xml(xml, value)
|
267
|
-
node
|
268
|
-
node['class'] = value.class.name
|
269
|
-
if (anchor = Serializable::Aliasing.get_anchor(value))
|
270
|
-
anchor_data = Serializable::Aliasing.get_anchor_data(value)
|
271
|
-
# retroactively insert the anchor in the anchored instance's serialization data
|
272
|
-
anchor_data['anchor'] = anchor unless anchor_data.has_attribute?('anchor')
|
273
|
-
node['alias'] = "#{anchor}"
|
274
|
-
else
|
275
|
-
# register anchor object **before** serializing properties to properly handle cycling (bidirectional
|
276
|
-
# references)
|
277
|
-
Serializable::Aliasing.register_anchor_object(value, node)
|
278
|
-
value.each do |v|
|
279
|
-
Serializable::XML.to_xml(node, v)
|
280
|
-
end
|
281
|
-
end
|
282
|
-
xml
|
308
|
+
build_xml(xml, value) { |node| value.each { |v| Serializable::XML.to_xml(node, v) } }
|
283
309
|
end
|
284
310
|
def from_xml(xml)
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
Serializable::Aliasing.resolve_anchor(klass, xml['alias'].to_i)
|
289
|
-
else
|
290
|
-
instance = klass.allocate
|
291
|
-
# in case this is an anchor restore the anchor instance before restoring the member values
|
292
|
-
# and afterwards initialize the instance with the restored member values
|
293
|
-
Serializable::Aliasing.restore_anchor(xml['anchor'].to_i, instance) if xml.has_attribute?('anchor')
|
294
|
-
instance.__send__(:initialize, *xml.elements.collect { |child| Serializable::XML.from_xml(child) })
|
295
|
-
instance
|
311
|
+
create_from_xml(xml) do |instance|
|
312
|
+
elems = xml.elements
|
313
|
+
instance.members.each_with_index { |n, i| instance[n] = Serializable::XML.from_xml(elems[i]) }
|
296
314
|
end
|
297
315
|
end
|
298
316
|
end
|
@@ -408,34 +426,35 @@ module FIRM
|
|
408
426
|
end
|
409
427
|
end
|
410
428
|
|
411
|
-
define_xml_handler(::Set) do
|
429
|
+
define_xml_handler(::Set, aliasable: true) do
|
412
430
|
def to_xml(xml, value)
|
413
|
-
node
|
414
|
-
value.each do |v|
|
415
|
-
Serializable::XML.to_xml(node, v)
|
416
|
-
end
|
417
|
-
xml
|
431
|
+
build_xml(xml, value) { |node| value.each { |v| Serializable::XML.to_xml(node, v) } }
|
418
432
|
end
|
419
433
|
def from_xml(xml)
|
420
|
-
|
434
|
+
create_from_xml(xml) do |instance|
|
435
|
+
instance.replace(xml.elements.collect { |child| Serializable::XML.from_xml(child) })
|
436
|
+
end
|
421
437
|
end
|
422
438
|
end
|
423
439
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
440
|
+
if ::Object.const_defined?(:OpenStruct)
|
441
|
+
define_xml_handler(::OpenStruct, aliasable: true) do
|
442
|
+
def to_xml(xml, value)
|
443
|
+
build_xml(xml, value) do |node|
|
444
|
+
value.each_pair do |k,v|
|
445
|
+
pair = node.add_child(Nokogiri::XML::Node.new('P', node.document))
|
446
|
+
Serializable::XML.to_xml(pair, k)
|
447
|
+
Serializable::XML.to_xml(pair, v)
|
448
|
+
end
|
449
|
+
end
|
431
450
|
end
|
432
|
-
xml
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
451
|
+
def from_xml(xml)
|
452
|
+
create_from_xml(xml) do |instance|
|
453
|
+
xml.elements.each do |pair|
|
454
|
+
k, v = pair.elements
|
455
|
+
instance[Serializable::XML.from_xml(k)] = Serializable::XML.from_xml(v)
|
456
|
+
end
|
457
|
+
end
|
439
458
|
end
|
440
459
|
end
|
441
460
|
end
|
data/lib/firm/serializer/yaml.rb
CHANGED
@@ -5,7 +5,11 @@
|
|
5
5
|
require 'yaml'
|
6
6
|
require 'date'
|
7
7
|
require 'set'
|
8
|
-
|
8
|
+
# from Ruby 3.5.0 OpenStruct will not be available by default anymore
|
9
|
+
begin
|
10
|
+
require 'ostruct'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
9
13
|
|
10
14
|
module FIRM
|
11
15
|
|
@@ -15,7 +19,8 @@ module FIRM
|
|
15
19
|
|
16
20
|
class << self
|
17
21
|
def serializables
|
18
|
-
list = [::Date, ::DateTime, ::Range, ::Rational, ::Complex, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set
|
22
|
+
list = [::Date, ::DateTime, ::Range, ::Rational, ::Complex, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set]
|
23
|
+
list.push(::OpenStruct) if ::Object.const_defined?(:OpenStruct)
|
19
24
|
list.push(::BigDecimal) if ::Object.const_defined?(:BigDecimal)
|
20
25
|
list
|
21
26
|
end
|
data/lib/firm/version.rb
CHANGED
data/tests/serializer_tests.rb
CHANGED
@@ -770,12 +770,56 @@ module SerializerTestMixin
|
|
770
770
|
struct = CyclicTest.new
|
771
771
|
struct.list = [struct]
|
772
772
|
obj_serial = struct.serialize
|
773
|
-
struct_new =
|
774
|
-
assert_nothing_raised { struct_new = FIRM.deserialize(obj_serial) }
|
773
|
+
struct_new = assert_nothing_raised { FIRM.deserialize(obj_serial) }
|
775
774
|
assert_instance_of(CyclicTest, struct_new)
|
776
775
|
assert_equal(struct_new.object_id, struct_new.list[0].object_id)
|
777
776
|
end
|
778
777
|
|
778
|
+
def test_cyclic_core_containers
|
779
|
+
|
780
|
+
array = [1, 2, 3]
|
781
|
+
array << array
|
782
|
+
obj_serial = array.serialize
|
783
|
+
arr_new = assert_nothing_raised { FIRM.deserialize(obj_serial) }
|
784
|
+
assert_instance_of(::Array, arr_new)
|
785
|
+
assert_instance_of(::Array, arr_new.last)
|
786
|
+
assert_equal(arr_new.size, arr_new.last.size)
|
787
|
+
assert_equal(arr_new.object_id, arr_new.last.object_id)
|
788
|
+
|
789
|
+
hash = { one: 1, two: 2 }
|
790
|
+
hash[:self] = hash
|
791
|
+
obj_serial = hash.serialize
|
792
|
+
hash_new = assert_nothing_raised { FIRM.deserialize(obj_serial) }
|
793
|
+
assert_instance_of(::Hash, hash_new)
|
794
|
+
assert_instance_of(::Hash, hash_new[:self])
|
795
|
+
assert_equal(hash_new.size, hash_new[:self].size)
|
796
|
+
assert_equal(hash_new.object_id, hash_new[:self].object_id)
|
797
|
+
|
798
|
+
# the JRuby Psych implementation has a bug preventing cyclic reference support
|
799
|
+
# for Set objects (https://github.com/jruby/jruby/issues/8352)
|
800
|
+
unless defined? JRUBY_VERSION
|
801
|
+
set = ::Set.new([[1,2], {one: 1}])
|
802
|
+
set << set
|
803
|
+
obj_serial = set.serialize(pretty: true)
|
804
|
+
set_new = assert_nothing_raised { FIRM.deserialize(obj_serial) }
|
805
|
+
assert_instance_of(::Set, set_new)
|
806
|
+
assert_true(set_new.any? { |e| ::Array === e })
|
807
|
+
assert_true(set_new.any? { |e| ::Hash === e })
|
808
|
+
assert_true(set_new.any? { |e| ::Set === e && set_new.object_id == e.object_id })
|
809
|
+
end
|
810
|
+
|
811
|
+
ostruct = ::OpenStruct.new(one: 1, two: 2)
|
812
|
+
ostruct.me = ostruct
|
813
|
+
obj_serial = ostruct.serialize
|
814
|
+
ostruct_new = assert_nothing_raised { FIRM.deserialize(obj_serial) }
|
815
|
+
assert_instance_of(::OpenStruct, ostruct_new)
|
816
|
+
assert_equal(1, ostruct_new.one)
|
817
|
+
assert_equal(2, ostruct_new.two)
|
818
|
+
assert_instance_of(::OpenStruct, ostruct_new.me)
|
819
|
+
assert_equal(ostruct_new.object_id, ostruct_new.me.object_id)
|
820
|
+
|
821
|
+
end
|
822
|
+
|
779
823
|
def test_nested_hash_with_complex_keys
|
780
824
|
id_obj = Identifiable.new(:one)
|
781
825
|
id_obj2 = Identifiable.new(:two)
|
@@ -1040,4 +1084,62 @@ module SerializerTestMixin
|
|
1040
1084
|
assert_equal(obj.symbol, obj_new.symbol)
|
1041
1085
|
end
|
1042
1086
|
|
1087
|
+
def run_test_threads
|
1088
|
+
data = { list: ::Array.new(5, [ PropTest.new, Point.new(0, 0), Point.new(10, 10), Point.new(100, 400), Rect.new(20, 20, 40, 40) ]) }
|
1089
|
+
results = []
|
1090
|
+
threads = ::Array.new(10) do
|
1091
|
+
Thread.new do
|
1092
|
+
results << run_test_fibers(data)
|
1093
|
+
end
|
1094
|
+
end
|
1095
|
+
threads.each { |t| t.join }
|
1096
|
+
results
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
def run_test_fibers(data)
|
1100
|
+
fibers = ::Array.new(100) do |_|
|
1101
|
+
Fiber.new do
|
1102
|
+
s = assert_nothing_raised { data.serialize }
|
1103
|
+
Fiber.yield nil
|
1104
|
+
new_data = assert_nothing_raised { FIRM.deserialize(s) }
|
1105
|
+
Fiber.yield nil
|
1106
|
+
assert_instance_of(::Hash, new_data)
|
1107
|
+
assert_instance_of(::Array, new_data[:list])
|
1108
|
+
assert_equal(5, new_data[:list].size)
|
1109
|
+
assert_true(new_data[:list].all? { |e| e.is_a?(::Array) && e.size == 5 })
|
1110
|
+
5.times { |i| assert_equal(data[:list].first[i], new_data[:list].first[i]) }
|
1111
|
+
4.times do |n|
|
1112
|
+
5.times { |i| assert_equal(new_data[:list].first[i].object_id, new_data[:list][n+1][i].object_id) }
|
1113
|
+
end
|
1114
|
+
new_data
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
results = []
|
1118
|
+
begin
|
1119
|
+
fibers = fibers.select do |fiber|
|
1120
|
+
if (rc = fiber.resume)
|
1121
|
+
results << rc
|
1122
|
+
false
|
1123
|
+
else
|
1124
|
+
true
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
end until fibers.empty?
|
1128
|
+
results
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
def test_threading
|
1132
|
+
|
1133
|
+
results = run_test_threads
|
1134
|
+
|
1135
|
+
set = results.inject(::Set.new) do |set, fiber_results|
|
1136
|
+
fiber_results.inject(set) { |set_, fiber_result| set_.merge(fiber_result[:list][1].collect { |o| o.object_id }) }
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
# although we started with a single unique dataset, distributing that through 10 threads * 100 fibers
|
1140
|
+
# to serialize and deserialize should result in 1000 distinct datasets with each 5 distinct data instances
|
1141
|
+
assert_equal(5000, set.size)
|
1142
|
+
|
1143
|
+
end
|
1144
|
+
|
1043
1145
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: firm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Corino
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|