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
@@ -2,164 +2,55 @@
2
2
 
3
3
  class Serega
4
4
  module SeregaPlugins
5
+ # `plugin :metadata` adds metadata defined with
6
+ # Serega::SeregaPlugins::Metadata::ClassMethods#meta_attribute. Load
7
+ # `plugin :root` first.
5
8
  #
6
- # Plugin :metadata
9
+ # ```ruby
10
+ # plugin :root
11
+ # plugin :metadata
7
12
  #
8
- # Depends on: `:root` plugin, that must be loaded first
9
- #
10
- # Adds metadata to the serialized response via the class-level
11
- # `meta_attribute` method, which accepts:
12
- #
13
- # - `*path` [Array<Symbol>] - nested hash keys
14
- # - `**options` [Hash]
15
- # - `:const` - a constant metadata value
16
- # - `:value` - a `#callable` producing the metadata value
17
- # - `:hide_nil` - hide the key if the value is nil (`false` by default)
18
- # - `:hide_empty` - hide the key if the value is nil or empty (`false` by default)
19
- # - `&block` - computes the value for this meta attribute
20
- #
21
- # @example
22
- # class AppSerializer < Serega
23
- # plugin :root
24
- # plugin :metadata
25
- #
26
- # meta_attribute(:version, const: '1.2.3')
27
- # meta_attribute(:ab_tests, :names, value: ABTests.new.method(:names))
28
- # meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
29
- # next unless records.respond_to?(:total_count)
30
- #
31
- # { page: records.page, per_page: records.per_page, total_count: records.total_count }
32
- # end
33
- # end
34
- #
35
- # AppSerializer.to_h(nil) # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=> ... }}
13
+ # meta_attribute :version, const: "1.2.3"
14
+ # ```
36
15
  #
16
+ # See [Metadata](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md#metadata).
37
17
  module Metadata
38
- # @return [Symbol] Plugin name
39
- # @private
40
- def self.plugin_name
41
- :metadata
42
- end
43
-
44
- # Checks requirements and loads additional plugins
45
- #
46
- # @param serializer_class [Class<Serega>] Current serializer class
47
- # @param _opts [Hash] Plugin options
48
- #
49
- # @return [void]
50
- #
51
- # @private
52
- def self.before_load_plugin(serializer_class, **_opts)
18
+ # Adds the plugin to the serializer
19
+ def self.apply(serializer_class, **_opts) # :nodoc:
53
20
  unless serializer_class.plugin_used?(:root)
54
- raise SeregaError, "Plugin #{plugin_name.inspect} must be loaded after the :root plugin. Please load the :root plugin first"
21
+ raise SeregaError, "Plugin :metadata must be loaded after the :root plugin. Please load the :root plugin first"
55
22
  end
56
- end
57
-
58
- #
59
- # Applies plugin code to specific serializer
60
- #
61
- # @param serializer_class [Class<Serega>] Current serializer class
62
- # @param _opts [Hash] Plugin options
63
- #
64
- # @return [void]
65
- #
66
- # @private
67
- def self.load_plugin(serializer_class, **_opts)
68
- serializer_class.extend(ClassMethods)
69
- serializer_class.include(InstanceMethods)
70
- serializer_class::SeregaConfig.include(ConfigInstanceMethods)
71
23
 
72
24
  require_relative "meta_attribute"
73
- require_relative "validations/check_block"
74
- require_relative "validations/check_opt_const"
75
- require_relative "validations/check_opt_hide_nil"
76
- require_relative "validations/check_opt_hide_empty"
77
- require_relative "validations/check_opt_value"
78
- require_relative "validations/check_opts"
79
- require_relative "validations/check_path"
80
-
81
- meta_attribute_class = Class.new(MetaAttribute)
82
- meta_attribute_class.serializer_class = serializer_class
83
- serializer_class.const_set(:MetaAttribute, meta_attribute_class)
84
- end
85
-
86
- #
87
- # Adds config options and runs other callbacks after plugin was loaded
88
- #
89
- # @param serializer_class [Class<Serega>] Current serializer class
90
- # @param _opts [Hash] Plugin options
91
- #
92
- # @return [void]
93
- #
94
- # @private
95
- def self.after_load_plugin(serializer_class, **_opts)
96
- serializer_class.config.opts[:metadata] = {attribute_keys: %i[const hide_nil hide_empty value]}
97
- end
25
+ require_relative "check_params"
98
26
 
