serega 0.41.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +123 -1205
  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 +106 -145
  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 -145
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +14 -100
  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 +108 -0
  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 +480 -372
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +43 -39
  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 -118
  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/presenter/presenter.rb +0 -242
  103. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  104. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  105. data/lib/serega/utils/symbol_name.rb +0 -39
  106. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  107. data/lib/serega/validations/check_initiate_params.rb +0 -57
  108. 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,208 +85,251 @@ 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
- require_relative "serega/object_serializer"
94
+ require_relative "serega/presenter"
95
+ require_relative "serega/source_group"
66
96
  require_relative "serega/plan_point"
67
97
  require_relative "serega/plan"
68
- require_relative "serega/data_builder"
98
+ require_relative "serega/result_code"
99
+ require_relative "serega/result_builder"
100
+ require_relative "serega/plan/cache"
69
101
  require_relative "serega/plugins"
70
102
 
71
103
  class Serega
72
- @config = SeregaConfig.new
73
-
74
- # Validates `Serializer.attribute` params
75
- check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
76
- check_attribute_params_class.serializer_class = self
77
- const_set(:CheckAttributeParams, check_attribute_params_class)
78
-
79
- # Validates `Serializer#new` params
80
- check_initiate_params_class = Class.new(SeregaValidations::CheckInitiateParams)
81
- check_initiate_params_class.serializer_class = self
82
- const_set(:CheckInitiateParams, check_initiate_params_class)
83
-
84
- # Validates `serializer#call(obj, PARAMS)` params
85
- check_serialize_params_class = Class.new(SeregaValidations::CheckSerializeParams)
86
- check_serialize_params_class.serializer_class = self
87
- const_set(:CheckSerializeParams, check_serialize_params_class)
88
-
89
- # Validates `Serializer.batch` params
90
- check_batch_loader_params_class = Class.new(SeregaValidations::CheckBatchLoaderParams)
91
- check_batch_loader_params_class.serializer_class = self
92
- const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
93
-
94
- # Assigns `SeregaEngineLoader` constant to current class
95
- engine_loader_class = Class.new(SeregaEngine::Loader)
96
- engine_loader_class.serializer_class = self
97
- const_set(:SeregaEngineLoader, engine_loader_class)
98
-
99
104
  #
100
- # Serializers class methods
105
+ # Class methods of serializers: they define attributes and serialize
101
106
  #
102
107
  module ClassMethods
103
- # Returns current config
104
- # @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).
105
115
  attr_reader :config
116
+
106
117
  #
107
- # Enables plugin for current serializer
118
+ # Turns on a built-in feature by name, with its options. Subclasses get it
119
+ # too.
108
120
  #
109
- # @param name [Symbol, Class<Module>] Plugin name or plugin module itself
110
- # @param opts [Hash>] Plugin options
121
+ # ```ruby
122
+ # plugin :root, root_one: :user, root_many: :users
123
+ # ```
111
124
  #
112
- # @return [class<Module>] Loaded plugin module
125
+ # See [Root Key and Metadata](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md).
113
126
  #
114
127
  def plugin(name, **opts)
128
+ check_unlocked
115
129
  raise SeregaError, "This plugin is already loaded" if plugin_used?(name)
116
130
 
117
- plugin = SeregaPlugins.find_plugin(name)
118
-
119
- # We split loading of plugin to three parts - before_load, load, after_load:
120
- #
121
- # - **before_load_plugin** usually used to check requirements and to load additional plugins
122
- # - **load_plugin** usually used to include plugin modules
123
- # - **after_load_plugin** usually used to add config options
124
- plugin.before_load_plugin(self, **opts) if plugin.respond_to?(:before_load_plugin)
125
- plugin.load_plugin(self, **opts) if plugin.respond_to?(:load_plugin)
126
- plugin.after_load_plugin(self, **opts) if plugin.respond_to?(:after_load_plugin)
127
-
128
- # Store attached plugins, so we can check it is loaded later
129
- 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
130
134
 
131
135
  plugin
132
136
  end
133
137
 
134
- #
135
138
  # Checks plugin is used
136
- #
137
- # @param name [Symbol, Class<Module>] Plugin name or plugin module itself
138
- #
139
- # @return [Boolean] Is plugin used
140
- #
141
- # @private
142
- def plugin_used?(name)
143
- plugin_name =
144
- case name
145
- when Module then name.respond_to?(:plugin_name) ? name.plugin_name : name
146
- else name
147
- end
148
-
149
- config.plugins.include?(plugin_name)
139
+ def plugin_used?(name) # :nodoc:
140
+ config.plugins.include?(name)
150
141
  end
151
142
 
152
143
  #
153
- # Lists attributes
144
+ # Attributes by name, hidden and inherited ones too
154
145
  #
155
- # @return [Hash] attributes list
146
+ # ```ruby
147
+ # UserSerializer.attributes.keys # => [:first_name, :email]
148
+ # ```
156
149
  #
157
- # @private
158
150
  def attributes
159
- @attributes ||= {}
151
+ @attributes
160
152
  end
161
153
 
162
- #
163
154
  # Lists defined batch loaders
164
- #
165
- # @return [Hash] batch loaders list
166
- #
167
- # @private
168
- def batch_loaders
169
- @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) }
184
+ end
185
+
186
+ # Lists blocks given to `presenter`, in definition order
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)
170
205
  end
171
206
 
172
207
  #
173
- # Adds attribute
208
+ # Adds an attribute. By default its value is `object.<name>`, and the
209
+ # options change that:
174
210
  #
175
- # Patched in:
176
- # - plugin :presenter (additionally adds method in Presenter class)
177
- #
178
- # @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
179
- # @param opts [Hash] Options to serialize attribute
180
- # @param block [Proc] Defines attributes of a nested anonymous serializer.
181
- # The block is executed in the context of that serializer, so everything
182
- # available in a serializer class body can be used inside. The attribute
183
- # value (found by name/:method/:value/:delegate/:const/:batch as usual)
184
- # is serialized with this nested serializer. The nested serializer
185
- # inherits from the `:base_serializer` attribute option or
186
- # `config.base_serializer` (one of them is required).
187
- #
188
- # @example
189
- # attribute :statistics, method: :itself do
190
- # attribute :likes_count
191
- # attribute :comments_count
192
- # end
211
+ # ```ruby
212
+ # attribute :first_name
213
+ # attribute :name, method: :full_name
214
+ # attribute :email, hide: true
215
+ # attribute :posts, serializer: PostSerializer
216
+ # ```
217
+ #
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
+ # ```
193
227
  #
194
- # @return [Serega::SeregaAttribute] Added attribute
228
+ # See [Attributes](https://github.com/andreyruby/serega/blob/master/docs/attributes.md#all-options)
229
+ # for all options.
195
230
  #
196
231
  def attribute(name, **opts, &block)
197
- 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}")
198
235
  attributes[attribute.name] = attribute
199
236
  end
200
237
 
201
238
  #
202
- # 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.
203
242
  #
204
- # @example
205
- # 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]) }
206
246
  #
207
- # @example with block
208
- # batch_loader(:tags) do |posts|
209
- # Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
210
- # end
247
+ # attribute :likes_count, batch: true
248
+ # ```
211
249
  #
212
- # 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).
213
251
  #
214
- # @example with context
215
- # batch_loader(:tags) do |posts, ctx:|
216
- # 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
+
217
260
  #
218
- # Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
219
- # 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.
220
266
  #
221
- # 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]}" }
222
271
  #
223
- # @param name [Symbol] A batch loader name
224
- # @param value [#call] Batch loader
225
- # @param block [Proc] Batch loader
272
+ # attribute :created_at, format: :iso8601
273
+ # ```
226
274
  #
227
- # @return [#call] Batch loader
275
+ # See [Formatters](https://github.com/andreyruby/serega/blob/master/docs/formatters.md).
228
276
  #
229
- def batch(name, value = nil, &block)
230
- 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
231
282
 
232
- batch_loader = self::SeregaEngineLoader.new(name: name, block: value || block)
233
- 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})
234
286
  end
235
287
 
236
288
  #
237
- # Registers (or returns) the handler used to preload an attribute's
238
- # associations onto the records gathered during serialization.
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.
292
+ #
293
+ # ```ruby
294
+ # attribute :name
239
295
  #
240
- # The handler is called once per preloaded attribute with the gathered
241
- # objects and that attribute's preloads. ORM plugins register a handler
242
- # that performs the actual eager loading.
296
+ # presenter do
297
+ # def name
298
+ # [first_name, last_name].compact.join(" ")
299
+ # end
300
+ # end
301
+ # ```
302
+ #
303
+ # See [Presenter](https://github.com/andreyruby/serega/blob/master/docs/presenter.md).
243
304
  #
244
- # @example with a block
245
- # preload_with { |objects, preloads| MyORM.eager_load(objects, preloads) }
305
+ def presenter(&block)
306
+ return @presenter_class unless block
307
+
308
+ check_unlocked
309
+ @presenter_class ||= Class.new(SeregaPresenter)
310
+ @presenter_class.class_exec(&block)
311
+ presenter_blocks << block
312
+ @presenter_class
313
+ end
314
+
246
315
  #
247
- # @example with a callable value
248
- # 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.
249
320
  #
250
- # @param value [#call, nil] Preload handler accepting two positional arguments
251
- # @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
+ # ```
252
325
  #
253
- # @return [#call, nil] The registered preload handler
326
+ # See [Preloads](https://github.com/andreyruby/serega/blob/master/docs/preloads.md).
254
327
  #
255
328
  def preload_with(value = nil, &block)
256
329
  return @preload_with if value.nil? && block.nil?
257
- raise SeregaError, "preload_with accepts a single callable or a block, not both" if value && block
258
-
259
- handler = value || block
260
- raise SeregaError, "preload_with value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
261
330
 
331
+ check_unlocked
332
+ handler = checked_handler(:preload_with, value, block)
262
333
  signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2)
263
334
  raise SeregaError, "preload_with handler must accept two positional arguments: (objects, preloads)" unless signature == "2"
264
335
 
@@ -266,45 +337,25 @@ class Serega
266
337
  end
267
338
 
268
339
  #
269
- # Registers (or returns) the handler that replaces the serialized objects
270
- # before serialization starts.
271
- #
272
- # The handler is called once per serialization with the objects provided to
273
- # `.call`/`.to_h`/`.to_data` and the serialization context. Its result is
274
- # serialized instead of the provided objects, which allows to accept ids or
275
- # other references and load the records in one place.
276
- #
277
- # The handler runs before the `:many` option is detected, so it may turn a
278
- # single object into a collection and back. A provided `:many` serialization
279
- # option is still used as is.
280
- #
281
- # The handler runs only for the serialized objects, and not for objects of
282
- # nested serializers.
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.
283
345
  #
284
- # @example with a block
285
- # prepare_initial_objects { |user_ids| User.where(id: user_ids) }
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
+ # ```
286
351
  #
287
- # @example with a context
288
- # prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
289
- #
290
- # @example with a keyword context
291
- # prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
292
- #
293
- # @example with a callable value
294
- # prepare_initial_objects UsersLoader
295
- #
296
- # @param value [#call, nil] Handler accepting (objects), (objects, context) or (objects, ctx:)
297
- # @param block [Proc] Handler accepting (objects), (objects, context) or (objects, ctx:)
298
- #
299
- # @return [#call, nil] The registered handler
352
+ # See [Prepare initial objects](https://github.com/andreyruby/serega/blob/master/docs/prepare-initial-objects.md).
300
353
  #
301
354
  def prepare_initial_objects(value = nil, &block)
302
355
  return @prepare_initial_objects if value.nil? && block.nil?
303
- raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
304
-
305
- handler = value || block
306
- raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
307
356
 
357
+ check_unlocked
358
+ handler = checked_handler(:prepare_initial_objects, value, block)
308
359
  signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
309
360
  raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
310
361
 
@@ -312,143 +363,222 @@ class Serega
312
363
  @prepare_initial_objects = handler
313
364
  end
314
365
 
315
- #
316
366
  # Signature of the registered prepare_initial_objects handler
317
- #
318
- # @return [String, nil] Handler signature
319
- #
320
- # @private
321
- def prepare_initial_objects_signature
322
- @prepare_initial_objects_signature
323
- end
367
+ attr_reader :prepare_initial_objects_signature # :nodoc:
324
368
 
369
+ # Patched in:
370
+ # - plugin :root (takes the options of plugins)
371
+
372
+ #
373
+ # Serializes an object or a list to a Hash or an Array of Hashes. `nil`
374
+ # serializes to `nil`.
325
375
  #
326
- # Serializes provided object to Hash
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.
327
383
  #
328
- # @param object [Object] Serialized object
329
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
330
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
331
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
332
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
333
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
334
- # @option opts [Hash] :context Serialization context
335
- # @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
+ # ```
336
389
  #
337
- # @return [Hash] Serialization result
390
+ # See [Serializing](https://github.com/andreyruby/serega/blob/master/docs/serializing.md).
338
391
  #
339
- def call(object, opts = nil)
340
- opts = opts&.transform_keys(&:to_sym)
341
- modifiers_opts = init_modifier_opts(opts)
342
- serialize_opts = init_serialize_opts(opts)
343
- 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)
344
395
  end
345
396
 
397
+ # Patched in:
398
+ # - plugin :root (takes the options of plugins)
399
+
346
400
  #
347
- # Serializes provided object to a tree of Ruby Data objects
348
- #
349
- # @param object [Object] Serialized object
350
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
351
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
352
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
353
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
354
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
355
- # @option opts [Hash] :context Serialization context
356
- # @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
357
402
  #
358
- # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
403
+ # ```ruby
404
+ # UserSerializer.to_data(user) # => #<data first_name="Bruce">
405
+ # ```
359
406
  #
360
- def to_data(object, opts = nil)
361
- opts = opts&.transform_keys(&:to_sym)
362
- modifiers_opts = init_modifier_opts(opts)
363
- serialize_opts = init_serialize_opts(opts)
364
- 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)
365
410
  end
366
411
 
