serega 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -12
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +2 -2
  5. data/lib/serega/attribute_normalizer.rb +8 -9
  6. data/lib/serega/config.rb +1 -1
  7. data/lib/serega/object_serializer.rb +1 -1
  8. data/lib/serega/plan.rb +11 -11
  9. data/lib/serega/plan_point.rb +5 -5
  10. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +1 -1
  11. data/lib/serega/plugins/batch/batch.rb +15 -15
  12. data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +2 -2
  13. data/lib/serega/plugins/camel_case/camel_case.rb +195 -0
  14. data/lib/serega/plugins/depth_limit/depth_limit.rb +185 -0
  15. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +1 -1
  16. data/lib/serega/plugins/if/if.rb +4 -4
  17. data/lib/serega/plugins/metadata/metadata.rb +6 -6
  18. data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +1 -1
  19. data/lib/serega/plugins/preloads/lib/preload_paths.rb +12 -5
  20. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +1 -1
  21. data/lib/serega/plugins/preloads/preloads.rb +12 -12
  22. data/lib/serega/plugins/root/root.rb +1 -1
  23. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +1 -1
  24. data/lib/serega/validations/attribute/check_opt_const.rb +1 -1
  25. data/lib/serega/validations/attribute/check_opt_delegate.rb +9 -4
  26. data/lib/serega/validations/attribute/{check_opt_key.rb → check_opt_method.rb} +8 -8
  27. data/lib/serega/validations/attribute/check_opt_value.rb +1 -1
  28. data/lib/serega/validations/check_attribute_params.rb +2 -2
  29. data/lib/serega/validations/check_initiate_params.rb +1 -1
  30. data/lib/serega/validations/check_serialize_params.rb +1 -1
  31. data/lib/serega/validations/utils/check_allowed_keys.rb +4 -2
  32. data/lib/serega.rb +24 -11
  33. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d780c3d344370b36d9e6db64dfdfa08878bfe32885469610ac2314638c878d87
4
- data.tar.gz: d282c6466f7b7205a7b524999b85a3e928d79c00ee11445abd26582e0146842c
3
+ metadata.gz: b291b12115b8ca239642972b103f896f13f02874acdc5663a8f404e02673ca8d
4
+ data.tar.gz: 18bd88ca4bd77ca147161189efe57b2c544da6cc1b460fc7c694855d4b22edc9
5
5
  SHA512:
6
- metadata.gz: e2a161487df001b0074d1864770a7dfecd77ef788e187de6e460cd3d2856162cba8777eb807a65a8d4d49471bb50380fc49ffd17f36c16eb52a2ab8b84ed7991
7
- data.tar.gz: 4a672090d0893ef8d3945b2528fc9861d245833ec4b21074b73481ab7acbf8fa5207c786cad8496bf022070f3341339522bbf4970c7a554cfa543f002f51f504
6
+ metadata.gz: d5c5114ed1c1b8b5a0714a340aca263983f5f8cdb24d15c8138124d64520ebafe03f6c64a7985f131ee399fab287ee578889b1affa1e2aa6f4ddacfbb9c8517e
7
+ data.tar.gz: 4d8191c7081b4650e3328141ec3cc2dc9128a7c4da3a38107e5f2c107d1d70446b67e718a3ffa2ff76e700f80dbdf975da9512d089c86ec552c3dc8a390345d1
data/README.md CHANGED
@@ -17,13 +17,16 @@ objects and to serialize them to Hash or JSON.
17
17
  It has some great features:
18
18
 
19
19
  - Manually [select serialized fields](#selecting-fields)
20
+ - Secure from malicious queries with [depth_limit][depth_limit] plugin
20
21
  - Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or
21
22
  [activerecord_preloads][activerecord_preloads] plugins)
22
23
  - Built-in object presenter ([presenter][presenter] plugin)
23
24
  - Adding custom metadata (via [metadata][metadata] or
24
25
  [context_metadata][context_metadata] plugins)
25
- - Attributes formatters ([formatters][formatters] plugin)
26
- - Conditional attributes ([if][if] plugin)
26
+ - Value formatters ([formatters][formatters] plugin) helps to transform
27
+ time, date, money, percentage and any other values same way keeping code dry
28
+ - Conditional attributes - ([if][if] plugin)
29
+ - Auto camelCase keys - [camel_case][camel_case] plugin
27
30
 
28
31
  ## Installation
29
32
 
@@ -64,8 +67,8 @@ class UserSerializer < Serega
64
67
  # Regular attribute
65
68
  attribute :first_name
66
69
 
67
- # Option :key specifies method in object
68
- attribute :first_name, key: :old_first_name
70
+ # Option :method specifies method that must be called on serialized object
71
+ attribute :first_name, method: :old_first_name
69
72
 
70
73
  # Block is used to define attribute value
71
74
  attribute(:first_name) { |user| user.profile&.first_name }
@@ -77,8 +80,9 @@ class UserSerializer < Serega
77
80
  # Sub-option :allow_nil by default is false
78
81
  attribute :first_name, delegate: { to: :profile, allow_nil: true }
79
82
 
80
- # Option :delegate can be used with :key sub-option
81
- attribute :first_name, delegate: { to: :profile, key: :fname }
83
+ # Option :delegate can be used with :method sub-option, so method chain here
84
+ # is user.profile.fname
85
+ attribute :first_name, delegate: { to: :profile, method: :fname }
82
86
 
83
87
  # Option :const specifies attribute with specific constant value
84
88
  attribute(:type, const: 'user')
@@ -282,7 +286,7 @@ UserSerializer.to_h(bruce, with: fields_as_string)
282
286
 
283
287
  # With not existing attribute
284
288
  fields = %i[first_name enemy]
285
- fields_as_string = 'first_name,enemy')
289
+ fields_as_string = 'first_name,enemy'
286
290
  UserSerializer.new(only: fields).to_h(bruce)
287
291
  UserSerializer.to_h(bruce, only: fields)
288
292
  UserSerializer.to_h(bruce, only: fields_as_string)
@@ -433,7 +437,7 @@ end
433
437
  class UserSerializer < AppSerializer
434
438
  attribute :username
435
439
  attribute :user_stats,
436
- serializer: 'UserStatSerializer'
440
+ serializer: 'UserStatSerializer',
437
441
  value: proc { |user| user },
438
442
  preload: nil
439
443
  end
@@ -545,15 +549,15 @@ It can be used to find value for attributes in optimal way:
545
549
  After including plugin, attributes gain new `:batch` option:
546
550
 