99
- #
100
- # Config for `metadata` plugin
101
- #
102
- class MetadataConfig
103
- # @return [Hash] metadata options
104
- attr_reader :opts
105
-
106
- #
107
- # Initializes context_metadata config object
108
- #
109
- # @param opts [Hash] options
110
- #
111
- # @return [Serega::SeregaPlugins::Metadata::MetadataConfig]
112
- #
113
- def initialize(opts)
114
- @opts = opts
115
- end
116
-
117
- #
118
- # Returns allowed metadata attribute keys
119
- #
120
- def attribute_keys
121
- opts.fetch(:attribute_keys)
122
- end
123
- end
124
-
125
- #
126
- # Config class additional/patched instance methods
127
- #
128
- # @see Serega::SeregaConfig
129
- #
130
- module ConfigInstanceMethods
131
- # @return [Serega::SeregaPlugins::Metadata::MetadataConfig] metadata config
132
- def metadata
133
- @metadata ||= MetadataConfig.new(opts.fetch(:metadata))
134
- end
27
+ serializer_class.extend(ClassMethods)
28
+ serializer_class.include(InstanceMethods)
29
+ serializer_class.instance_variable_set(:@meta_attributes, {})
135
30
  end
136
31
 
137
- #
138
- # Serega class additional/patched class methods
139
- #
140
- # @see Serega::SeregaConfig
141
- #
32
+ # Class methods the plugin adds to serializers
142
33
  module ClassMethods
143
- #
144
34
  # List of added metadata attributes
145
- #
146
- # @return [Hash<Symbol => Serega::SeregaPlugins::Metadata::MetaAttribute>] Added metadata attributes
147
- #
148
- def meta_attributes
149
- @meta_attributes ||= {}
35
+ def meta_attributes # :nodoc:
36
+ @meta_attributes
150
37
  end
151
38
 
39
+ # Adds metadata under a path of keys. The value is `const:`, a `value:`
40
+ # callable or a block, which receive the serialized objects and the
41
+ # context. `hide_nil: true` skips `nil` values, `hide_empty: true` skips
42
+ # `nil` and empty values.
152
43
  #
153
- # Define metadata attribute
154
- #
155
- # @param path [String, Symbol] Metadata attribute path keys
156
- # @param opts [Hash] Metadata attribute options
157
- # @param block [Proc] Block to fetch metadata attribute value
158
- #
159
- # @return [Serega::SeregaPlugins::Metadata::MetaAttribute] Added metadata attribute
44
+ # ```ruby
45
+ # meta_attribute :meta, :paging, hide_nil: true do |records, ctx|
46
+ # next unless records.respond_to?(:total_count)
160
47
  #
48
+ # {page: records.current_page, total_count: records.total_count}
49
+ # end
50
+ # ```
161
51
  def meta_attribute(*path, **opts, &block)
162
- attribute = self::MetaAttribute.new(path: path, opts: opts, block: block)
52
+ check_unlocked
53
+ attribute = MetaAttribute.new(self, path: path, opts: opts, block: block)
163
54
  meta_attributes[attribute.name] = attribute
164
55
  end
165
56
 
@@ -168,9 +59,7 @@ class Serega
168
59
  def inherited(subclass)
169
60
  super
170
61
 
171
- meta_attribute_class = Class.new(self::MetaAttribute)
172
- meta_attribute_class.serializer_class = subclass
173
- subclass.const_set(:MetaAttribute, meta_attribute_class)
62
+ subclass.instance_variable_set(:@meta_attributes, {})
174
63
 
175
64
  # Assign same metadata attributes
176
65
  meta_attributes.each_value do |attr|
@@ -179,23 +68,18 @@ class Serega
179
68
  end
180
69
  end
181
70
 
182
- #
183
71
  # Serega additional/patched instance methods
184
- #
185
- # @see Serega
186
- #
187
- # @private
188
- module InstanceMethods
72
+ module InstanceMethods # :nodoc:
189
73
  private
190
74
 
191
- def serialize(object, opts)
75
+ def serialize(object, opts, context, many, mode)
192
76
  result = super
