serega 0.35.0 → 0.37.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.
data/lib/serega.rb CHANGED
@@ -11,16 +11,17 @@ class Serega
11
11
  # Frozen array
12
12
  # @return [Array] frozen array
13
13
  FROZEN_EMPTY_ARRAY = [].freeze
14
+
15
+ # Empty modifiers/serialization options (used when serializing with no opts provided)
16
+ FROZEN_EMPTY_OPTS = [FROZEN_EMPTY_HASH, nil].freeze
17
+ private_constant :FROZEN_EMPTY_OPTS
14
18
  end
15
19
 
16
20
  require_relative "serega/errors"
17
21
  require_relative "serega/helpers/serializer_class_helper"
18
22
  require_relative "serega/utils/enum_deep_dup"
19
23
  require_relative "serega/utils/enum_deep_freeze"
20
- require_relative "serega/utils/format_user_preloads"
21
24
  require_relative "serega/utils/method_signature"
22
- require_relative "serega/utils/preload_paths"
23
- require_relative "serega/utils/preloads_constructor"
24
25
  require_relative "serega/utils/symbol_name"
25
26
  require_relative "serega/utils/to_hash"
26
27
  require_relative "serega/attribute_value_resolvers/batch"
@@ -31,6 +32,7 @@ require_relative "serega/attribute"
31
32
  require_relative "serega/attribute_normalizer"
32
33
  require_relative "serega/batch/attribute_loader"
33
34
  require_relative "serega/batch/attribute_loaders"
35
+ require_relative "serega/batch/level"
34
36
  require_relative "serega/batch/loader"
35
37
  require_relative "serega/validations/utils/check_allowed_keys"
36
38
  require_relative "serega/validations/utils/check_opt_is_bool"
@@ -45,7 +47,6 @@ require_relative "serega/validations/attribute/check_opt_batch"
45
47
  require_relative "serega/validations/attribute/check_opt_many"
46
48
  require_relative "serega/validations/attribute/check_opt_method"
47
49
  require_relative "serega/validations/attribute/check_opt_preload"
48
- require_relative "serega/validations/attribute/check_opt_preload_path"
49
50
  require_relative "serega/validations/attribute/check_opt_serializer"
50
51
  require_relative "serega/validations/attribute/check_opt_value"
51
52
  require_relative "serega/validations/initiate/check_modifiers"
@@ -58,6 +59,7 @@ require_relative "serega/config"
58
59
  require_relative "serega/object_serializer"
59
60
  require_relative "serega/plan_point"
60
61
  require_relative "serega/plan"
62
+ require_relative "serega/data_builder"
61
63
  require_relative "serega/plugins"
62
64
 
63
65
  class Serega
@@ -215,6 +217,38 @@ class Serega
215
217
  batch_loaders[batch_loader.name] = batch_loader
216
218
  end
217
219
 
220
+ #
221
+ # Registers (or returns) the handler used to preload an attribute's
222
+ # associations onto the records gathered during serialization.
223
+ #
224
+ # The handler is called once per preloaded attribute with the gathered
225
+ # objects and that attribute's preloads. ORM plugins register a handler
226
+ # that performs the actual eager loading.
227
+ #
228
+ # @example with a block
229
+ # preload_with { |objects, preloads| MyORM.eager_load(objects, preloads) }
230
+ #
231
+ # @example with a callable value
232
+ # preload_with MyPreloader
233
+ #
234
+ # @param value [#call, nil] Preload handler accepting two positional arguments
235
+ # @param block [Proc] Preload handler accepting two positional arguments
236
+ #
237
+ # @return [#call, nil] The registered preload handler
238
+ #
239
+ def preload_with(value = nil, &block)
240
+ return @preload_with if value.nil? && block.nil?
241
+ raise SeregaError, "preload_with accepts a single callable or a block, not both" if value && block
242
+
243
+ handler = value || block
244
+ raise SeregaError, "preload_with value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
245
+
246
+ signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2)
247
+ raise SeregaError, "preload_with handler must accept two positional arguments: (objects, preloads)" unless signature == "2"
248
+
249
+ @preload_with = handler
250
+ end
251
+
218
252
  #
219
253
  # Serializes provided object to Hash
220
254
  #
@@ -230,25 +264,45 @@ class Serega
230
264
  # @return [Hash] Serialization result
231
265
  #
