serega 0.11.2 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +163 -13
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +9 -4
  5. data/lib/serega/attribute_normalizer.rb +4 -13
  6. data/lib/serega/object_serializer.rb +11 -0
  7. data/lib/serega/plan.rb +20 -25
  8. data/lib/serega/plan_point.rb +13 -16
  9. data/lib/serega/plugins/batch/lib/loader.rb +25 -7
  10. data/lib/serega/plugins/batch/lib/modules/attribute_normalizer.rb +1 -9
  11. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +69 -0
  12. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +35 -0
  13. data/lib/serega/plugins/metadata/metadata.rb +5 -0
  14. data/lib/serega/plugins/openapi/lib/modules/config.rb +23 -0
  15. data/lib/serega/plugins/openapi/lib/openapi_config.rb +101 -0
  16. data/lib/serega/plugins/openapi/openapi.rb +245 -0
  17. data/lib/serega/plugins/preloads/lib/modules/attribute.rb +28 -0
  18. data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +99 -0
  19. data/lib/serega/plugins/preloads/lib/modules/check_attribute_params.rb +22 -0
  20. data/lib/serega/plugins/preloads/lib/modules/config.rb +19 -0
  21. data/lib/serega/plugins/preloads/lib/modules/plan_point.rb +41 -0
  22. data/lib/serega/plugins/preloads/lib/preload_paths.rb +46 -0
  23. data/lib/serega/plugins/preloads/lib/preloads_config.rb +62 -0
  24. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +20 -7
  25. data/lib/serega/plugins/preloads/preloads.rb +12 -210
  26. data/lib/serega/plugins/preloads/validations/check_opt_preload_path.rb +54 -15
  27. metadata +18 -7
  28. data/lib/serega/plugins/preloads/lib/main_preload_path.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b4a68e91572d22cf24e20a79cc788f647eb63e16582f0e33497eec459692de1
4
- data.tar.gz: 8d3a55c8fd783b00c55cb08ed8f62c0f47cb79573ccbdc9f65c6710489d0dd0f
3
+ metadata.gz: f11821b5953500ef9f895ad8c96e97951734615363f3147c623b70519b6199a5
4
+ data.tar.gz: 8b03241a9def2e5a8777a75e61062f8b81544e497a1a1e490e085d52f14a326c
5
5
  SHA512:
6
- metadata.gz: 67cb15aafd0dd90dd301a2ae79643c93204c52c26752649f96e904d2e1fe75d37709c91474c891eb73d1ee913002c214fc2c0288a613f82ad082ee385b149338
7
- data.tar.gz: c421133bba5f4be2227a266bf6eb6fa6013f0320741f4e842575823d3a6d9e93bf37e8029b9fa801b2bcede964679c19fe794a1f7f0dceb10909c98251db588b
6
+ metadata.gz: eb5cb48e74c0e9c8307c6ceb6c47b918eb46ef3c160de90d0eb976239bbc036f1d3fb78219abd5567977f348e3e03d7e492e29140e6d85223fcf14289d794d89
7
+ data.tar.gz: acb7623ea317aab640aae70d0a5dfa0c436b706529741b36b15d938ab6af7523576f72ab4633b591b61dcdc38df768818eab3811005c248c3a00d2872cdccc46
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
440
+ ```
441
+
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=>{}}}
427
474
  ```