547
551
  ```ruby
548
- attribute :name, batch: { key: :id, loader: :name_loader, default: nil }
552
+ attribute :name, batch: { loader: :name_loader, key: :id, default: nil }
549
553
  ```
550
554
 
551
555
  `:batch` option must be a hash with this keys:
552
556
 
553
- - `key` (required) [Symbol, Proc, callable] - Defines current object identifier.
554
- Later `loader` will accept array of `keys` to detect this keys values.
555
557
  - `loader` (required) [Symbol, Proc, callable] - Defines how to fetch values for
556
558
  batch of keys. Receives 3 parameters: keys, context, plan_point.
559
+ - `key` (required) [Symbol, Proc, callable] - Defines current object identifier.
560
+ Key is optional if plugin was defined with `default_key` option.
557
561
  - `default` (optional) - Default value for attribute.
558
562
  By default it is `nil` or `[]` when attribute has option `many: true`
559
563
  (ex: `attribute :tags, many: true, batch: { ... }`).
@@ -765,7 +769,7 @@ UserSerializer.to_h(nil, meta: { version: '1.0.1' })
765
769
 
766
770
  ### Plugin :formatters
767
771
 
768
- Allows to define `formatters` and apply them on attributes.
772
+ Allows to define `formatters` and apply them on attribute values.
769
773
 
770
774
  Config option `config.formatters.add` can be used to add formatters.
771
775
 
