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.
@@ -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)}" }