232
266
  def call(object, opts = nil)
233
- opts ||= FROZEN_EMPTY_HASH
234
- initiate_keys = config.initiate_keys
235
-
236
- if opts.empty?
237
- modifiers_opts = FROZEN_EMPTY_HASH
238
- serialize_opts = nil
239
- else
240
- opts.transform_keys!(&:to_sym)
241
- serialize_opts = opts.except(*initiate_keys)
242
- modifiers_opts = opts.slice(*initiate_keys)
243
- end
244
-
267
+ opts = opts&.transform_keys(&:to_sym)
268
+ modifiers_opts = init_modifier_opts(opts)
269
+ serialize_opts = init_serialize_opts(opts)
245
270
  new(modifiers_opts).to_h(object, serialize_opts)
246
271
  end
247
272
 
273
+ #
274
+ # Serializes provided object to a tree of Ruby Data objects
275
+ #
276
+ # @param object [Object] Serialized object
277
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
278
+ # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
279
+ # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
280
+ # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
281
+ # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
282
+ # @option opts [Hash] :context Serialization context
283
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
284
+ #
285
+ # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
286
+ #
287
+ def to_data(object, opts = nil)
288
+ opts = opts&.transform_keys(&:to_sym)
289
+ modifiers_opts = init_modifier_opts(opts)
290
+ serialize_opts = init_serialize_opts(opts)
291
+ new(modifiers_opts).to_data(object, serialize_opts)
292
+ end
293
+
248
294
  alias_method :to_h, :call
249
295
 
250
296
  private
251
297
 
