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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # Named batch loader. Loads values of many objects in one call.
6
+ #
7
+ class SeregaBatchLoader # :nodoc:
8
+ # Batch loader name
9
+ attr_reader :name
10
+
11
+ # Batch loader block
12
+ attr_reader :block
13
+
14
+ # Initializes new batch loader
15
+ def initialize(name:, block:)
16
+ SeregaValidations::CheckBatchLoaderParams.new(name, block).validate
17
+
18
+ @name = name.to_sym
19
+ @block = block
20
+ @signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [:ctx])
21
+ end
22
+
23
+ # Loads values for objects
24
+ def load(objects, context)
25
+ case signature
26
+ when "1" then block.call(objects)
27
+ when "2" then block.call(objects, context)
28
+ else block.call(objects, ctx: context) # "1_ctx"
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Batch loader block signature
35
+ attr_reader :signature
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # The :if, :unless, :if_value and :unless_value conditions of one attribute.
6
+ # Each object defines `#satisfy?(object, context)`, which checks :if and
7
+ # :unless, and `#satisfy_value?(value, context)`, which checks :if_value
8
+ # and :unless_value. Each method is one line of code at the file and line
9
+ # of the attribute, and calls the conditions directly, for example:
10
+ #
11
+ # def satisfy?(object, context) = (begin; object.active; rescue => error; ...; end)
12
+ #
13
+ class SeregaConditions # :nodoc:
14
+ def initialize(attribute, conditions)
15
+ @attribute = attribute
16
+ @conditions = conditions
17
+ satisfy_code = "def satisfy?(object, context) = #{check_code(:if, :unless)}"
18
+ satisfy_value_code = "def satisfy_value?(object, context) = #{check_code(:if_value, :unless_value)}"
19
+ attribute.define_methods(singleton_class, "#{satisfy_code}; #{satisfy_value_code}")
20
+ end
21
+
22
+ private
23
+
24
+ # Code that is true when the object passes the :if and does not match the
25
+ # :unless condition
26
+ def check_code(if_name, unless_name)
27
+ checks = []
28
+ checks << rescued_call_code(if_name) if @conditions.key?(if_name)
29
+ checks << "!#{rescued_call_code(unless_name)}" if @conditions.key?(unless_name)
30
+ checks.empty? ? "true" : checks.join(" && ")
31
+ end
32
+
33
+ # Code that calls the condition. An error raised there gets the condition
34
+ # name.
35
+ def rescued_call_code(name)
36
+ "(begin; #{call_code(name)}; rescue => error; " \
37
+ "Serega::SeregaUtils::SerializedAttributeError.condition(error, @attribute, #{name.inspect}); end)"
38
+ end
39
+
40
+ # Code that calls the condition with the parameters of its signature
41
+ def call_code(name)
42
+ condition = @conditions[name]
43
+ keyword_code = condition.code("object") if condition.is_a?(AttributeValueResolvers::Keyword)
44
+ return keyword_code if keyword_code
45
+
46
+ callable = "@conditions[#{name.inspect}]"
47
+ case SeregaUtils::MethodSignature.call(condition, pos_limit: 2, keyword_args: [:ctx])
48
+ when "1" then "#{callable}.call(object)"
49
+ when "2" then "#{callable}.call(object, context)"
50
+ when "1_ctx" then "#{callable}.call(object, ctx: context)"
51
+ when "2_ctx" then "#{callable}.call(object, context, ctx: context)"
52
+ else "#{callable}.call" # "0"
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/serega/config.rb CHANGED
@@ -1,37 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
+ # Config of a serializer, `config` in a serializer class. Subclasses copy
5
+ # the config of their parent and can change it.
4
6
  #
5
- # Stores serialization config
7
+ # ```ruby
8
+ # class AppSerializer < Serega
9
+ # config.auto_preload = true
10
+ # config.hide_by_default = :auto
11
+ # end
12
+ # ```
6
13
  #
14
+ # See [Configuration](https://github.com/andreyruby/serega/blob/master/docs/configuration.md).
7
15
  class SeregaConfig
8
16
  # :nocov: We can't use both :oj and :json adapters together
9
17
 
10
18
  #
11
19
  # Default config options
12
20
  #