428
475
 
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:
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
@@ -839,6 +888,107 @@ Look at [select serialized fields](#selecting-fields) for `:hide` usage examples
839
888
  end
840
889
  ```
841
890
 
891
+ ### Plugin :openapi
892
+
893
+ Helps to build OpenAPI schemas
894
+
895
+ This schemas can be easily used with [rswag](https://github.com/rswag/rswag#referenced-parameters-and-schema-definitions)"
896
+ gem by adding them to "config.swagger_docs"
897
+
898
+ Schemas properties will have no any "type" or other limits specified by default,
899
+ you should provide them as new attribute `:openapi` option.
900
+
901
+ This plugin adds type "object" or "array" only for relationships and marks
902
+ attributes as **required** if they have no `:hide` option set
903
+ (manually or automatically).
904
+
905
+ After enabling this plugin attributes with :serializer option will have
906
+ to have `:many` option set to construct "object" or "array" openapi
907
+ property type.
908
+
909
+ - constructing all serializers schemas:
910
+ `Serega::OpenAPI.schemas`
911
+ - constructing specific serializers schemas:
912
+ `Serega::OpenAPI.schemas(Serega::OpenAPI.serializers - [MyBaseSerializer])`
913
+ - constructing one serializer schema:
914
+ `SomeSerializer.openapi_schema`
915
+
916
+ ```ruby
917
+ class BaseSerializer < Serega
918
+ plugin :openapi
919
+ end
920
+
921
+ class UserSerializer < BaseSerializer
922
+ attribute :name, openapi: { type: "string" }
923
+
924
+ openapi_properties(
925
+ name: { type: :string }
926
+ )
927
+ end
928
+
929
+ class PostSerializer < BaseSerializer
930
+ attribute :text, openapi: { type: "string" }
931
+ attribute :user, serializer: UserSerializer, many: false
932
+ attribute :comments, serializer: PostSerializer, many: true, hide: true
933
+
934
+ openapi_properties(
935
+ text: { type: :string },
936
+ user: { type: 'object' }, # `$ref` added automatically
937
+ comments: { type: 'array' } # `items` option with `$ref` added automatically
938
+ )
939
+ end
940
+
941
+ puts Serega::OpenAPI.schemas
942
+ # =>
943
+ # {
944
+ # "PostSerializer" => {
945
+ # type: "object",
946
+ # properties: {
947
+ # text: {type: "string"},
948
+ # user: {:$ref => "#/components/schemas/UserSerializer"},
949
+ # comments: {type: "array", items: {:$ref => "#/components/schemas/PostSerializer"}}
950
+ # },
951
+ # required: [:text, :comments],
952
+ # additionalProperties: false
953
+ # },
954
+ # "UserSerializer" => {
955
+ # type: "object",
956
+ # properties: {
957
+ # name: {type: "string"}
958
+ # },
959
+ # required: [:name],
960
+ # additionalProperties: false
961
+ # }
962
+ # }
963
+ ```
964
+
965
+ ### Plugin :explicit_many_option
966
+
967
+ Plugin requires to add :many option when adding relationships
968
+ (relationships are attributes with :serializer option specified)
969
+
970
+ Adding this plugin makes clearer to find if relationship returns array or single
971
+ object
972
+
973
+ Also plugin `:openapi` load this plugin automatically as it need to know if
974
+ relationship is array
975
+
976
+ ```ruby
977
+ class BaseSerializer < Serega
978
+ plugin :explicit_many_option
979
+ end
980
+
981
+ class UserSerializer < BaseSerializer
982
+ attribute :name
983
+ end
984
+
985
+ class PostSerializer < BaseSerializer
986
+ attribute :text
987
+ attribute :user, serializer: UserSerializer, many: false
988
+ attribute :comments, serializer: PostSerializer, many: true
989
+ end
990
+ ```
991
+
842
992
  ## Errors
843
993
 
844
994
  - `Serega::SeregaError` is a base error raised by this gem.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.2
1
+ 0.14.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
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ #
6
+ # Plugin :explicit_many_option
7
+ #
8
+ # Plugin requires to add :many option when adding relationships
9
+ # (relationships are attributes with :serializer option specified)
10
+ #
11
+ # Adding this plugin makes clearer to find if relationship returns array or single object
12
+ #
13
+ # Also some plugins like :openapi load this plugin automatically as they need to know if
14
+ # relationship is array
15
+ #
16
+ # @example
17
+ # class BaseSerializer < Serega
18
+ # plugin :explicit_many_option
19
+ # end
20
+ #
21
+ # class UserSerializer < BaseSerializer
22
+ # attribute :name
23
+ # end
24
+ #
25
+ # class PostSerializer < BaseSerializer
26
+ # attribute :text
27
+ # attribute :user, serializer: UserSerializer, many: false
28
+ # attribute :comments, serializer: PostSerializer, many: true
29
+ # end
30
+ #
31
+ module ExplicitManyOption
32
+ # @return [Symbol] Plugin name
33
+ def self.plugin_name
34
+ :explicit_many_option
35
+ end
36
+
37
+ #
38
+ # Applies plugin code to specific serializer
39
+ #
40
+ # @param serializer_class [Class<Serega>] Current serializer class
41
+ # @param _opts [Hash] Loaded plugins options
42
+ #
43
+ # @return [void]
44
+ #
45
+ def self.load_plugin(serializer_class, **_opts)
46
+ require_relative "./validations/check_opt_many"
47
+
48
+ serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
49
+ end
50
+
51
+ #
52
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
53
+ #
54
+ # @see Serega::SeregaValidations::CheckAttributeParams
55
+ #
56
+ module CheckAttributeParamsInstanceMethods
57
+ private
58
+
59
+ def check_opts
60
+ super
61
+
62
+ CheckOptMany.call(opts)
63
+ end
64
+ end
65
+ end
66
+
67
+ register_plugin(ExplicitManyOption.plugin_name, ExplicitManyOption)
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module ExplicitManyOption
6
+ #
7
+ # Validator for attribute :many option
8
+ #
9
+ class CheckOptMany
10
+ class << self
11
+ #
12
+ # Checks attribute :many option must be provided with relations
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ serializer = opts[:serializer]
22
+ return unless serializer
23
+
24
+ many_option_exists = opts.key?(:many)
25
+ return if many_option_exists
26
+
27
+ raise SeregaError,
28
+ "Attribute option :many [Boolean] must be provided" \
29
+ " for attributes with :serializer option"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ 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