193
77
  return result unless result.is_a?(Hash) # return earlier if not a hash, so no root was added
194
78
 
195
- root = build_root(object, opts)
79
+ root = build_root(opts, many)
196
80
  return result unless root # return earlier when no root
197
81
 
198
- add_metadata(object, opts[:context], result)
82
+ add_metadata(object, context, result)
199
83
  result
200
84
  end
201
85
 
@@ -204,7 +88,7 @@ class Serega
204
88
  metadata = meta_attribute_value(object, context, meta_attribute)
205
89
  next unless metadata
206
90
 
207
- deep_merge_metadata(hash, metadata)
91
+ SeregaUtils::DeepMerge.call(hash, metadata)
208
92
  rescue => error
209
93
  raise error.exception(<<~MESSAGE.strip)
210
94
  #{error.message}
@@ -217,23 +101,11 @@ class Serega
217
101
  value = meta_attribute.value(object, context)
218
102
  return if meta_attribute.hide?(value)
219
103
 
220
- # Example:
221
- # [:foo, :bar].reverse_each.inject(:bazz) { |val, key| { key => val } } # => { foo: { bar: :bazz } }
222
- meta_attribute.path.reverse_each.inject(value) { |val, key| {key => val} }
223
- end
224
-
225
- def deep_merge_metadata(hash, metadata)
226
- hash.merge!(metadata) do |_key, this_val, other_val|
227
- if this_val.is_a?(Hash) && other_val.is_a?(Hash)
228
- deep_merge_metadata(this_val, other_val)
229
- else
230
- other_val
231
- end
232
- end
104
+ # Example: path [:foo, :bar] and value :bazz give { foo: { bar: :bazz } }
105
+ meta_attribute.path.reverse_each { |key| value = {key => value} }
106
+ value
233
107
  end
234
108
  end
235
109
  end
236
-
237
- register_plugin(Metadata.plugin_name, Metadata)
238
110
  end
239
111
  end
@@ -2,110 +2,30 @@
2
2
 
3
3
  class Serega
4
4
  module SeregaPlugins
5
+ # `plugin :root` wraps results in a root key, `:data` by default
5
6
  #
6
- # Plugin :root
7
+ # ```ruby
8
+ # plugin :root # {data: ...}
9
+ # plugin :root, root: :users # one key for objects and lists
10
+ # plugin :root, root_one: :user, root_many: :users # separate keys
7
11
  #
8
- # Adds a root key to serialized data.
9
- #
10
- # Accepts options:
11
- # - :root - specifies root for all responses
12
- # - :root_one - specifies root for single object serialization only
13
- # - :root_many - specifies root for multiple objects serialization only
14
- #
15
- # Adds additional config options:
16
- # - config.root.one
17
- # - config.root.many
18
- # - config.root.one=
19
- # - config.root.many=
20
- #
21
- # Default root is `:data`.
22
- #
23
- # Root can also be changed per serialization, or removed entirely by
24
- # providing `root: nil` (per serialization it can still be added back).
25
- #
26
- # @example Define plugin
27
- # class UserSerializer < Serega
28
- # plugin :root # default root is :data
29
- # end
30
- #
31
- # class UserSerializer < Serega
32
- # plugin :root, root: :users
33
- # end
34
- #
35
- # class UserSerializer < Serega
36
- # plugin :root, root_one: :user, root_many: :people
37
- # end
38
- #
39
- # class UserSerializer < Serega
40
- # plugin :root, root: nil # no root by default
41
- # end
42
- #
43
- # @example Change root per serialization:
44
- # class UserSerializer < Serega
45
- # plugin :root
46
- # end
47
- #
48
- # UserSerializer.to_h(nil) # => {:data=>nil}
49
- # UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
50
- # UserSerializer.to_h(nil, root: nil) # => nil
12
+ # UserSerializer.to_h(user, root: :user) # => {user: {username: "batman"}}
13
+ # UserSerializer.to_h(user, root: nil) # => {username: "batman"}
14
+ # ```
51
15
  #
16
+ # See [Root key](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md#root-key).
52
17
  module Root
53
- # @return [Symbol] Default response root key
54
- ROOT_DEFAULT = :data
18
+ # Default response root key
19
+ ROOT_DEFAULT = :data # :nodoc:
55
20
 
