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
data/lib/serega.rb CHANGED
@@ -1,53 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "serega/version"
4
5
 
5
- # Parent class for your serializers
6
+ # Parent class of serializers. A serializer lists attributes and serializes
7
+ # objects to Hashes, JSON, Data or Struct objects:
8
+ #
9
+ # ```ruby
10
+ # class UserSerializer < Serega
11
+ # attribute :first_name
12
+ # attribute :email, hide: true
13
+ # end
14
+ #
15
+ # UserSerializer.to_h(user) # => {first_name: "Bruce"}
16
+ # UserSerializer.to_h(user, with: :email) # => {first_name: "Bruce", email: "bruce@wayne.com"}
17
+ # ```
18
+ #
19
+ # See the [guides](https://github.com/andreyruby/serega/blob/master/docs/README.md).
6
20
  class Serega
7
21
  # Frozen hash
8
- # @return [Hash] frozen hash
9
- # @private
10
- FROZEN_EMPTY_HASH = {}.freeze
22
+ FROZEN_EMPTY_HASH = {}.freeze # :nodoc:
11
23
 
12
24
  # Frozen array
13
- # @return [Array] frozen array
14
- # @private
15
- FROZEN_EMPTY_ARRAY = [].freeze
25
+ FROZEN_EMPTY_ARRAY = [].freeze # :nodoc:
16
26
 
17
- # Empty modifiers/serialization options (used when serializing with no opts provided)
18
- FROZEN_EMPTY_OPTS = [FROZEN_EMPTY_HASH, nil].freeze
19
- private_constant :FROZEN_EMPTY_OPTS
27
+ # Locks serializers one at a time
28
+ LOCK_MUTEX = Mutex.new # :nodoc:
29
+
30
+ # Errors raised when another Ractor reads a value that is not shareable,
31
+ # empty on a Ruby without Ractors
32
+ # :nocov:
33
+ RACTOR_ISOLATION_ERRORS = (defined?(Ractor) ? [Ractor::IsolationError] : []).freeze # :nodoc:
34
+ # :nocov:
35
+
36
+ # Path prefix of the Serega source files
37
+ LIB_PATH = File.expand_path("serega", __dir__).freeze # :nodoc:
20
38
  end
21
39
 
22
40
  require_relative "serega/errors"
23
- require_relative "serega/helpers/serializer_class_helper"
24
41
  require_relative "serega/utils/collection_detector"
42
+ require_relative "serega/utils/deep_merge"
25
43
  require_relative "serega/utils/enum_deep_dup"
26
44
  require_relative "serega/utils/enum_deep_freeze"
27
45
  require_relative "serega/utils/method_signature"
46
+ require_relative "serega/utils/parse_string_modifiers"
47
+ require_relative "serega/utils/ractor_local"
28
48
  require_relative "serega/utils/serialized_attribute_error"
29
- require_relative "serega/utils/symbol_name"
30
49
  require_relative "serega/utils/to_hash"
31
50
  require_relative "serega/attribute_value_resolvers/batch"
32
51
  require_relative "serega/attribute_value_resolvers/const"
33
52
  require_relative "serega/attribute_value_resolvers/delegate"
34
53
  require_relative "serega/attribute_value_resolvers/hash_access"
35
54
  require_relative "serega/attribute_value_resolvers/keyword"
55
+ require_relative "serega/attribute_values"
56
+ require_relative "serega/conditions"
57
+ require_relative "serega/formatter"
36
58
  require_relative "serega/attribute"
59
+ require_relative "serega/attribute_normalizers/batch"
60
+ require_relative "serega/attribute_normalizers/block"
61
+ require_relative "serega/attribute_normalizers/delegate"
62
+ require_relative "serega/attribute_normalizers/hash_access"
63
+ require_relative "serega/attribute_normalizers/preloads"
37
64
  require_relative "serega/attribute_normalizer"
38
- require_relative "serega/engine/level_queue"
39
- require_relative "serega/engine/level"
40
- require_relative "serega/engine/loader"
65
+ require_relative "serega/engine/run"
66
+ require_relative "serega/engine/pulls"
41
67
  require_relative "serega/validations/utils/check_allowed_keys"
68
+ require_relative "serega/validations/utils/check_hash_access_value"
42
69
  require_relative "serega/validations/utils/check_opt_is_bool"
43
70
  require_relative "serega/validations/utils/check_opt_is_hash"
44
71
  require_relative "serega/validations/utils/check_opt_is_string_or_symbol"
45
72
  require_relative "serega/validations/attribute/check_block"
46
73
  require_relative "serega/validations/attribute/check_name"
47
74
  require_relative "serega/validations/attribute/check_opt_base_serializer"
48
- require_relative "serega/validations/attribute/check_opt_const"
75
+ require_relative "serega/validations/attribute/check_opt_condition"
49
76
  require_relative "serega/validations/attribute/check_opt_hide"
50
77
  require_relative "serega/validations/attribute/check_opt_delegate"
78
+ require_relative "serega/validations/attribute/check_opt_format"
51
79
  require_relative "serega/validations/attribute/check_opt_batch"
52
80
  require_relative "serega/validations/attribute/check_opt_hash_access"
53
81
  require_relative "serega/validations/attribute/check_opt_many"
@@ -57,209 +85,227 @@ require_relative "serega/validations/attribute/check_opt_serializer"
57
85
  require_relative "serega/validations/attribute/check_opt_value"
58
86
  require_relative "serega/validations/initiate/check_modifiers"
59
87
  require_relative "serega/validations/check_attribute_params"
60
- require_relative "serega/validations/check_initiate_params"
61
88
  require_relative "serega/validations/check_batch_loader_params"
62
- require_relative "serega/validations/check_serialize_params"
89
+ require_relative "serega/validations/check_formatter"
90
+ require_relative "serega/validations/check_serialize_keywords"
63
91
 
