serega 0.15.0 → 0.16.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 (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