firm 0.9.1
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 +7 -0
- data/.yardopts +12 -0
- data/LICENSE +21 -0
- data/README.md +209 -0
- data/lib/firm/serializable.rb +733 -0
- data/lib/firm/serialize/core.rb +43 -0
- data/lib/firm/serialize/id.rb +104 -0
- data/lib/firm/serializer/json.rb +394 -0
- data/lib/firm/serializer/xml.rb +544 -0
- data/lib/firm/serializer/yaml.rb +118 -0
- data/lib/firm/version.rb +9 -0
- data/lib/firm.rb +5 -0
- data/rakelib/yard/templates/default/fulldoc/html/css/firm.css +97 -0
- data/rakelib/yard/templates/default/fulldoc/html/setup.rb +25 -0
- data/rakelib/yard/templates/default/layout/html/setup.rb +5 -0
- data/rakelib/yard/yard/relative_markdown_links/version.rb +8 -0
- data/rakelib/yard/yard/relative_markdown_links.rb +39 -0
- data/rakelib/yard/yard-custom-templates.rb +2 -0
- data/rakelib/yard/yard-relative_markdown_links.rb +4 -0
- data/tests/serializer_tests.rb +945 -0
- data/tests/test_serialize.rb +8 -0
- data/tests/test_serialize_xml.rb +22 -0
- data/tests/test_serialize_yaml.rb +18 -0
- metadata +110 -0
@@ -0,0 +1,733 @@
|
|
1
|
+
# FIRM::Serializable - serializable mixin
|
2
|
+
# Copyright (c) M.J.N. Corino, The Netherlands
|
3
|
+
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module FIRM
|
8
|
+
|
9
|
+
# Mixin module providing (de-)serialization support for user defined classes.
|
10
|
+
module Serializable
|
11
|
+
|
12
|
+
class Exception < RuntimeError; end
|
13
|
+
|
14
|
+
class Property
|
15
|
+
def initialize(klass, prop, proc=nil, force: false, handler: nil, &block)
|
16
|
+
::Kernel.raise ArgumentError, "Invalid property id [#{prop}]" unless ::String === prop || ::Symbol === prop
|
17
|
+
::Kernel.raise ArgumentError, "Duplicate property id [#{prop}]" if klass.has_serializer_property?(prop)
|
18
|
+
@klass = klass
|
19
|
+
@id = prop.to_sym
|
20
|
+
@forced = force
|
21
|
+
if block || handler
|
22
|
+
if handler
|
23
|
+
::Kernel.raise ArgumentError,
|
24
|
+
"Invalid property handler #{handler} for #{prop}" unless ::Proc === handler || ::Symbol === handler
|
25
|
+
if handler.is_a?(::Proc)
|
26
|
+
::Kernel.raise ArgumentError, "Invalid property block #{proc} for #{prop}" unless block.arity == -3
|
27
|
+
@getter = ->(obj) { handler.call(@id, obj) }
|
28
|
+
@setter = ->(obj, val) { handler.call(@id, obj, val) }
|
29
|
+
else
|
30
|
+
@getter = ->(obj) { obj.send(handler, @id) }
|
31
|
+
@setter = ->(obj, val) { obj.send(handler, @id, val) }
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# any property block MUST accept 2 or 3 args; property name, instance and value (for setter)
|
35
|
+
::Kernel.raise ArgumentError, "Invalid property block #{proc} for #{prop}" unless block.arity == -3
|
36
|
+
@getter = ->(obj) { block.call(@id, obj) }
|
37
|
+
@setter = ->(obj, val) { block.call(@id, obj, val) }
|
38
|
+
end
|
39
|
+
elsif proc
|
40
|
+
::Kernel.raise ArgumentError,
|
41
|
+
"Invalid property proc #{proc} for #{prop}" unless ::Proc === proc || ::Symbol === proc
|
42
|
+
if ::Proc === proc
|
43
|
+
# any property proc should be callable with a single arg (instance)
|
44
|
+
@getter = proc
|
45
|
+
# a property proc combining getter/setter functionality should accept a single or more args (instance + value)
|
46
|
+
@setter = (proc.arity == -2) ? proc : nil
|
47
|
+
else
|
48
|
+
@getter = ->(obj) { obj.send(proc) }
|
49
|
+
@setter = ->(obj, val) { obj.send(proc, val) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :id
|
55
|
+
|
56
|
+
def serialize(obj, data, excludes)
|
57
|
+
unless excludes.include?(@id)
|
58
|
+
val = getter.call(obj)
|
59
|
+
unless Serializable === val && val.serialize_disabled? && !@forced
|
60
|
+
data[@id] = case val
|
61
|
+
when ::Array
|
62
|
+
val.select { |elem| !(Serializable === elem && elem.serialize_disabled?) }
|
63
|
+
when ::Set
|
64
|
+
::Set.new(val.select { |elem| !(Serializable === elem && elem.serialize_disabled?) })
|
65
|
+
else
|
66
|
+
val
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def deserialize(obj, data)
|
73
|
+
if data.has_key?(@id)
|
74
|
+
setter.call(obj, data[@id])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def get(obj)
|
79
|
+
getter.call(obj)
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_method(id)
|
83
|
+
begin
|
84
|
+
@klass.instance_method(id)
|
85
|
+
rescue NameError
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
private :get_method
|
90
|
+
|
91
|
+
def getter
|
92
|
+
unless @getter
|
93
|
+
inst_meth = get_method(@id)
|
94
|
+
inst_meth = get_method("get_#{@id}") unless inst_meth
|
95
|
+
if inst_meth
|
96
|
+
@getter = ->(obj) { inst_meth.bind(obj).call }
|
97
|
+
else
|
98
|
+
return self.method(:getter_fail)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
@getter
|
102
|
+
end
|
103
|
+
private :getter
|
104
|
+
|
105
|
+
def setter
|
106
|
+
unless @setter
|
107
|
+
inst_meth = get_method("#{@id}=")
|
108
|
+
inst_meth = get_method("set_#{@id}") unless inst_meth
|
109
|
+
unless inst_meth
|
110
|
+
im = get_method(@id)
|
111
|
+
if im && im.arity == -1
|
112
|
+
inst_meth = im
|
113
|
+
else
|
114
|
+
inst_meth = nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
if inst_meth
|
118
|
+
@setter = ->(obj, val) { inst_meth.bind(obj).call(val) }
|
119
|
+
else
|
120
|
+
return self.method(:setter_noop)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
@setter
|
124
|
+
end
|
125
|
+
private :setter
|
126
|
+
|
127
|
+
def getter_fail(_obj)
|
128
|
+
::Kernel.raise Serializable::Exception, "Missing getter for property #{@id} of #{@klass}"
|
129
|
+
end
|
130
|
+
private :getter_fail
|
131
|
+
|
132
|
+
def setter_noop(_, _)
|
133
|
+
# do nothing
|
134
|
+
end
|
135
|
+
private :setter_noop
|
136
|
+
end
|
137
|
+
|
138
|
+
# Serializable unique ids.
|
139
|
+
# This class makes sure to maintain uniqueness across serialization/deserialization cycles
|
140
|
+
# and keeps all shared instances within a single (serialized/deserialized) object set in
|
141
|
+
# sync.
|
142
|
+
class ID; end
|
143
|
+
|
144
|
+
class << self
|
145
|
+
|
146
|
+
def serializables
|
147
|
+
@serializables ||= ::Set.new
|
148
|
+
end
|
149
|
+
|
150
|
+
def formatters
|
151
|
+
@formatters ||= {}
|
152
|
+
end
|
153
|
+
private :formatters
|
154
|
+
|
155
|
+
# Registers a serialization formatting engine
|
156
|
+
# @param [Symbol,String] format format id
|
157
|
+
# @param [Object] engine formatting engine
|
158
|
+
def register(format, engine)
|
159
|
+
if formatters.has_key?(format.to_s.downcase)
|
160
|
+
::Kernel.raise ArgumentError,
|
161
|
+
"Duplicate serialization formatter registration for #{format}"
|
162
|
+
end
|
163
|
+
formatters[format.to_s.downcase] = engine
|
164
|
+
end
|
165
|
+
|
166
|
+
# Return a serialization formatting engine
|
167
|
+
# @param [Symbol,String] format format id
|
168
|
+
# @return [Object] formatting engine
|
169
|
+
def [](format)
|
170
|
+
::Kernel.raise ArgumentError, "Format #{format} is not supported." unless formatters.has_key?(format.to_s.downcase)
|
171
|
+
formatters[format.to_s.downcase]
|
172
|
+
end
|
173
|
+
|
174
|
+
# Return the default output format symbol id (:json, :yaml, :xml).
|
175
|
+
# By default returns :json.
|
176
|
+
# @return [Symbol]
|
177
|
+
def default_format
|
178
|
+
@default_format ||= :json
|
179
|
+
end
|
180
|
+
|
181
|
+
# Set the default output format.
|
182
|
+
# @param [Symbol] format Output format id. By default :json, :yaml and :xml (if nokogiri gem is installed) are supported.
|
183
|
+
# @return [Symbol] default format
|
184
|
+
def default_format=(format)
|
185
|
+
@default_format = format
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
# This module provides alias (de-)serialization management functionality for
|
191
|
+
# output engines that do not provide this support out of the box.
|
192
|
+
module AliasManagement
|
193
|
+
|
194
|
+
TLS_ANCHOR_OBJECTS_KEY = :firm_anchors_objects.freeze
|
195
|
+
private_constant :TLS_ANCHOR_OBJECTS_KEY
|
196
|
+
|
197
|
+
TLS_ALIAS_STACK_KEY = :firm_anchor_reference_stack.freeze
|
198
|
+
private_constant :TLS_ALIAS_STACK_KEY
|
199
|
+
|
200
|
+
def anchor_object_registry_stack
|
201
|
+
::Thread.current[TLS_ANCHOR_OBJECTS_KEY] ||= []
|
202
|
+
end
|
203
|
+
private :anchor_object_registry_stack
|
204
|
+
|
205
|
+
def start_anchor_object_registry
|
206
|
+
anchor_object_registry_stack.push({})
|
207
|
+
end
|
208
|
+
|
209
|
+
def clear_anchor_object_registry
|
210
|
+
anchor_object_registry_stack.pop
|
211
|
+
end
|
212
|
+
|
213
|
+
def anchor_object_registry
|
214
|
+
anchor_object_registry_stack.last
|
215
|
+
end
|
216
|
+
private :anchor_object_registry
|
217
|
+
|
218
|
+
def class_anchor_objects(klass)
|
219
|
+
anchor_object_registry[klass] ||= {}
|
220
|
+
end
|
221
|
+
private :class_anchor_objects
|
222
|
+
|
223
|
+
# Registers a new anchor object.
|
224
|
+
# @param [Object] object anchor instance
|
225
|
+
# @param [Object] data serialized property collection object
|
226
|
+
# @return [Object] serialized property collection object
|
227
|
+
def register_anchor_object(object, data)
|
228
|
+
anchors = class_anchor_objects(object.class)
|
229
|
+
raise Serializable::Exception, "Duplicate anchor creation for #{object}" if anchors.has_key?(object.object_id)
|
230
|
+
anchors[object.object_id] = data
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns true if the object has an anchor registration, false otherwise.
|
234
|
+
# @return [Boolean]
|
235
|
+
def anchored?(object)
|
236
|
+
class_anchor_objects(object.class).has_key?(object.object_id)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the anchor id if anchored, nil otherwise.
|
240
|
+
# @param [Object] object anchor instance
|
241
|
+
# @return [Integer, nil]
|
242
|
+
def get_anchor(object)
|
243
|
+
anchored?(object) ? object.object_id : nil
|
244
|
+
end
|
245
|
+
|
246
|
+
# Retrieves the anchor serialization collection data for an anchored object.
|
247
|
+
# Returns nil if the object is not anchored.
|
248
|
+
# @return [nil,Object]
|
249
|
+
def get_anchor_data(object)
|
250
|
+
anchors = class_anchor_objects(object.class)
|
251
|
+
anchors[object.object_id]
|
252
|
+
end
|
253
|
+
|
254
|
+
def anchor_references_stack
|
255
|
+
::Thread.current[TLS_ALIAS_STACK_KEY] ||= []
|
256
|
+
end
|
257
|
+
private :anchor_references_stack
|
258
|
+
|
259
|
+
def start_anchor_references
|
260
|
+
anchor_references_stack.push({})
|
261
|
+
end
|
262
|
+
|
263
|
+
def clear_anchor_references
|
264
|
+
anchor_references_stack.pop
|
265
|
+
end
|
266
|
+
|
267
|
+
def anchor_references
|
268
|
+
anchor_references_stack.last
|
269
|
+
end
|
270
|
+
private :anchor_references
|
271
|
+
|
272
|
+
def class_anchor_references(klass)
|
273
|
+
anchor_references[klass] ||= {}
|
274
|
+
end
|
275
|
+
private :class_anchor_references
|
276
|
+
|
277
|
+
# Registers a restored anchor object and it's ID.
|
278
|
+
# @param [Integer] id anchor ID
|
279
|
+
# @param [Object] object anchor instance
|
280
|
+
# @return [Object] anchor instance
|
281
|
+
def restore_anchor(id, object)
|
282
|
+
class_anchor_references(object.class)[id] = object
|
283
|
+
end
|
284
|
+
|
285
|
+
# Returns true if the anchor object for the given class and id has been restored, false otherwise.
|
286
|
+
# @param [Class] klass aliasable class of the anchor instance
|
287
|
+
# @param [Integer] id anchor id
|
288
|
+
# @return [Boolean]
|
289
|
+
def restored?(klass, id)
|
290
|
+
class_anchor_references(klass).has_key?(id)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Resolves a referenced anchor instance.
|
294
|
+
# Returns the instance if found, nil otherwise.
|
295
|
+
# @param [Class] klass aliasable class of the anchor instance
|
296
|
+
# @param [Integer] id anchor id
|
297
|
+
# @return [nil,Object]
|
298
|
+
def resolve_anchor(klass, id)
|
299
|
+
class_anchor_references(klass)[id]
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
# Mixin module for classes that get FIRM::Serializable included.
|
305
|
+
# This module is used to extend the class methods of the serializable class.
|
306
|
+
module SerializeClassMethods
|
307
|
+
|
308
|
+
# Adds (a) serializable property(-ies) for instances of his class (and derived classes)
|
309
|
+
# @overload property(*props, force: false)
|
310
|
+
# Specifies one or more serialized properties.
|
311
|
+
# The serialization framework will determine the availability of setter and getter methods
|
312
|
+
# automatically by looking for methods <code>"#{prop_id}=(v)"</code>, <code>"#set_{prop_id}(v)"</code> or <code>"#{prop_id}(v)"</code>
|
313
|
+
# for setters and <code>"#{prop_id}()"</code> or <code>"#get_{prop_id}"</code> for getters.
|
314
|
+
# @param [Symbol,String] props one or more ids of serializable properties
|
315
|
+
# @param [Boolean] force overrides any #disable_serialize for the properties specified
|
316
|
+
# @return [void]
|
317
|
+
# @overload property(hash, force: false)
|
318
|
+
# Specifies one or more serialized properties with associated setter/getter method ids/procs/lambda-s.
|
319
|
+
# @example
|
320
|
+
# property(
|
321
|
+
# prop_a: ->(obj, *val) {
|
322
|
+
# obj.my_prop_a_setter(val.first) unless val.empty?
|
323
|
+
# obj.my_prop_a_getter
|
324
|
+
# },
|
325
|
+
# prop_b: Proc.new { |obj, *val|
|
326
|
+
# obj.my_prop_b_setter(val.first) unless val.empty?
|
327
|
+
# obj.my_prop_b_getter
|
328
|
+
# },
|
329
|
+
# prop_c: :serialization_method)
|
330
|
+
# Procs with setter support MUST accept 1 or 2 arguments (1 for getter, 2 for setter) where the first
|
331
|
+
# argument will always be the property owner's object instance and the second (in case of a setter proc) the
|
332
|
+
# value to restore.
|
333
|
+
# @note Use `*val` to specify the optional value argument for setter requests instead of `val=nil`
|
334
|
+
# to be able to support setting explicit nil values.
|
335
|
+
# @param [Hash] hash a hash of pairs of property ids and getter/setter procs
|
336
|
+
# @param [Boolean] force overrides any #disable_serialize for the properties specified
|
337
|
+
# @return [void]
|
338
|
+
# @overload property(*props, force: false, handler: nil, &block)
|
339
|
+
# Specifies one or more serialized properties with a getter/setter handler proc/method/block.
|
340
|
+
# The getter/setter proc or block should accept either 2 (property id and object for getter) or 3 arguments
|
341
|
+
# (property id, object and value for setter) and is assumed to handle getter/setter requests
|
342
|
+
# for all specified properties.
|
343
|
+
# The getter/setter method should accept either 1 (property id for getter) or 2 arguments
|
344
|
+
# (property id and value for setter) and is assumed to handle getter/setter requests
|
345
|
+
# for all specified properties.
|
346
|
+
# @example
|
347
|
+
# property(:property_a, :property_b, :property_c) do |id, obj, *val|
|
348
|
+
# case id
|
349
|
+
# when :property_a
|
350
|
+
# ...
|
351
|
+
# when :property_b
|
352
|
+
# ...
|
353
|
+
# when :property_c
|
354
|
+
# ...
|
355
|
+
# end
|
356
|
+
# end
|
357
|
+
# @note Use `*val` to specify the optional value argument for setter requests instead of `val=nil`
|
358
|
+
# to be able to support setting explicit nil values.
|
359
|
+
# @param [Symbol,String] props one or more ids of serializable properties
|
360
|
+
# @param [Boolean] force overrides any #disable_serialize for the properties specified
|
361
|
+
# @yieldparam [Symbol,String] id property id
|
362
|
+
# @yieldparam [Object] obj object instance
|
363
|
+
# @yieldparam [Object] val optional property value to set in case of setter request
|
364
|
+
# @return [void]
|
365
|
+
def property(*props, **kwargs, &block)
|
366
|
+
forced = !!kwargs.delete(:force)
|
367
|
+
if block || kwargs[:handler]
|
368
|
+
props.each do |prop|
|
369
|
+
serializer_properties << Property.new(self, prop, force: forced, handler: kwargs[:handler], &block)
|
370
|
+
end
|
371
|
+
else
|
372
|
+
props.flatten.each do |prop|
|
373
|
+
if ::Hash === prop
|
374
|
+
prop.each_pair do |pn, pp|
|
375
|
+
serializer_properties << Property.new(self, pn, pp, force: forced)
|
376
|
+
end
|
377
|
+
else
|
378
|
+
serializer_properties << Property.new(self, prop, force: forced)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
unless kwargs.empty?
|
382
|
+
kwargs.each_pair do |pn, pp|
|
383
|
+
serializer_properties << Property.new(self, pn, pp, force: forced)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
alias :properties :property
|
389
|
+
alias :contains :property
|
390
|
+
|
391
|
+
# Excludes a serializable property for instances of this class.
|
392
|
+
# (mostly/only useful to exclude properties from base classes which
|
393
|
+
# do not require serialization for derived class)
|
394
|
+
# @param [Symbol,String] props one or more ids of serializable properties
|
395
|
+
# @return [void]
|
396
|
+
def excluded_property(*props)
|
397
|
+
excluded_serializer_properties.merge props.flatten.collect { |prop| prop }
|
398
|
+
end
|
399
|
+
alias :excluded_properties :excluded_property
|
400
|
+
alias :excludes :excluded_property
|
401
|
+
|
402
|
+
# Defines a finalizer method/proc/block to be called after all properties
|
403
|
+
# have been deserialized and restored.
|
404
|
+
# Procs or blocks will be called with the deserialized object as the single argument.
|
405
|
+
# Unbound methods will be bound to the deserialized object before calling.
|
406
|
+
# Explicitly specifying nil will undefine the finalizer.
|
407
|
+
# @param [Symbol, String, Proc, UnboundMethod, nil] meth name of instance method, proc or method to call for finalizing
|
408
|
+
# @yieldparam [Object] obj deserialized object to finalize
|
409
|
+
# @return [void]
|
410
|
+
def define_deserialize_finalizer(meth=nil, &block)
|
411
|
+
if block and meth.nil?
|
412
|
+
# the given block should expect and use the given object instance
|
413
|
+
set_deserialize_finalizer(block)
|
414
|
+
elsif meth and block.nil?
|
415
|
+
h_meth = case meth
|
416
|
+
when ::Symbol, ::String
|
417
|
+
Serializable::MethodResolver.new(self, meth)
|
418
|
+
when ::Proc
|
419
|
+
# check arity == 1
|
420
|
+
if meth.arity != 1
|
421
|
+
Kernel.raise ArgumentError,
|
422
|
+
"Deserialize finalizer Proc should expect a single argument",
|
423
|
+
caller
|
424
|
+
end
|
425
|
+
meth
|
426
|
+
when ::UnboundMethod
|
427
|
+
# check arity == 0
|
428
|
+
if meth.arity>0
|
429
|
+
Kernel.raise ArgumentError,
|
430
|
+
"Deserialize finalizer method should not expect any argument",
|
431
|
+
caller
|
432
|
+
end
|
433
|
+
->(obj) { meth.bind(obj).call }
|
434
|
+
else
|
435
|
+
Kernel.raise ArgumentError,
|
436
|
+
"Specify deserialize finalizer with a method, name, proc OR block",
|
437
|
+
caller
|
438
|
+
end
|
439
|
+
set_deserialize_finalizer(h_meth)
|
440
|
+
elsif meth.nil? and block.nil?
|
441
|
+
set_deserialize_finalizer(nil)
|
442
|
+
else
|
443
|
+
Kernel.raise ArgumentError,
|
444
|
+
"Specify deserialize finalizer with a method, name, proc OR block",
|
445
|
+
caller
|
446
|
+
end
|
447
|
+
nil
|
448
|
+
end
|
449
|
+
alias :deserialize_finalizer :define_deserialize_finalizer
|
450
|
+
|
451
|
+
# Deserializes object from source data
|
452
|
+
# @param [IO,String] source source data (String or IO(-like object))
|
453
|
+
# @param [Symbol, String] format data format of source
|
454
|
+
# @return [Object] deserialized object
|
455
|
+
def deserialize(source, format: Serializable.default_format)
|
456
|
+
Serializable.deserialize(source, format: format)
|
457
|
+
end
|
458
|
+
|
459
|
+
end
|
460
|
+
|
461
|
+
# Mixin module for classes that get FIRM::Serializable included.
|
462
|
+
# This module is used to extend the instance methods of the serializable class.
|
463
|
+
module SerializeInstanceMethods
|
464
|
+
|
465
|
+
# Serialize this object
|
466
|
+
# @overload serialize(pretty: false, format: Serializable.default_format)
|
467
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
468
|
+
# @param [Symbol,String] format specifies output format
|
469
|
+
# @return [String] serialized data
|
470
|
+
# @overload serialize(io, pretty: false, format: Serializable.default_format)
|
471
|
+
# @param [IO] io output stream to write serialized data to
|
472
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
473
|
+
# @param [Symbol,String] format specifies output format
|
474
|
+
# @return [IO]
|
475
|
+
def serialize(io = nil, pretty: false, format: Serializable.default_format)
|
476
|
+
Serializable[format].dump(self, io, pretty: pretty)
|
477
|
+
end
|
478
|
+
|
479
|
+
# Returns true if regular serialization for this object has been disabled, false otherwise (default).
|
480
|
+
# Disabled serialization can be overridden for single objects (not objects maintained in property containers
|
481
|
+
# like arrays and sets).
|
482
|
+
# @return [true,false]
|
483
|
+
def serialize_disabled?
|
484
|
+
!!@serialize_disabled # true for any value but false
|
485
|
+
end
|
486
|
+
|
487
|
+
# Disables serialization for this object as a single property or as part of a property container
|
488
|
+
# (array or set).
|
489
|
+
# @return [void]
|
490
|
+
def disable_serialize
|
491
|
+
# by default unset (nil) so serializing enabled
|
492
|
+
@serialize_disabled = true
|
493
|
+
end
|
494
|
+
|
495
|
+
# @!method for_serialize(hash, excludes = Set.new)
|
496
|
+
# Serializes the properties of a serializable instance to the given hash
|
497
|
+
# except when the property id is included in excludes.
|
498
|
+
# @param [Object] hash hash-like property serialization container
|
499
|
+
# @param [Set] excludes set with excluded property ids
|
500
|
+
# @return [Object] hash-like property serialization container
|
501
|
+
|
502
|
+
# @!method from_serialized(hash)
|
503
|
+
# Restores the properties of a deserialized instance.
|
504
|
+
# @param [Object] hash hash-like property deserialization container
|
505
|
+
# @return [self]
|
506
|
+
|
507
|
+
# #!method finalize_from_serialized()
|
508
|
+
# Finalizes the instance initialization after property restoration.
|
509
|
+
# Calls any user defined finalizer.
|
510
|
+
# @return [self]
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
# Serialize the given object
|
515
|
+
# @overload serialize(obj, pretty: false, format: Serializable.default_format)
|
516
|
+
# @param [Object] obj object to serialize
|
517
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
518
|
+
# @param [Symbol,String] format specifies output format
|
519
|
+
# @return [String] serialized data
|
520
|
+
# @overload serialize(obj, io, pretty: false, format: Serializable.default_format)
|
521
|
+
# @param [Object] obj object to serialize
|
522
|
+
# @param [IO] io output stream to write serialized data to
|
523
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
524
|
+
# @param [Symbol,String] format specifies output format
|
525
|
+
# @return [IO]
|
526
|
+
def self.serialize(obj, io = nil, pretty: false, format: Serializable.default_format)
|
527
|
+
self[format].dump(obj, io, pretty: pretty)
|
528
|
+
end
|
529
|
+
|
530
|
+
# Deserializes object from source data
|
531
|
+
# @param [IO,String] source source data (stream)
|
532
|
+
# @param [Symbol, String] format data format of source
|
533
|
+
# @return [Object] deserialized object
|
534
|
+
def self.deserialize(source, format: Serializable.default_format)
|
535
|
+
self[format].load(::IO === source || source.respond_to?(:read) ? source.read : source)
|
536
|
+
end
|
537
|
+
|
538
|
+
# Small utility class for delayed method resolving
|
539
|
+
class MethodResolver
|
540
|
+
def initialize(klass, mtd_id, default=false)
|
541
|
+
@klass = klass
|
542
|
+
@mtd_id = mtd_id
|
543
|
+
@default = default
|
544
|
+
end
|
545
|
+
|
546
|
+
def resolve
|
547
|
+
m = @klass.instance_method(@mtd_id) rescue nil
|
548
|
+
if m
|
549
|
+
# check arity == 0
|
550
|
+
if m.arity>0
|
551
|
+
unless @default
|
552
|
+
Kernel.raise ArgumentError,
|
553
|
+
"Deserialize finalizer method #{@klass}#{@mtd_id} should not expect any argument",
|
554
|
+
caller
|
555
|
+
end
|
556
|
+
else
|
557
|
+
return ->(obj) { m.bind(obj).call }
|
558
|
+
end
|
559
|
+
end
|
560
|
+
nil
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
|
565
|
+
def self.included(base)
|
566
|
+
::Kernel.raise RuntimeError, "#{self} should only be included in classes" if base.instance_of?(::Module)
|
567
|
+
|
568
|
+
::Kernel.raise RuntimeError, "#{self} should be included only once in #{base}" if Serializable.serializables.include?(base.name)
|
569
|
+
|
570
|
+
# register as serializable class
|
571
|
+
Serializable.serializables << base
|
572
|
+
|
573
|
+
return if base == Serializable::ID # special case which does not need the rest
|
574
|
+
|
575
|
+
# provide serialized property definition support
|
576
|
+
|
577
|
+
# provide serialized classes with their own serialized properties (exclusion) list
|
578
|
+
# and a deserialization finalizer setter/getter
|
579
|
+
base.singleton_class.class_eval do
|
580
|
+
def serializer_properties
|
581
|
+
@serializer_props ||= []
|
582
|
+
end
|
583
|
+
def excluded_serializer_properties
|
584
|
+
@excluded_serializer_props ||= ::Set.new
|
585
|
+
end
|
586
|
+
def set_deserialize_finalizer(fin)
|
587
|
+
@finalize_from_deserialized = fin
|
588
|
+
end
|
589
|
+
private :set_deserialize_finalizer
|
590
|
+
def get_deserialize_finalizer
|
591
|
+
case @finalize_from_deserialized
|
592
|
+
when Serializable::MethodResolver
|
593
|
+
@finalize_from_deserialized = @finalize_from_deserialized.resolve
|
594
|
+
else
|
595
|
+
@finalize_from_deserialized
|
596
|
+
end
|
597
|
+
end
|
598
|
+
private :get_deserialize_finalizer
|
599
|
+
def find_deserialize_finalizer
|
600
|
+
get_deserialize_finalizer
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
base.class_eval do
|
605
|
+
|
606
|
+
# Initializes a newly allocated instance for subsequent deserialization (optionally initializing
|
607
|
+
# using the given data hash).
|
608
|
+
# The default implementation calls the standard #initialize method without arguments (default constructor)
|
609
|
+
# and leaves the property restoration to a subsequent call to the instance method #from_serialized(data).
|
610
|
+
# Classes that do not support a default constructor can override this class method and
|
611
|
+
# implement a custom initialization scheme.
|
612
|
+
# @param [Object] _data hash-like object containing deserialized property data (symbol keys)
|
613
|
+
# @return [Object] the initialized object
|
614
|
+
def init_from_serialized(_data)
|
615
|
+
initialize
|
616
|
+
self
|
617
|
+
end
|
618
|
+
protected :init_from_serialized
|
619
|
+
|
620
|
+
# Check if the class has the default deserialize finalizer method defined (a #create method
|
621
|
+
# without arguments). If so install that method as the deserialize finalizer.
|
622
|
+
set_deserialize_finalizer(Serializable::MethodResolver.new(self, :create, true))
|
623
|
+
end
|
624
|
+
|
625
|
+
# add class methods
|
626
|
+
base.extend(SerializeClassMethods)
|
627
|
+
|
628
|
+
# add instance property (de-)serialization methods for base class
|
629
|
+
base.class_eval <<~__CODE
|
630
|
+
def for_serialize(hash, excludes = ::Set.new)
|
631
|
+
#{base.name}.serializer_properties.each { |prop, h| prop.serialize(self, hash, excludes) }
|
632
|
+
hash
|
633
|
+
end
|
634
|
+
protected :for_serialize
|
635
|
+
|
636
|
+
def from_serialized(hash)
|
637
|
+
#{base.name}.serializer_properties.each { |prop| prop.deserialize(self, hash) }
|
638
|
+
self
|
639
|
+
end
|
640
|
+
protected :from_serialized
|
641
|
+
|
642
|
+
def finalize_from_serialized
|
643
|
+
if (f = self.class.find_deserialize_finalizer)
|
644
|
+
f.call(self)
|
645
|
+
end
|
646
|
+
self
|
647
|
+
end
|
648
|
+
protected :finalize_from_serialized
|
649
|
+
|
650
|
+
def self.has_serializer_property?(id)
|
651
|
+
self.serializer_properties.any? { |p| p.id == id.to_sym }
|
652
|
+
end
|
653
|
+
__CODE
|
654
|
+
# add inheritance support
|
655
|
+
base.class_eval do
|
656
|
+
def self.inherited(derived)
|
657
|
+
# add instance property (de-)serialization methods for derived classes
|
658
|
+
derived.class_eval <<~__CODE
|
659
|
+
module SerializerMethods
|
660
|
+
def for_serialize(hash, excludes = ::Set.new)
|
661
|
+
hash = super(hash, excludes | #{derived.name}.excluded_serializer_properties)
|
662
|
+
#{derived.name}.serializer_properties.each { |prop| prop.serialize(self, hash, excludes) }
|
663
|
+
hash
|
664
|
+
end
|
665
|
+
protected :for_serialize
|
666
|
+
|
667
|
+
def from_serialized(hash)
|
668
|
+
#{derived.name}.serializer_properties.each { |prop| prop.deserialize(self, hash) }
|
669
|
+
super(hash)
|
670
|
+
end
|
671
|
+
protected :from_serialized
|
672
|
+
end
|
673
|
+
include SerializerMethods
|
674
|
+
__CODE
|
675
|
+
derived.class_eval do
|
676
|
+
def self.has_serializer_property?(id)
|
677
|
+
self.serializer_properties.any? { |p| p.id == id.to_sym } || self.superclass.has_serializer_property?(id)
|
678
|
+
end
|
679
|
+
end
|
680
|
+
# add derived class support for deserialization finalizer
|
681
|
+
derived.singleton_class.class_eval <<~__CODE
|
682
|
+
def find_deserialize_finalizer
|
683
|
+
get_deserialize_finalizer || #{derived.name}.superclass.find_deserialize_finalizer
|
684
|
+
end
|
685
|
+
__CODE
|
686
|
+
|
687
|
+
# Check if the derived class has the default deserialize finalizer method defined (a #create method
|
688
|
+
# without arguments) defined. If so install that method as the deserialize finalizer (it is expected
|
689
|
+
# this method will call any superclass finalizer that may be defined).
|
690
|
+
derived.class_eval do
|
691
|
+
set_deserialize_finalizer(Serializable::MethodResolver.new(self, :create, true))
|
692
|
+
end
|
693
|
+
|
694
|
+
# register as serializable class
|
695
|
+
Serializable.serializables << derived
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# add instance serialization method
|
700
|
+
base.include(SerializeInstanceMethods)
|
701
|
+
end
|
702
|
+
|
703
|
+
end # module Serializable
|
704
|
+
|
705
|
+
|
706
|
+
# Serialize the given object
|
707
|
+
# @overload serialize(obj, pretty: false, format: Serializable.default_format)
|
708
|
+
# @param [Object] obj object to serialize
|
709
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
710
|
+
# @param [Symbol,String] format specifies output format
|
711
|
+
# @return [String] serialized data
|
712
|
+
# @overload serialize(obj, io, pretty: false, format: Serializable.default_format)
|
713
|
+
# @param [Object] obj object to serialize
|
714
|
+
# @param [IO] io output stream (IO(-like object)) to write serialized data to
|
715
|
+
# @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
|
716
|
+
# @param [Symbol,String] format specifies output format
|
717
|
+
# @return [IO]
|
718
|
+
def self.serialize(obj, io = nil, pretty: false, format: Serializable.default_format)
|
719
|
+
Serializable.serialize(obj, io, pretty: pretty, format: format)
|
720
|
+
end
|
721
|
+
|
722
|
+
# Deserializes object from source data
|
723
|
+
# @param [IO,String] source source data (String or IO(-like object))
|
724
|
+
# @param [Symbol, String] format data format of source
|
725
|
+
# @return [Object] deserialized object
|
726
|
+
def self.deserialize(source, format: Serializable.default_format)
|
727
|
+
Serializable.deserialize(source, format: format)
|
728
|
+
end
|
729
|
+
|
730
|
+
end # module FIRM
|
731
|
+
|
732
|
+
Dir[File.join(__dir__, 'serializer', '*.rb')].each { |fnm| require "firm/serializer/#{File.basename(fnm)}" }
|
733
|
+
Dir[File.join(__dir__, 'serialize', '*.rb')].each { |fnm| require "firm/serialize/#{File.basename(fnm)}" }
|