@@ -888,6 +892,88 @@ Look at [select serialized fields](#selecting-fields) for `:hide` usage examples
888
892
  end
889
893
  ```
890
894
 
895
+ ### Plugin :camel_case
896
+
897
+ By default when we add attribute like `attribute :first_name` this means:
898
+
899
+ - adding a `:first_name` key to resulted hash
900
+ - adding a `#first_name` method call result as value
901
+
902
+ But its often desired to response with *camelCased* keys.
903
+ By default this can be achieved by specifying attribute name and method directly
904
+ for each attribute: `attribute :firstName, method: first_name`
905
+
906
+ This plugin transforms all attribute names automatically.
907
+ We use simple regular expression to replace `_x` to `X` for the whole string.
908
+ We make this transformation only once when attribute is defined.
909
+
910
+ You can provide your own callable transformation when defining plugin,
911
+ for example `plugin :camel_case, transform: ->(name) { name.camelize }`
912
+
913
+ For any attribute camelCase-behavior can be skipped when
914
+ `camel_case: false` attribute option provided.
915
+
916
+ This plugin transforms only attribute keys,
917
+ without affecting `root`, `metadata`, `context_metadata` plugins keys.
918
+
919
+ If you wish to [select serialized fields](#selecting-fields), you should
920
+ provide them camelCased.
921
+
922
+ ```ruby
923
+ class AppSerializer < Serega
924
+ plugin :camel_case
925
+ end
926
+
927
+ class UserSerializer < AppSerializer
928
+ attribute :first_name
929
+ attribute :last_name
930
+ attribute :full_name, camel_case: false,
931
+ value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
932
+ end
933
+
934
+ require "ostruct"
935
+ user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
936
+ UserSerializer.to_h(user)
937
+ # => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
938
+
939
+ UserSerializer.new(only: %i[firstName lastName]).to_h(user)
940
+ # => {firstName: "Bruce", lastName: "Wayne"}
941
+ ```
942
+
943
+ ### Plugin :depth_limit
944
+
945
+ Helps to secure from malicious queries that require to serialize too much
946
+ or from accidental serializing of objects with cyclic relations.
947
+
948
+ Depth limit is checked when constructing a serialization plan, that is when
949
+ `#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
950
+ It can be useful to instantiate serializer before any other business logic
951
+ to get possible errors earlier.
952
+
953
+ Any class-level serialization methods also check depth limit as they also
954
+ instantiate serializer.
955
+
956
+ When depth limit is exceeded `Serega::DepthLimitError` is raised.
957
+ Depth limit error details can be found in additional
958
+ `Serega::DepthLimitError#details` method
959
+
960
+ Limit can be checked or changed with next config options:
961
+
962
+ - `config.depth_limit.limit`
963
+ - `config.depth_limit.limit=`
964
+
965
+ There are no default limit, but it should be set when enabling plugin.
966
+
967
+ ```ruby
968
+ class AppSerializer < Serega
969
+ plugin :depth_limit, limit: 10 # set limit for all child classes
970
+ end
971
+
972
+ class UserSerializer < AppSerializer
973
+ config.depth_limit.limit = 5 # overrides limit for UserSerializer
974
+ end
975
+ ```
976
+
891
977
  ### Plugin :explicit_many_option
892
978
 
893
979
  Plugin requires to add :many option when adding relationships
@@ -941,6 +1027,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
941
1027
 
942
1028
  [activerecord_preloads]: #plugin-activerecord_preloads
943
1029
  [batch]: #plugin-batch
1030
+ [camel_case]: #plugin-camel_case
944
1031
  [context_metadata]: #plugin-context_metadata
945
1032
  [formatters]: #plugin-formatters
946
1033
  [metadata]: #plugin-metadata
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.15.0
1
+ 0.16.0
@@ -22,7 +22,7 @@ class Serega
22
22
  attr_reader :many
23
23
 
24
24
  # Attribute :hide option
25
- # @return [Boolean nil] Attribute :hide option
25
+ # @return [Boolean, nil] Attribute :hide option
26
26
  attr_reader :hide
27
27
 
28
28
  #
@@ -30,7 +30,7 @@ class Serega
30
30
  #
31
31
  # @param name [Symbol, String] Name of attribute
32
32
  # @param opts [Hash] Attribute options
33
- # @option opts [Symbol] :key Object method name to fetch attribute value
33
+ # @option opts [Symbol] :method Object method name to fetch attribute value
34
34
  # @option opts [Hash] :delegate Allows to fetch value from nested object
35
35
  # @option opts [Boolean] :hide Specify `true` to not serialize this attribute by default
36
36
  # @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable)
@@ -36,12 +36,12 @@ class Serega
36
36
  end
37
37
 
38
38
  #
39
- # Symbolized initial attribute key or attribute name if key is empty
39
+ # Symbolized initial attribute method name
40
40
  #
41
- # @return [Symbol] Attribute normalized name
41
+ # @return [Symbol] Attribute normalized method name
42
42
  #
43
- def key
44
- @key ||= prepare_key
43
+ def method_name
44
+ @method_name ||= prepare_method_name
45
45
  end
46
46
 
47
47
  #
@@ -121,9 +121,8 @@ class Serega
121
121
  init_opts[:serializer]
122
122
  end
123
123
 
124
- def prepare_key
125
- key = init_opts[:key]
126
- key ? key.to_sym : name
124
+ def prepare_method_name
125
+ (init_opts[:method] || init_name).to_sym
127
126
  end
128
127
 
129
128
  def prepare_const_block
@@ -134,7 +133,7 @@ class Serega
134
133
  end
135
134
 
136
135
  def prepare_keyword_block
137
- key_method_name = key
136
+ key_method_name = method_name
138
137
  proc do |object|
139
138
  object.public_send(key_method_name)
140
139
  end
@@ -144,7 +143,7 @@ class Serega
144
143
  delegate = init_opts[:delegate]
145
144
  return unless delegate
146
145
 
147
- key_method_name = delegate[:key] || key
146
+ key_method_name = delegate[:method] || method_name
148
147
  delegate_to = delegate[:to]
149
148
 
150
149
  allow_nil = delegate.fetch(:allow_nil) { self.class.serializer_class.config.delegate_default_allow_nil }
data/lib/serega/config.rb CHANGED
@@ -15,7 +15,7 @@ class Serega
15
15
  DEFAULTS = {
16
16
  plugins: [],
17
17
  initiate_keys: %i[only with except check_initiate_params].freeze,
18
- attribute_keys: %i[key value serializer many hide const delegate].freeze,
18
+ attribute_keys: %i[method value serializer many hide const delegate].freeze,
19
19
  serialize_keys: %i[context many].freeze,
20
20
  check_attribute_name: true,
21
21
  check_initiate_params: true,
@@ -14,7 +14,7 @@ class Serega
14
14
 
15
15
  # @param plan [SeregaPlan] Serialization plan
16
16
  # @param context [Hash] Serialization context
17
- # @param many [TrueClass|FalseClass] is object is enumerable
17
+ # @param many [Boolean] is object is enumerable
18
18
  # @param opts [Hash] Any custom options
19
19
  #
20
20
  # @return [SeregaObjectSerializer] New SeregaObjectSerializer
data/lib/serega/plan.rb CHANGED
@@ -22,7 +22,7 @@ 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 new(opts) if max_cache_size.zero?
25
+ return new(nil, opts) if max_cache_size.zero?
26
26
 
27
27
  cached_plan_for(opts, max_cache_size)
28
28
  end
@@ -33,7 +33,7 @@ class Serega
33
33
  @cache ||= {}
34
34
  cache_key = construct_cache_key(opts)
35
35
 
36
- plan = @cache[cache_key] ||= new(opts)
36
+ plan = @cache[cache_key] ||= new(nil, opts)
37
37
  @cache.shift if @cache.length > max_cache_size
38
38
  plan
39
39
  end
@@ -61,25 +61,26 @@ class Serega
61
61
  # @return [SeregaPlanPoint, nil]
62
62
  attr_reader :parent_plan_point
63
63
 
64
- # Sets new parent plan point
65
- # @return [SeregaPlanPoint] new parent plan point
66
- attr_writer :parent_plan_point
67
-
68
64
  # Serialization points
69
65
  # @return [Array<SeregaPlanPoint>] points to serialize
70
66
  attr_reader :points
71
67
 
72
68
  #
73
- # Instantiate new serialization plan.
69
+ # Instantiate new serialization plan
70
+ #
71
+ # Patched by
72
+ # - depth_limit plugin, which checks depth limit is not exceeded when adding new plan
74
73
  #
75
- # @param modifiers Serialization parameters
74
+ # @param parent_plan_point [SeregaPlanPoint, nil] Parent plan_point
75
+ # @param modifiers [Hash] Serialization parameters
76
76
  # @option modifiers [Hash] :only The only attributes to serialize
77
77
  # @option modifiers [Hash] :except Attributes to hide
78
78
  # @option modifiers [Hash] :with Hidden attributes to serialize additionally
79
79
  #
80
80
  # @return [SeregaPlan] Serialization plan
81
81
  #
82
- def initialize(modifiers)
82
+ def initialize(parent_plan_point, modifiers)
83
+ @parent_plan_point = parent_plan_point
83
84
  @points = attributes_points(modifiers)
84
85
  end
85
86
 
@@ -107,8 +108,7 @@ class Serega
107
108
  {only: only[name], with: with[name], except: except[name]}
108
109
  end
109
110
 
110
- point = serializer_class::SeregaPlanPoint.new(attribute, child_fields)
111
- point.plan = self
111
+ point = serializer_class::SeregaPlanPoint.new(self, attribute, child_fields)
112
112
  points << point.freeze
113
113
  end
114
114
 
@@ -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_accessor :plan
16
+ attr_reader :plan
17
17
 
18
18
  # Shows current attribute
19
19
  # @return [SeregaAttribute] Current attribute
@@ -44,6 +44,7 @@ class Serega
44
44
  #
45
45
  # Initializes plan point
46
46
  #
47
+ # @param plan [SeregaPlan] Current plan this point belongs to
47
48
  # @param attribute [SeregaAttribute] Attribute to construct plan point
48
49
  # @param modifiers Serialization parameters
49
50
  # @option modifiers [Hash] :only The only attributes to serialize
@@ -52,7 +53,8 @@ class Serega
52
53
  #
53
54
  # @return [SeregaPlanPoint] New plan point
54
55
  #
55
- def initialize(attribute, modifiers = nil)
56
+ def initialize(plan, attribute, modifiers = nil)
57
+ @plan = plan
56
58
  @attribute = attribute
57
59
  @modifiers = modifiers
58
60
  set_normalized_vars
@@ -79,9 +81,7 @@ class Serega
79
81
 
80
82
  fields = modifiers || FROZEN_EMPTY_HASH
81
83
 
82
- plan = serializer::SeregaPlan.new(fields)
83
- plan.parent_plan_point = self
84
- plan
84
+ serializer::SeregaPlan.new(self, fields)
85
85
  end
86
86
  end
87
87
 
@@ -77,7 +77,7 @@ class Serega
77
77
  # @return [void]
78
78
  #
79
79
  def self.load_plugin(serializer_class, **_opts)
80
- require_relative "./lib/preloader"
80
+ require_relative "lib/preloader"
81
81
 
82
82
  serializer_class.include(InstanceMethods)
83
83
  end
@@ -79,18 +79,18 @@ class Serega
79
79
  # @return [void]
80
80
  #
81
81
  def self.load_plugin(serializer_class, **_opts)
82
- require_relative "./lib/batch_config"
83
- require_relative "./lib/loader"
84
- require_relative "./lib/loaders"
85
- require_relative "./lib/modules/attribute"
86
- require_relative "./lib/modules/attribute_normalizer"
87
- require_relative "./lib/modules/check_attribute_params"
88
- require_relative "./lib/modules/config"
89
- require_relative "./lib/modules/object_serializer"
90
- require_relative "./lib/modules/plan_point"
91
- require_relative "./lib/validations/check_batch_opt_key"
92
- require_relative "./lib/validations/check_batch_opt_loader"
93
- require_relative "./lib/validations/check_opt_batch"
82
+ require_relative "lib/batch_config"
83
+ require_relative "lib/loader"
84
+ require_relative "lib/loaders"
85
+ require_relative "lib/modules/attribute"
86
+ require_relative "lib/modules/attribute_normalizer"
87
+ require_relative "lib/modules/check_attribute_params"
88
+ require_relative "lib/modules/config"
89
+ require_relative "lib/modules/object_serializer"
90
+ require_relative "lib/modules/plan_point"
91
+ require_relative "lib/validations/check_batch_opt_key"
92
+ require_relative "lib/validations/check_batch_opt_loader"
93
+ require_relative "lib/validations/check_opt_batch"
94
94
 
95
95
  serializer_class.extend(ClassMethods)
96
96
  serializer_class.include(InstanceMethods)
@@ -121,18 +121,18 @@ class Serega
121
121
  serializer_class.const_set(:SeregaBatchLoader, batch_loader_class)
122
122
 
123
123
  if serializer_class.plugin_used?(:activerecord_preloads)
124
- require_relative "./lib/plugins_extensions/activerecord_preloads"
124
+ require_relative "lib/plugins_extensions/activerecord_preloads"
125
125
  serializer_class::SeregaBatchLoader.include(PluginsExtensions::ActiveRecordPreloads::BatchLoaderInstanceMethods)
126
126
  end
127
127
 
128
128
  if serializer_class.plugin_used?(:formatters)
129
- require_relative "./lib/plugins_extensions/formatters"
129
+ require_relative "lib/plugins_extensions/formatters"
130
130
  serializer_class::SeregaBatchLoader.include(PluginsExtensions::Formatters::BatchLoaderInstanceMethods)
131
131
  serializer_class::SeregaAttribute.include(PluginsExtensions::Formatters::SeregaAttributeInstanceMethods)
132
132
  end
133
133
 
134
134
  if serializer_class.plugin_used?(:preloads)
135
- require_relative "./lib/plugins_extensions/preloads"
135
+ require_relative "lib/plugins_extensions/preloads"
136
136
  serializer_class::SeregaAttributeNormalizer.include(PluginsExtensions::Preloads::AttributeNormalizerInstanceMethods)
137
137
  end
138
138
 
@@ -23,7 +23,7 @@ class Serega
23
23
  SeregaValidations::Utils::CheckOptIsHash.call(opts, :batch)
24
24
 
25
25
  batch = opts[:batch]
26
- SeregaValidations::Utils::CheckAllowedKeys.call(batch, %i[key loader default])
26
+ SeregaValidations::Utils::CheckAllowedKeys.call(batch, %i[key loader default], :batch)
27
27
 
28
28
  check_batch_opt_key(batch, serializer_class)
29
29
  check_batch_opt_loader(batch)
@@ -50,7 +50,7 @@ class Serega
50
50
  end
51
51
 
52
52
  def check_usage_with_other_params(opts, block)
53
- raise SeregaError, "Option :batch can not be used together with option :key" if opts.key?(:key)
53
+ raise SeregaError, "Option :batch can not be used together with option :method" if opts.key?(:method)
54
54
  raise SeregaError, "Option :batch can not be used together with option :value" if opts.key?(:value)
55
55
  raise SeregaError, "Option :batch can not be used together with option :const" if opts.key?(:const)
56
56
  raise SeregaError, "Option :batch can not be used together with option :delegate" if opts.key?(:delegate)
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ #
6
+ # Plugin :camel_case
7
+ #
8
+ # By default when we add attribute like `attribute :first_name` this means:
9
+ # - adding a `:first_name` key to resulted hash
10
+ # - adding a `#first_name` method call result as value
11
+ #
12
+ # But its often desired to response with *camelCased* keys.
13
+ # Earlier this can be achieved by specifying attribute name and method directly
14
+ # for each attribute: `attribute :firstName, method: first_name`
15
+ #
16
+ # Now this plugin transforms all attribute names automatically.
17
+ # We use simple regular expression to replace `_x` to `X` for the whole string.
18
+ # You can provide your own callable transformation when defining plugin,
19
+ # for example `plugin :camel_case, transform: ->(name) { name.camelize }`
20
+ #
21
+ # For any attribute camelCase-behavior can be skipped when
22
+ # `camel_case: false` attribute option provided.
23
+ #
24
+ # @example Define plugin
25
+ # class AppSerializer < Serega
26
+ # plugin :camel_case
27
+ # end
28
+ #
29
+ # class UserSerializer < AppSerializer
30
+ # attribute :first_name
31
+ # attribute :last_name
32
+ # attribute :full_name, camel_case: false, value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
33
+ # end
34
+ #
35
+ # require "ostruct"
36
+ # user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
37
+ # UserSerializer.to_h(user) # {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
38
+ #
39
+ module CamelCase
40
+ # Default camel-case transformation
41
+ TRANSFORM_DEFAULT = proc { |attribute_name|
42
+ attribute_name.gsub!(/_[a-z]/) { |m| m[-1].upcase! }
43
+ attribute_name
44
+ }
45
+
46
+ # @return [Symbol] Plugin name
47
+ def self.plugin_name
48
+ :camel_case
49
+ end
50
+
51
+ #
52
+ # Applies plugin code to specific serializer
53
+ #
54
+ # @param serializer_class [Class<Serega>] Current serializer class
55
+ # @param _opts [Hash] Loaded plugins options
56
+ #
57
+ # @return [void]
58
+ #
59
+ def self.load_plugin(serializer_class, **_opts)
60
+ serializer_class::SeregaConfig.include(ConfigInstanceMethods)
61
+ serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
62
+ serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
63
+ end
64
+
65
+ #
66
+ # Adds config options and runs other callbacks after plugin was loaded
67
+ #
68
+ # @param serializer_class [Class<Serega>] Current serializer class
69
+ # @param opts [Hash] loaded plugins opts
70
+ #
71
+ # @return [void]
72
+ #
73
+ def self.after_load_plugin(serializer_class, **opts)
74
+ config = serializer_class.config
75
+ config.opts[:camel_case] = {}
76
+ config.camel_case.transform = opts[:transform] || TRANSFORM_DEFAULT
77
+
78
+ config.attribute_keys << :camel_case
79
+ end
80
+
81
+ #
82
+ # Config class additional/patched instance methods
83
+ #
84
+ # @see Serega::SeregaConfig
85
+ #
86
+ module ConfigInstanceMethods
87
+ # @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] `camel_case` plugin config
88
+ def camel_case
89
+ @camel_case ||= CamelCaseConfig.new(opts.fetch(:camel_case))
90
+ end
91
+ end
92
+
93
+ #
94
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
95
+ #
96
+ # @see Serega::SeregaValidations::CheckAttributeParams
97
+ #
98
+ module CheckAttributeParamsInstanceMethods
99
+ private
100
+
101
+ def check_opts
102
+ super
103
+ CheckOptCamelCase.call(opts)
104
+ end
105
+ end
106
+
107
+ #
108
+ # Validator for attribute :camel_case option
109
+ #
110
+ class CheckOptCamelCase
111
+ class << self
112
+ #
113
+ # Checks attribute :camel_case option must be boolean
114
+ #
115
+ # @param opts [Hash] Attribute options
116
+ #
117
+ # @raise [SeregaError] Attribute validation error
118
+ #
119
+ # @return [void]
120
+ #
121
+ def call(opts)
122
+ camel_case_option_exists = opts.key?(:camel_case)
123
+ return unless camel_case_option_exists
124
+
125
+ value = opts[:camel_case]
126
+ return if value.equal?(true) || value.equal?(false)
127
+
128
+ raise SeregaError, "Attribute option :camel_case must have a boolean value, but #{value.class} was provided"
129
+ end
130
+ end
131
+ end
132
+
133
+ # CamelCase config object
134
+ class CamelCaseConfig
135
+ attr_reader :opts
136
+
137
+ #
138
+ # Initializes CamelCaseConfig object
139
+ #
140
+ # @param opts [Hash] camel_case plugin options
141
+ # @option opts [#call] :transform Callable object that transforms original attribute name
142
+ #
143
+ # @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] CamelCaseConfig object
144
+ #
145
+ def initialize(opts)
146
+ @opts = opts
147
+ end
148
+
149
+ # @return [#call] defined object that transforms name
150
+ def transform
151
+ opts.fetch(:transform)
152
+ end
153
+
154
+ # Sets transformation callable object
155
+ #
156
+ # @param value [#call] transformation
157
+ #
158
+ # @return [#call] camel_case plugin transformation callable object
159
+ def transform=(value)
160
+ raise SeregaError, "Transform value must respond to #call" unless value.respond_to?(:call)
161
+
162
+ params = value.is_a?(Proc) ? value.parameters : value.method(:call).parameters
163
+ if params.count != 1 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) }
164
+ raise SeregaError, "Transform value must respond to #call and accept 1 regular parameter"
165
+ end
166
+
167
+ opts[:transform] = value
168
+ end
169
+ end
170
+
171
+ #
172
+ # SeregaAttributeNormalizer additional/patched instance methods
173
+ #
174
+ # @see SeregaAttributeNormalizer::AttributeInstanceMethods
175
+ #
176
+ module AttributeNormalizerInstanceMethods
177
+ private
178
+
179
+ #
180
+ # Patch for original `prepare_name` method
181
+ #
182
+ # Makes camelCased name
183
+ #
184
+ def prepare_name
185
+ res = super
186
+ return res if init_opts[:camel_case] == false
187
+
188
+ self.class.serializer_class.config.camel_case.transform.call(res.to_s).to_sym
189
+ end
190
+ end
191
+ end
192
+
193
+ register_plugin(CamelCase.plugin_name, CamelCase)
194
+ end
195
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ #
6
+ # Plugin :depth_limit
7
+ #
8
+ # Helps to secure from malicious queries that require to serialize too much
9
+ # or from accidental serializing of objects with cyclic relations.
10
+ #
11
+ # Depth limit is checked when constructing a serialization plan, that is when
12
+ # `#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
13
+ # It can be useful to instantiate serializer before any other business logic
14
+ # to get possible errors earlier.
15
+ #
16
+ # Any class-level serialization methods also check depth limit as they also instantiate serializer.
17
+ #
18
+ # When depth limit is exceeded `Serega::DepthLimitError` is raised.
19
+ # Depth limit error details can be found in additional `Serega::DepthLimitError#details` method
20
+ #
21
+ # Limit can be checked or changed with next config options:
22
+ #
23
+ # - config.depth_limit.limit
24
+ # - config.depth_limit.limit=
25
+ #
26
+ # There are no default limit, but it should be set when enabling plugin.
27
+ #
28
+ # @example
29
+ #
30
+ # class AppSerializer < Serega
31
+ # plugin :depth_limit, limit: 10 # set limit for all child classes
32
+ # end
33
+ #
34
+ # class UserSerializer < AppSerializer
35
+ # config.depth_limit.limit = 5 # overrides limit for UserSerializer
36
+ # end
37
+ #
38
+ module DepthLimit
39
+ # @return [Symbol] Plugin name
40
+ def self.plugin_name
41
+ :depth_limit
42
+ end
43
+
44
+ #
45
+ # Applies plugin code to specific serializer
46
+ #
47
+ # @param serializer_class [Class<Serega>] Current serializer class
48
+ # @param _opts [Hash] Loaded plugins options
49
+ #
50
+ # @return [void]
51
+ #
52
+ def self.load_plugin(serializer_class, **_opts)
53
+ serializer_class::SeregaPlan.include(PlanInstanceMethods)
54
+ serializer_class::SeregaConfig.include(ConfigInstanceMethods)
55
+ end
56
+
57
+ #
58
+ # Adds config options and runs other callbacks after plugin was loaded
59
+ #
60
+ # @param serializer_class [Class<Serega>] Current serializer class
61
+ # @param opts [Hash] loaded plugins opts
62
+ #
63
+ # @return [void]
64
+ #
65
+ def self.after_load_plugin(serializer_class, **opts)
66
+ config = serializer_class.config
67
+ limit = opts.fetch(:limit) { raise SeregaError, "Please provide :limit option. Example: `plugin :depth_limit, limit: 10`" }
68
+ config.opts[:depth_limit] = {}
69
+ config.depth_limit.limit = limit
70
+ end
71
+
72
+ # DepthLimit config object
73
+ class DepthLimitConfig
74
+ attr_reader :opts
75
+
76
+ #
77
+ # Initializes DepthLimitConfig object
78
+ #
79
+ # @param opts [Hash] depth_limit plugin options
80
+ #
81
+ # @return [SeregaPlugins::DepthLimit::DepthLimitConfig] DepthLimitConfig object
82
+ #
83
+ def initialize(opts)
84
+ @opts = opts
85
+ end
86
+
87
+ # @return [Integer] defined depth limit
88
+ def limit
89
+ opts.fetch(:limit)
90
+ end
91
+
92
+ #
93
+ # Set depth limit
94
+ #
95
+ # @param value [Integer] depth limit
96
+ #
97
+ # @return [Integer] depth limit
98
+ def limit=(value)
99
+ raise SeregaError, "Depth limit must be an Integer" unless value.is_a?(Integer)
100
+
101
+ opts[:limit] = value
102
+ end
103
+ end
104
+
105
+ #
106
+ # Serega::SeregaConfig additional/patched class methods
107
+ #
108
+ # @see Serega::SeregaConfig
109
+ #
110
+ module ConfigInstanceMethods
111
+ # @return [Serega::SeregaPlugins::DepthLimit::DepthLimitConfig] current depth_limit config
112
+ def depth_limit
113
+ @depth_limit ||= DepthLimitConfig.new(opts.fetch(:depth_limit))
114
+ end
115
+ end
116
+
117
+ #
118
+ # SeregaPlan additional/patched instance methods
119
+ #
120
+ # @see SeregaPlan
121
+ #
122
+ module PlanInstanceMethods
123
+ #
124
+ # Initializes serialization plan
125
+ # Overrides original method (adds depth_limit validation)
126
+ #
127
+ def initialize(parent_plan_point, *)
128
+ check_depth_limit_exceeded(parent_plan_point)
129
+ super
130
+ end
131
+
132
+ private
133
+
134
+ def check_depth_limit_exceeded(current_point)
135
+ plan = self
136
+ depth_level = 1
137
+ point = current_point
138
+
139
+ while point
140
+ depth_level += 1
141
+ plan = point.plan
142
+ point = plan.parent_plan_point
143
+ end
144
+
145
+ root_serializer = plan.class.serializer_class
146
+ root_depth_limit = root_serializer.config.depth_limit.limit
147
+
148
+ if depth_level > root_depth_limit
149
+ fields_chain = [current_point.name]
150
+ fields_chain << current_point.name while (current_point = current_point.plan.parent_plan_point)
151
+ details = "#{root_serializer} (depth limit: #{root_depth_limit}) -> #{fields_chain.reverse!.join(" -> ")}"
152
+ raise DepthLimitError.new("Depth limit was exceeded", details)
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ register_plugin(DepthLimit.plugin_name, DepthLimit)
159
+ end
160
+
161
+ #
162
+ # Special error for depth_limit plugin
163
+ #
164
+ class DepthLimitError < SeregaError
165
+ #
166
+ # Details of why depth limit error happens.
167
+ #
168
+ # @return [String] error details
169
+ #
170
+ attr_reader :details
171
+
172
+ #
173
+ # Initializes new error
174
+ #
175
+ # @param message [String] Error message
176
+ # @param details [String] Error additional details
177
+ #
178
+ # @return [DepthLimitError] error instance
179
+ #
180
+ def initialize(message, details = nil)
181
+ super(message)
182
+ @details = details
183
+ end
184
+ end
185
+ end
@@ -40,7 +40,7 @@ class Serega
40
40
  # @return [void]
41
41
  #
42
42
  def self.load_plugin(serializer_class, **_opts)
43
- require_relative "./validations/check_opt_many"
43
+ require_relative "validations/check_opt_many"
44
44
 
45
45
  serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
46
46
  end
@@ -56,10 +56,10 @@ class Serega
56
56
  # @return [void]
57
57
  #
58
58
  def self.load_plugin(serializer_class, **_opts)
59
- require_relative "./validations/check_opt_if"
60
- require_relative "./validations/check_opt_if_value"
61
- require_relative "./validations/check_opt_unless"
62
- require_relative "./validations/check_opt_unless_value"
59
+ require_relative "validations/check_opt_if"
60
+ require_relative "validations/check_opt_if_value"
61
+ require_relative "validations/check_opt_unless"
62
+ require_relative "validations/check_opt_unless_value"
63
63
 
64
64
  serializer_class::SeregaAttribute.include(SeregaAttributeInstanceMethods)
65
65
  serializer_class::SeregaPlanPoint.include(PlanPointInstanceMethods)
@@ -62,12 +62,12 @@ class Serega
62
62
  serializer_class.include(InstanceMethods)
63
63
  serializer_class::SeregaConfig.include(ConfigInstanceMethods)
64
64
 
65
- require_relative "./meta_attribute"
66
- require_relative "./validations/check_block"
67
- require_relative "./validations/check_opt_hide_nil"
68
- require_relative "./validations/check_opt_hide_empty"
69
- require_relative "./validations/check_opts"
70
- require_relative "./validations/check_path"
65
+ require_relative "meta_attribute"
66
+ require_relative "validations/check_block"
67
+ require_relative "validations/check_opt_hide_nil"
68
+ require_relative "validations/check_opt_hide_empty"
69
+ require_relative "validations/check_opts"
70
+ require_relative "validations/check_path"
71
71
 
72
72
  meta_attribute_class = Class.new(MetaAttribute)
73
73
  meta_attribute_class.serializer_class = serializer_class
@@ -36,7 +36,7 @@ class Serega
36
36
  if preloads_provided
37
37
  opts[:preload]
38
38
  elsif opts.key?(:serializer) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_serializer
39
- key
39
+ method_name
40
40
  elsif opts.key?(:delegate) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_delegate
41
41
  opts[:delegate].fetch(:to)
42
42
  end
@@ -22,18 +22,25 @@ class Serega
22
22
  #
23
23
  # Transforms user provided preloads to array of paths
24
24
  #
25
- # @param value [Array,Hash,String,Symbol,nil,false] preloads
25
+ # @param preloads [Array,Hash,String,Symbol,nil,false] association(s) to preload
26
26
  #
27
27
  # @return [Hash] preloads transformed to hash
28
28
  #
29
- def call(preloads, path = [], result = [])
30
- preloads = FormatUserPreloads.call(preloads)
29
+ def call(preloads)
30
+ formatted_preloads = FormatUserPreloads.call(preloads)
31
+ return FROZEN_EMPTY_ARRAY if formatted_preloads.empty?
31
32
 
32
- preloads.each do |key, nested_preloads|
33
+ paths(formatted_preloads, [], [])
34
+ end
35
+
36
+ private
37
+
38
+ def paths(formatted_preloads, path, result)
39
+ formatted_preloads.each do |key, nested_preloads|
33
40
  path << key
34
41
  result << path.dup
35
42
 
36
- call(nested_preloads, path, result)
43
+ paths(nested_preloads, path, result)
37
44
  path.pop
38
45
  end
39
46
 
@@ -11,7 +11,7 @@ class Serega
11
11
  #
12
12
  # Constructs preloads hash for given attributes plan
13
13
  #
14
- # @param plan [Array<Serega::PlanPoint>] Serialization plan
14
+ # @param plan [Array<SeregaPlanPoint>] Serialization plan
15
15
  #
16
16
  # @return [Hash]
17
17
  #
@@ -15,7 +15,7 @@ class Serega
15
15
  #
16
16
  # This options are very handy if you want to forget about finding preloads manually.
17
17
  #
18
- # Preloads can be disabled with `preload: false` attribute option option.
18
+ # Preloads can be disabled with `preload: false` attribute option.
19
19
  # Also automatically added preloads can be overwritten with manually specified `preload: :another_value`.
20
20
  #
21
21
  # Some examples, **please read comments in the code below**
@@ -79,17 +79,17 @@ 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"
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
93
 
94
94
  serializer_class.include(InstanceMethods)
95
95
  serializer_class::SeregaAttribute.include(AttributeInstanceMethods)
@@ -127,7 +127,7 @@ class Serega
127
127
  # @option opts [Symbol, String, nil] :one root for single-object serialization
128
128
  # @option opts [Symbol, String, nil] :many root for many-objects serialization
129
129
  #
130
- # @return [Serega::SeregaPlugins::Root::RootConfig] RootConfig object
130
+ # @return [SeregaPlugins::Root::RootConfig] RootConfig object
131
131
  #
132
132
  def initialize(opts)
133
133
  @opts = opts
@@ -18,7 +18,7 @@ class Serega
18
18
  #
19
19
  def self.load_plugin(serializer_class, **_opts)
20
20
  serializer_class.include(InstanceMethods)
21
- require_relative "./parse_string_modifiers"
21
+ require_relative "parse_string_modifiers"
22
22
  end
23
23
 
24
24
  #
@@ -26,7 +26,7 @@ class Serega
26
26
  private
27
27
 
28
28
  def check_usage_with_other_params(opts, block)
29
- raise SeregaError, "Option :const can not be used together with option :key" if opts.key?(:key)
29
+ raise SeregaError, "Option :const can not be used together with option :method" if opts.key?(:method)
30
30
  raise SeregaError, "Option :const can not be used together with option :value" if opts.key?(:value)
31
31
  raise SeregaError, "Option :const can not be used together with block" if block
32
32
  end
@@ -32,8 +32,9 @@ class Serega
32
32
 
33
33
  delegate_opts = opts[:delegate]
34
34
  check_opt_delegate_to(delegate_opts)
35
- check_opt_delegate_key(delegate_opts)
35
+ check_opt_delegate_method(delegate_opts)
36
36
  check_opt_delegate_allow_nil(delegate_opts)
37
+ check_opt_delegate_extra_opts(delegate_opts)
37
38
  end
38
39
 
39
40
  def check_opt_delegate_to(delegate_opts)
@@ -43,16 +44,20 @@ class Serega
43
44
  Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :to)
