firm 0.9.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a4a4e31a319e92f4e5efb708562832f1072602084f5c0b4907c88d1e28d2c1b
4
- data.tar.gz: 0e9e64b8fb301c3f895fe0b76978d6794389d0ca651339fd132396866d05eefa
3
+ metadata.gz: 0ebb1f6594b3ecc4e56863a0f04ce3eb44fc5bfad0218a183298b44e56e9cf8e
4
+ data.tar.gz: 4e2349b45ae0479c968406fcd2eafb7ac4e76c8317cbbeeea20ecb784e998115
5
5
  SHA512:
6
- metadata.gz: 6144308bc7ba7bba7cac24c173a8fbabcac75e3074d29205593c64173696c7b9c41b47c5747b1265ad680f437d87abbce400e9ea0f311b8417403be4faeb55a8
7
- data.tar.gz: 32353706f381e81dcacb705bbe144464fef1233df920dcb45d21e3386fca873bbd6ae52b7a1825cff84fec88d43a68935c89ad7577e9fa8f31f1e618c6cba8c4
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 output
12
- format independent object (de-)serialization support.
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
- FIRM also supports a simple scheme to provide (de-)serialization support for user defined classes.
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. All user defined serializable class as well as **named** `Struct`-derived classes support aliasing.
53
+ by YAML.<br>
54
+ In addition FIRM automatically recognizes and handles cyclic references of aliasable objects.
54
55
 
55
- FIRM also automatically recognizes and handles cyclic references of aliasable objects.
56
+ FIRM serialization is also thread safe and supports re-entrancy (i.e. nested serialization).
56
57
 
57
58
  ## Installing FIRM
58
59
 
@@ -32,12 +32,20 @@ module FIRM
32
32
  end
33
33
 
34
34
  require 'set'
35
- require 'ostruct'
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, ::OpenStruct, ::Time, ::Date, ::DateTime].each do |c|
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
@@ -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
- require 'json/add/ostruct'
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
- # Mixin module to patch singleton_clas of the Hash class to make Hash-es
48
- # JSON creatable (#json_creatable? returns true).
49
- module HashClassPatch
50
- # Create a new Hash instance from deserialized JSON data.
51
- # @param [Hash] object deserialized JSON object
52
- # @return [Hash] restored Hash instance
53
- def json_create(object)
54
- object['data'].to_h
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, ::OpenStruct])
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
- **{create_additions: true,
135
- object_class: Serializable::JSON::ObjectHash})
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
- collect { |e| e.respond_to?(:as_json) ? e.as_json : e }
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
- include FIRM::Serializable::JSON::HashClassPatch
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
- ::JSON.create_id => self.class.name,
294
- 'data' => collect { |k,v| [k.respond_to?(:as_json) ? k.as_json : k, v.respond_to?(:as_json) ? v.as_json : v] }
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
- JSON.create_id => self.class.name,
303
- 'a' => to_a.as_json,
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
- # deserializing (anchor) object or alias
312
- if object.has_key?('*id')
313
- if FIRM::Serializable::Aliasing.restored?(self, object['*id'])
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
- klass = self.class.name
345
- klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
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
- class ::OpenStruct
369
- def as_json(*)
370
- klass = self.class.name
371
- klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
372
- {
373
- JSON.create_id => klass,
374
- 't' => table.as_json,
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
 
@@ -3,7 +3,11 @@
3
3
 
4
4
 
5
5
  require 'set'
6
- require 'ostruct'
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(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(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 = create_type_node(xml)
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.elements.collect { |child| Serializable::XML.from_xml(child) }
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
- node = create_type_node(xml)
249
- value.each_pair do |k,v|
250
- pair = node.add_child(Nokogiri::XML::Node.new('P', node.document))
251
- Serializable::XML.to_xml(pair, k)
252
- Serializable::XML.to_xml(pair, v)
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.elements.inject({}) do |hash, pair|
258
- k, v = pair.elements
259
- hash[Serializable::XML.from_xml(k)] = Serializable::XML.from_xml(v)
260
- hash
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 = create_type_node(xml)
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
- # deserializing alias
286
- klass = ::Object.const_get(xml['class'])
287
- if xml.has_attribute?('alias')
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 = create_type_node(xml)
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
- ::Set.new(xml.elements.collect { |child| Serializable::XML.from_xml(child) })
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
- define_xml_handler(::OpenStruct) do
425
- def to_xml(xml, value)
426
- node = create_type_node(xml)
427
- value.each_pair do |k,v|
428
- pair = node.add_child(Nokogiri::XML::Node.new('P', node.document))
429
- Serializable::XML.to_xml(pair, k)
430
- Serializable::XML.to_xml(pair, v)
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
- end
434
- def from_xml(xml)
435
- xml.elements.inject(::OpenStruct.new) do |hash, pair|
436
- k, v = pair.elements
437
- hash[Serializable::XML.from_xml(k)] = Serializable::XML.from_xml(v)
438
- hash
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
@@ -5,7 +5,11 @@
5
5
  require 'yaml'
6
6
  require 'date'
7
7
  require 'set'
8
- require 'ostruct'
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, ::OpenStruct]
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
@@ -4,6 +4,6 @@
4
4
  module FIRM
5
5
 
6
6
  # FIRM version
7
- VERSION = "0.9.7"
7
+ VERSION = "1.0.0"
8
8
 
9
9
  end
@@ -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 = nil
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.9.7
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-09-22 00:00:00.000000000 Z
11
+ date: 2024-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake