serega 0.11.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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