44
45
  end
45
46
 
46
- def check_opt_delegate_key(delegate_opts)
47
- Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :key)
47
+ def check_opt_delegate_method(delegate_opts)
48
+ Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :method)
48
49
  end
49
50
 
50
51
  def check_opt_delegate_allow_nil(delegate_opts)
51
52
  Utils::CheckOptIsBool.call(delegate_opts, :allow_nil)
52
53
  end
53
54
 
55
+ def check_opt_delegate_extra_opts(delegate_opts)
56
+ Utils::CheckAllowedKeys.call(delegate_opts, %i[to method allow_nil], :delegate)
57
+ end
58
+
54
59
  def check_usage_with_other_params(opts, block)
55
- raise SeregaError, "Option :delegate can not be used together with option :key" if opts.key?(:key)
60
+ raise SeregaError, "Option :delegate can not be used together with option :method" if opts.key?(:method)
56
61
  raise SeregaError, "Option :delegate can not be used together with option :const" if opts.key?(:const)
57
62
  raise SeregaError, "Option :delegate can not be used together with option :value" if opts.key?(:value)
58
63
  raise SeregaError, "Option :delegate can not be used together with block" if block
@@ -4,12 +4,12 @@ class Serega
4
4
  module SeregaValidations
5
5
  module Attribute
6
6
  #