92
+ require_relative "serega/batch_loader"
64
93
  require_relative "serega/config"
65
94
  require_relative "serega/presenter"
66
- require_relative "serega/object_serializer"
95
+ require_relative "serega/source_group"
67
96
  require_relative "serega/plan_point"
68
97
  require_relative "serega/plan"
69
- require_relative "serega/data_builder"
98
+ require_relative "serega/result_code"
99
+ require_relative "serega/result_builder"
100
+ require_relative "serega/plan/cache"
70
101
  require_relative "serega/plugins"
71
102
 
72
103
  class Serega
73
- @config = SeregaConfig.new
74
- @presenter_class = nil
75
-
76
- # Validates `Serializer.attribute` params
77
- check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
78
- check_attribute_params_class.serializer_class = self
79
- const_set(:CheckAttributeParams, check_attribute_params_class)
80
-
81
- # Validates `Serializer#new` params
82
- check_initiate_params_class = Class.new(SeregaValidations::CheckInitiateParams)
83
- check_initiate_params_class.serializer_class = self
84
- const_set(:CheckInitiateParams, check_initiate_params_class)
85
-
86
- # Validates `serializer#call(obj, PARAMS)` params
87
- check_serialize_params_class = Class.new(SeregaValidations::CheckSerializeParams)
88
- check_serialize_params_class.serializer_class = self
89
- const_set(:CheckSerializeParams, check_serialize_params_class)
90
-
91
- # Validates `Serializer.batch` params
92
- check_batch_loader_params_class = Class.new(SeregaValidations::CheckBatchLoaderParams)
93
- check_batch_loader_params_class.serializer_class = self
94
- const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
95
-
96
- # Assigns `SeregaEngineLoader` constant to current class
97
- engine_loader_class = Class.new(SeregaEngine::Loader)
98
- engine_loader_class.serializer_class = self
99
- const_set(:SeregaEngineLoader, engine_loader_class)
100
-
101
104
  #
102
- # Serializers class methods
105
+ # Class methods of serializers: they define attributes and serialize
103
106
  #
104
107
  module ClassMethods
105
- # Returns current config
106
- # @return [SeregaConfig] current serializer config
108
+ # Config of the serializer. Subclasses copy it.
109
+ #
110
+ # ```ruby
111
+ # config.hide_by_default = true
112
+ # ```
113
+ #
114
+ # See [Configuration](https://github.com/andreyruby/serega/blob/master/docs/configuration.md).
107
115
  attr_reader :config
116
+
108
117
  #
109
- # Enables plugin for current serializer
118
+ # Turns on a built-in feature by name, with its options. Subclasses get it
119
+ # too.
110
120
  #
111
- # @param name [Symbol, Class<Module>] Plugin name or plugin module itself
112
- # @param opts [Hash>] Plugin options
121
+ # ```ruby
122
+ # plugin :root, root_one: :user, root_many: :users
123
+ # ```
113
124
  #
114
- # @return [class<Module>] Loaded plugin module
125
+ # See [Root Key and Metadata](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md).
115
126
  #
116
127
  def plugin(name, **opts)
128
+ check_unlocked
117
129
  raise SeregaError, "This plugin is already loaded" if plugin_used?(name)
118
130
 
119
- plugin = SeregaPlugins.find_plugin(name)
120
-
121
- # We split loading of plugin to three parts - before_load, load, after_load:
122
- #
123
- # - **before_load_plugin** usually used to check requirements and to load additional plugins
124
- # - **load_plugin** usually used to include plugin modules
125
- # - **after_load_plugin** usually used to add config options
126
- plugin.before_load_plugin(self, **opts) if plugin.respond_to?(:before_load_plugin)
127
- plugin.load_plugin(self, **opts) if plugin.respond_to?(:load_plugin)
128
- plugin.after_load_plugin(self, **opts) if plugin.respond_to?(:after_load_plugin)
129
-
130
- # Store attached plugins, so we can check it is loaded later
131
- config.plugins << (plugin.respond_to?(:plugin_name) ? plugin.plugin_name : plugin)
131
+ plugin = SeregaPlugins.find(name)
132
+ plugin.apply(self, **opts)
133
+ config.plugins << name
132
134
 
133
135
  plugin
134
136
  end
135
137
 
136
- #
137
138
  # Checks plugin is used
138
- #
139
- # @param name [Symbol, Class<Module>] Plugin name or plugin module itself
140
- #
141
- # @return [Boolean] Is plugin used
142
- #
143
- # @private
144
- def plugin_used?(name)
145
- plugin_name =
146
- case name
147
- when Module then name.respond_to?(:plugin_name) ? name.plugin_name : name
148
- else name
149
- end
150
-
151
- config.plugins.include?(plugin_name)
139
+ def plugin_used?(name) # :nodoc:
140
+ config.plugins.include?(name)
152
141
  end
153
142
 
154
143
  #
155
- # Lists attributes
144
+ # Attributes by name, hidden and inherited ones too
156
145
  #
157
- # @return [Hash] attributes list
146
+ # ```ruby
147
+ # UserSerializer.attributes.keys # => [:first_name, :email]
148
+ # ```
158
149
  #
159
- # @private
160
150
  def attributes
161
- @attributes ||= {}
151
+ @attributes
162
152
  end
163
153
 
164
- #
165
154
  # Lists defined batch loaders
