serega 0.11.2 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b4a68e91572d22cf24e20a79cc788f647eb63e16582f0e33497eec459692de1
4
- data.tar.gz: 8d3a55c8fd783b00c55cb08ed8f62c0f47cb79573ccbdc9f65c6710489d0dd0f
3
+ metadata.gz: 52535c7fab3897f65aec4e156ab5681e8595b4519df1e79a777688cee486a480
4
+ data.tar.gz: 16adbd9f036b0e23928049df873966f264db1004d2c4c1bc9e28422d687efd12
5
5
  SHA512:
6
- metadata.gz: 67cb15aafd0dd90dd301a2ae79643c93204c52c26752649f96e904d2e1fe75d37709c91474c891eb73d1ee913002c214fc2c0288a613f82ad082ee385b149338
7
- data.tar.gz: c421133bba5f4be2227a266bf6eb6fa6013f0320741f4e842575823d3a6d9e93bf37e8029b9fa801b2bcede964679c19fe794a1f7f0dceb10909c98251db588b
6
+ metadata.gz: 93a6aaefdf954556e31bd604ead1e376d5ffbb74225251e635bf5133af53da26f47e924f1f88349fa4656e228ddca573347d9f8fa15cc30bc6052f7e5dfec3bd
7
+ data.tar.gz: c84135d04c79aae1ccd03a3f3ac7dbed53dbf75165575afbb61ad6e32a558318cac2a8311c31b76df90fb74823facdf4677821d8f7ae8284f04782d33b04ad41
data/README.md CHANGED
@@ -415,30 +415,79 @@ UserSerializer.new(
415
415
  ```
416
416
 
417
417
  ---
418
- One tricky case, that you will probably never see in real life:
419
418
 
420
- Manually you can preload multiple associations, like this:
419
+ #### SPECIFIC CASE #1: Serializing same object as association
420
+
421
+ For example you decided to show your current user as "user" and "user_stats".
422
+ Where stats rely on user fields and some other associations.
423
+ You should specify `preload: nil` to preload nested associations, if any, to "user".
421
424
 
422
425
  ```ruby
423
- attribute :image,
424
- serializer: ImageSerializer,
425
- preload: { attachment: :blob },
426
- value: proc { |record| record.attachment }
426
+ class AppSerializer < Serega
427
+ plugin :preloads,
428
+ auto_preload_attributes_with_delegate: true,
429
+ auto_preload_attributes_with_serializer: true,
430
+ auto_hide_attributes_with_preload: true
431
+ end
432
+
433
+ class UserSerializer < AppSerializer
434
+ attribute :username
435
+ attribute :user_stats,
436
+ serializer: 'UserStatSerializer'
437
+ value: proc { |user| user },
438
+ preload: nil
439
+ end
427
440
  ```
428
441
 
429
- In this case we mark last element (in this case it will be `blob`) as main,
430
- so nested associations, if any, will be preloaded to this `blob`.
431
- If you need to preload them to `attachment`,
432
- please specify additionally `:preload_path` option like this:
442
+ #### SPECIFIC CASE #2: Serializing multiple associations as single relation
443
+
444
+ For example "user" has two relations - "new_profile", "old_profile", and also
445
+ profiles have "avatar" association. And you decided to serialize profiles in one
446
+ array. You can specify `preload_path: [[:new_profile], [:old_profile]]` to
447
+ achieve this:
448
+
449
+ ```ruby
450
+ class AppSerializer < Serega
451
+ plugin :preloads,
452
+ auto_preload_attributes_with_delegate: true,
453
+ auto_preload_attributes_with_serializer: true
454
+ end
455
+
456
+ class UserSerializer < AppSerializer
457
+ attribute :username
458
+ attribute :profiles,
459
+ serializer: 'ProfileSerializer',
460
+ value: proc { |user| [user.new_profile, user.old_profile] },
461
+ preload: [:new_profile, :old_profile],
462
+ preload_path: [[:new_profile], [:old_profile]] # <--- like here
463
+ end
464
+
465
+ class ProfileSerializer < AppSerializer
466
+ attribute :avatar, serializer: 'AvatarSerializer'
467
+ end
468
+
469
+ class AvatarSerializer < AppSerializer
470
+ end
471
+
472
+ UserSerializer.new.preloads
473
+ # => {:new_profile=>{:avatar=>{}}, :old_profile=>{:avatar=>{}}}
474
+ ```
475
+
476
+ #### SPECIFIC CASE #3: Preload association through another association
433
477
 
434
478
  ```ruby
435
479
  attribute :image,
480
+ preload: { attachment: :blob }, # <--------- like this one
481
+ value: proc { |record| record.attachment },
436
482
  serializer: ImageSerializer,
437
- preload: { attachment: :blob },
438
- preload_path: %i[attachment],
439
- value: proc { |record| record.attachment }
483
+ preload_path: [:attachment] # or preload_path: [:attachment, :blob]
440
484
  ```
441
485
 
486
+ In this case we don't know if preloads defined in ImageSerializer, should be
487
+ preloaded to `attachment` or `blob`, so please specify `preload_path` manually.
488
+ You can specify `preload_path: nil` if you are sure that there are no preloads
489
+ inside ImageSerializer.
490
+
442
491
  ---
443
492
 
444
493
  📌 Plugin `:preloads` only allows to group preloads together in single Hash, but
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.2
1
+ 0.12.0
@@ -82,13 +82,18 @@ class Serega
82
82
  #
83
83
  # Checks if attribute must be added to serialized response
84
84
  #
85
- # @param except [Hash] manually hidden attributes
86
- # @param only [Hash] manually enforced exposed attributes, other attributes are enforced to be hidden
87
- # @param with [Hash] manually enforced exposed attributes
85
+ # @param modifiers [Hash] Serialization modifiers
86
+ # @option modifiers [Hash] :only The only attributes to serialize
87
+ # @option modifiers [Hash] :except Attributes to hide
88
+ # @option modifiers [Hash] :with Hidden attributes to serialize additionally
88
89
  #
89
90
  # @return [Boolean]
90
91
  #
91
- def visible?(except:, only:, with:)
92
+ def visible?(modifiers)
93
+ except = modifiers[:except] || FROZEN_EMPTY_HASH
94
+ only = modifiers[:only] || FROZEN_EMPTY_HASH
95
+ with = modifiers[:with] || FROZEN_EMPTY_HASH
96
+
92
97
  return false if except.member?(name) && except[name].empty?
93
98
  return true if only.member?(name)
94
99
  return true if with.member?(name)
@@ -107,6 +107,7 @@ class Serega
107
107
  #
108
108
  # Patched in:
109
109
  # - plugin :preloads (returns true by default if config option auto_hide_attribute_with_preloads is enabled)
110
+ # - plugin :batch (returns true by default if auto_hide option was set and attribute has batch loader)
110
111
  #
111
112
  def prepare_hide
112
113
  init_opts[:hide]
@@ -135,7 +136,7 @@ class Serega
135
136
  def prepare_keyword_block
136
137
  key_method_name = key
137
138
  proc do |object|
138
- handle_no_method_error { object.public_send(key_method_name) }
139
+ object.public_send(key_method_name)
139
140
  end
140
141
  end
141
142
 
@@ -150,24 +151,14 @@ class Serega
150
151
 
151
152
  if allow_nil
152
153
  proc do |object|
153
- handle_no_method_error do
154
- object.public_send(delegate_to)&.public_send(key_method_name)
155
- end
154
+ object.public_send(delegate_to)&.public_send(key_method_name)
156
155
  end
157
156
  else
158
157
  proc do |object|
159
- handle_no_method_error do
160
- object.public_send(delegate_to).public_send(key_method_name)
161
- end
158
+ object.public_send(delegate_to).public_send(key_method_name)
162
159
  end
163
160
  end
164
161
  end
165
-
166
- def handle_no_method_error
167
- yield
168
- rescue NoMethodError => error
169
- raise error, "NoMethodError when serializing '#{name}' attribute in #{self.class.serializer_class}\n\n#{error.message}", error.backtrace
170
- end
171
162
  end
172
163
 
173
164
  extend Serega::SeregaHelpers::SerializerClassHelper
@@ -47,6 +47,10 @@ class Serega
47
47
  def serialize_object(object)
48
48
  plan.points.each_with_object({}) do |point, container|
49
49
  serialize_point(object, point, container)
50
+ rescue SeregaError
51
+ raise
52
+ rescue => error
53
+ reraise_with_serialized_attribute_details(error, point)
50
54
  end
51
55
  end
52
56
 
@@ -85,6 +89,13 @@ class Serega
85
89
  def array?(object, many)
86
90
  many.nil? ? object.is_a?(Enumerable) : many
87
91
  end
92
+
93
+ def reraise_with_serialized_attribute_details(error, point)
94
+ raise error.exception(<<~MESSAGE.strip)
95
+ #{error.message}
96
+ (when serializing '#{point.name}' attribute in #{self.class.serializer_class})
97
+ MESSAGE
98
+ end
88
99
  end
89
100
 
90
101
  extend Serega::SeregaHelpers::SerializerClassHelper
data/lib/serega/plan.rb CHANGED
@@ -22,34 +22,22 @@ class Serega
22
22
  #
23
23
  def call(opts)
24
24
  max_cache_size = serializer_class.config.max_cached_plans_per_serializer_count
25
- return plan_for(opts) if max_cache_size.zero?
25
+ return new(opts) if max_cache_size.zero?
26
26
 
27
27
  cached_plan_for(opts, max_cache_size)
28
28
  end
29
29
 
30
30
  private
31
31
 
32
- def plan_for(opts)
33
- new(**modifiers(opts))
34
- end
35
-
36
32
  def cached_plan_for(opts, max_cache_size)
37
33
  @cache ||= {}
38
34
  cache_key = construct_cache_key(opts)
39
35
 
40
- plan = @cache[cache_key] ||= plan_for(opts)
36
+ plan = @cache[cache_key] ||= new(opts)
41
37
  @cache.shift if @cache.length > max_cache_size
42
38
  plan
43
39
  end
44
40
 
45
- def modifiers(opts)
46
- {
47
- only: opts[:only] || FROZEN_EMPTY_HASH,
48
- except: opts[:except] || FROZEN_EMPTY_HASH,
49
- with: opts[:with] || FROZEN_EMPTY_HASH
50
- }
51
- end
52
-
53
41
  def construct_cache_key(opts, cache_key = nil)
54
42
  return nil if opts.empty?
55
43
 
@@ -69,10 +57,14 @@ class Serega
69
57
  # SeregaPlan instance methods
70
58
  #
71
59
  module InstanceMethods
72
- # Parent plan point, if exists
60
+ # Parent plan point
73
61
  # @return [SeregaPlanPoint, nil]
74
62
  attr_reader :parent_plan_point
75
63
 
64
+ # Sets new parent plan point
65
+ # @return [SeregaPlanPoint] new parent plan point
66
+ attr_writer :parent_plan_point
67
+
76
68
  # Serialization points
77
69
  # @return [Array<SeregaPlanPoint>] points to serialize
78
70
  attr_reader :points
@@ -80,17 +72,15 @@ class Serega
80
72
  #
81
73
  # Instantiate new serialization plan.
82
74
  #
83
- # @param opts Serialization parameters
84
- # @option opts [Hash] :only The only attributes to serialize
85
- # @option opts [Hash] :except Attributes to hide
86
- # @option opts [Hash] :with Attributes (usually marked hide: true`) to serialize additionally
87
- # @option opts [Hash] :with Attributes (usually marked hide: true`) to serialize additionally
75
+ # @param modifiers Serialization parameters
76
+ # @option modifiers [Hash] :only The only attributes to serialize
77
+ # @option modifiers [Hash] :except Attributes to hide
78
+ # @option modifiers [Hash] :with Hidden attributes to serialize additionally
88
79
  #
89
80
  # @return [SeregaPlan] Serialization plan
90
81
  #
91
- def initialize(only:, except:, with:, parent_plan_point: nil)
92
- @parent_plan_point = parent_plan_point
93
- @points = attributes_points(only: only, except: except, with: with)
82
+ def initialize(modifiers)
83
+ @points = attributes_points(modifiers)
94
84
  end
95
85
 
96
86
  #
@@ -102,7 +92,10 @@ class Serega
102
92
 
103
93
  private
104
94
 
105
- def attributes_points(only:, except:, with:)
95
+ def attributes_points(modifiers)
96
+ only = modifiers[:only] || FROZEN_EMPTY_HASH
97
+ except = modifiers[:except] || FROZEN_EMPTY_HASH
98
+ with = modifiers[:with] || FROZEN_EMPTY_HASH
106
99
  points = []
107
100
 
108
101
  serializer_class.attributes.each_value do |attribute|
@@ -114,7 +107,9 @@ class Serega
114
107
  {only: only[name], with: with[name], except: except[name]}
115
108
  end
116
109
 
117
- points << serializer_class::SeregaPlanPoint.new(attribute, self, child_fields)
110
+ point = serializer_class::SeregaPlanPoint.new(attribute, child_fields)
111
+ point.plan = self
112
+ points << point.freeze
118
113
  end