56
- # @return [Symbol] Plugin name
57
- # @private
58
- def self.plugin_name
59
- :root
60
- end
21
+ # Adds the plugin to the serializer
22
+ def self.apply(serializer_class, **opts) # :nodoc:
23
+ SeregaValidations::Utils::CheckAllowedKeys.call(opts, %i[root root_one root_many], "plugin :root")
61
24
 
62
- # Checks requirements to load plugin
63
- #
64
- # @param serializer_class [Class<Serega>] Current serializer class
65
- # @param opts [Hash] plugin options
66
- #
67
- # @return [void]
68
- #
69
- # @private
70
- def self.before_load_plugin(serializer_class, **opts)
71
- allowed_keys = %i[root root_one root_many]
72
- opts.each_key do |key|
73
- next if allowed_keys.include?(key)
74
-
75
- raise SeregaError,
76
- "Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. Allowed options:\n" \
77
- " - :root [String, Symbol, nil] Specifies common root keyword used when serializing one or multiple objects\n" \
78
- " - :root_one [String, Symbol, nil] Specifies root keyword used when serializing one object\n" \
79
- " - :root_many [String, Symbol, nil] Specifies root keyword used when serializing multiple objects" \
80
- end
81
- end
82
-
83
- #
84
- # Applies plugin code to specific serializer
85
- #
86
- # @param serializer_class [Class<Serega>] Current serializer class
87
- # @param _opts [Hash] Plugin options
88
- #
89
- # @return [void]
90
- #
91
- # @private
92
- def self.load_plugin(serializer_class, **_opts)
93
25
  serializer_class.extend(ClassMethods)
94
26
  serializer_class.include(InstanceMethods)
95
27
  serializer_class::SeregaConfig.include(ConfigInstanceMethods)
96
- serializer_class::SeregaDataBuilder.extend(DataBuilderClassMethods)
97
- end
98
28
 
99
- #
100
- # Adds config options and runs other callbacks after plugin was loaded
101
- #
102
- # @param serializer_class [Class<Serega>] Current serializer class
103
- # @param opts [Hash] Plugin options
104
- #
105
- # @return [void]
106
- #
107
- # @private
108
- def self.after_load_plugin(serializer_class, **opts)
109
29
  config = serializer_class.config
110
30
  default = opts.fetch(:root, ROOT_DEFAULT)
111
31
  one = opts.fetch(:root_one, default)
@@ -116,21 +36,37 @@ class Serega
116
36
  config.serialize_keys << :root
117
37
  end
118
38
 
119
- #
120
- # Serega additional/patched class methods
121
- #
122
- # @see Serega
123
- #
39
+ # Class methods the plugin adds to serializers
124
40
  module ClassMethods
41
+ def call(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil, **opts) # :nodoc:
42
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
43
+ .to_h(object, context: context, many: many, **opts)
44
+ end
45
+
46
+ def to_data(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil, **opts) # :nodoc:
47
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
48
+ .to_data(object, context: context, many: many, **opts)
49
+ end
50
+
51
+ def to_struct(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil, **opts) # :nodoc:
52
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
53
+ .to_struct(object, context: context, many: many, **opts)
54
+ end
55
+
56
+ def to_json(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil, **opts) # :nodoc:
57
+ new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
58
+ .to_json(object, context: context, many: many, **opts)
59
+ end
60
+
61
+ alias_method :to_h, :call # :nodoc:
62
+
63
+ # Sets the root keys: one key for objects and lists, or separate `one:`
64
+ # and `many:` keys
125
65
  #
126
- # Configures response root key
127
- #
128
- # @param root [String, Symbol, nil] Specifies common root keyword used when serializing one or multiple objects
129
- # @param one [String, Symbol, nil] Specifies root keyword used when serializing one object
130
- # @param many [String, Symbol, nil] Specifies root keyword used when serializing multiple objects
131
- #
132
- # @return [Hash] Configured root names
133
- #
66
+ # ```ruby
67
+ # root :users
68
+ # root one: :user, many: :users
69
+ # ```
134
70
  def root(root = nil, one: nil, many: nil)
135
71
  one ||= root
136
72
  many ||= root
@@ -139,185 +75,105 @@ class Serega
139
75
  end
140
76
  end
