firm 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ebb1f6594b3ecc4e56863a0f04ce3eb44fc5bfad0218a183298b44e56e9cf8e
4
- data.tar.gz: 4e2349b45ae0479c968406fcd2eafb7ac4e76c8317cbbeeea20ecb784e998115
3
+ metadata.gz: 9bde55f78b14c4b8c8cac20c709c2826391616cf5d30972bcdeff2613d9389f5
4
+ data.tar.gz: f4e7a9b3269ea524699693968c88d75372f6bf51dbe54c7e61b3bee349da3b71
5
5
  SHA512:
6
- metadata.gz: 247ca7e7c9eb24fc5ca31c8cfb3f06ec9e4c3779627f3e35722d2c51a630c255cc4d182d434d35da7207f112b0905635a5717fd767f3c904a251b7ab84873cbd
7
- data.tar.gz: 2953c510c88ed9078f777a0c8e23399f48339ef5f6c85fcf264fe025de795ecc187407d7d7bddf77b89dbb182eb158123e62aad909c91fbfffc04f613cf8280f
6
+ metadata.gz: 3768d2dda3e137c33b54ecfe27eda8f052c9eb8e21809e9e8f82eb434f9ce381fdf5cd430d9d118aacb75a6047c4db2bd175d2e9937be3fccc6b587f8cc0b683
7
+ data.tar.gz: 329dff1413b582f472b099181769c16afc91aeaa9fe6b827b1cb753be68df14d6dbda11e5d60f3da77e97feb22833b7b78db0a231b09a605c5f78d79845f16e0
@@ -11,17 +11,35 @@ module FIRM
11
11
 
12
12
  class Exception < RuntimeError; end
13
13
 
14
+ # This class encapsulates a serializable property definition.
14
15
  class Property
15
- def initialize(klass, prop, proc=nil, force: false, handler: nil, &block)
16
+ def initialize(klass, prop, proc=nil, force: false, handler: nil, optional: false, &block)
16
17
  ::Kernel.raise ArgumentError, "Invalid property id [#{prop}]" unless ::String === prop || ::Symbol === prop
17
18
  ::Kernel.raise ArgumentError, "Duplicate property id [#{prop}]" if klass.has_serializer_property?(prop)
18
19
  @klass = klass
19
20
  @id = prop.to_sym
20
21
  @forced = force
22
+ @optional = optional.nil? || optional
23
+ @default = if @optional
24
+ case optional
25
+ when Proc
26
+ ::Kernel.raise ArgumentError,
27
+ 'Invalid optional value proc' unless optional.arity.abs == 2
28
+ optional
29
+ when UnboundMethod
30
+ ::Kernel.raise ArgumentError,
31
+ 'Invalid optional value method' unless optional.arity.abs == 1
32
+ ->(obj, id) { optional.bind(obj).call(id) }
33
+ else
34
+ optional == true ? nil : optional
35
+ end
36
+ else
37
+ nil
38
+ end
21
39
  if block || handler
22
40
  if handler
23
41
  ::Kernel.raise ArgumentError,
24
- "Invalid property handler #{handler} for #{prop}" unless ::Proc === handler || ::Symbol === handler
42
+ "Invalid property handler #{handler} for #{prop}" unless ::Proc === handler || ::Symbol === handler || ::String === handler
25
43
  if handler.is_a?(::Proc)
26
44
  ::Kernel.raise ArgumentError, "Invalid property block #{proc} for #{prop}" unless block.arity == -3
27
45
  @getter = ->(obj) { handler.call(@id, obj) }
@@ -53,10 +71,15 @@ module FIRM
53
71
 
54
72
  attr_reader :id
55
73
 
74
+ # Serializes the defined property for the given object and inserts the serialized data
75
+ # into the given data object unless included in the given excludes list.
76
+ # @param [Object] obj
77
+ # @param [Object] data hash-like object
78
+ # @param [Array<Symbol>] excludes
56
79
  def serialize(obj, data, excludes)
57
80
  unless excludes.include?(@id)
58
81
  val = getter.call(obj)
59
- unless Serializable === val && val.serialize_disabled? && !@forced
82
+ unless optional?(obj, val) || (Serializable === val && val.serialize_disabled? && !@forced)
60
83
  data[@id] = case val
61
84
  when ::Array
62
85
  val.select { |elem| !(Serializable === elem && elem.serialize_disabled?) }
@@ -69,15 +92,27 @@ module FIRM
69
92
  end
70
93
  end
71
94
 
95
+ # Returns the (unserialized) property value for the given object.
96
+ # @param [Object] obj
97
+ def get(obj)
98
+ getter.call(obj)
99
+ end
100
+
101
+ # Restores the defined property for the given object using the deserialized data
102
+ # extracted from the given data object.
103
+ # @param [Object] obj
104
+ # @param [Object] data hash-like object
105
+ # @return [void]
72
106
  def deserialize(obj, data)