119
114
 
120
115
  points.freeze
@@ -13,7 +13,7 @@ class Serega
13
13
 
14
14
  # Link to current plan this point belongs to
15
15
  # @return [SeregaAttribute] Current plan
16
- attr_reader :plan
16
+ attr_accessor :plan
17
17
 
18
18
  # Shows current attribute
19
19
  # @return [SeregaAttribute] Current attribute
@@ -24,8 +24,8 @@ class Serega
24
24
  attr_reader :child_plan
25
25
 
26
26
  # Child fields to serialize
27
- # @return [SeregaPlan, nil] Attribute serialization plan
28
- attr_reader :child_fields
27
+ # @return [Hash] Attributes to serialize
28
+ attr_reader :modifiers
29
29
 
30
30
  # @!method name
31
31
  # Attribute `name`
@@ -44,18 +44,18 @@ class Serega
44
44
  #
45
45
  # Initializes plan point
46
46
  #
47
- # @param plan [SeregaPlan] Plan where this point belongs to.
48
47
  # @param attribute [SeregaAttribute] Attribute to construct plan point
49
- # @param child_fields [Hash, nil] Child fields (:only, :with, :except)
48
+ # @param modifiers Serialization parameters
49
+ # @option modifiers [Hash] :only The only attributes to serialize
50
+ # @option modifiers [Hash] :except Attributes to hide
51
+ # @option modifiers [Hash] :with Hidden attributes to serialize additionally
50
52
  #
51
53
  # @return [SeregaPlanPoint] New plan point
52
54
  #
53
- def initialize(attribute, plan = nil, child_fields = nil)
54
- @plan = plan
55
+ def initialize(attribute, modifiers = nil)
55
56
  @attribute = attribute
56
- @child_fields = child_fields
57
+ @modifiers = modifiers
57
58
  set_normalized_vars
58
- freeze
59
59
  end
60
60
 
61
61
  #