13
- DEFAULTS = {
21
+ DEFAULTS = { # :nodoc:
14
22
  plugins: [],
15
- initiate_keys: %i[only with except check_initiate_params].freeze,
16
- attribute_keys: %i[
17
- method
18
- value
19
- serializer
20
- many
21
- hide
22
- const
23
- delegate
24
- default
25
- preload
26
- batch
27
- base_serializer
28
- hash_access
29
- ].freeze,
30
23
  serialize_keys: %i[context many].freeze,
31
24
  check_attribute_name: true,
25
+ require_many: false,
32
26
  check_initiate_params: true,
33
27
  delegate_default_allow_nil: false,
34
- max_cached_plans_per_serializer_count: 0,
28
+ max_cached_plans_per_serializer_count: 20,
29
+ depth_limit: nil,
35
30
  auto_preload: {has_delegate_option: false, has_serializer_option: false},
36
31
  auto_preload_excluded_methods: %i[itself __getobj__].freeze,
37
32
  hide_by_default: false,
@@ -41,99 +36,58 @@ class Serega
41
36
  }.freeze
42
37
  # :nocov:
43
38
 
44
- # SeregaConfig Instance methods
39
+ # Config options
45
40
  module SeregaConfigInstanceMethods
46
- #
47
41
  # Shows current config as Hash
48
- #
49
- # @return [Hash] config options
50
- #
51
- attr_reader :opts
42
+ attr_reader :opts # :nodoc:
52
43
 
53
- #
54
44
  # Initializes new config instance.
55
- #
56
- # @param opts [Hash] Initial config options
57
- #
58
- def initialize(opts = nil)
45
+ def initialize(opts = nil) # :nodoc:
59
46
  opts ||= DEFAULTS
60
47
  @opts = SeregaUtils::EnumDeepDup.call(opts)
48
+ @hash_access = nil
49
+ @plugin_configs = {}
61
50
  end
62
51
 
63
- #
64
52
  # Shows used plugins
65
- #
66
- # @return [Array] Used plugins
67
- #
68
- def plugins
53
+ def plugins # :nodoc:
69
54
  opts.fetch(:plugins)
70
55
  end
71
56
 
72
- # Returns options names allowed in `Serega#new` method
73
- # @return [Array<Symbol>] allowed options keys
74
- def initiate_keys
75
- opts.fetch(:initiate_keys)
76
- end
77
-
78
- # Returns options names allowed in `Serega.attribute` method
79
- # @return [Array<Symbol>] Allowed options keys for attribute initialization
80
- def attribute_keys
81
- opts.fetch(:attribute_keys)
82
- end
83
-
84
57
  # Returns options names allowed in `call, to_h` methods
85
- # @return [Array<Symbol>] Allowed options keys for serialization
86
- def serialize_keys
58
+ def serialize_keys # :nodoc:
87
59
  opts.fetch(:serialize_keys)
88
60
  end
89
61
 
90
- # Returns :check_initiate_params config option
91
- # @return [Boolean] Current :check_initiate_params config option
62
+ # Whether unknown fields of `only:`, `except:` and `with:` raise
63
+ # `Serega::AttributeNotExist`. `true` by default.
92
64
  def check_initiate_params
93
65
  opts.fetch(:check_initiate_params)
94
66
  end
95
67
 
96
- # Sets :check_initiate_params config option
97
- #
98
- # @param value [Boolean] Set :check_initiate_params config option
99
- #
100
- # @return [Boolean] :check_initiate_params config option
68
+ # Sets #check_initiate_params
101
69
  def check_initiate_params=(value)
102
- raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
103
- opts[:check_initiate_params] = value
70
+ opts[:check_initiate_params] = checked_boolean(value)
104
71
  end
105
72
 
106
- # Returns :delegate_default_allow_nil config option
107
- # @return [Boolean] Current :delegate_default_allow_nil config option
73
+ # Default `allow_nil:` of the `:delegate` attribute option. `false` by
74
+ # default.
108
75
  def delegate_default_allow_nil
109
76
  opts.fetch(:delegate_default_allow_nil)
110
77
  end
111
78
 
112
- # Sets :delegate_default_allow_nil config option
113
- #
114
- # @param value [Boolean] Set :delegate_default_allow_nil config option
115
- #
116
- # @return [Boolean] :delegate_default_allow_nil config option
79
+ # Sets #delegate_default_allow_nil
117
80
  def delegate_default_allow_nil=(value)
118
- raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
119
- opts[:delegate_default_allow_nil] = value
81
+ opts[:delegate_default_allow_nil] = checked_boolean(value)
120
82
  end
121
83
 
122
- # Returns :auto_preload_excluded_methods config option — methods that are
123
- # never auto-preloaded, as they return the serialized object itself and
124
- # not an association
125
- # @return [Array<Symbol>] Current :auto_preload_excluded_methods config option
84
+ # Methods that #auto_preload never preloads, as they return the object
85
+ # itself. `[:itself, :__getobj__]` by default.
126
86
  def auto_preload_excluded_methods
127
87
  opts.fetch(:auto_preload_excluded_methods)
128
88
  end
129
89
 
130
- # Sets :auto_preload_excluded_methods config option — methods that are
131
- # never auto-preloaded. Applies to the `:method` option of attributes
132
- # with a serializer and to the `delegate: {to: ...}` option.
133
- #
134
- # @param value [Array<Symbol>] Method names to skip in auto-preload
135
- #
136
- # @return [Array<Symbol>] New :auto_preload_excluded_methods config option
90
+ # Sets #auto_preload_excluded_methods
137
91
  def auto_preload_excluded_methods=(value)
138
92
  unless value.is_a?(Array) && value.all?(Symbol)
139
93
  raise SeregaError, "Must be an Array of Symbols, #{value.inspect} provided"
@@ -142,41 +96,31 @@ class Serega
142
96
  opts[:auto_preload_excluded_methods] = value
143
97
  end
144
98
 
145
- # Returns :base_serializer config option — the parent class for nested
146
- # serializers defined with attribute blocks
147
- # @return [Class, nil] Current :base_serializer config option
99
+ # Parent class of nested serializers defined with an attribute block.
100
+ # `nil` by default.
148
101
  def base_serializer
149
102
  opts.fetch(:base_serializer)
150
103
  end
151
104
 
152
- # Sets :base_serializer config option — the parent class for nested
153
- # serializers defined with attribute blocks. Usually a settings-only
154
- # serializer, e.g. `config.base_serializer = self` in an application
155
- # base serializer class.
156
- #
157
- # @param value [Class] Serega or its subclass
105
+ # Sets #base_serializer, a Serega subclass
158
106
  #
159
- # @return [Class] New :base_serializer config option
107
+ # ```ruby
108
+ # config.base_serializer = self
109
+ # ```
160
110
  def base_serializer=(value)
161
111
  raise SeregaError, "Must be a Serega subclass, #{value.inspect} provided" if !value.is_a?(Class) || !(value <= Serega)
162
112
 
163
113
  opts[:base_serializer] = value
164
114
  end
165
115
 
166
- # Returns :hide_by_default config option
167
- # @return [Boolean, Symbol] Current :hide_by_default config option
116
+ # Which attributes are hidden without the `:hide` option: `false` for none
117
+ # (the default), `true` for all, `:auto` for attributes with preloads or
118
+ # batch loaders.
168
119
  def hide_by_default
169
120
  opts.fetch(:hide_by_default)
170
121
  end
171
122
 
172
- # Sets :hide_by_default config option
173
- #
174
- # @param value [Boolean, Symbol] Accepted values:
175
- # - false (default) — nothing is hidden by default
176
- # - true — all attributes are hidden by default
177
- # - :auto — hides attributes that declare :preload or :batch
178
- #
179
- # @return [Boolean, Symbol] New :hide_by_default config option
123
+ # Sets #hide_by_default
180
124
  def hide_by_default=(value)
181
125
  opts[:hide_by_default] =
182
126
  case value
@@ -187,13 +131,15 @@ class Serega
187
131
  end
188
132
  end
189
133
 
190
- # @return [Hash] auto_preload option
134
+ # Attributes that get preloads automatically: `:delegate` attributes with
135
+ # `has_delegate_option` and `:serializer` attributes with
136
+ # `has_serializer_option`. Both are `false` by default.
191
137
  def auto_preload
192
138
  opts.fetch(:auto_preload)
193
139
  end
194
140
 
195
- # Validates and sets auto_preload option
196
- # @return [Hash] New auto_preload option with attributes that trigger auto preload
141
+ # Sets #auto_preload: `true` or `false` for both kinds of attributes, or a
142
+ # Hash with `has_delegate_option` and `has_serializer_option`
197
143
  def auto_preload=(value)
198
144
  opts[:auto_preload] =
199
145
  case value
@@ -207,85 +153,102 @@ class Serega
207
153
  end
208
154
  end
209
155
 
210
- # Returns the hash_access config object
211
- # @return [Serega::SeregaConfig::HashAccessConfig] hash_access config object
156
+ # Builds lazy config objects before freezing, thus a frozen config
157
+ # returns them
158
+ def freeze # :nodoc:
159
+ hash_access
160
+ super
161
+ end
162
+
163
+ # Defaults of the `hash_access:` attribute option
212
164
  def hash_access
213
165
  @hash_access ||= HashAccessConfig.new(opts.fetch(:hash_access))
214
166
  end
215
167
 
216
- # Returns :max_cached_plans_per_serializer_count config option
217
- # @return [Boolean] Current :max_cached_plans_per_serializer_count config option
168
+ # How many serialization plans with field selection the serializer
169
+ # caches, `20` by default. `0` disables the cache.
218
170
  def max_cached_plans_per_serializer_count
219
171
  opts.fetch(:max_cached_plans_per_serializer_count)
220
172
  end
221
173
 
222
- # Sets :max_cached_plans_per_serializer_count config option
223
- #
224
- # @param value [Boolean] Set :check_initiate_params config option
225
- #
226
- # @return [Boolean] New :max_cached_plans_per_serializer_count config option
174
+ # Sets #max_cached_plans_per_serializer_count
227
175
  def max_cached_plans_per_serializer_count=(value)
228
176
  raise SeregaError, "Must have Integer value, #{value.inspect} provided" unless value.is_a?(Integer)
229
177
  opts[:max_cached_plans_per_serializer_count] = value
230
178
  end
231
179
 
232
- # Returns whether attributes names check is disabled
180
+ # Deepest nesting of serialized fields, `nil` for no limit (the default).
181
+ # The limit of the serializer that starts the serialization applies.
182
+ def depth_limit
183
+ opts.fetch(:depth_limit)
184
+ end
185
+
186
+ # Sets #depth_limit: a positive Integer or `nil`
187
+ def depth_limit=(value)
188
+ valid = value.nil? || (value.is_a?(Integer) && value.positive?)
189
+ raise SeregaError, "Must have positive Integer or nil value, #{value.inspect} provided" unless valid
190
+ opts[:depth_limit] = value
191
+ end
192
+
193
+ # Whether attribute names may contain only `a-z`, `A-Z`, `0-9`, `_`, `-`
194
+ # and `~`. `true` by default.
233
195
  def check_attribute_name
234
196
  opts.fetch(:check_attribute_name)
235
197
  end
236
198
 
237
- # Sets :check_attribute_name config option
238
- #
239
- # @param value [Boolean] Set :check_attribute_name config option
240
- #
241
- # @return [Boolean] New :check_attribute_name config option
199
+ # Sets #check_attribute_name
242
200
  def check_attribute_name=(value)
243
- raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
244
- opts[:check_attribute_name] = value
201
+ opts[:check_attribute_name] = checked_boolean(value)
202
+ end
203
+
204
+ # Whether every relation, an attribute with `:serializer` or a block,
205
+ # must have the `:many` option. `false` by default.
206
+ def require_many
207
+ opts.fetch(:require_many)
208
+ end
209
+
210
+ # Sets #require_many
211
+ def require_many=(value)
212
+ opts[:require_many] = checked_boolean(value)
245
213
  end
246
214
 
247
- # Returns current batch_id_option
215
+ # Method that returns the id of an object for batch loaders, `:id` by
216
+ # default
248
217
  def batch_id_option
249
218
  opts.fetch(:batch_id_option)
250
219
  end
251
220
 
252
- # Sets :batch_id_option config option
253
- #
254
- # @param value [Symbol] Set :batch_id_option config option
255
- #
256
- # @return [Symbol] New :check_attribute_name config option
221
+ # Sets #batch_id_option
257
222
  def batch_id_option=(value)
258
223
  raise SeregaError, "Must have Symbol value, #{value.inspect} provided" unless value.is_a?(Symbol)
259
224
  opts[:batch_id_option] = value
260
225
  end
226
+
227
+ private
228
+
229
+ def checked_boolean(value)
230
+ raise SeregaError, "Must have boolean value, #{value.inspect} provided" unless value == true || value == false
231
+
232
+ value
233
+ end
261
234
  end
262
235
 
263
- #
264
- # Config for the `hash_access:` attribute option
265
- #
236
+ # Defaults of the `hash_access:` attribute option, `config.hash_access`
266
237
  class HashAccessConfig
267
- # @return [Hash] hash_access config options
268
- attr_reader :opts
238
+ # hash_access config options
239
+ attr_reader :opts # :nodoc:
269
240
 
270
- #
271
241
  # Initializes HashAccessConfig object
272
- #
273
- # @param opts [Hash] hash_access config options
274
- #
275
- # @return [Serega::SeregaConfig::HashAccessConfig]
276
- #
277
- def initialize(opts)
242
+ def initialize(opts) # :nodoc:
278
243
  @opts = opts
279
244
  end
280
245
 
281
- # @return [Symbol] mode used by `hash_access: true` (default :symbol)
246
+ # Mode of `hash_access: true`: `:symbol` (the default) or `:string` keys
282
247
  def default_mode
283
248
  opts.fetch(:default_mode)
284
249
  end
285
250
 
286
- # Sets the mode used by `hash_access: true`
287
- # @param value [Symbol] one of :symbol, :string
288
- # @return [Symbol] new default mode
251
+ # Sets #default_mode
289
252
  def default_mode=(value)
290
253
  unless AttributeValueResolvers::HashAccessResolver::MODES.include?(value)
291
254
  raise SeregaError, "Invalid hash_access default_mode #{value.inspect}. Allowed modes: :symbol, :string"
@@ -294,14 +257,13 @@ class Serega
294
257
  opts[:default_mode] = value
295
258
  end
296
259
 
297
- # @return [Boolean] allow_missing_key used when an attribute omits it
260
+ # Whether a missing Hash key gives `nil` instead of raising `KeyError`.
261
+ # `false` by default.
298
262
  def default_allow_missing_key
299
263
  opts.fetch(:default_allow_missing_key)
300
264
  end
301
265
 
302
- # Sets the allow_missing_key used when an attribute omits it
303
- # @param value [Boolean]
304
- # @return [Boolean] new default allow_missing_key
266
+ # Sets #default_allow_missing_key
305
267
  def default_allow_missing_key=(value)
306
268
  unless value == true || value == false
307
269
  raise SeregaError, "Invalid hash_access default_allow_missing_key #{value.inspect}. Must be a Boolean"
@@ -312,6 +274,5 @@ class Serega
312
274
  end
313
275
 
314
276
  include SeregaConfigInstanceMethods
315
- extend Serega::SeregaHelpers::SerializerClassHelper
316
277
  end
317
278
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaEngine # :nodoc:
5
+ #
6
+ # Pulls of relation sources. A pull says what to take for one relation
7
+ # source from the serialized objects of its child group:
8
+ # - the count of a collection: an Array of this count of serialized objects
9
+ # - SINGLE_SOURCE for one source: one serialized object
10
+ # - nil for nil: nothing, the relation value is nil
11
+ # - SKIP for a relation skipped by its condition: nothing, the relation
12
+ # value is SKIP
13
+ #
14
+ module Pulls # :nodoc:
15
+ # Pull of one source that is not in a collection. Its relation value is
16
+ # one serialized object, and not an Array.
17
+ SINGLE_SOURCE = Object.new.freeze
18
+
19
+ module_function
20
+
21
+ # Appends the sources of the relation sources to sources, and returns
22
+ # the pull of each relation source
23
+ def collect(sources, relation_sources, many, known_classes)
24
+ relation_sources.map { |relation_source| append(sources, relation_source, many, known_classes) }
25
+ end
26
+
27
+ # Collects like `.collect`, and keeps SKIP of a skipped relation source
28
+ # as its pull
29
+ def collect_conditional(sources, relation_sources, many, known_classes)
30
+ relation_sources.map do |relation_source|
31
+ SKIP.equal?(relation_source) ? relation_source : append(sources, relation_source, many, known_classes)
32
+ end
33
+ end
34
+
35
+ # Appends the source(s) of one relation source to sources, and returns
36
+ # its pull
37
+ def append(sources, relation_source, many, known_classes)
38
+ return if relation_source.nil?
39
+
40
+ if many != false && SeregaUtils::CollectionDetector.call(relation_source, known_classes)
41
+ collection = relation_source.to_a
42
+ sources.concat(collection)
43
+ collection.size
44
+ else
45
+ sources << relation_source
46
+ many ? 1 : SINGLE_SOURCE # `many` on, but a sole source was given — wrap it, don't raise
47
+ end
48
+ end
49
+
50
+ # Takes the relation value of each pull from the front of the
51
+ # serialized objects. The serialized objects follow the order of the
52
+ # pulls. Empties the serialized objects.
53
+ def take!(serialized, pulls)
54
+ pulls.map do |pull|
55
+ case pull
56
+ when Integer then serialized.shift(pull)
57
+ when SINGLE_SOURCE then serialized.shift
58
+ else pull # nil or SKIP
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # Serialization engine
6
+ #
7
+ module SeregaEngine # :nodoc:
8
+ # Value of an attribute skipped by its conditions. It is also the pull of
9
+ # a skipped relation.
10
+ SKIP = Object.new.freeze
11
+
12
+ #
13
+ # One serialization run. Serializes the source(s) with a plan, and all
14
+ # their relation sources. A source is an object to serialize.
15
+ #
16
+ # The run makes source groups. A group holds all sources of one plan, for
17
+ # example the posts of all users, thus batch loaders and preloads run once
18
+ # per group. The run serializes the groups in two passes:
19
+ # - Discover pass, from the root group down. Each group runs its preloads
20
+ # and makes one child group per relation.
21
+ # - Build pass, from the last group up. Each group builds its serialized
22
+ # objects. The serialized related objects are ready at this time.
23
+ #
24
+ class Run # :nodoc:
25
+ class << self
26
+ # Serializes the object(s) with the plan in a new run. A flat plan
27
+ # builds its serialized object(s) with its result builder, without a
28
+ # run, and one source without the sources Array.
29
+ def call(plan, object, many, mode, context)
30
+ return new(mode, context).call(plan, object, many: many) unless plan.flat?
31
+ return if object.nil?
32
+
33
+ builder = plan.result_builder(mode)
34
+ return builder.call_one(object, context) if many == false
35
+
36
+ sources = []
37
+ pull = Pulls.append(sources, object, many, plan.collection_classes)
38
+ Pulls::SINGLE_SOURCE.equal?(pull) ? builder.call_one(object, context) : builder.call(sources, context, nil, nil)
39
+ end
40
+ end
41
+
42
+ # Serialization mode
43
+ attr_reader :mode
44
+
45
+ # Serialization context
46
+ attr_reader :context
47
+
48
+ def initialize(mode, context)
49
+ @mode = mode
50
+ @context = context
51
+ end
52
+
53
+ # Serializes the object(s) with the plan.
54
+ def call(plan, object, many:)
55
+ return if object.nil?
56
+
57
+ root_group = root_source_group(plan, object, many)
58
+ source_groups = discover(root_group)
59
+ build(source_groups)
60
+ root_value(root_group)
61
+ end
62
+
63
+ # Makes a source group of the plan.
64
+ def new_source_group(plan, sources, pulls)
65
+ SeregaSourceGroup.new(self, plan, sources, pulls)
66
+ end
67
+
68
+ private
69
+
70
+ # Makes the root group: the root source(s) with one pull
71
+ def root_source_group(plan, object, many)
72
+ sources = []
73
+ pull = Pulls.append(sources, object, many, plan.collection_classes)
74
+ new_source_group(plan, sources, [pull])
75
+ end
76
+
77
+ # Discovers the root group and its child groups, from the root down.
78
+ # Returns all groups in discover order: a child group comes after its
79
+ # parent group.
80
+ def discover(root_group)
81
+ source_groups = [root_group]
82
+ index = 0
83
+
84
+ while index < source_groups.size
85
+ source_groups.concat(source_groups[index].discover)
86
+ index += 1
87
+ end
88
+
89
+ source_groups
90
+ end
91
+
92
+ # Builds the groups from the last one up, thus a child group is built
93
+ # before its parent group
94
+ def build(source_groups)
95
+ source_groups.reverse_each(&:build)
96
+ end
97
+
98
+ # Returns the serialized root: one serialized object for SINGLE_SOURCE,
99
+ # or all serialized objects of the root group
100
+ def root_value(root_group)
101
+ serialized = root_group.serialized
102
+ Pulls::SINGLE_SOURCE.equal?(root_group.pulls[0]) ? serialized[0] : serialized
103
+ end
104
+ end
105
+ end
106
+ end