166
- #
167
- # @return [Hash] batch loaders list
168
- #
169
- # @private
170
- def batch_loaders
171
- @batch_loaders ||= {}
155
+ def batch_loaders # :nodoc:
156
+ @batch_loaders
157
+ end
158
+
159
+ # Lists defined formatters
160
+ def formatters # :nodoc:
161
+ @formatters
162
+ end
163
+
164
+ # Cached serialization plans
165
+ def plan_cache # :nodoc:
166
+ @plan_cache
167
+ # :nocov:
168
+ rescue *RACTOR_ISOLATION_ERRORS
169
+ raise SeregaError, @unshareable_reason if @unshareable_reason
170
+
171
+ raise SeregaError, "#{self} can not serialize in other Ractors. Call `make_shareable!` at the end of its definition"
172
+ # :nocov:
173
+ end
174
+
175
+ # Returns the presenter class that wraps objects in the current Ractor,
176
+ # `nil` without presenter methods. A shareable serializer gives each
177
+ # Ractor its own subclass of the presenter, which defines delegators
178
+ # in its own `Delegators` module.
179
+ def local_presenter # :nodoc:
180
+ key = @presenter_key
181
+ return @presenter_class unless key
182
+
183
+ SeregaUtils::RactorLocal.fetch(key) { Class.new(@presenter_class) }
172
184
  end
173
185
 
174
- #
175
186
  # Lists blocks given to `presenter`, in definition order
176
- #
177
- # @return [Array<Proc>] presenter blocks list
178
- #
179
- # @private
180
- def presenter_blocks
181
- @presenter_blocks ||= []
187
+ def presenter_blocks # :nodoc:
188
+ @presenter_blocks
189
+ end
190
+
191
+ # Returns (and caches) the Data class of the serialized objects with the
192
+ # given members
193
+ def data_class_for(point_names) # :nodoc:
194
+ classes = @data_classes
195
+ classes = SeregaUtils::RactorLocal.fetch(SeregaUtils::RactorLocal.key(self, :data_classes)) { {} } if classes.frozen?
196
+ classes[point_names] ||= Data.define(*point_names)
197
+ end
198
+
199
+ # Returns (and caches) the Struct class of the serialized objects with
200
+ # the given members
201
+ def struct_class_for(point_names) # :nodoc:
202
+ classes = @struct_classes
203
+ classes = SeregaUtils::RactorLocal.fetch(SeregaUtils::RactorLocal.key(self, :struct_classes)) { {} } if classes.frozen?
204
+ classes[point_names] ||= Struct.new(*point_names)
182
205
  end
183
206
 
184
207
  #
185
- # Adds attribute
208
+ # Adds an attribute. By default its value is `object.<name>`, and the
209
+ # options change that:
186
210
  #
187
- # @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
188
- # @param opts [Hash] Options to serialize attribute
189
- # @param block [Proc] Defines attributes of a nested anonymous serializer.
190
- # The block is executed in the context of that serializer, so everything
191
- # available in a serializer class body can be used inside. The attribute
192
- # value (found by name/:method/:value/:delegate/:const/:batch as usual)
193
- # is serialized with this nested serializer. The nested serializer
194
- # inherits from the `:base_serializer` attribute option or
195
- # `config.base_serializer` (one of them is required).
211
+ # ```ruby
212
+ # attribute :first_name
213
+ # attribute :name, method: :full_name
214
+ # attribute :email, hide: true
215
+ # attribute :posts, serializer: PostSerializer
216
+ # ```
196
217
  #
197
- # @example
198
- # attribute :statistics, method: :itself do
199
- # attribute :likes_count
200
- # attribute :comments_count
201
- # end
218
+ # A block defines the attributes of a nested serializer. It inherits from
219
+ # the `:base_serializer` option or `config.base_serializer`:
220
+ #
221
+ # ```ruby
222
+ # attribute :statistics, method: :itself do
223
+ # attribute :likes_count
224
+ # attribute :comments_count
225
+ # end
226
+ # ```
202
227
  #
203
- # @return [Serega::SeregaAttribute] Added attribute
228
+ # See [Attributes](https://github.com/andreyruby/serega/blob/master/docs/attributes.md#all-options)
229
+ # for all options.
204
230
  #
205
231
  def attribute(name, **opts, &block)
206
- attribute = self::SeregaAttribute.new(name: name, opts: opts, block: block)
232
+ check_unlocked
233
+ location = caller_locations.find { |caller_location| !caller_location.path.start_with?(LIB_PATH) }
234
+ attribute = SeregaAttribute.new(self, name: name, opts: opts, block: block, location: location && "#{location.path}:#{location.lineno}")
207
235
  attributes[attribute.name] = attribute
208
236
  end
209
237
 
210
238
  #
211
- # Defines a batch loader
239
+ # Defines a named batch loader: a callable or a block that receives the
240
+ # objects, and the context as a second parameter or as the `:ctx`
241
+ # keyword. It returns the values by object id. Subclasses inherit it.
212
242
  #
213
- # @example
214
- # batch :tags, PostTagsLoader
243
+ # ```ruby
244
+ # batch :likes_count, ->(users) { Like.where(user: users).group(:user_id).count }
245
+ # batch(:rank) { |users, ctx:| Rank.for(users, ctx[:season]) }
215
246
  #
216
- # @example with block
217
- # batch_loader(:tags) do |posts|
218
- # Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
219
- # end
247
+ # attribute :likes_count, batch: true
248
+ # ```
220
249
  #
221
- # attribute :tags, batch: :tags, value: { |post, batch:| batch[:tags][post.id] }
250
+ # See [Batch loading](https://github.com/andreyruby/serega/blob/master/docs/batch-loading.md).
222
251
  #
223
- # @example with context
224
- # batch_loader(:tags) do |posts, ctx:|
225
- # next {} if ctx[:bot]
252
+ def batch(name, value = nil, &block)
253
+ check_unlocked
254
+ raise SeregaError, "Batch loader must be defined with a callable value or block" if !value == !block
255
+
256
+ batch_loader = SeregaBatchLoader.new(name: name, block: value || block)
257
+ batch_loaders[batch_loader.name] = batch_loader
258
+ end
259
+
226
260
  #
