serega 0.42.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module AttributeNormalizers # :nodoc:
5
+ #
6
+ # Builds an anonymous nested serializer from the attribute block.
7
+ #
8
+ # The serializer is a regular subclass of an explicitly chosen base —
9
+ # the :base_serializer attribute option or `config.base_serializer` —
10
+ # so it inherits everything the base has: plugins, config, attributes,
11
+ # batch loaders, the preload handler. Nested values can be any objects,
12
+ # which is why the base is chosen explicitly instead of inheriting from
13
+ # the current serializer. Its `inspect` is overridden to
14
+ # `CurrentSerializer.<attribute_name>` so error messages and debugging
15
+ # output point back to the defining attribute.
16
+ #
17
+ class Block # :nodoc:
18
+ class << self
19
+ # Builds the nested serializer of the attribute block. `base_serializer`
20
+ # is the :base_serializer attribute option.
21
+ #
22
+ # Guards against cyclic definitions before building: inheriting the base
23
+ # serializer copies its attributes, and copying a block attribute builds
24
+ # its nested serializer — meeting the same block again while it is being
25
+ # built means the base serializer (transitively) contains the block
26
+ # attribute being built, so inheriting from it would recurse forever.
27
+ # The "in progress" mark is kept on the block itself — its identity is
28
+ # what defines the cycle (the same Proc object is shared by all copies
29
+ # of the attribute).
30
+ def call(serializer_class, attribute_name, block, base_serializer)
31
+ base_serializer = chosen_base_serializer(serializer_class, base_serializer)
32
+
33
+ if block.instance_variable_defined?(:@serega_building_nested_serializer)
34
+ raise SeregaError,
35
+ "Can not define a nested serializer for attribute :#{attribute_name} —" \
36
+ " its base serializer #{base_serializer.inspect} (transitively)" \
37
+ " contains this same block attribute (cyclic definition)"
38
+ end
39
+
40
+ block.instance_variable_set(:@serega_building_nested_serializer, true)
41
+ begin
42
+ build(serializer_class, attribute_name, block, base_serializer)
43
+ ensure
44
+ block.remove_instance_variable(:@serega_building_nested_serializer)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def build(serializer_class, attribute_name, block, base_serializer)
51
+ label = "#{serializer_class.inspect}.<#{attribute_name}>"
52
+
53
+ serializer = Class.new(base_serializer) do
54
+ define_singleton_method(:inspect) { label }
55
+ define_singleton_method(:to_s) { label }
56
+ instance_exec(&block)
57
+ end
58
+
59
+ # An empty nested serializer means the block was intended as an
60
+ # old-style value block — raise the explaining error.
61
+ raise SeregaError, SeregaValidations::Attribute::CheckBlock::ERROR_MESSAGE if serializer.attributes.empty?
62
+
63
+ serializer
64
+ end
65
+
66
+ # Base class for the nested serializer. Must be chosen explicitly —
67
+ # usually a settings-only serializer holding plugins and configuration
68
+ # (e.g. `config.base_serializer = self` in an application base class).
69
+ def chosen_base_serializer(serializer_class, base_serializer)
70
+ base_serializer ||= serializer_class.config.base_serializer
71
+ return base_serializer if base_serializer
72
+
73
+ raise SeregaError,
74
+ "Attribute block requires a base serializer for the nested serializer." \
75
+ " Provide the `base_serializer: <SerializerClass>` attribute option" \
76
+ " or set `config.base_serializer = <SerializerClass>`"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module AttributeNormalizers # :nodoc:
5
+ #
6
+ # Builds the value resolver of the attribute :delegate option
7
+ #
8
+ class Delegate # :nodoc:
9
+ class << self
10
+ # Resolver that reads `delegate[:to]` of the object, then
11
+ # `delegate[:method]` (the attribute method by default) of the result.
12
+ # `delegate: {to_hash_access: ...}` configures hash access for the
13
+ # intermediate (:to) read, `delegate: {hash_access: ...}` for the final
14
+ # (:method) read. A step without its sub-option keeps a plain method
15
+ # read.
16
+ def call(delegate, method_name, config)
17
+ key_method_name = delegate[:method] || method_name
18
+ delegate_to = delegate[:to]
19
+ allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
20
+
21
+ to_access = delegate[:to_hash_access]
22
+ final_access = delegate[:hash_access]
23
+ unless to_access || final_access
24
+ return AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, allow_nil)
25
+ end
26
+
27
+ to_step = HashAccess.call(to_access, delegate_to, config.hash_access)
28
+ final_step = HashAccess.call(final_access, key_method_name, config.hash_access)
29
+ AttributeValueResolvers::HashAccessDelegateResolver.get(to_step, final_step, allow_nil)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module AttributeNormalizers # :nodoc:
5
+ #
6
+ # Builds the value resolver of one read: a method call, or a Hash read when
7
+ # a :hash_access option is given
8
+ #
9
+ class HashAccess # :nodoc:
10
+ class << self
11
+ # Resolver that reads `key` of the object. `option` is a :hash_access
12
+ # value: `true`, a Symbol mode or a `{mode:, allow_missing_key:}` Hash;
13
+ # `true` and a Hash without a key take `config.hash_access` defaults.
14
+ # Without the option the resolver calls the method `key`.
15
+ def call(option, key, defaults)
16
+ return AttributeValueResolvers::Keyword.new(key) unless option
17
+
18
+ mode, allow_missing_key = parse(option, defaults)
19
+ AttributeValueResolvers::HashAccessKeyword.new(key, mode, allow_missing_key)
20
+ end
21
+
22
+ private
23
+
24
+ def parse(option, defaults)
25
+ case option
26
+ when true then [defaults.default_mode, defaults.default_allow_missing_key]
27
+ when Symbol then [option, defaults.default_allow_missing_key]
28
+ else
29
+ mode = option.fetch(:mode) { defaults.default_mode }
30
+ allow_missing_key = option.fetch(:allow_missing_key) { defaults.default_allow_missing_key }
31
+ [mode, allow_missing_key]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module AttributeNormalizers # :nodoc:
5
+ #
6
+ # Finds the preloads of an attribute
7
+ #
8
+ class Preloads # :nodoc:
9
+ class << self
10
+ # Preloads of the attribute with options `opts` and block `block`.
11
+ #
12
+ # The value is passed through as provided (Symbol, Array, Hash, or any
13
+ # custom value an ORM understands) — it is the data handed to the
14
+ # serializer's `preload_with` handler.
15
+ def call(opts, block, method_name, config)
16
+ # Explicit preload option (false or nil disables preloading)
17
+ return opts[:preload] || nil if opts.key?(:preload)
18
+
19
+ # Auto-preload the delegated association
20
+ if config.auto_preload.fetch(:has_delegate_option) && opts[:delegate]
21
+ return auto_preload_value(opts[:delegate][:to], config)
22
+ end
23
+
24
+ # Auto-preload the nested serializer's association
25
+ # (a block defines a nested serializer, same as the :serializer option)
26
+ if config.auto_preload.fetch(:has_serializer_option) && (opts[:serializer] || block) && !opts.key?(:batch)
27
+ return auto_preload_value(method_name, config)
28
+ end
29
+
30
+ nil
31
+ end
32
+
33
+ private
34
+
35
+ # Skips auto-preloading of methods that return the object
36
+ # itself (:itself by default) — they are not associations,
37
+ # so preloading them would fail or make no sense.
38
+ def auto_preload_value(preload_method, config)
39
+ return nil if config.auto_preload_excluded_methods.include?(preload_method.to_sym)
40
+
41
+ preload_method
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -4,67 +4,18 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
- # @private
8
- module AttributeValueResolvers
7
+ module AttributeValueResolvers # :nodoc:
9
8
  #