7
- # Attribute `:key` option validator
7
+ # Attribute `:method` option validator
8
8
  #
9
- class CheckOptKey
9
+ class CheckOptMethod
10
10
  class << self
11
11
  #
12
- # Checks attribute :key option
12
+ # Checks attribute :method option
13
13
  #
14
14
  # @param opts [Hash] Attribute options
15
15
  #
@@ -18,18 +18,18 @@ class Serega
18
18
  # @return [void]
19
19
  #
20
20
  def call(opts, block = nil)
21
- return unless opts.key?(:key)
21
+ return unless opts.key?(:method)
22
22
 
23
23
  check_usage_with_other_params(opts, block)
24
- Utils::CheckOptIsStringOrSymbol.call(opts, :key)
24
+ Utils::CheckOptIsStringOrSymbol.call(opts, :method)
25
25
  end
26
26
 
27
27
  private
28
28
 
29
29
  def check_usage_with_other_params(opts, block)
30
- raise SeregaError, "Option :key can not be used together with option :const" if opts.key?(:const)
31
- raise SeregaError, "Option :key can not be used together with option :value" if opts.key?(:value)
32
- raise SeregaError, "Option :key can not be used together with block" if block
30
+ raise SeregaError, "Option :method can not be used together with option :const" if opts.key?(:const)
31
+ raise SeregaError, "Option :method can not be used together with option :value" if opts.key?(:value)
32
+ raise SeregaError, "Option :method can not be used together with block" if block
33
33
  end
