serega 0.11.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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