@@ -77,14 +77,11 @@ class Serega
77
77
  def prepare_child_plan
78
78
  return unless serializer
79
79
 
80
- fields = child_fields || FROZEN_EMPTY_HASH
80
+ fields = modifiers || FROZEN_EMPTY_HASH
81
81
 
82
- serializer::SeregaPlan.new(
83
- parent_plan_point: self,
84
- only: fields[:only] || FROZEN_EMPTY_HASH,
85
- with: fields[:with] || FROZEN_EMPTY_HASH,
86
- except: fields[:except] || FROZEN_EMPTY_HASH
87
- )
82
+ plan = serializer::SeregaPlan.new(fields)
83
+ plan.parent_plan_point = self
84
+ plan
88
85
  end
89
86
  end
90
87
 
@@ -60,6 +60,10 @@ class Serega
60
60
 
61
61
  private
62
62
 
63
+ def keys
64
+ @keys ||= {}
65
+ end
66
+
63
67
  def each_key
64
68
  keys.each do |key, containers|
65
69
  containers.each do |container|
@@ -74,16 +78,30 @@ class Serega
74
78
  def keys_values
75
79
  ids = keys.keys
76
80
 
77
- point.batch[:loader].call(ids, object_serializer.context, point).tap do |vals|
78
- next if vals.is_a?(Hash)
81
+ keys_values = load_keys_values(ids)
82
+ validate(keys_values)
79
83
 
80
- attribute_name = "#{point.class.serializer_class}.#{point.name}"
81
- raise SeregaError, "Batch loader for `#{attribute_name}` must return Hash, but #{vals.inspect} was returned"
82
- end
84
+ keys_values
83
85
  end
84
86
 