34
34
  end
35
35
  end
@@ -27,7 +27,7 @@ class Serega
27
27
  private
28
28
 
29
29
  def check_usage_with_other_params(opts, block)
30
- raise SeregaError, "Option :value can not be used together with option :key" if opts.key?(:key)
30
+ raise SeregaError, "Option :value can not be used together with option :method" if opts.key?(:method)
31
31
  raise SeregaError, "Option :value can not be used together with option :const" if opts.key?(:const)
32
32
  raise SeregaError, "Option :value can not be used together with block" if block
33
33
  end
@@ -58,12 +58,12 @@ class Serega
58
58
  # - plugin :if (checks :if, :if_value, :unless, :unless_value options)
59
59
  # - plugin :preloads (checks :preload option)
60
60
  def check_opts
61
- Utils::CheckAllowedKeys.call(opts, allowed_opts_keys)
61
+ Utils::CheckAllowedKeys.call(opts, allowed_opts_keys, :attribute)
62
62
 
63
63
  Attribute::CheckOptConst.call(opts, block)
64
64
  Attribute::CheckOptDelegate.call(opts, block)
65
65
  Attribute::CheckOptHide.call(opts)
66
- Attribute::CheckOptKey.call(opts, block)
66
+ Attribute::CheckOptMethod.call(opts, block)
67
67
  Attribute::CheckOptMany.call(opts)