10
- # Builds value resolver class for attributes with :batch option
9
+ # Value resolver of attributes with the :batch option and without the
10
+ # :value option. Reads the value of the object from the loaded batch.
11
11
  #
12
- # @private
13
- class BatchResolver
14
- #
15
- # Generates callable block to find attribute value when attribute with :batch
16
- # option has no block or manual :value option.
17
- #
18
- # In other cases we should never get here as attribute value/block option must be manually defined.
19
- #
20
- # It handles this cases:
21
- # - `attribute :foo, batch: true`
22
- # - `attribute :foo, batch: FooLoader`
23
- # - `attribute :foo, batch: :foo_loader`
24
- # - `attribute :foo, batch: { id: :foo_id }`
25
- # - `attribute :foo, batch: { use: FooLoader, id: foo_id }`
26
- # - `attribute :foo, batch: { use: :foo_loader, id: foo_id }`
27
- #
28
- def self.get(serializer_class, attribute_name, batch_opt)
29
- default_method = serializer_class.config.batch_id_option
30
-
31
- if batch_opt == true # ex: `batch: true`
32
- batch_name = attribute_name
33
- batch_id_method = default_method
34
- elsif batch_opt.respond_to?(:call) # ex: `batch: FooLoader`
35
- serializer_class.batch(attribute_name, batch_opt)
36
- batch_name = attribute_name
37
- batch_id_method = default_method
38
- elsif batch_opt.is_a?(Symbol) || batch_opt.is_a?(String) # ex: `batch: :foo_loader`
39
- batch_name = batch_opt.to_sym
40
- batch_id_method = default_method
41
- else
42
- use = batch_opt[:use]
43
- batch_id_method = batch_opt[:id] || default_method
44
-
45
- if use.respond_to?(:call) # ex: `batch: { use: FooLoader }`
46
- batch_name = attribute_name
47
- serializer_class.batch(batch_name, use)
48
- else # ex: `batch: { use: :foo }` || batch: { id: :some_id }
49
- batch_name = use || attribute_name
50
- end
51
- end
52
-
53
- Batch.new(batch_name, batch_id_method)
54
- end
55
- end
56
-
57
- #
58
- # Builds value resolver class for attributes with :batch option
59
- #
60
- # @private
61
- class Batch
12
+ class Batch # :nodoc:
62
13
  def initialize(loader_name, id_method)