367
- alias_method :to_h, :call
368
-
369
- private
370
-
371
- def init_modifier_opts(opts)
372
- (!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
373
- end
412
+ # Patched in:
413
+ # - plugin :root (takes the options of plugins)
374
414
 
375
- def init_serialize_opts(opts)
376
- (!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)
377
425
  end
378
426
 
379
427
  # Patched in:
380
- # - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
381
- # - plugin :presenter (defines Presenter)
382
- def inherited(subclass)
383
- config_class = Class.new(self::SeregaConfig)
384
- config_class.serializer_class = subclass
385
- subclass.const_set(:SeregaConfig, config_class)
386
- subclass.instance_variable_set(:@config, subclass::SeregaConfig.new(config.opts))
387
-
388
- attribute_class = Class.new(self::SeregaAttribute)
389
- attribute_class.serializer_class = subclass
390
- subclass.const_set(:SeregaAttribute, attribute_class)
428
+ # - plugin :root (takes the options of plugins)
391
429
 
392
- attribute_normalizer_class = Class.new(self::SeregaAttributeNormalizer)
393
- attribute_normalizer_class.serializer_class = subclass
394
- 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
395
442
 
396
- data_builder_class = Class.new(self::SeregaDataBuilder)
397
- data_builder_class.serializer_class = subclass
398
- subclass.const_set(:SeregaDataBuilder, data_builder_class)
443
+ alias_method :to_h, :call
399
444
 
400
- plan_class = Class.new(self::SeregaPlan)
401
- plan_class.serializer_class = subclass
402
- 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
403
449
 
404
- plan_point_class = Class.new(self::SeregaPlanPoint)
405
- plan_point_class.serializer_class = subclass
406
- subclass.const_set(:SeregaPlanPoint, plan_point_class)
450
+ LOCK_MUTEX.synchronize do
451
+ next if @locked
407
452
 
408
- engine_loader_class = Class.new(self::SeregaEngineLoader)
409
- engine_loader_class.serializer_class = subclass
410
- 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
411
458
 
412
- object_serializer_class = Class.new(self::SeregaObjectSerializer)
413
- object_serializer_class.serializer_class = subclass
414
- 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
415
497
 
416
- check_attribute_params_class = Class.new(self::CheckAttributeParams)
417
- check_attribute_params_class.serializer_class = subclass
418
- subclass.const_set(:CheckAttributeParams, check_attribute_params_class)
498
+ # Shows whether `make_shareable!` was called
499
+ def shareable? # :nodoc:
500
+ @shareable
501
+ end
419
502
 
420
- check_initiate_params_class = Class.new(self::CheckInitiateParams)
421
- check_initiate_params_class.serializer_class = subclass
422
- subclass.const_set(:CheckInitiateParams, check_initiate_params_class)
503
+ private
423
504
 
424
- check_serialize_params_class = Class.new(self::CheckSerializeParams)
425
- check_serialize_params_class.serializer_class = subclass
426
- 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
427
512
 
428
- check_batch_loader_params_class = Class.new(self::CheckBatchLoaderParams)
429
- check_batch_loader_params_class.serializer_class = self
430
- 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
431
517
 
432
- # Assign same attributes
433
- attributes.each_value do |attr|
434
- params = attr.initials
435
- subclass.attribute(params[:name], **params[:opts], &params[:block])
436
- 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)
437
524
 
438
- # Assign same batch loaders
525
+ # Assign same batch loaders before the attributes that use them
439
526
  batch_loaders.each_value do |loader|
440
527
  subclass.batch(loader.name, loader.block)
441
528
  end
442
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
+
443
536
  # Assign same preload handler
444
537
  subclass.preload_with(preload_with) if preload_with
445
538
 
446
539
  # Assign same initial objects handler
447
540
  subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
448
541
 
542
+ # Assign same presenter blocks
543
+ presenter_blocks.each do |presenter_block|
544
+ subclass.presenter(&presenter_block)
545
+ end
546
+
449
547
  super
450
548
  end
451
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
+
452
582
  def prepare_initial_objects_signature_error
453
583
  <<~ERR.strip
454
584
  prepare_initial_objects handler arguments should have one of this signatures:
@@ -460,118 +590,103 @@ class Serega
460
590
  end
461
591
 
462
592
  #
463
- # Serializers instance methods
593
+ # Instance methods of serializers. An instance keeps its field selection
594
+ # and serializes many times.
464
595
  #
465
596
  module InstanceMethods
466
597
  #
467
- # Instantiates new Serega class
468
- #
469
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
470
- # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
471
- # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
472
- # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
473
- # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
474
- #
475
- def initialize(opts = nil)
476
- @opts =
477
- if opts.nil? || opts.empty?
478
- FROZEN_EMPTY_HASH
479
- else
480
- opts.transform_keys!(&:to_sym)
481
- parse_modifiers(opts)
482
- end
483
-
484
- self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
485
-
486
- @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
487
612
  end
488
613
 
489
- #
490
614
  # Plan for serialization.
491
615
  # This plan can be traversed to find serialized attributes and nested attributes.
492
- #
493
- # @return [Serega::SeregaPlan] Serialization plan
494
- # @private
495
- attr_reader :plan
616
+ attr_reader :plan # :nodoc:
617
+
618
+ # Patched in:
619
+ # - plugin :root (takes the options of plugins)
496
620
 
497
621
  #
498
- # Serializes provided object to Hash
499
- #
500
- # @param object [Object] Serialized object
501
- # @param opts [Hash, nil] Serializing options
502
- # @option opts [Hash] :context Serialization context
503
- # @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
504
624
  #