85
- def keys
86
- @keys ||= {}
87
+ def load_keys_values(ids)
88
+ point.batch[:loader].call(ids, object_serializer.context, point)
89
+ rescue => error
90
+ raise reraise_with_serialized_attribute_details(error, point)
91
+ end
92
+
93
+ def validate(keys_values)
94
+ return if keys_values.is_a?(Hash)
95
+
96
+ attribute_name = "#{point.class.serializer_class}.#{point.name}"
97
+ raise SeregaError, "Batch loader for `#{attribute_name}` must return Hash, but #{keys_values.inspect} was returned"
98
+ end
99
+
100
+ def reraise_with_serialized_attribute_details(error, point)
101
+ raise error.exception(<<~MESSAGE.strip)
102
+ #{error.message}
103
+ (when serializing '#{point.name}' attribute in #{self.class.serializer_class})
104
+ MESSAGE
87
105
  end
88
106
  end
89
107
 
@@ -49,9 +49,7 @@ class Serega
49
49
  key = batch[:key] || self.class.serializer_class.config.batch.default_key
50
50
  proc_key =
51
51
  if key.is_a?(Symbol)
52
- proc do |object|
53
- handle_no_method_error { object.public_send(key) }
54
- end
52
+ proc { |object| object.public_send(key) }
55
53
  else
56
54
  key
57
55
  end
@@ -61,12 +59,6 @@ class Serega
61
59
 
62
60
  {loader: loader, key: proc_key, default: default}
63
61
  end
64
-
65
- def handle_no_method_error
66
- yield
67
- rescue NoMethodError => error
68
- raise error, "NoMethodError when serializing '#{name}' attribute in #{self.class.serializer_class}\n\n#{error.message}", error.backtrace
69
- end
70
62
  end
71
63
  end
72
64
  end
@@ -194,6 +194,11 @@ class Serega
194
194
  next unless metadata
195
195
 
196
196
  deep_merge_metadata(hash, metadata)
197
+ rescue => error
198
+ raise error.exception(<<~MESSAGE.strip)
199
+ #{error.message}
200
+ (when serializing meta_attribute #{meta_attribute.path.inspect} in #{self.class})
201
+ MESSAGE
197
202
  end
198
203
  end
199
204
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaAttribute additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaAttribute::AttributeInstanceMethods
10
+ #
11
+ module AttributeInstanceMethods
12
+ # @return [Hash, nil] normalized preloads of current attribute
13
+ attr_reader :preloads
14
+
15
+ # @return [Array] normalized preloads_path of current attribute
16
+ attr_reader :preloads_path
17
+
18
+ private
19
+
20
+ def set_normalized_vars(normalizer)
21
+ super
22
+ @preloads = normalizer.preloads
23
+ @preloads_path = normalizer.preloads_path
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaAttributeNormalizer additional/patched instance methods
8
+ #
9
+ # @see SeregaAttributeNormalizer::AttributeNormalizerInstanceMethods
10
+ #
11
+ module AttributeNormalizerInstanceMethods
12
+ # @return [Hash,nil] normalized attribute preloads
13
+ def preloads
14
+ return @preloads if instance_variable_defined?(:@preloads)
15
+
16
+ @preloads = prepare_preloads
17
+ end
18
+
19
+ # @return [Array, nil] normalized attribute preloads path
20
+ def preloads_path
21
+ return @preloads_path if instance_variable_defined?(:@preloads_path)
22
+
23
+ @preloads_path = prepare_preloads_path
24
+ end
25
+
26
+ private
27
+
28
+ #
29
+ # Patched in:
30
+ # - plugin :batch (extension :preloads - skips auto preloads when batch option provided)
31
+ #
32
+ def prepare_preloads
33
+ opts = init_opts
34
+ preloads_provided = opts.key?(:preload)
35
+ preloads =
36
+ if preloads_provided
37
+ opts[:preload]
38
+ elsif opts.key?(:serializer) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_serializer
39
+ key
40
+ elsif opts.key?(:delegate) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_delegate
41
+ opts[:delegate].fetch(:to)
42
+ end
43
+
44
+ # Nil and empty hash differs as we can preload nested results to
45
+ # empty hash, but we will skip nested preloading if nil or false provided
46
+ return if preloads_provided && !preloads
47
+
48
+ FormatUserPreloads.call(preloads)
49
+ end
50
+
51
+ def prepare_preloads_path
52
+ path = init_opts.fetch(:preload_path) { default_preload_path(preloads) }
53
+
54
+ if path && path[0].is_a?(Array)
55
+ prepare_many_preload_paths(path)
56
+ else
57
+ prepare_one_preload_path(path)
58
+ end
59
+ end
60
+
61
+ def prepare_one_preload_path(path)
62
+ return unless path
63
+
64
+ case path
65
+ when Array
66
+ path.map(&:to_sym).freeze
67
+ else
68
+ [path.to_sym].freeze
69
+ end
70
+ end
71
+
72
+ def prepare_many_preload_paths(paths)
73
+ paths.map { |path| prepare_one_preload_path(path) }.freeze
74
+ end
75
+
76
+ def default_preload_path(preloads)
77
+ return FROZEN_EMPTY_ARRAY if !preloads || preloads.empty?
78
+
79
+ [preloads.keys.first]
80
+ end
81
+
82
+ #
83
+ # Patch for original `prepare_hide` method
84
+ # @see
85
+ #
86
+ # Marks attribute hidden if auto_hide_attribute_with_preloads option was set and attribute has preloads
87
+ #
88
+ def prepare_hide
89
+ res = super
90
+ return res unless res.nil?
91
+
92
+ if preloads && !preloads.empty?
93
+ self.class.serializer_class.config.preloads.auto_hide_attributes_with_preload || nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
8
+ #
9
+ # @see Serega::SeregaValidations::CheckAttributeParams
10
+ #
11
+ module CheckAttributeParamsInstanceMethods
12
+ private
13
+
14
+ def check_opts
15
+ super
16
+ CheckOptPreload.call(opts)
17
+ CheckOptPreloadPath.call(opts)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Config class additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaConfig
10
+ #
11
+ module ConfigInstanceMethods
12
+ # @return [Serega::SeregaPlugins::Preloads::PreloadsConfig] `preloads` plugin config
13
+ def preloads
14
+ @preloads ||= PreloadsConfig.new(opts.fetch(:preloads))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaPlanPoint additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaPlanPoint::InstanceMethods
10
+ #
11
+ module PlanPointInstanceMethods
12
+ #
13
+ # @return [Hash] preloads for nested attributes
14
+ #
15
+ attr_reader :preloads
16
+
17
+ #
18
+ # @return [Array<Symbol>] preloads path for current attribute
19
+ #
20
+ attr_reader :preloads_path
21
+
22
+ private
23
+
24
+ def set_normalized_vars
25
+ super
26
+
27
+ @preloads = prepare_preloads
28
+ @preloads_path = prepare_preloads_path
29
+ end
30
+
31
+ def prepare_preloads
32
+ PreloadsConstructor.call(child_plan)
33
+ end
34
+
35
+ def prepare_preloads_path
36
+ attribute.preloads_path
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Utility that helps to transform preloads to array of paths
8
+ #
9
+ # Example:
10
+ #
11
+ # call({ a: { b: { c: {}, d: {} } }, e: {} })
12
+ #
13
+ # => [
14
+ # [:a],
15
+ # [:a, :b],
16
+ # [:a, :b, :c],
17
+ # [:a, :b, :d],
18
+ # [:e]
19
+ # ]
20
+ class PreloadPaths
21
+ class << self
22
+ #
23
+ # Transforms user provided preloads to array of paths
24
+ #
25
+ # @param value [Array,Hash,String,Symbol,nil,false] preloads
26
+ #
27
+ # @return [Hash] preloads transformed to hash
28
+ #
29
+ def call(preloads, path = [], result = [])
30
+ preloads = FormatUserPreloads.call(preloads)
31
+
32
+ preloads.each do |key, nested_preloads|
33
+ path << key
34
+ result << path.dup
35
+
36
+ call(nested_preloads, path, result)
37
+ path.pop
38
+ end
39
+
40
+ result
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Config for `preloads` plugin
8
+ #
9
+ class PreloadsConfig
10
+ # @return [Hash] preloads plugin options
11
+ attr_reader :opts
12
+
13
+ #
14
+ # Initializes context_metadata config object
15
+ #
16
+ # @param opts [Hash] options
17
+ #
18
+ # @return [Serega::SeregaPlugins::Metadata::MetadataConfig]
19
+ #
20
+ def initialize(opts)
21
+ @opts = opts
22
+ end
23
+
24
+ # @!method auto_preload_attributes_with_delegate
25
+ # @return [Boolean, nil] option value
26
+ #
27
+ # @!method auto_preload_attributes_with_delegate=(value)
28
+ # @param value [Boolean] New option value
29
+ # @return [Boolean] New option value
30
+ #
31
+ # @!method auto_preload_attributes_with_serializer
32
+ # @return [Boolean, nil] option value
33
+ #
34
+ # @!method auto_preload_attributes_with_serializer=(value)
35
+ # @param value [Boolean] New option value
36
+ # @return [Boolean] New option value
37
+ #
38
+ # @!method auto_hide_attributes_with_preload
39
+ # @return [Boolean, nil] option value
40
+ #
41
+ # @!method auto_hide_attributes_with_preload=(value)
42
+ # @param value [Boolean] New option value
43
+ # @return [Boolean] New option value
44
+ #
45
+ %i[
46
+ auto_preload_attributes_with_delegate
47
+ auto_preload_attributes_with_serializer
48
+ auto_hide_attributes_with_preload
49
+ ].each do |method_name|
50
+ define_method(method_name) do
51
+ opts.fetch(method_name)
52
+ end
53
+
54
+ define_method("#{method_name}=") do |value|
55
+ raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
56
+ opts[method_name] = value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -35,8 +35,9 @@ class Serega
35
35
  append_current(preloads, current_preloads)
36
36
  next unless child_plan
37
37
 
38
- child_preloads = dig?(preloads, point.preloads_path)
39
- append_many(child_preloads, child_plan)
38
+ each_child_preloads(preloads, point.preloads_path) do |child_preloads|
39
+ append_many(child_preloads, child_plan)
40
+ end
40
41
  end
41
42
  end
42
43
 
@@ -50,14 +51,26 @@ class Serega
50
51
  end
51
52
  end
52
53
 
53
- def dig?(hash, path)
54
- return hash if !path || path.empty?
54
+ def each_child_preloads(preloads, preloads_path)
55
+ return yield(preloads) if preloads_path.nil?
55
56
 
56
- path.each do |point|
57
- hash = hash[point]
57
+ if preloads_path[0].is_a?(Array)
58
+ preloads_path.each do |path|
59
+ yield dig_fetch(preloads, path)
60
+ end
61
+ else
62
+ yield dig_fetch(preloads, preloads_path)
58
63
  end
64
+ end
65
+
66
+ def dig_fetch(preloads, preloads_path)
67
+ return preloads if !preloads_path || preloads_path.empty?
59
68
 
60
- hash
69
+ preloads_path.each do |path|
70
+ preloads = preloads.fetch(path)
71
+ end
72
+
73
+ preloads
61
74
  end
62
75
  end
63
76
  end
@@ -79,6 +79,18 @@ class Serega
79
79
  # @return [void]
80
80
  #
81
81
  def self.load_plugin(serializer_class, **_opts)
82
+ require_relative "./lib/format_user_preloads"
83
+ require_relative "./lib/modules/attribute"
84
+ require_relative "./lib/modules/attribute_normalizer"
85
+ require_relative "./lib/modules/check_attribute_params"
86
+ require_relative "./lib/modules/config"
87
+ require_relative "./lib/modules/plan_point"
88
+ require_relative "./lib/preload_paths"
89
+ require_relative "./lib/preloads_config"
90
+ require_relative "./lib/preloads_constructor"
91
+ require_relative "./validations/check_opt_preload"
92
+ require_relative "./validations/check_opt_preload_path"
93
+
82
94
  serializer_class.include(InstanceMethods)
83
95
  serializer_class::SeregaAttribute.include(AttributeInstanceMethods)
84
96
  serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
@@ -86,12 +98,6 @@ class Serega
86
98
  serializer_class::SeregaPlanPoint.include(PlanPointInstanceMethods)
87
99
 
88
100
  serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
89
-
90
- require_relative "./lib/format_user_preloads"
91
- require_relative "./lib/main_preload_path"
92
- require_relative "./lib/preloads_constructor"
93
- require_relative "./validations/check_opt_preload"
94
- require_relative "./validations/check_opt_preload_path"
95
101
  end
96
102
 
97
103
  #
@@ -125,210 +131,6 @@ class Serega
125
131
  @preloads ||= PreloadsConstructor.call(plan)
126
132
  end
127
133
  end
128
-
129
- #
130
- # Config for `preloads` plugin
131
- #
132
- class PreloadsConfig
133
- # @return [Hash] preloads plugin options
134
- attr_reader :opts
135
-
136
- #
137
- # Initializes context_metadata config object
138
- #
139
- # @param opts [Hash] options
140
- #
141
- # @return [Serega::SeregaPlugins::Metadata::MetadataConfig]
142
- #
143
- def initialize(opts)
144
- @opts = opts
145
- end
146
-
147
- # @!method auto_preload_attributes_with_delegate
148
- # @return [Boolean, nil] option value
149
- #
150
- # @!method auto_preload_attributes_with_delegate=(value)
151
- # @param value [Boolean] New option value
152
- # @return [Boolean] New option value
153
- #
154
- # @!method auto_preload_attributes_with_serializer
155
- # @return [Boolean, nil] option value
156
- #
157
- # @!method auto_preload_attributes_with_serializer=(value)
158
- # @param value [Boolean] New option value
159
- # @return [Boolean] New option value
160
- #
161
- # @!method auto_hide_attributes_with_preload
162
- # @return [Boolean, nil] option value
163
- #
164
- # @!method auto_hide_attributes_with_preload=(value)
165
- # @param value [Boolean] New option value
166
- # @return [Boolean] New option value
167
- #
168
- %i[
169
- auto_preload_attributes_with_delegate
170
- auto_preload_attributes_with_serializer
171
- auto_hide_attributes_with_preload
172
- ].each do |method_name|
173
- define_method(method_name) do
174
- opts.fetch(method_name)
175
- end
176
-
177
- define_method("#{method_name}=") do |value|
178
- raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
179
- opts[method_name] = value
180
- end
181
- end
182
- end
183
-
184
- #
185
- # Config class additional/patched instance methods
186
- #
187
- # @see Serega::SeregaConfig
188
- #
189
- module ConfigInstanceMethods
190
- # @return [Serega::SeregaPlugins::Preloads::PreloadsConfig] `preloads` plugin config
191
- def preloads
192
- @preloads ||= PreloadsConfig.new(opts.fetch(:preloads))
193
- end
194
- end
195
-
196
- #
197
- # Serega::SeregaAttribute additional/patched instance methods
198
- #
199
- # @see Serega::SeregaAttribute::AttributeInstanceMethods
200
- #
201
- module AttributeInstanceMethods
202
- # @return [Hash, nil] normalized preloads of current attribute
203
- attr_reader :preloads
204
-
205
- # @return [Array] normalized preloads_path of current attribute
206
- attr_reader :preloads_path
207
-
208
- private
209
-
210
- def set_normalized_vars(normalizer)
211
- super
212
- @preloads = normalizer.preloads
213
- @preloads_path = normalizer.preloads_path
214
- end
215
- end
216
-
217
- #
218
- # SeregaAttributeNormalizer additional/patched instance methods
219
- #
220
- # @see SeregaAttributeNormalizer::AttributeInstanceMethods
221
- #
222
- module AttributeNormalizerInstanceMethods
223
- # @return [Hash,nil] normalized attribute preloads
224
- def preloads
225
- return @preloads if instance_variable_defined?(:@preloads)
226
-
227
- @preloads = prepare_preloads
228
- end
229
-
230
- # @return [Array, nil] normalized attribute preloads path
231
- def preloads_path
232
- return @preloads_path if instance_variable_defined?(:@preloads_path)
233
-
234
- @preloads_path = prepare_preloads_path
235
- end
236
-
237
- private
238
-
239
- #
240
- # Patched in:
241
- # - plugin :batch (extension :preloads - skips auto preloads when batch option provided)
242
- #
243
- def prepare_preloads
244
- opts = init_opts
245
- preloads_provided = opts.key?(:preload)
246
- preloads =
247
- if preloads_provided
248
- opts[:preload]
249
- elsif opts.key?(:serializer) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_serializer
250
- key
251
- elsif opts.key?(:delegate) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_delegate
252
- opts[:delegate].fetch(:to)
253
- end
254
-
255
- # Nil and empty hash differs as we can preload nested results to
256
- # empty hash, but we will skip nested preloading if nil or false provided
257
- return if preloads_provided && !preloads
258
-
259
- FormatUserPreloads.call(preloads)
260
- end
261
-
262
- def prepare_preloads_path
263
- opts = init_opts
264
- path = Array(opts[:preload_path]).map!(&:to_sym).freeze
265
- path = MainPreloadPath.call(preloads) if path.empty?
266
- path
267
- end
268
-
269
- #
270
- # Patch for original `prepare_hide` method
271
- #
272
- # Marks attribute hidden if auto_hide_attribute_with_preloads option was set and attribute has preloads
273
- #
274
- def prepare_hide
275
- res = super
276
- return res unless res.nil?
277
-
278
- if preloads && !preloads.empty?
279
- self.class.serializer_class.config.preloads.auto_hide_attributes_with_preload || nil
280
- end
281
- end
282
- end
283
-
284
- #
285
- # Serega::SeregaPlanPoint additional/patched instance methods
286
- #
287
- # @see Serega::SeregaPlanPoint::InstanceMethods
288
- #
289
- module PlanPointInstanceMethods
290
- #
291
- # @return [Hash] preloads for nested attributes
292
- #
293
- attr_reader :preloads
294
-
295
- #
296
- # @return [Array<Symbol>] preloads path for current attribute
297
- #
298
- attr_reader :preloads_path
299
-
300
- private
301
-
302
- def set_normalized_vars
303
- super
304
-
305
- @preloads = prepare_preloads
306
- @preloads_path = prepare_preloads_path
307
- end
308
-
309
- def prepare_preloads
310
- PreloadsConstructor.call(child_plan)
311
- end
312
-
313
- def prepare_preloads_path
314
- attribute.preloads_path
315
- end
316
- end
317
-
318
- #
319
- # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
320
- #
321
- # @see Serega::SeregaValidations::CheckAttributeParams
322
- #
323
- module CheckAttributeParamsInstanceMethods
324
- private
325
-
326
- def check_opts
327
- super
328
- CheckOptPreload.call(opts)
329
- CheckOptPreloadPath.call(opts)
330
- end
331
- end
332
134
  end
333
135
 
334
136
  register_plugin(Preloads.plugin_name, Preloads)
@@ -18,30 +18,69 @@ class Serega
18
18
  # @return [void]
19
19
  #
20
20
  def call(opts)
21
- return unless opts.key?(:preload_path)
21
+ return if exactly_nil?(opts, :preload_path) # allow to provide nil anyway
22
22
 
23
- value = opts[:preload_path]
24
- raise SeregaError, "Invalid option :preload_path => #{value.inspect}. Can be provided only when :preload option provided" unless opts[:preload]
25
- raise SeregaError, "Invalid option :preload_path => #{value.inspect}. Can be provided only when :serializer option provided" unless opts[:serializer]
23
+ path = opts[:preload_path]
24
+ check_usage_with_other_options(path, opts)
25
+ return unless opts[:serializer]
26
26
 
27
- path = Array(value).map!(&:to_sym)
28
- preloads = FormatUserPreloads.call(opts[:preload])
29
- allowed_paths = paths(preloads)
30
- raise SeregaError, "Invalid option :preload_path => #{value.inspect}. Can be one of #{allowed_paths.inspect[1..-2]}" unless allowed_paths.include?(path)
27
+ check_allowed(path, opts)
31
28
  end
32
29
 
33
30
  private
34
31
 
35
- def paths(preloads, path = [], result = [])
36
- preloads.each do |key, nested_preloads|
37
- path << key
38
- result << path.dup
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 = 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
39
47
 
40
- paths(nested_preloads, path, result)
41
- path.pop
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)
42
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
43
80
 
44
- result
81
+ # Check value is Array in Array
82
+ def multiple_preload_paths_provided?(value)
83
+ value.is_a?(Array) && value[0].is_a?(Array)
45
84
  end
46
85
  end
47
86
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Glushkov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-30 00:00:00.000000000 Z
11
+ date: 2023-07-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  JSON Serializer
@@ -73,7 +73,13 @@ files:
73
73
  - lib/serega/plugins/metadata/validations/check_opts.rb
74
74
  - lib/serega/plugins/metadata/validations/check_path.rb
75
75
  - lib/serega/plugins/preloads/lib/format_user_preloads.rb
76
- - lib/serega/plugins/preloads/lib/main_preload_path.rb
76
+ - lib/serega/plugins/preloads/lib/modules/attribute.rb
77
+ - lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb
78
+ - lib/serega/plugins/preloads/lib/modules/check_attribute_params.rb
79
+ - lib/serega/plugins/preloads/lib/modules/config.rb
80
+ - lib/serega/plugins/preloads/lib/modules/plan_point.rb
81
+ - lib/serega/plugins/preloads/lib/preload_paths.rb
82
+ - lib/serega/plugins/preloads/lib/preloads_config.rb
77
83
  - lib/serega/plugins/preloads/lib/preloads_constructor.rb
78
84
  - lib/serega/plugins/preloads/preloads.rb
79
85
  - lib/serega/plugins/preloads/validations/check_opt_preload.rb
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Preloads
6
- #
7
- # Class that constructs main preloads path.
8
- #
9
- # When we have nested preloads we will use this path to dig to `main` element and
10
- # assign nested preloads to it.
11
- #
12
- # By default its a path to latest provided preload
13
- #
14
- # @example
15
- # MainPreloadPath.(a: { b: { c: {} }, d: {} }) # => [:a, :d]
16
- #
17
- class MainPreloadPath
18
- class << self
19
- # Finds default preload path
20
- #
21
- # @param preloads [Hash] Formatted user provided preloads hash
22
- #
23
- # @return [Array<Symbol>] Preloads path to `main` element
24
- def call(preloads)
25
- return FROZEN_EMPTY_ARRAY if !preloads || preloads.empty?
26
-
27
- main_path(preloads).freeze
28
- end
29
-
30
- private
31
-
32
- # Generates path (Array) to the last included resource.
33
- # We need to know this path to include nested associations.
34
- #
35
- # main_path(a: { b: { c: {} }, d: {} }) # => [:a, :d]
36
- #
37
- def main_path(hash, path = [])
38
- current_level = path.size
39
-
40
- hash.each do |key, data|
41
- path.pop(path.size - current_level)
42
- path << key
43
-
44
- main_path(data, path)
45
- end
46
-
47
- path
48
- end
49
- end
50
- end
51
- end
52
- end
53
- end