73
107
  if data.has_key?(@id)
74
108
  setter.call(obj, data[@id])
75
109
  end
76
110
  end
77
111
 
78
- def get(obj)
79
- getter.call(obj)
112
+ def optional?(obj, val)
113
+ @optional && val == (Proc === @default ? @default.call(obj, @id) : @default)
80
114
  end
115
+ private :optional?
81
116
 
82
117
  def get_method(id)
83
118
  begin
@@ -312,15 +347,22 @@ module FIRM
312
347
  module SerializeClassMethods
313
348
 
314
349
  # Adds (a) serializable property(-ies) for instances of his class (and derived classes)
315
- # @overload property(*props, force: false)
350
+ # @overload property(*props, force: false, optional: false)
316
351
  # Specifies one or more serialized properties.
317
352
  # The serialization framework will determine the availability of setter and getter methods
318
353
  # automatically by looking for methods <code>"#{prop_id}=(v)"</code>, <code>"#set_{prop_id}(v)"</code> or <code>"#{prop_id}(v)"</code>
319
354
  # for setters and <code>"#{prop_id}()"</code> or <code>"#get_{prop_id}"</code> for getters.
320
355
  # @param [Symbol,String] props one or more ids of serializable properties
321
356
  # @param [Boolean] force overrides any #disable_serialize for the properties specified
357
+ # @param [Object] optional indicates optionality;
358
+ # if `false` the property will not be optional;
359
+ # `true` means optional if the serialized value == `nil`;
360
+ # any value other than 'false' or 'true' means optional if the serialize value equals that value;
361
+ # alternatively a Proc, Lambda (gets the object and the property id passed) or
362
+ # UnboundMethod (gets the property id passed) can be specified which
363
+ # is called at serialization time to determine the default (optional) value
322
364
  # @return [void]
323
- # @overload property(hash, force: false)
365
+ # @overload property(hash, force: false, optional: false)
324
366
  # Specifies one or more serialized properties with associated setter/getter method ids/procs/lambda-s.
325
367
  # @example
326
368
  # property(
@@ -340,8 +382,15 @@ module FIRM
340
382
  # to be able to support setting explicit nil values.
341
383
  # @param [Hash] hash a hash of pairs of property ids and getter/setter procs
342
384
  # @param [Boolean] force overrides any #disable_serialize for the properties specified
385
+ # @param [Object] optional indicates optionality;
386
+ # if `false` the property will not be optional;
387
+ # `true` means optional if the serialized value == `nil`;
388
+ # any value other than 'false' or 'true' means optional if the serialize value equals that value;
389
+ # alternatively a Proc, Lambda (gets the object and the property id passed) or
390
+ # UnboundMethod (gets the property id passed) can be specified which
391
+ # is called at serialization time to determine the default (optional) value
343
392
  # @return [void]
344
- # @overload property(*props, force: false, handler: nil, &block)
393
+ # @overload property(*props, force: false, handler: nil, optional: false, &block)
345
394
  # Specifies one or more serialized properties with a getter/setter handler proc/method/block.
346
395
  # The getter/setter proc or block should accept either 2 (property id and object for getter) or 3 arguments
347
396
  # (property id, object and value for setter) and is assumed to handle getter/setter requests
@@ -364,29 +413,38 @@ module FIRM
364
413
  # to be able to support setting explicit nil values.
365
414
  # @param [Symbol,String] props one or more ids of serializable properties
366
415
  # @param [Boolean] force overrides any #disable_serialize for the properties specified
416
+ # @param [Symbol,String,Proc] handler serialization handler method name or Proc
417
+ # @param [Object] optional indicates optionality;
418
+ # if `false` the property will not be optional;
419
+ # `true` means optional if the serialized value == `nil`;
420
+ # any value other than 'false' or 'true' means optional if the serialize value equals that value;
421
+ # alternatively a Proc, Lambda (gets the object and the property id passed) or
422
+ # UnboundMethod (gets the property id passed) can be specified which
423
+ # is called at serialization time to determine the default (optional) value
367
424
  # @yieldparam [Symbol,String] id property id
368
425
  # @yieldparam [Object] obj object instance
369
426
  # @yieldparam [Object] val optional property value to set in case of setter request
370
427
  # @return [void]
371
428
  def property(*props, **kwargs, &block)
372
429
  forced = !!kwargs.delete(:force)
430
+ optional = kwargs.has_key?(:optional) ? kwargs.delete(:optional) : false
373
431
  if block || kwargs[:handler]
374
432
  props.each do |prop|
375
- serializer_properties << Property.new(self, prop, force: forced, handler: kwargs[:handler], &block)
433
+ serializer_properties << Property.new(self, prop, force: forced, handler: kwargs[:handler], optional: optional, &block)
376
434
  end