505
- # @return [Hash] Serialization result
625
+ # ```ruby
626
+ # UserSerializer.new.to_h(user, context: {admin: true})
627
+ # ```
506
628
  #
507
- def call(object, opts = nil)
508
- opts = normalize_serialization_opts(opts)
509
- object = prepare_objects(object, opts[:context])
510
- opts = prepare_initial_serialization_opts(object, opts)
511
- serialize(object, opts)
512
- 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
513
635
 
514
- # @see #call
515
- def to_h(object, opts = nil)
516
- call(object, opts)
636
+ serialize_to(:hash, object, context, many, FROZEN_EMPTY_HASH)
517
637
  end
518
638
 
519
- #
520
- # Serializes provided object to Data objects
639
+ alias_method :to_h, :call
640
+
521
641
  # Patched in:
522
- # - plugin :root (adds a data-object for a root level keys)
523
- #
524
- # @param object [Object] Serialized object
525
- # @param opts [Hash, nil] Serializing options
526
- # @option opts [Hash] :context Serialization context
527
- # @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
+
528
644
  #
529
- # @return [Data] Serialization result
645
+ # Serializes like #to_h, to `Data` objects
530
646
  #
531
- def to_data(object, opts = nil)
532
- opts = normalize_serialization_opts(opts)
533
- object = prepare_objects(object, opts[:context])
534
- opts = prepare_initial_serialization_opts(object, opts)
535
- serialized_data = serialize(object, opts)
536
- 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)
537
649
  end
538
650
 
539
- private
540
-
541
- attr_reader :opts
651
+ # Patched in:
652
+ # - plugin :root (takes the options of plugins)
542
653
 
543
- def config
544
- 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)
545
659
  end
546
660
 
547
- def parse_modifiers(opts)
548
- result = {}
549
-
550
- opts.each do |key, value|
551
- value = parse_modifier(value) if (key == :only) || (key == :except) || (key == :with)
552
- result[key] = value
553
- end
661
+ # Patched in:
662
+ # - plugin :root (takes the options of plugins)
554
663
 
555
- 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))
556
670
  end
557
671
 
558
- # Patched in:
559
- # - plugin :string_modifiers (parses string modifiers differently)
560
- def parse_modifier(value)
561
- SeregaUtils::ToHash.call(value)
562
- end
672
+ private
563
673
 
564
- def normalize_serialization_opts(opts)
565
- opts = opts ? opts.transform_keys(&:to_sym) : {}
566
- self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
674
+ def config
675
+ self.class.config
676
+ end
567
677
 
568
- opts[:context] ||= {}
569
- 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)
570
686
  end
571
687
 
572
688
  def prepare_objects(objects, context)
573
689
  handler = self.class.prepare_initial_objects
574
- return objects unless handler
575
690
 
576
691
  case self.class.prepare_initial_objects_signature
577
692
  when "1" then handler.call(objects)
@@ -580,24 +695,17 @@ class Serega
580
695
  end
581
696
  end
582
697
 
583
- def prepare_initial_serialization_opts(object, opts)
584
- opts[:level_queue] = SeregaEngine::LevelQueue.new
585
- opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
586
- opts[:plan] = plan
587
- opts
588
- end
589
-
590
698
  # Patched in:
591
699
  # - plugin :root (wraps result `{ root => result }`)
592
700
  # - plugin :context_metadata (adds context metadata to final result)
593
701
  # - plugin :metadata (adds metadata to final result)
594
- def serialize(object, opts)
595
- result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
596
- opts[:level_queue].run
597
- result
702
+ def serialize(object, _opts, context, many, mode)
703
+ SeregaEngine::Run.call(@plan, object, many, mode, context)
598
704
  end
599
705
  end
600
706
 
601
707
  extend ClassMethods
602
708
  include InstanceMethods
709
+
710
+ initialize_definition(SeregaConfig.new, {})
603
711
  end