298
+ def init_modifier_opts(opts)
299
+ (!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
300
+ end
301
+
302
+ def init_serialize_opts(opts)
303
+ (!opts || opts.empty?) ? nil : opts.except(*config.initiate_keys)
304
+ end
305
+
252
306
  # Patched in:
253
307
  # - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
254
308
  # - plugin :presenter (defines Presenter)
@@ -266,6 +320,10 @@ class Serega
266
320
  attribute_normalizer_class.serializer_class = subclass
267
321
  subclass.const_set(:SeregaAttributeNormalizer, attribute_normalizer_class)
268
322
 
323
+ data_builder_class = Class.new(self::SeregaDataBuilder)
324
+ data_builder_class.serializer_class = subclass
325
+ subclass.const_set(:SeregaDataBuilder, data_builder_class)
326
+
269
327
  plan_class = Class.new(self::SeregaPlan)
270
328
  plan_class.serializer_class = subclass
271
329
  subclass.const_set(:SeregaPlan, plan_class)
@@ -313,6 +371,9 @@ class Serega
313
371
  subclass.batch(loader.name, loader.block)
314
372
  end
315
373
 
374
+ # Assign same preload handler
375
+ subclass.preload_with(preload_with) if preload_with
376
+
316
377
  super
317
378
  end
318
379
  end
@@ -355,17 +416,14 @@ class Serega
355
416
  # Serializes provided object to Hash
356
417
  #
357
418
  # @param object [Object] Serialized object
358
- # @param opts [Hash, nil] Serializer modifiers and other instantiating options
419
+ # @param opts [Hash, nil] Serializing options
359
420
  # @option opts [Hash] :context Serialization context
360
421
  # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
361
422
  #
362
423
  # @return [Hash] Serialization result
363
424
  #
364
425
  def call(object, opts = nil)
365
- opts = opts ? opts.transform_keys!(&:to_sym) : {}
366
- self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
367
-
368
- opts[:context] ||= {}
426
+ opts = prepare_initial_serialization_opts(object, opts)
369
427
  serialize(object, opts)
370
428
  end
371
429
 
@@ -374,9 +432,22 @@ class Serega
374
432
  call(object, opts)
375
433
  end
376
434
 
377
- # @return [Hash] merged preloads of all serialized attributes
378
- def preloads
379
- @preloads ||= SeregaUtils::PreloadsConstructor.call(plan)
435
+ #
436
+ # Serializes provided object to Data objects
437
+ # Patched in:
438
+ # - plugin :root (adds a data-object for a root level keys)
439
+ #
440
+ # @param object [Object] Serialized object
441
+ # @param opts [Hash, nil] Serializing options
442
+ # @option opts [Hash] :context Serialization context
443
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
444
+ #
445
+ # @return [Data] Serialization result
446
+ #
447
+ def to_data(object, opts = nil)
448
+ opts = prepare_initial_serialization_opts(object, opts)
449
+ serialized_data = serialize(object, opts)
450
+ self.class::SeregaDataBuilder.call(self, serialized_data)
380
451
  end
381
452
 
382
453
  private
@@ -404,15 +475,24 @@ class Serega
404
475
  SeregaUtils::ToHash.call(value)
405
476
  end
406
477
 
478
+ def prepare_initial_serialization_opts(object, opts)
479
+ opts = opts ? opts.transform_keys(&:to_sym) : {}
480
+ self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
481
+
482
+ opts[:context] ||= {}
483
+ opts[:batch_loaders] = SeregaBatch::AttributeLoaders.new(opts[:context])
484
+ opts[:many] = object.is_a?(Enumerable) unless opts.key?(:many)
485
+ opts[:plan] = plan
486
+ opts
487
+ end
488
+
407
489
  # Patched in:
408
- # - plugin :activerecord_preloads (loads defined :preloads to object)
409
490
  # - plugin :root (wraps result `{ root => result }`)
410
491
  # - plugin :context_metadata (adds context metadata to final result)
411
492
  # - plugin :metadata (adds metadata to final result)
412
493
  def serialize(object, opts)
413
- batch_loaders = plan.has_batch_points ? SeregaBatch::AttributeLoaders.new : nil
414
- result = self.class::SeregaObjectSerializer.new(**opts, batch_loaders: batch_loaders, plan: plan).serialize(object)
415
- batch_loaders&.load_all(opts[:context])
494
+ result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
495
+ opts[:batch_loaders].load_all
416
496
  result
417
497
  end
418
498
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.0
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Glushkov
@@ -35,8 +35,10 @@ files:
35
35
  - lib/serega/attribute_value_resolvers/keyword.rb
36
36
  - lib/serega/batch/attribute_loader.rb
37
37
  - lib/serega/batch/attribute_loaders.rb
38
+ - lib/serega/batch/level.rb
38
39
  - lib/serega/batch/loader.rb
39
40
  - lib/serega/config.rb
41
+ - lib/serega/data_builder.rb
40
42
  - lib/serega/errors.rb
41
43
  - lib/serega/helpers/serializer_class_helper.rb
42
44
  - lib/serega/object_serializer.rb
@@ -44,7 +46,6 @@ files:
44
46
  - lib/serega/plan_point.rb
45
47
  - lib/serega/plugins.rb
46
48
  - lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb
47
- - lib/serega/plugins/activerecord_preloads/lib/active_record_objects.rb
48
49
  - lib/serega/plugins/activerecord_preloads/lib/preloader.rb
49
50
  - lib/serega/plugins/camel_case/camel_case.rb
50
51
  - lib/serega/plugins/context_metadata/context_metadata.rb
@@ -72,10 +73,7 @@ files:
72
73
  - lib/serega/plugins/string_modifiers/string_modifiers.rb
73
74
  - lib/serega/utils/enum_deep_dup.rb
74
75
  - lib/serega/utils/enum_deep_freeze.rb
75
- - lib/serega/utils/format_user_preloads.rb
76
76
  - lib/serega/utils/method_signature.rb
77
- - lib/serega/utils/preload_paths.rb
78
- - lib/serega/utils/preloads_constructor.rb
79
77
  - lib/serega/utils/symbol_name.rb
80
78
  - lib/serega/utils/to_hash.rb
81
79
  - lib/serega/validations/attribute/check_block.rb
@@ -87,7 +85,6 @@ files:
87
85
  - lib/serega/validations/attribute/check_opt_many.rb
88
86
  - lib/serega/validations/attribute/check_opt_method.rb
89
87
  - lib/serega/validations/attribute/check_opt_preload.rb
90
- - lib/serega/validations/attribute/check_opt_preload_path.rb
91
88
  - lib/serega/validations/attribute/check_opt_serializer.rb
92
89
  - lib/serega/validations/attribute/check_opt_value.rb
93
90
  - lib/serega/validations/check_attribute_params.rb
@@ -114,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
111
  requirements:
115
112
  - - ">="
116
113
  - !ruby/object:Gem::Version
117
- version: 2.6.0
114
+ version: 3.2.0
118
115
  required_rubygems_version: !ruby/object:Gem::Requirement
119
116
  requirements:
120
117
  - - ">="
121
118
  - !ruby/object:Gem::Version
122
119
  version: '0'
123
120
  requirements: []
124
- rubygems_version: 4.0.11
121
+ rubygems_version: 4.0.13
125
122
  specification_version: 4
126
123
  summary: JSON Serializer
127
124
  test_files: []
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module ActiverecordPreloads
6
- # Detects and returns found ActiveRecord objects
7
- class ActiveRecordObjects
8
- class << self
9
- # Iterates over provided data and selects ActiveRecord objects
10
- # @param data [Hash,Array,Object] any data
11
- # @return [Array] Found ActiveRecord objects
12
- def call(data)
13
- res = []
14
- extract(data, res)
15
- res
16
- end
17
-
18
- private
19
-
20
- def extract(data, res)
21
- case data
22
- when ActiveRecord::Base then res << data
23
- when Array then data.each { |value| extract(value, res) }
24
- when Hash then data.each_value { |value| extract(value, res) }
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaUtils
5
- #
6
- # Utility that helps to transform user provided preloads to hash
7
- #
8
- class FormatUserPreloads
9
- class << self
10
- #
11
- # Transforms user provided preloads to hash
12
- #
13
- # @param value [Array,Hash,String,Symbol,nil,false] preloads
14
- #
15
- # @return [Hash] preloads transformed to hash
16
- #
17
- def call(value)
18
- case value
19
- when Array then array_to_hash(value)
20
- when FalseClass then nil_to_hash(value)
21
- when Hash then hash_to_hash(value)
22
- when NilClass then nil_to_hash(value)
23
- when String then string_to_hash(value)
24
- when Symbol then symbol_to_hash(value)
25
- else raise Serega::SeregaError,
26
- "Preload option value can consist from Symbols, Arrays, Hashes (#{value.class} #{value.inspect} was provided)"
27
- end
28
- end
29
-
30
- private
31
-
32
- def array_to_hash(values)
33
- values.each_with_object({}) do |value, obj|
34
- obj.merge!(call(value))
35
- end
36
- end
37
-
38
- def hash_to_hash(values)
39
- values.each_with_object({}) do |(key, value), obj|
40
- obj[key.to_sym] = call(value)
41
- end
42
- end
43
-
44
- def nil_to_hash(_value)
45
- {}
46
- end
47
-
48
- def string_to_hash(value)
49
- {value.to_sym => {}}
50
- end
51
-
52
- def symbol_to_hash(value)
53
- {value => {}}
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaUtils
5
- #
6
- # Utility that helps to transform preloads to array of paths
7
- # It is used to validate manually set `:preload_path` attribute option has one of allowed values.
8
- # `:preload_path` option can be used to specify where nested preloads must be attached.
9
- #
10
- # Example:
11
- #
12
- # call({ a: { b: { c: {}, d: {} } }, e: {} })
13
- #
14
- # => [
15
- # [:a],
16
- # [:a, :b],
17
- # [:a, :b, :c],
18
- # [:a, :b, :d],
19
- # [:e]
20
- # ]
21
- class PreloadPaths
22
- class << self
23
- #
24
- # Transforms user provided preloads to array of paths
25
- #
26
- # @param preloads [Array,Hash,String,Symbol,nil,false] association(s) to preload
27
- #
28
- # @return [Array] transformed preloads
29
- #
30
- def call(preloads)
31
- formatted_preloads = FormatUserPreloads.call(preloads)
32
- return FROZEN_EMPTY_ARRAY if formatted_preloads.empty?
33
-
34
- paths(formatted_preloads, [], [])
35
- end
36
-
37
- private
38
-
39
- def paths(formatted_preloads, path, result)
40
- formatted_preloads.each do |key, nested_preloads|
41
- path << key
42
- result << path.dup
43
-
44
- paths(nested_preloads, path, result)
45
- path.pop
46
- end
47
-
48
- result
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaUtils
5
- #
6
- # Finds preloads for provided attributes plan
7
- #
8
- class PreloadsConstructor
9
- class << self
10
- #
11
- # Constructs preloads hash for given attributes plan
12
- #
13
- # @param plan [Array<SeregaPlanPoint>] Serialization plan
14
- #
15
- # @return [Hash]
16
- #
17
- def call(plan)
18
- return FROZEN_EMPTY_HASH unless plan
19
-
20
- preloads = {}
21
- append_many(preloads, plan)
22
- preloads
23
- end
24
-
25
- private
26
-
27
- def append_many(preloads, plan)
28
- plan.points.each do |point|
29
- current_preloads = point.attribute.preloads
30
- next unless current_preloads
31
-
32
- child_plan = point.child_plan
33
- current_preloads = SeregaUtils::EnumDeepDup.call(current_preloads) if child_plan
34
- append_current(preloads, current_preloads)
35
- next unless child_plan
36
-
37
- each_child_preloads(preloads, point.preloads_path) do |child_preloads|
38
- append_many(child_preloads, child_plan)
39
- end
40
- end
41
- end
42
-
43
- def append_current(preloads, current_preloads)
44
- merge(preloads, current_preloads) unless current_preloads.empty?
45
- end
46
-
47
- def merge(preloads, current_preloads)
48
- preloads.merge!(current_preloads) do |_key, value_one, value_two|
49
- merge(value_one, value_two)
50
- end
51
- end
52
-
53
- def each_child_preloads(preloads, preloads_path)
54
- return yield(preloads) if preloads_path.nil?
55
-
56
- if preloads_path[0].is_a?(Array)
57
- preloads_path.each do |path|
58
- yield dig_fetch(preloads, path)
59
- end
60
- else
61
- yield dig_fetch(preloads, preloads_path)
62
- end
63
- end
64
-
65
- def dig_fetch(preloads, preloads_path)
66
- return preloads if !preloads_path || preloads_path.empty?
67
-
68
- preloads_path.each do |path|
69
- preloads = preloads.fetch(path)
70
- end
71
-
72
- preloads
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaValidations
5
- module Attribute
6
- #
7
- # Validator for attribute :preload_path option
8
- #
9
- class CheckOptPreloadPath
10
- class << self
11
- #
12
- # Checks preload_path option
13
- #
14
- # @param opts [Hash] Attribute options
15
- #
16
- # @raise [SeregaError] validation error
17
- #
18
- # @return [void]
19
- #
20
- def call(opts)
21
- return if exactly_nil?(opts, :preload_path) # allow to provide nil anyway
22
-
23
- path = opts[:preload_path]
24
- check_usage_with_other_options(path, opts)
25
- return unless opts[:serializer]
26
-
27
- check_allowed(path, opts)
28
- end
29
-
30
- private
31
-
32
- def exactly_nil?(opts, opt_name)
33
- opts.fetch(opt_name, false).nil?
34
- end
35
-
36
- def check_allowed(path, opts)
37
- allowed_paths = SeregaUtils::PreloadPaths.call(opts[:preload])
38
- check_required_when_many_allowed(path, allowed_paths)
39
- check_in_allowed(path, allowed_paths)
40
- end
41
-
42
- def check_usage_with_other_options(path, opts)
43
- return unless path
44
-
45
- preload = opts[:preload]
46
- raise SeregaError, "Invalid option preload_path: #{path.inspect}. Can be provided only when :preload option provided" unless preload
47
-
48
- serializer = opts[:serializer]
49
- raise SeregaError, "Invalid option preload_path: #{path.inspect}. Can be provided only when :serializer option provided" unless serializer
50
- end
51
-
52
- def check_required_when_many_allowed(path, allowed)
53
- return if path || (allowed.size < 2)
54
-
55
- raise SeregaError, "Option :preload_path must be provided. Possible values: #{allowed.inspect[1..-2]}"
56
- end
57
-
58
- def check_in_allowed(path, allowed)
59
- return if !path && allowed.size <= 1
60
-
61
- if multiple_preload_paths_provided?(path)
62
- check_many(path, allowed)
63
- else
64
- check_one(path, allowed)
65
- end
66
- end
67
-
68
- def check_one(path, allowed)
69
- formatted_path = Array(path).map(&:to_sym)
70
- return if allowed.include?(formatted_path)
71
-
72
- raise SeregaError,
73
- "Invalid preload_path (#{path.inspect}). " \
74
- "Can be one of #{allowed.inspect[1..-2]}"
75
- end
76
-
77
- def check_many(paths, allowed)
78
- paths.each { |path| check_one(path, allowed) }
79
- end
80
-
81
- # Check value is Array in Array
82
- def multiple_preload_paths_provided?(value)
83
- value.is_a?(Array) && value[0].is_a?(Array)
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end