377
435
  else
378
436
  props.flatten.each do |prop|
379
437
  if ::Hash === prop
380
438
  prop.each_pair do |pn, pp|
381
- serializer_properties << Property.new(self, pn, pp, force: forced)
439
+ serializer_properties << Property.new(self, pn, pp, force: forced, optional: optional)
382
440
  end
383
441
  else
384
- serializer_properties << Property.new(self, prop, force: forced)
442
+ serializer_properties << Property.new(self, prop, force: forced, optional: optional)
385
443
  end
386
444
  end
387
445
  unless kwargs.empty?
388
446
  kwargs.each_pair do |pn, pp|
389
- serializer_properties << Property.new(self, pn, pp, force: forced)
447
+ serializer_properties << Property.new(self, pn, pp, force: forced, optional: optional)
390
448
  end
391
449
  end
392
450
  end
data/lib/firm/version.rb CHANGED
@@ -4,6 +4,6 @@
4
4
  module FIRM
5
5
 
6
6
  # FIRM version
7
- VERSION = "1.0.0"
7
+ VERSION = "1.1.1"
8
8
 
9
9
  end
@@ -691,6 +691,69 @@ module SerializerTestMixin
691
691
 
692
692
  end
693
693
 
694
+ class SomeClass
695
+
696
+ include FIRM::Serializable
697
+
698
+ property :always
699
+ property :not_when_nil, optional: true # alternatively you can use `optional: nil`
700
+ property :not_when_some_value, optional: -1
701
+ property :not_when_some_derived_value, optional: ->(_obj, _id) { :derived_value }
702
+
703
+ def initialize(always, not_when_nil = nil, not_when_some_value = -1, not_when_some_derived_value = :derived_value)
704
+ @always = always
705
+ @not_when_nil = not_when_nil
706
+ @not_when_some_value = not_when_some_value
707
+ @not_when_some_derived_value = not_when_some_derived_value
708
+ end
709
+
710
+ attr_accessor :always, :not_when_nil, :not_when_some_value, :not_when_some_derived_value
711
+
712
+ class << self
713
+
714
+ def deserialize_initializer
715
+ @init ||= ->(obj, _data) { obj }
716
+ end
717
+
718
+ def deserialize_initializer=(init)
719
+ @init = init
720
+ end
721
+
722
+ end
723
+
724
+
725
+ def init_from_serialized(_data)
726
+ initialize(nil) # first call the initialize method
727
+ self.class.deserialize_initializer.call(self, _data)
728
+ self
729
+ end
730
+ protected :init_from_serialized
731
+
732
+ end
733
+
734
+ def test_optional
735
+ SomeClass.deserialize_initializer = ->(obj, _data) {
736
+ obj.always = 'NOT'
737
+ obj.not_when_nil = :not_nil
738
+ obj.not_when_some_value = 10
739
+ obj.not_when_some_derived_value = nil
740
+ }
741
+
742
+ obj_serial = SomeClass.new('Always').serialize
743
+ obj_new = assert_nothing_raised { SomeClass.deserialize(obj_serial) }
744
+ assert_equal('Always', obj_new.always) # always (de-)serialized, so overwrites custom init on deserialize
745
+ assert_equal(:not_nil, obj_new.not_when_nil) # not serialized since nil, so leaves custom init
746
+ assert_equal(10, obj_new.not_when_some_value) # not serialized since default value, so leaves custom init
747
+ assert_nil(obj_new.not_when_some_derived_value) # not serialized since default value, so leaves custom init
748
+
749
+ obj_serial = SomeClass.new('Always', :ok, 99, :specific_value).serialize
750
+ obj_new = assert_nothing_raised { SomeClass.deserialize(obj_serial) }
751
+ assert_equal('Always', obj_new.always) # always (de-)serialized, so overwrites custom init on deserialize
752
+ assert_equal(:ok, obj_new.not_when_nil) # serialized since not default, so overwrites custom init
753
+ assert_equal(99, obj_new.not_when_some_value) # serialized since not default, so overwrites custom init
754
+ assert_equal(:specific_value, obj_new.not_when_some_derived_value) # serialized since not default, so overwrites custom init
755
+ end
756
+
694
757
  class House
695
758
 
696
759
  include FIRM::Serializable
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: 1.0.0
4
+ version: 1.1.1
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-10-08 00:00:00.000000000 Z
11
+ date: 2024-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -103,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
103
  - !ruby/object:Gem::Version
104
104
  version: '0'
105
105
  requirements: []
106
- rubygems_version: 3.5.16
106
+ rubygems_version: 3.5.22
107
107
  signing_key:
108
108
  specification_version: 4
109
109
  summary: Format independent Ruby object marshalling