63
14
  @loader_name = loader_name
64
15
  @id_method = id_method
65
16
  end
66
17
 
67
- # Finds object attribute value from hash of batch_loaded values
18
+ # Finds the object attribute value in the loaded batches
68
19
  def call(obj, batches:)
69
20
  batches.fetch(@loader_name)[obj.public_send(@id_method)]
70
21
  end
@@ -4,38 +4,16 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
- # @private
8
- module AttributeValueResolvers
9
- #
10
- # Builds value resolver class for attributes with :const option
11
- #
12
- # @private
13
- class ConstResolver
14
- #
15
- # Creates resolver that returns constant value
16
- #
17
- # @param const_value [Object] constant value to return
18
- # @return [Const] resolver instance
19
- #
20
- def self.get(const_value)
21
- Const.new(const_value)
22
- end
23
- end
24
-
7
+ module AttributeValueResolvers # :nodoc:
25
8
  #
26
9
  # Value resolver class for attributes with :const option
27
10
  #
28
- # @private
29
- class Const
11
+ class Const # :nodoc:
30
12
  def initialize(const_value)
31
13
  @const_value = const_value
32
14
  end
33
15
 
34
- #
35
16
  # Returns the constant value
36
- #
37
- # @return [Object] the constant value
38
- #
39
17
  def call
40
18
  @const_value
41
19
  end
@@ -4,21 +4,12 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
- # @private
8
- module AttributeValueResolvers
7
+ module AttributeValueResolvers # :nodoc:
9
8
  #
10
9
  # Builds value resolver class for attributes with :delegate option
11
10
  #
12
- # @private
13
- class DelegateResolver
14
- #
11
+ class DelegateResolver # :nodoc:
15
12
  # Creates resolver that delegates method call to another object