227
- # Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
228
- # end
261
+ # Defines a named formatter for the `:format` attribute option. A formatter
262
+ # is a method name called on the value, an Array of a method name and its
263
+ # arguments, or a callable or a block that receives the value, and the
264
+ # context as a second parameter or as the `:ctx` keyword. With
265
+ # `allow_nil: true` a `nil` value stays `nil`. Subclasses inherit it.
229
266
  #
230
- # attribute :tags, batch: :tags, value: { |post, batch:| batch[:tags][post.id] }
267
+ # ```ruby
268
+ # formatter :string, :to_s
269
+ # formatter :iso8601, [:iso8601, 3]
270
+ # formatter(:money) { |cents, ctx| "#{cents / 100.0} #{ctx[:currency]}" }
231
271
  #
232
- # @param name [Symbol] A batch loader name
233
- # @param value [#call] Batch loader
234
- # @param block [Proc] Batch loader
272
+ # attribute :created_at, format: :iso8601
273
+ # ```
235
274
  #
236
- # @return [#call] Batch loader
275
+ # See [Formatters](https://github.com/andreyruby/serega/blob/master/docs/formatters.md).
237
276
  #
238
- def batch(name, value = nil, &block)
239
- raise SeregaError, "Batch loader must be defined with a callable value or block" if (value && block) || (!value && !block)
277
+ def formatter(name, value = nil, allow_nil: false, &block)
278
+ check_unlocked
279
+ raise SeregaError, "Formatter name must be a Symbol" unless name.is_a?(Symbol)
280
+ raise SeregaError, "Formatter must be defined with a value or a block" if !value == !block
281
+ raise SeregaError, "Invalid option :allow_nil => #{allow_nil.inspect}. Must have a boolean value" unless allow_nil == true || allow_nil == false
240
282
 
241
- batch_loader = self::SeregaEngineLoader.new(name: name, block: value || block)
242
- batch_loaders[batch_loader.name] = batch_loader
283
+ value ||= block
284
+ SeregaValidations::CheckFormatter.call(name, value)
285
+ formatters[name] = SeregaFormatter.new({use: value, allow_nil: allow_nil})
243
286
  end
244
287
 
245
288
  #
246
- # Defines presenter methods — evaluates the block inside the serializer's own
247
- # presenter class. Multiple blocks accumulate.
289
+ # Defines presenter methods: attributes read their values from them. The
290
+ # block runs in the presenter class of the serializer, and each call adds
291
+ # methods. Returns the presenter class, `nil` without one.
248
292
  #
249
- # presenter do
250
- # def name
251
- # [first_name, last_name].compact.join(" ")
252
- # end
253
- # end
293
+ # ```ruby
294
+ # attribute :name
254
295
  #
255
- # @param block [Proc] Presenter methods
296
+ # presenter do
297
+ # def name
298
+ # [first_name, last_name].compact.join(" ")
299
+ # end
300
+ # end
301
+ # ```
256
302
  #
257
- # @return [Class<SeregaPresenter>, nil] the serializer presenter class,
258
- # nil when the serializer has none
303
+ # See [Presenter](https://github.com/andreyruby/serega/blob/master/docs/presenter.md).
259
304
  #
260
305
  def presenter(&block)
261
306
  return @presenter_class unless block
262
307
 
308
+ check_unlocked
263
309
  @presenter_class ||= Class.new(SeregaPresenter)
264
310
  @presenter_class.class_exec(&block)
265
311
  presenter_blocks << block
@@ -267,31 +313,23 @@ class Serega
267
313
  end
268
314
 
269
315
  #
270
- # Registers (or returns) the handler used to preload an attribute's
271
- # associations onto the records gathered during serialization.
272
- #
273
- # The handler is called once per preloaded attribute with the gathered
274
- # objects and that attribute's preloads. ORM plugins register a handler
275
- # that performs the actual eager loading.
276
- #
277
- # @example with a block
278
- # preload_with { |objects, preloads| MyORM.eager_load(objects, preloads) }
279
- #
280
- # @example with a callable value
281
- # preload_with MyPreloader
316
+ # Sets the handler that preloads the `:preload` value of an attribute. It
317
+ # runs once per attribute with a preload, with the objects to serialize
318
+ # and the preloads. Without arguments, returns the handler. Subclasses
319
+ # inherit it.
282
320
  #
283
- # @param value [#call, nil] Preload handler accepting two positional arguments
284
- # @param block [Proc] Preload handler accepting two positional arguments
321
+ # ```ruby
322
+ # preload_with { |objects, preloads| RecordPreloader.call(objects, preloads) }
323
+ # preload_with AppPreloader
324
+ # ```
285
325
  #
286
- # @return [#call, nil] The registered preload handler
326
+ # See [Preloads](https://github.com/andreyruby/serega/blob/master/docs/preloads.md).
287
327
  #
288
328
  def preload_with(value = nil, &block)
289
329
  return @preload_with if value.nil? && block.nil?
290
- raise SeregaError, "preload_with accepts a single callable or a block, not both" if value && block
291
-
292
- handler = value || block
293
- raise SeregaError, "preload_with value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
294
330
 
331
+ check_unlocked
332
+ handler = checked_handler(:preload_with, value, block)
295
333
  signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2)
296
334
  raise SeregaError, "preload_with handler must accept two positional arguments: (objects, preloads)" unless signature == "2"
297
335
 
@@ -299,45 +337,25 @@ class Serega
299
337
  end
300
338
 
301
339
  #