68
68
  Attribute::CheckOptSerializer.call(opts)
69
69
  Attribute::CheckOptValue.call(opts, block)
@@ -35,7 +35,7 @@ class Serega
35
35
  private
36
36
 
37
37
  def check_allowed_keys
38
- Utils::CheckAllowedKeys.call(opts, serializer_class.config.initiate_keys)
38
+ Utils::CheckAllowedKeys.call(opts, serializer_class.config.initiate_keys, :initiate)
39
39
  end
40
40
 
41
41
  def check_modifiers
@@ -33,7 +33,7 @@ class Serega
33
33
  private
34
34
 
35
35
  def check_opts
36
- Utils::CheckAllowedKeys.call(opts, serializer_class.config.serialize_keys)
36
+ Utils::CheckAllowedKeys.call(opts, serializer_class.config.serialize_keys, :serialize)
37
37
 
38
38
  Utils::CheckOptIsHash.call(opts, :context)
39
39
  Utils::CheckOptIsBool.call(opts, :many)
@@ -18,11 +18,13 @@ class Serega
18
18
  # @raise [Serega::SeregaError] error when any hash key is not allowed
19
19
  #
20
20
  # @return [void]
21
- def self.call(opts, allowed_keys)
21
+ def self.call(opts, allowed_keys, parameter_name)
22
22
  opts.each_key do |key|