16
- #
17
- # @param delegate_to [Symbol] method to call on object to get delegated object
18
- # @param method_name [Symbol] method to call on delegated object
19
- # @param allow_nil [Boolean] whether to use safe navigation when delegated object is nil
20
- # @return [Delegate, DelegateAllowNil] resolver instance
21
- #
22
13
  def self.get(delegate_to, method_name, allow_nil)
23
14
  allow_nil ? DelegateAllowNil.new(delegate_to, method_name) : Delegate.new(delegate_to, method_name)
24
15
  end
@@ -27,47 +18,45 @@ class Serega
27
18
  #
28
19
  # Value resolver class for attributes with :delegate (with :allow_nil) option
29
20
  #
30
- # @private
31
- class DelegateAllowNil
21
+ class DelegateAllowNil # :nodoc:
32
22
  def initialize(delegate_to, method_name)
33
23
  @delegate_to = delegate_to
34
24
  @method_name = method_name
35
25
  end
36
26
 
37
- #
38
27
  # Delegates method call to another object with safe navigation
39
- #
40
- # @param object [Object] the object to delegate from
41
- # @return [Object, nil] result of delegated method call or nil if delegated object is nil
42
- #
43
28
  def call(object)
44
- object.public_send(delegate_to)&.public_send(method_name)
29
+ object.public_send(@delegate_to)&.public_send(@method_name)
45
30
  end
46
31
 
47
- private
32
+ # Ruby code that delegates the method call
33
+ def code(source_variable)
34
+ return unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@delegate_to) && SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
48
35
 
49
- attr_reader :delegate_to, :method_name
36
+ "#{source_variable}.#{@delegate_to}&.#{@method_name}"
37
+ end
50
38
  end
51
39
 
52
40
  #
53
41
  # Value resolver class for attributes with :delegate (without :allow_nil) option
54
42
  #
55
- # @private
56
- class Delegate
43
+ class Delegate # :nodoc:
57
44
  def initialize(delegate_to, method_name)
58
45
  @delegate_to = delegate_to
59
46
  @method_name = method_name
60
47
  end
61
48
 
62
- #
63
49
  # Delegates method call to another object
64
- #
65
- # @param object [Object] the object to delegate from
66
- # @return [Object] result of delegated method call
67
- #
68
50
  def call(object)
69
51
  object.public_send(@delegate_to).public_send(@method_name)
70
52
  end
53
+
54
+ # Ruby code that delegates the method call
55
+ def code(source_variable)
56
+ return unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@delegate_to) && SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
57
+
58
+ "#{source_variable}.#{@delegate_to}.#{@method_name}"
59
+ end
71
60
  end
72
61
  end
73
62
  end
@@ -1,45 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # @private
5
- module AttributeValueResolvers
4
+ module AttributeValueResolvers # :nodoc:
6
5
  #
7
- # Builds value resolver for attributes with the :hash_access option
6
+ # Settings of the :hash_access option
8
7
  #
9
- # @private
10
- class HashAccessResolver
8
+ class HashAccessResolver # :nodoc:
11
9
  # Allowed hash access modes
12
10
  MODES = %i[symbol string].freeze
13
-
14
- #
15
- # Creates resolver that reads a key from Hash records
16
- #
17
- # @param name [Symbol, String] hash key
18
- # @param mode [Symbol] hash access mode (:symbol, :string)
19
- # @param allow_missing_key [Boolean] whether a missing key is read via `record[key]` rather than raising
20
- #
21
- # @return [HashAccessKeyword] resolver instance
22
- #
23
- def self.get(name, mode, allow_missing_key)
24
- HashAccessKeyword.new(name, mode, allow_missing_key)
25
- end
26
11
  end
27
12
 
28
13
  #
29
14
  # Builds value resolver for attributes with the :delegate option using
30
15
  # hash access on any of its steps
31
16
  #