302
- # Registers (or returns) the handler that replaces the serialized objects
303
- # before serialization starts.
304
- #
305
- # The handler is called once per serialization with the objects provided to
306
- # `.call`/`.to_h`/`.to_data` and the serialization context. Its result is
307
- # serialized instead of the provided objects, which allows to accept ids or
308
- # other references and load the records in one place.
309
- #
310
- # The handler runs before the `:many` option is detected, so it may turn a
311
- # single object into a collection and back. A provided `:many` serialization
312
- # option is still used as is.
313
- #
314
- # The handler runs only for the serialized objects, and not for objects of
315
- # nested serializers.
316
- #
317
- # @example with a block
318
- # prepare_initial_objects { |user_ids| User.where(id: user_ids) }
319
- #
320
- # @example with a context
321
- # prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
322
- #
323
- # @example with a keyword context
324
- # prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
325
- #
326
- # @example with a callable value
327
- # prepare_initial_objects UsersLoader
340
+ # Sets the handler that replaces the objects given to the serializer, for
341
+ # example ids with records. It runs once per serialization, before Serega
342
+ # detects a list, and receives the objects, and the context as a second
343
+ # parameter or as the `:ctx` keyword. Objects of relations skip it.
344
+ # Without arguments, returns the handler.
328
345
  #
329
- # @param value [#call, nil] Handler accepting (objects), (objects, context) or (objects, ctx:)
330
- # @param block [Proc] Handler accepting (objects), (objects, context) or (objects, ctx:)
346
+ # ```ruby
347
+ # prepare_initial_objects { |user_ids| User.where(id: user_ids) }
348
+ # prepare_initial_objects { |ids, ctx:| User.where(id: ids, account: ctx[:account]) }
349
+ # prepare_initial_objects UsersLoader
350
+ # ```
331
351
  #
332
- # @return [#call, nil] The registered handler
352
+ # See [Prepare initial objects](https://github.com/andreyruby/serega/blob/master/docs/prepare-initial-objects.md).
333
353
  #
334
354
  def prepare_initial_objects(value = nil, &block)
335
355
  return @prepare_initial_objects if value.nil? && block.nil?
336
- raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
337
-
338
- handler = value || block
339
- raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
340
356
 
357
+ check_unlocked
358
+ handler = checked_handler(:prepare_initial_objects, value, block)
341
359
  signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
342
360
  raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
343
361
 
@@ -345,133 +363,176 @@ class Serega
345
363
  @prepare_initial_objects = handler
346
364
  end
347
365
 
348
- #
349
366
  # Signature of the registered prepare_initial_objects handler
350
- #
351
- # @return [String, nil] Handler signature
352
- #
353
- # @private
354
- def prepare_initial_objects_signature
355
- @prepare_initial_objects_signature
356
- end
367
+ attr_reader :prepare_initial_objects_signature # :nodoc:
368
+
369
+ # Patched in:
370
+ # - plugin :root (takes the options of plugins)
357
371
 
358
372
  #
359
- # Serializes provided object to Hash
373
+ # Serializes an object or a list to a Hash or an Array of Hashes. `nil`
374
+ # serializes to `nil`.
375
+ #
376
+ # - `only`, `except`, `with`: fields to serialize, to skip and to add,
377
+ # see [Selecting fields](https://github.com/andreyruby/serega/blob/master/docs/selecting-fields.md);
378
+ # - `check_initiate_params`: `false` skips unknown fields, by default
379
+ # `config.check_initiate_params`;
380
+ # - `context`: data for attribute values and conditions;
381
+ # - `many`: `true` serializes a list, `false` one object. By default an
382
+ # `Enumerable` that is not a `Hash` or `Struct` is a list.
360
383
  #
361
- # @param object [Object] Serialized object
362
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
363
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
364
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
365
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
366
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
367
- # @option opts [Hash] :context Serialization context
368
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
384
+ # ```ruby
385
+ # UserSerializer.to_h(user) # => {first_name: "Bruce"}
386
+ # UserSerializer.to_h([user], with: :email) # => [{first_name: "Bruce", email: "bruce@wayne.com"}]
387
+ # UserSerializer.to_h(user, context: {admin: true})
388
+ # ```
369
389
  #
370
- # @return [Hash] Serialization result
390
+ # See [Serializing](https://github.com/andreyruby/serega/blob/master/docs/serializing.md).
371
391
  #