23
23
  next if allowed_keys.include?(key)
24
24
 
25
- raise SeregaError, "Invalid option #{key.inspect}. Allowed options are: #{allowed_keys.map(&:inspect).join(", ")}"
25
+ raise SeregaError,
26
+ "Invalid #{parameter_name} option #{key.inspect}." \
27
+ " Allowed options are: #{allowed_keys.map(&:inspect).sort.join(", ")}"
26
28
  end
27
29
  end
28
30
  end
data/lib/serega.rb CHANGED
@@ -32,8 +32,8 @@ require_relative "serega/validations/attribute/check_name"
32
32
  require_relative "serega/validations/attribute/check_opt_const"
33
33
  require_relative "serega/validations/attribute/check_opt_hide"
34
34
  require_relative "serega/validations/attribute/check_opt_delegate"
35
- require_relative "serega/validations/attribute/check_opt_key"
36
35
  require_relative "serega/validations/attribute/check_opt_many"
36
+ require_relative "serega/validations/attribute/check_opt_method"
37
37
  require_relative "serega/validations/attribute/check_opt_serializer"
38
38
  require_relative "serega/validations/attribute/check_opt_value"
39
39
  require_relative "serega/validations/initiate/check_modifiers"
@@ -172,7 +172,20 @@ class Serega
172
172
  new(modifiers_opts).to_h(object, serialize_opts)
173
173
  end
174
174
 
175
- # @see #call
175
+ #
176
+ # Serializes provided object to Hash
177
+ #
178
+ # @param object [Object] Serialized object
179
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
180
+ # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
181
+ # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
182
+ # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
183
+ # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
184
+ # @option opts [Hash] :context Serialization context
185
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
186
+ #
187
+ # @return [Hash] Serialization result
188
+ #
176
189
  def to_h(object, opts = nil)
177
190
  call(object, opts)
178
191
  end
@@ -283,8 +296,17 @@ class Serega
283
296
  def initialize(opts = nil)
284
297
  @opts = (opts.nil? || opts.empty?) ? FROZEN_EMPTY_HASH : parse_modifiers(opts)
285
298
  self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
299
+
300
+ @plan = self.class::SeregaPlan.call(@opts)
286
301
  end
287
302
 
303
+ #
304
+ # Plan for serialization.
305
+ # This plan can be traversed to find serialized attributes and nested attributes.
306
+ #
307
+ # @return [Serega::SeregaPlan] Serialization plan
308
+ attr_reader :plan
309
+
288
310
  #
289
311
  # Serializes provided object to Hash
290
312
  #
@@ -338,15 +360,6 @@ class Serega
338
360
  config.from_json.call(json)
339
361
  end
340
362
 
341
- #
342
- # Plan for serialization.
343
- # This plan can be traversed to find serialized attributes and nested attributes.
344
- #
345
- # @return [Array<Serega::SeregaPlanPoint>] plan
346
- def plan
347
- @plan ||= self.class::SeregaPlan.call(opts)
348
- end
349
-
350
363
  private
351
364
 
352
365
  attr_reader :opts
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.15.0
4
+ version: 0.16.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-08-13 00:00:00.000000000 Z
11
+ date: 2023-10-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  JSON Serializer
@@ -58,7 +58,9 @@ files:
58
58
  - lib/serega/plugins/batch/lib/validations/check_batch_opt_key.rb
59
59
  - lib/serega/plugins/batch/lib/validations/check_batch_opt_loader.rb
60
60
  - lib/serega/plugins/batch/lib/validations/check_opt_batch.rb
61
+ - lib/serega/plugins/camel_case/camel_case.rb
61
62
  - lib/serega/plugins/context_metadata/context_metadata.rb
63
+ - lib/serega/plugins/depth_limit/depth_limit.rb
62
64
  - lib/serega/plugins/explicit_many_option/explicit_many_option.rb
63
65
  - lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb
64
66
  - lib/serega/plugins/formatters/formatters.rb
@@ -99,8 +101,8 @@ files:
99
101
  - lib/serega/validations/attribute/check_opt_const.rb
100
102
  - lib/serega/validations/attribute/check_opt_delegate.rb
101
103
  - lib/serega/validations/attribute/check_opt_hide.rb
102
- - lib/serega/validations/attribute/check_opt_key.rb
103
104
  - lib/serega/validations/attribute/check_opt_many.rb
105
+ - lib/serega/validations/attribute/check_opt_method.rb
104
106
  - lib/serega/validations/attribute/check_opt_serializer.rb
105
107
  - lib/serega/validations/attribute/check_opt_value.rb
106
108
  - lib/serega/validations/check_attribute_params.rb