32
- # @private
33
- class HashAccessDelegateResolver
34
- #
17
+ class HashAccessDelegateResolver # :nodoc:
35
18
  # Creates resolver that delegates through the provided step readers
36
- #
37
- # @param to_step [#call] reader of the intermediate object
38
- # @param final_step [#call] reader of the final value
39
- # @param delegate_allow_nil [Boolean] whether a nil intermediate object resolves to nil
40
- #
41
- # @return [HashAccessDelegate, HashAccessDelegateAllowNil] resolver instance
42
- #
43
19
  def self.get(to_step, final_step, delegate_allow_nil)
44
20
  delegate_allow_nil ? HashAccessDelegateAllowNil.new(to_step, final_step) : HashAccessDelegate.new(to_step, final_step)
45
21
  end
@@ -48,75 +24,81 @@ class Serega
48
24
  #
49
25
  # Value resolver for attributes with the :hash_access option
50
26
  #
51
- # @private
52
- class HashAccessKeyword
27
+ class HashAccessKeyword # :nodoc:
53
28
  def initialize(name, mode, allow_missing_key)
54
29
  @key = (mode == :symbol) ? name.to_sym : name.to_s
55
30
  @allow_missing_key = allow_missing_key
56
31
  end
57
32
 
58
- #
59
33
  # Reads the key from the record
60
- #
61
- # @param object [Object] serialized object or delegation step value
62
- # @return [Object] the value found
63
- #
64
34
  def call(object)
65
35
  return object[@key] if @allow_missing_key
66
36
 
67
37
  object.fetch(@key) do
68
38
  default = object[@key]
69
- next default unless default.nil?
70
-
71
- raise KeyError.new("key not found: #{@key.inspect}", key: @key, receiver: object)
39
+ default.nil? ? object.fetch(@key) : default
72
40
  end
73
41
  end
42
+
43
+ # Ruby code that reads the key, the same way as #call
44
+ def code(source_variable)
45
+ key = @key.is_a?(String) ? "#{@key.inspect}.freeze" : @key.inspect
46
+ return "#{source_variable}[#{key}]" if @allow_missing_key
47
+
48
+ "#{source_variable}.fetch(#{key}) { (found = #{source_variable}[#{key}]).nil? ? #{source_variable}.fetch(#{key}) : found }"
49
+ end
74
50
  end
75
51
 
76
52
  #
77
53
  # Value resolver for attributes with :hash_access and :delegate (without :allow_nil) options
78
54
  #
79
- # @private
80
- class HashAccessDelegate
55
+ class HashAccessDelegate # :nodoc:
81
56
  def initialize(to_step, final_step)
82
57
  @to_step = to_step
83
58
  @final_step = final_step
84
59
  end
85
60
 
86
- #
87
61
  # Delegates the value reading through the intermediate object
88
- #
89
- # @param object [Object] serialized object
90
- # @return [Object] the value found
91
- #
92
62
  def call(object)
93
63
  @final_step.call(@to_step.call(object))
94
64
  end
65
+
66
+ # Ruby code that delegates the value reading, the same way as #call
67
+ def code(source_variable)
68
+ to_code = @to_step.code(source_variable)
69
+ final_code = @final_step.code("intermediate")
70
+ return unless to_code && final_code
71
+
72
+ "(intermediate = #{to_code}; #{final_code})"
73
+ end
95
74
  end
96
75
 
97
76
  #
98
77
  # Value resolver for attributes with :hash_access and :delegate (with :allow_nil) options
99
78
  #
100
- # @private
101
- class HashAccessDelegateAllowNil
79
+ class HashAccessDelegateAllowNil # :nodoc:
102
80
  def initialize(to_step, final_step)
103
81
  @to_step = to_step
104
82
  @final_step = final_step
105
83
  end
106
84
 
107
- #
108
85
  # Delegates the value reading through the intermediate object,
109
86
  # resolving a nil intermediate to nil
110
- #
111
- # @param object [Object] serialized object
112
- # @return [Object, nil] the value found
113
- #
114
87
  def call(object)