372
- def call(object, opts = nil)
373
- opts = opts&.transform_keys(&:to_sym)
374
- modifiers_opts = init_modifier_opts(opts)
375
- serialize_opts = init_serialize_opts(opts)
376
- new(modifiers_opts).to_h(object, serialize_opts)
392
+ def call(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
393
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
394
+ .to_h(object, context: context, many: many)
377
395
  end
378
396
 
397
+ # Patched in:
398
+ # - plugin :root (takes the options of plugins)
399
+
379
400
  #
380
- # Serializes provided object to a tree of Ruby Data objects
381
- #
382
- # @param object [Object] Serialized object
383
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
384
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
385
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
386
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
387
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
388
- # @option opts [Hash] :context Serialization context
389
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
401
+ # Serializes like #to_h, to `Data` objects
390
402
  #
391
- # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
403
+ # ```ruby
404
+ # UserSerializer.to_data(user) # => #<data first_name="Bruce">
405
+ # ```
392
406
  #
393
- def to_data(object, opts = nil)
394
- opts = opts&.transform_keys(&:to_sym)
395
- modifiers_opts = init_modifier_opts(opts)
396
- serialize_opts = init_serialize_opts(opts)
397
- new(modifiers_opts).to_data(object, serialize_opts)
407
+ def to_data(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
408
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
409
+ .to_data(object, context: context, many: many)
398
410
  end
399
411
 
400
- alias_method :to_h, :call
401
-
402
- private
403
-
404
- def init_modifier_opts(opts)
405
- (!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
406
- end
412
+ # Patched in:
413
+ # - plugin :root (takes the options of plugins)
407
414
 
408
- def init_serialize_opts(opts)
409
- (!opts || opts.empty?) ? nil : opts.except(*config.initiate_keys)
415
+ #
416
+ # Serializes like #to_h, to `Struct` objects
417
+ #
418
+ # ```ruby
419
+ # UserSerializer.to_struct(user) # => #<struct first_name="Bruce">
420
+ # ```
421
+ #
422
+ def to_struct(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
423
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
424
+ .to_struct(object, context: context, many: many)
410
425
  end
411
426
 
412
427
  # Patched in:
413
- # - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
414
- def inherited(subclass)
415
- config_class = Class.new(self::SeregaConfig)
416
- config_class.serializer_class = subclass
417
- subclass.const_set(:SeregaConfig, config_class)
418
- subclass.instance_variable_set(:@config, subclass::SeregaConfig.new(config.opts))
419
-
420
- attribute_class = Class.new(self::SeregaAttribute)
421
- attribute_class.serializer_class = subclass
422
- subclass.const_set(:SeregaAttribute, attribute_class)
428
+ # - plugin :root (takes the options of plugins)
423
429
 
424
- attribute_normalizer_class = Class.new(self::SeregaAttributeNormalizer)
425
- attribute_normalizer_class.serializer_class = subclass
426
- subclass.const_set(:SeregaAttributeNormalizer, attribute_normalizer_class)
430
+ #
431
+ # Serializes like #to_h, to a JSON String: `JSON.generate` of the #to_h
432
+ # result
433
+ #
434
+ # ```ruby
435
+ # UserSerializer.to_json(user) # => "{\"first_name\":\"Bruce\"}"
436
+ # ```
437
+ #
438
+ def to_json(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
439
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
440
+ .to_json(object, context: context, many: many)
441
+ end
427
442
 
428
- data_builder_class = Class.new(self::SeregaDataBuilder)
429
- data_builder_class.serializer_class = subclass
430
- subclass.const_set(:SeregaDataBuilder, data_builder_class)
443
+ alias_method :to_h, :call
431
444
 
432
- plan_class = Class.new(self::SeregaPlan)
433
- plan_class.serializer_class = subclass
434
- subclass.const_set(:SeregaPlan, plan_class)
445
+ # Locks serializer definitions and config from changes.
446
+ # Called when a serialization plan is built for the serializer.
447
+ def lock # :nodoc:
448
+ return if @locked
435
449
 
436
- plan_point_class = Class.new(self::SeregaPlanPoint)
437
- plan_point_class.serializer_class = subclass
438
- subclass.const_set(:SeregaPlanPoint, plan_point_class)
450
+ LOCK_MUTEX.synchronize do
451
+ next if @locked
439
452
 
440
- engine_loader_class = Class.new(self::SeregaEngineLoader)
441
- engine_loader_class.serializer_class = subclass
442
- subclass.const_set(:SeregaEngineLoader, engine_loader_class)
453
+ SeregaUtils::EnumDeepFreeze.call(config.opts)
454
+ define_presenter_delegators if @presenter_class
455
+ @locked = true
456
+ end
457
+ end
443
458
 
444
- object_serializer_class = Class.new(self::SeregaObjectSerializer)
445
- object_serializer_class.serializer_class = subclass
446
- subclass.const_set(:SeregaObjectSerializer, object_serializer_class)
459
+ #
460
+ # Makes the serializer shareable between Ractors. Call it at the end of a
461
+ # definition to serialize in other Ractors. It locks the serializer and
462
+ # its nested serializers defined with blocks. A serializer with a
463
+ # definition that can not be shared serializes in the main Ractor only.
464
+ #
465
+ # ```ruby
466
+ # class UserSerializer < Serega
467
+ # attribute :first_name
468
+ #
469
+ # make_shareable!
470
+ # end
471
+ # ```
472
+ #
473
+ # See [Ractors](https://github.com/andreyruby/serega/blob/master/docs/ractors.md).
474
+ #
475
+ def make_shareable!
476
+ return self if @shareable
477
+
478
+ lock
479
+ attributes.each_value { |attribute| attribute.serializer.make_shareable! if attribute.initials[:block] }
480
+ @shareable = true
481
+ # :nocov:
482
+ if defined?(Ractor)
483
+ require_relative "serega/shareable"
484
+ require_relative "serega/plan/per_ractor_cache"
485
+ # Other Ractors can not define methods in the presenter, thus each
486
+ # Ractor wraps objects in its own subclass of it
487
+ if @presenter_class
488
+ @presenter_class::Delegators.freeze
489
+ @presenter_key = SeregaUtils::RactorLocal.key(self, :presenter)
490
+ end
491
+ @unshareable_reason = SeregaShareable.call(self)
492
+ @plan_cache = SeregaPlan::PerRactorCache.new(self) unless @unshareable_reason
493
+ end
494
+ # :nocov:
495
+ self
496
+ end
447
497
 
448
- check_attribute_params_class = Class.new(self::CheckAttributeParams)
449
- check_attribute_params_class.serializer_class = subclass
450
- subclass.const_set(:CheckAttributeParams, check_attribute_params_class)
498
+ # Shows whether `make_shareable!` was called
499
+ def shareable? # :nodoc:
500
+ @shareable
501
+ end
451
502
 
452
- check_initiate_params_class = Class.new(self::CheckInitiateParams)
453
- check_initiate_params_class.serializer_class = subclass
454
- subclass.const_set(:CheckInitiateParams, check_initiate_params_class)
503
+ private
455
504
 
456
- check_serialize_params_class = Class.new(self::CheckSerializeParams)
457
- check_serialize_params_class.serializer_class = subclass
458
- subclass.const_set(:CheckSerializeParams, check_serialize_params_class)
505
+ # Defines the presenter delegators of the attributes that read a method of
506
+ # the object. Later reads skip method_missing, also in other Ractors,
507
+ # which can not define methods in classes of the main Ractor.
508
+ def define_presenter_delegators
509
+ object_methods = attributes.each_value.filter_map(&:object_method)
510
+ @presenter_class.define_delegators(object_methods)
511
+ end
459
512
 
460
- check_batch_loader_params_class = Class.new(self::CheckBatchLoaderParams)
461
- check_batch_loader_params_class.serializer_class = self
462
- subclass.const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
513
+ def check_unlocked
514
+ raise SeregaError, "#{self} can not be changed after it was made shareable" if @shareable
515
+ raise SeregaError, "#{self} can not be changed after it was used for serialization" if @locked
516
+ end
463
517
 
464
- # Assign same attributes
465
- attributes.each_value do |attr|
466
- params = attr.initials
467
- subclass.attribute(params[:name], **params[:opts], &params[:block])
468
- end
518
+ # Patched in:
519
+ # - plugin :metadata (copies meta_attributes to subclasses)
520
+ def inherited(subclass)
521
+ subclass.const_set(:SeregaConfig, Class.new(self::SeregaConfig))
522
+ subclass.const_set(:SeregaAttributeValues, Class.new(self::SeregaAttributeValues))
523
+ subclass.send(:initialize_definition, subclass::SeregaConfig.new(config.opts), formatters.dup)
469
524
 
470
- # Assign same batch loaders
525
+ # Assign same batch loaders before the attributes that use them
471
526
  batch_loaders.each_value do |loader|
472
527
  subclass.batch(loader.name, loader.block)
473
528
  end
474
529
 
530
+ # Assign same attributes
531
+ attributes.each_value do |attr|
532
+ subclass_attribute = SeregaAttribute.new(subclass, **attr.initials)
533
+ subclass.attributes[subclass_attribute.name] = subclass_attribute
534
+ end
535
+
475
536
  # Assign same preload handler
476
537
  subclass.preload_with(preload_with) if preload_with
477
538
 
@@ -486,6 +547,38 @@ class Serega
486
547
  super
487
548
  end
488
549
 
550
+ # Starts the definition of a serializer class with its config and the
551
+ # formatters it inherits. Serega and each subclass set the same instance
552
+ # variables in the same order.
553
+ def initialize_definition(config, formatters)
554
+ @config = config
555
+ @attributes = {}
556
+ @batch_loaders = {}
557
+ @formatters = formatters
558
+ @presenter_class = nil
559
+ @presenter_blocks = []
560
+ @presenter_key = nil
561
+ @data_classes = {}
562
+ @struct_classes = {}
563
+ @preload_with = nil
564
+ @prepare_initial_objects = nil
565
+ @prepare_initial_objects_signature = nil
566
+ @locked = false
567
+ @shareable = false
568
+ @unshareable_reason = nil
569
+ @plan_cache = SeregaPlan::Cache.new(self)
570
+ end
571
+
572
+ # The handler given to `method_name` as a value or a block
573
+ def checked_handler(method_name, value, block)
574
+ raise SeregaError, "#{method_name} accepts a single callable or a block, not both" if value && block
575
+
576
+ handler = value || block
577
+ raise SeregaError, "#{method_name} value must be a Proc or respond to #call" unless handler.respond_to?(:call)
578
+
579
+ handler
580
+ end
581
+
489
582
  def prepare_initial_objects_signature_error
490
583
  <<~ERR.strip
491
584
  prepare_initial_objects handler arguments should have one of this signatures:
@@ -497,118 +590,103 @@ class Serega
497
590
  end
498
591
 
499
592
  #
500
- # Serializers instance methods
593
+ # Instance methods of serializers. An instance keeps its field selection
594
+ # and serializes many times.
501
595
  #
502
596
  module InstanceMethods
503
597
  #
504
- # Instantiates new Serega class
505
- #
506
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
507
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
508
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
509
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
510
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
511
- #
512
- def initialize(opts = nil)
513
- @opts =
514
- if opts.nil? || opts.empty?
515
- FROZEN_EMPTY_HASH
516
- else
517
- opts.transform_keys!(&:to_sym)
518
- parse_modifiers(opts)
519
- end
520
-
521
- self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
522
-
523
- @plan = self.class::SeregaPlan.call(@opts)
598
+ # Prepares a serializer with a field selection. The options are the field
599
+ # options of Serega::ClassMethods#to_h.
600
+ #
601
+ # ```ruby
602
+ # serializer = UserSerializer.new(only: [:first_name])
603
+ # serializer.to_h(user) # => {first_name: "Bruce"}
604
+ # serializer.to_h(users) # => [{first_name: "Bruce"}, {first_name: "Alfred"}]
605
+ # ```
606
+ #
607
+ def initialize(only: nil, with: nil, except: nil, check_initiate_params: nil)
608
+ modifiers = !(only.nil? && with.nil? && except.nil?)
609
+ check_initiate_params = config.check_initiate_params if modifiers && check_initiate_params.nil?
610
+ @plan = self.class.plan_cache.fetch(only, with, except, check_initiate_params: modifiers && check_initiate_params)
611
+ @direct_hash_builder = @plan.direct_hash_builder
524
612
  end
525
613
 
526
- #
527
614
  # Plan for serialization.
528
615
  # This plan can be traversed to find serialized attributes and nested attributes.
529
- #
530
- # @return [Serega::SeregaPlan] Serialization plan
531
- # @private
532
- attr_reader :plan
616
+ attr_reader :plan # :nodoc:
617
+
618
+ # Patched in:
619
+ # - plugin :root (takes the options of plugins)
533
620
 
534
621
  #
535
- # Serializes provided object to Hash
536
- #
537
- # @param object [Object] Serialized object
538
- # @param opts [Hash, nil] Serializing options
539
- # @option opts [Hash] :context Serialization context
540
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
622
+ # Serializes an object or a list to a Hash or an Array of Hashes, with the
623
+ # serialization options of Serega::ClassMethods#to_h
541
624
  #
542
- # @return [Hash] Serialization result
625
+ # ```ruby
626
+ # UserSerializer.new.to_h(user, context: {admin: true})
627
+ # ```
543
628
  #
544
- def call(object, opts = nil)
545
- opts = normalize_serialization_opts(opts)
546
- object = prepare_objects(object, opts[:context])
547
- opts = prepare_initial_serialization_opts(object, opts)
548
- serialize(object, opts)
549
- end
629
+ def call(object, context: nil, many: nil)
630
+ builder = @direct_hash_builder
631
+ if builder && object && !many && (context.nil? || context.is_a?(Hash))
632
+ many = SeregaUtils::CollectionDetector.call(object, @plan.collection_classes) if many.nil?
633
+ return builder.call_one(object, context.nil? ? FROZEN_EMPTY_HASH : context) unless many
634
+ end
550
635
 
551
- # @see #call
552
- def to_h(object, opts = nil)
553
- call(object, opts)
636
+ serialize_to(:hash, object, context, many, FROZEN_EMPTY_HASH)
554
637
  end
555
638
 
556
- #
557
- # Serializes provided object to Data objects
639
+ alias_method :to_h, :call
640
+
558
641
  # Patched in:
559
- # - plugin :root (adds a data-object for a root level keys)
560
- #
561
- # @param object [Object] Serialized object
562
- # @param opts [Hash, nil] Serializing options
563
- # @option opts [Hash] :context Serialization context
564
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
642
+ # - plugin :root (takes the options of plugins)
643
+
565
644
  #
566
- # @return [Data] Serialization result
645
+ # Serializes like #to_h, to `Data` objects
567
646
  #
568
- def to_data(object, opts = nil)
569
- opts = normalize_serialization_opts(opts)
570
- object = prepare_objects(object, opts[:context])
571
- opts = prepare_initial_serialization_opts(object, opts)
572
- serialized_data = serialize(object, opts)
573
- self.class::SeregaDataBuilder.call(self, serialized_data)
647
+ def to_data(object, context: nil, many: nil)
648
+ serialize_to(:data, object, context, many, FROZEN_EMPTY_HASH)
574
649
  end
575
650
 
576
- private
577
-
578
- attr_reader :opts
651
+ # Patched in:
652
+ # - plugin :root (takes the options of plugins)
579
653
 
580
- def config
581
- self.class.config
654
+ #
655
+ # Serializes like #to_h, to `Struct` objects
656
+ #
657
+ def to_struct(object, context: nil, many: nil)
658
+ serialize_to(:struct, object, context, many, FROZEN_EMPTY_HASH)
582
659
  end
583
660
 
584
- def parse_modifiers(opts)
585
- result = {}
586
-
587
- opts.each do |key, value|
588
- value = parse_modifier(value) if (key == :only) || (key == :except) || (key == :with)
589
- result[key] = value
590
- end
661
+ # Patched in:
662
+ # - plugin :root (takes the options of plugins)
591
663
 
592
- result
664
+ #
665
+ # Serializes like #to_h, to a JSON String: `JSON.generate` of the #to_h
666
+ # result
667
+ #
668
+ def to_json(object, context: nil, many: nil)
669
+ JSON.generate(call(object, context: context, many: many))
593
670
  end
594
671
 
595
- # Patched in:
596
- # - plugin :string_modifiers (parses string modifiers differently)
597
- def parse_modifier(value)
598
- SeregaUtils::ToHash.call(value)
599
- end
672
+ private
600
673
 
601
- def normalize_serialization_opts(opts)
602
- opts = opts ? opts.transform_keys(&:to_sym) : {}
603
- self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
674
+ def config
675
+ self.class.config
676
+ end
604
677
 
605
- opts[:context] ||= {}
606
- opts
678
+ # Serializes the object with the serialization options. `opts` holds the
679
+ # options other than :context and :many.
680
+ def serialize_to(mode, object, context, many, opts)
681
+ SeregaValidations::CheckSerializeKeywords.call(context, many)
682
+ context ||= FROZEN_EMPTY_HASH
683
+ object = prepare_objects(object, context) if self.class.prepare_initial_objects_signature
684
+ many = SeregaUtils::CollectionDetector.call(object, @plan.collection_classes) if many.nil?
685
+ serialize(object, opts, context, many, mode)
607
686
  end
608
687
 
609
688
  def prepare_objects(objects, context)
610
689
  handler = self.class.prepare_initial_objects
611
- return objects unless handler
612
690
 
613
691
  case self.class.prepare_initial_objects_signature
614
692
  when "1" then handler.call(objects)
@@ -617,24 +695,17 @@ class Serega
617
695
  end
618
696
  end
619
697
 
620
- def prepare_initial_serialization_opts(object, opts)
621
- opts[:level_queue] = SeregaEngine::LevelQueue.new
622
- opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
623
- opts[:plan] = plan
624
- opts
625
- end
626
-
627
698
  # Patched in:
628
699
  # - plugin :root (wraps result `{ root => result }`)
629
700
  # - plugin :context_metadata (adds context metadata to final result)
630
701
  # - plugin :metadata (adds metadata to final result)
631
- def serialize(object, opts)
632
- result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
633
- opts[:level_queue].run
634
- result
702
+ def serialize(object, _opts, context, many, mode)
703
+ SeregaEngine::Run.call(@plan, object, many, mode, context)
635
704
  end
636
705
  end
637
706
 
638
707
  extend ClassMethods
639
708
  include InstanceMethods
709
+
710
+ initialize_definition(SeregaConfig.new, {})
640
711
  end