141
77
 
142
- # Root config object
78
+ # Root keys, `config.root`
143
79
  class RootConfig
144
- attr_reader :opts
80
+ attr_reader :opts # :nodoc:
145
81
 
146
- #
147
82
  # Initializes RootConfig object
148
- #
149
- # @param opts [Hash] root options
150
- # @option opts [Symbol, String, nil] :one root for single-object serialization
151
- # @option opts [Symbol, String, nil] :many root for many-objects serialization
152
- #
153
- # @return [SeregaPlugins::Root::RootConfig] RootConfig object
154
- #
155
- def initialize(opts)
83
+ def initialize(opts) # :nodoc:
156
84
  @opts = opts
157
85
  end
158
86
 
159
- # @return [Symbol, String, nil] defined root for single-object serialization
87
+ # Root key of one object
160
88
  def one
161
89
  opts.fetch(:one)
162
90
  end
163
91
 
164
- # @return [Symbol, String, nil] defined root for many-objects serialization
92
+ # Root key of a list
165
93
  def many
166
94
  opts.fetch(:many)
167
95
  end
168
96
 
169
- #
170
- # Set root for single-object serialization
171
- #
172
- # @param value [Symbol, String, nil] root key
173
- #
174
- # @return [Symbol, String, nil] root key for single-object serialization
97
+ # Sets #one
175
98
  def one=(value)
176
99
  opts[:one] = value
177
100
  end
178
101
 
179
- #
180
- # Set root for multiple-object serialization
181
- #
182
- # @param value [Symbol, String, nil] root key
183
- #
184
- # @return [Symbol, String, nil] root key for multiple-object serialization
102
+ # Sets #many
185
103
  def many=(value)
186
104
  opts[:many] = value
187
105
  end
188
106
  end
189
107
 
190
- #
191
- # Serega::SeregaConfig additional/patched class methods
192
- #
193
- # @see Serega::SeregaConfig
194
- #
108
+ # Config methods the plugin adds to Serega::SeregaConfig
195
109
  module ConfigInstanceMethods
196
- # @return [Serega::SeregaPlugins::Root::RootConfig] current root config
110
+ # Root keys
197
111
  def root
198
- @root ||= RootConfig.new(opts.fetch(:root))
112
+ @plugin_configs[:root] ||= RootConfig.new(opts.fetch(:root))
199
113
  end
200
114
 
201
- # Set root for one-object and many-objects serialization types
202
- #
203
- # @param value [Hash]
204
- # @option value [Symbol, String, nil] :one Root for one-object serialization type
205
- # @option value [Symbol, String, nil] :many Root for many-objects serialization type
206
- #
207
- # @return [void]
115
+ # Builds the root config before freezing
116
+ def freeze # :nodoc:
117
+ root
118
+ super
119
+ end
120
+
121
+ # Sets the root keys from a Hash with `:one` and `:many`
208
122
  def root=(value)
209
123
  root.one = value.fetch(:one)
210
124
  root.many = value.fetch(:many)
211
125
  end
212
126
  end
213
127
 
214
- #
215
128
  # Serega additional/patched instance methods
216
129
  #
217
- # @see Serega
218
- #
219
- # @private
220
- module InstanceMethods
221
- #
222
- # Serializes provided object to a tree of Ruby Data objects.
223
- # When a root key is configured, the result is wrapped in an outer Data object
224
- # whose members include the root key plus any metadata keys.
225
- #
226
- # @param object [Object] Serialized object
227
- # @param opts [Hash, nil] Serializing options (`:root`, `:many`, `:context`, etc.)
228
- #
229
- # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
230
- #
231
- def to_data(object, opts = nil)
232
- opts = normalize_serialization_opts(opts)
233
- object = prepare_objects(object, opts[:context])
234
- opts = prepare_initial_serialization_opts(object, opts)
235
- serialized_data = serialize(object, opts)
236
- self.class::SeregaDataBuilder.call(self, serialized_data, opts)
130
+ # The serialization methods take `**opts`, see ClassMethods. The
131
+ # options reach `#serialize` as its `opts`, after #checked_opts
132
+ # validates them.
133
+ module InstanceMethods # :nodoc:
134
+ def call(object, context: nil, many: nil, **opts)
135
+ serialize_to(:hash, object, context, many, checked_opts(opts))
237
136
  end