115
88
  intermediate = @to_step.call(object)
116
89
  return if intermediate.nil?
117
90
 
118
91
  @final_step.call(intermediate)
119
92
  end
93
+
94
+ # Ruby code that delegates the value reading, the same way as #call
95
+ def code(source_variable)
96
+ to_code = @to_step.code(source_variable)
97
+ final_code = @final_step.code("intermediate")
98
+ return unless to_code && final_code
99
+
100
+ "((intermediate = #{to_code}).nil? ? nil : #{final_code})"
101
+ end
120
102
  end
121
103
  end
122
104
  end
@@ -4,42 +4,27 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
- # @private
8
- module AttributeValueResolvers
9
- #
10
- # Builds value resolver class for attributes with :keyword option
11
- #
12
- # @private
13
- class KeywordResolver
14
- #
15
- # Creates resolver that calls method on object
16
- #
17
- # @param keyword [Symbol] method name to call on object
18
- # @return [Keyword] resolver instance
19
- #
20
- def self.get(keyword)
21
- Keyword.new(keyword)
22
- end
23
- end
24
-
7
+ module AttributeValueResolvers # :nodoc:
25
8
  #
26
9
  # Value resolver class for attributes with :keyword option
27
10
  #
28
- # @private
29
- class Keyword
11
+ class Keyword # :nodoc:
30
12
  def initialize(keyword)
31
13
  @keyword = keyword
32
14
  end
33
15
 
34
- #
16
+ # Name of the method the value comes from
17
+ attr_reader :keyword
18
+
35
19
  # Calls the keyword method on the object
36
- #
37
- # @param object [Object] the object to call method on
38
- # @return [Object] result of method call
39
- #
40
20
  def call(object)
41
21
  object.public_send(@keyword)
42
22
  end
23
+
24
+ # Ruby code that calls the keyword method
25
+ def code(source_variable)
26
+ "#{source_variable}.#{@keyword}" if SeregaResultCode::PLAIN_METHOD_NAME.match?(@keyword)
27
+ end
43
28
  end
44
29
  end
45
30
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # One generated method per attribute that is read with plain Ruby code,
6
+ # for example `def read_full_name(source) = source.full_name`. Each method is
7
+ # defined with the file and line of its attribute, thus a backtrace of an
8
+ # error points to the attribute.
9
+ #
10
+ # `CONSTANTS` and `DEFAULTS` hold the `:const` and `:default` values of the
11
+ # attributes, `FORMATTERS` the formatter callables and `FORMATTER_ARGS` the
12
+ # arguments of method formatters, by attribute name.
13
+ #
14
+ # Each method name is `read_` and the attribute name with `_` in place of
15
+ # other characters than letters, digits and `_`, for example
16
+ # `read_first_name` for `first-name`. A name of another attribute gets a
17
+ # number: `read_first_name_2`. Thus attribute names do not clash with
18
+ # methods of Object, and generated code calls the methods directly.
19
+ #
20
+ class SeregaAttributeValues # :nodoc:
21
+ # `:const` values by attribute name
22
+ CONSTANTS = {}
23
+
24
+ # `:default` values by attribute name
25
+ DEFAULTS = {}
26
+
27
+ # Formatter callables by attribute name
28
+ FORMATTERS = {}
29
+
30
+ # Arguments of method formatters by attribute name
31
+ FORMATTER_ARGS = {}
32
+
33
+ # Method names by attribute name
34
+ METHOD_NAMES = {}
35
+
36
+ # Gives each subclass its own values
37
+ def self.inherited(subclass)
38
+ super
39
+ subclass.const_set(:CONSTANTS, {})
40
+ subclass.const_set(:DEFAULTS, {})
41
+ subclass.const_set(:FORMATTERS, {})
42
+ subclass.const_set(:FORMATTER_ARGS, {})
43
+ subclass.const_set(:METHOD_NAMES, {})
44
+ end
45
+ end
46
+ end