238
137
 
239
- private
240
-
241
- def serialize(object, opts)
242
- result = super
243
- root = build_root(object, opts)
244
- result = {root => result} if root
245
- result
138
+ def to_data(object, context: nil, many: nil, **opts)
139
+ serialize_to(:data, object, context, many, checked_opts(opts))
246
140
  end
247
141
 
248
- def build_root(object, opts)
249
- return opts[:root] if opts.key?(:root)
250
-
251
- root = self.class.config.root
252
- (opts.fetch(:many) { SeregaUtils::CollectionDetector.call(object) }) ? root.many : root.one
142
+ def to_struct(object, context: nil, many: nil, **opts)
143
+ serialize_to(:struct, object, context, many, checked_opts(opts))
253
144
  end
254
- end
255
145
 
256
- #
257
- # SeregaDataBuilder additional/patched class methods
258
- #
259
- # Overrides `call` to handle the root key: the serialized result is
260
- # unwrapped from the root key, converted to a Data tree by the base
261
- # implementation, and then re-wrapped together with any metadata keys
262
- # in an outer Data object. Metadata values (arbitrary hashes/arrays)
263
- # are recursively converted to Data objects via `deep_hash_to_data`.
264
- #
265
- # When the root key is `nil` (disabled), delegates directly to the base.
266
- #
267
- # @see Serega::SeregaDataBuilder
268
- #
269
- # @private
270
- module DataBuilderClassMethods
271
- #
272
- # @param serializer [Serega] Serializer instance carrying the plan
273
- # @param serialized_result [Hash, Array, nil] Full serialized output (possibly wrapped under root key)
274
- # @param serialization_opts [Hash] Serialization options used to determine the root key
275
- #
276
- # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
277
- #
278
- def call(serializer, serialized_result, serialization_opts)
279
- root_key = resolve_root_key(serializer, serialization_opts)
280
- return super(serializer, serialized_result) unless root_key
281
-
282
- root_data = super(serializer, serialized_result.fetch(root_key))
283
- build_data_objects(serialized_result, root_key, root_data)
146
+ def to_json(object, context: nil, many: nil, **opts)
147
+ JSON.generate(call(object, context: context, many: many, **opts))
284
148
  end
285
149
 
150
+ alias_method :to_h, :call
151
+
286
152
  private
287
153
 
288
- def resolve_root_key(serializer, serialization_opts)
289
- if serialization_opts.key?(:root)
290
- serialization_opts[:root]
291
- else
292
- config_root = serializer.class.config.root
293
- serialization_opts[:many] ? config_root.many : config_root.one
294
- end
295
- end
154
+ # Patched in:
155
+ # - plugin :context_metadata (checks the context metadata is a Hash)
156
+ def checked_opts(opts)
157
+ return FROZEN_EMPTY_HASH if opts.empty?
296
158
 
297
- def build_data_objects(serialized_result, root_key, root_data)
298
- converted_hash =
299
- serialized_result.to_h do |key, val|
300
- value = (key == root_key) ? root_data : deep_hash_to_data(val)
301
- [key, value]
302
- end
159
+ SeregaValidations::Utils::CheckAllowedKeys.call(opts, self.class.config.serialize_keys, :serialize)
160
+ opts
161
+ end
303
162
 
304
- Data.define(*converted_hash.keys).new(**converted_hash)
163
+ def serialize(object, opts, context, many, mode)
164
+ result = super
165
+ root = build_root(opts, many)
166
+ result = {root => result} if root
167
+ result
305
168
  end
306
169
 
307
- def deep_hash_to_data(value)
308
- case value
309
- when Hash
310
- converted = value.transform_values { |nested| deep_hash_to_data(nested) }
311
- Data.define(*value.keys).new(**converted)
312
- when Array
313
- value.map { |item| deep_hash_to_data(item) }
314
- else
315
- value
316
- end
170
+ def build_root(opts, many)
171
+ return opts[:root] if opts.key?(:root)
172
+
173
+ root = self.class.config.root
174
+ many ? root.many : root.one
317
175
  end
318
176
  end
319
177
  end
320
-
321
- register_plugin(Root.plugin_name, Root)
322
178
  end
323
179
  end