serega 0.15.0 → 0.17.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +121 -22
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +5 -5
  5. data/lib/serega/attribute_normalizer.rb +29 -11
  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/batch_config.rb +11 -8
  13. data/lib/serega/plugins/batch/lib/modules/attribute_normalizer.rb +26 -11
  14. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_key.rb +5 -35
  15. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_loader.rb +5 -35
  16. data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +2 -2
  17. data/lib/serega/plugins/camel_case/camel_case.rb +195 -0
  18. data/lib/serega/plugins/depth_limit/depth_limit.rb +185 -0
  19. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +1 -1
  20. data/lib/serega/plugins/formatters/formatters.rb +88 -14
  21. data/lib/serega/plugins/if/if.rb +47 -23
  22. data/lib/serega/plugins/if/validations/check_opt_if.rb +4 -36
  23. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -39
  24. data/lib/serega/plugins/if/validations/check_opt_unless.rb +4 -45
  25. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -39
  26. data/lib/serega/plugins/metadata/meta_attribute.rb +21 -5
  27. data/lib/serega/plugins/metadata/metadata.rb +22 -13
  28. data/lib/serega/plugins/metadata/validations/check_block.rb +10 -10
  29. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +38 -0
  30. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +61 -0
  31. data/lib/serega/plugins/metadata/validations/check_opts.rb +24 -10
  32. data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +1 -1
  33. data/lib/serega/plugins/preloads/lib/preload_paths.rb +12 -5
  34. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +1 -1
  35. data/lib/serega/plugins/preloads/preloads.rb +12 -12
  36. data/lib/serega/plugins/root/root.rb +1 -1
  37. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +1 -1
  38. data/lib/serega/utils/params_count.rb +50 -0
  39. data/lib/serega/utils/to_hash.rb +1 -1
  40. data/lib/serega/validations/attribute/check_block.rb +4 -5
  41. data/lib/serega/validations/attribute/check_opt_const.rb +1 -1
  42. data/lib/serega/validations/attribute/check_opt_delegate.rb +9 -4
  43. data/lib/serega/validations/attribute/{check_opt_key.rb → check_opt_method.rb} +8 -8
  44. data/lib/serega/validations/attribute/check_opt_value.rb +17 -13
  45. data/lib/serega/validations/check_attribute_params.rb +3 -2
  46. data/lib/serega/validations/check_initiate_params.rb +1 -1
  47. data/lib/serega/validations/check_serialize_params.rb +1 -1
  48. data/lib/serega/validations/utils/check_allowed_keys.rb +4 -2
  49. data/lib/serega/validations/utils/check_extra_keyword_arg.rb +33 -0
  50. data/lib/serega.rb +26 -11
  51. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d780c3d344370b36d9e6db64dfdfa08878bfe32885469610ac2314638c878d87
4
- data.tar.gz: d282c6466f7b7205a7b524999b85a3e928d79c00ee11445abd26582e0146842c
3
+ metadata.gz: 47eacc613b0a58af4dc552e09e0f55ed731f7e2a852e5c668794e7eb5128be69
4
+ data.tar.gz: bb9fec8432d12108e374bf15da74a230a4efda994835e9480e795b7222bb9ced
5
5
  SHA512:
6
- metadata.gz: e2a161487df001b0074d1864770a7dfecd77ef788e187de6e460cd3d2856162cba8777eb807a65a8d4d49471bb50380fc49ffd17f36c16eb52a2ab8b84ed7991
7
- data.tar.gz: 4a672090d0893ef8d3945b2528fc9861d245833ec4b21074b73481ab7acbf8fa5207c786cad8496bf022070f3341339522bbf4970c7a554cfa543f002f51f504
6
+ metadata.gz: d7536453cd03fd6e0f65faf22f85f5a12de61ef2bda50d674cd5b31ab1fba815c4b94d5118d82902f5a434d06112cfdc16757eefd334ba217946f9e920442e05
7
+ data.tar.gz: 9bcd5c46c1ffa32f95bc02e821e94acd46408e4e65a0861e4738454e24a57767105a5d3a9af48d52c4662a1030819963f11ee8c317c84ed179eb44b282a02c73
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,21 +67,23 @@ 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 }
72
75
 
73
- # Option :value can be used with callable object to define attribute value
76
+ # Option :value can be used with proc or callable object to define attribute value
77
+ attribute :first_name, value: UserProfile.new # must have #call method
74
78
  attribute :first_name, value: proc { |user| user.profile&.first_name }
75
79
 
76
80
  # Option :delegate can be used to define attribute value.
77
81
  # Sub-option :allow_nil by default is false
78
82
  attribute :first_name, delegate: { to: :profile, allow_nil: true }
79
83
 
80
- # Option :delegate can be used with :key sub-option
81
- attribute :first_name, delegate: { to: :profile, key: :fname }
84
+ # Option :delegate can be used with :method sub-option, so method chain here
85
+ # is user.profile.fname
86
+ attribute :first_name, delegate: { to: :profile, method: :fname }
82
87
 
83
88
  # Option :const specifies attribute with specific constant value
84
89
  attribute(:type, const: 'user')
@@ -282,7 +287,7 @@ UserSerializer.to_h(bruce, with: fields_as_string)
282
287
 
283
288
  # With not existing attribute
284
289
  fields = %i[first_name enemy]
285
- fields_as_string = 'first_name,enemy')
290
+ fields_as_string = 'first_name,enemy'
286
291
  UserSerializer.new(only: fields).to_h(bruce)
287
292
  UserSerializer.to_h(bruce, only: fields)
288
293
  UserSerializer.to_h(bruce, only: fields_as_string)
@@ -433,7 +438,7 @@ end
433
438
  class UserSerializer < AppSerializer
434
439
  attribute :username
435
440
  attribute :user_stats,
436
- serializer: 'UserStatSerializer'
441
+ serializer: 'UserStatSerializer',
437
442
  value: proc { |user| user },
438
443
  preload: nil
439
444
  end
@@ -545,15 +550,15 @@ It can be used to find value for attributes in optimal way:
545
550
  After including plugin, attributes gain new `:batch` option:
546
551
 
547
552
  ```ruby
548
- attribute :name, batch: { key: :id, loader: :name_loader, default: nil }
553
+ attribute :name, batch: { loader: :name_loader, key: :id, default: nil }
549
554
  ```
550
555
 
551
556
  `:batch` option must be a hash with this keys:
552
557
 
553
- - `key` (required) [Symbol, Proc, callable] - Defines current object identifier.
554
- Later `loader` will accept array of `keys` to detect this keys values.
555
558
  - `loader` (required) [Symbol, Proc, callable] - Defines how to fetch values for
556
- batch of keys. Receives 3 parameters: keys, context, plan_point.
559
+ batch of keys. Receives 3 parameters: keys, context, plan.
560
+ - `key` (required) [Symbol, Proc, callable] - Defines current object identifier.
561
+ Key is optional if plugin was defined with `default_key` option.
557
562
  - `default` (optional) - Default value for attribute.
558
563
  By default it is `nil` or `[]` when attribute has option `many: true`
559
564
  (ex: `attribute :tags, many: true, batch: { ... }`).
@@ -711,17 +716,24 @@ Adds ability to describe metadata and adds it to serialized response
711
716
 
712
717
  Added class-level method `:meta_attribute`, to define metadata, it accepts:
713
718
 
714
- - *path [Array of Symbols] - nested hash keys.
715
- - **options [Hash] - defaults are `hide_nil: false, hide_empty: false`
716
- - &block [Proc] - describes value for current meta attribute
719
+ - `*path` [Array of Symbols] - nested hash keys.
720
+ - `**options` [Hash]
721
+
722
+ - `:const` - describes metadata value (if it is constant)
723
+ - `:value` - describes metadata value as any `#callable` instance
724
+ - `:hide_nil` - does not show metadata key if value is nil, `false` by default
725
+ - `:hide_empty`, does not show metadata key if value is nil or empty,
726
+ `false` by default
727
+
728
+ - `&block` [Proc] - describes value for current meta attribute
717
729
 
718
730
  ```ruby
719
731
  class AppSerializer < Serega
720
732
  plugin :root
721
733
  plugin :metadata
722
734
 
723
- meta_attribute(:version) { '1.2.3' }
724
- meta_attribute(:ab_tests, :names) { %i[foo bar] }
735
+ meta_attribute(:version, const: '1.2.3')
736
+ meta_attribute(:ab_tests, :names, value: ABTests.new.method(:names))
725
737
  meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
726
738
  next unless records.respond_to?(:total_count)
727
739
 
@@ -734,7 +746,7 @@ class AppSerializer < Serega
734
746
  end
735
747
 
736
748
  AppSerializer.to_h(nil)
737
- # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}}
749
+ # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=> ... }}
738
750
  ```
739
751
 
740
752
  ### Plugin :context_metadata
@@ -765,19 +777,22 @@ UserSerializer.to_h(nil, meta: { version: '1.0.1' })
765
777
 
766
778
  ### Plugin :formatters
767
779
 
768
- Allows to define `formatters` and apply them on attributes.
780
+ Allows to define `formatters` and apply them on attribute values.
769
781
 
770
782
  Config option `config.formatters.add` can be used to add formatters.
771
783
 
772
- Attribute option `:format` now can be used with name of formatter or with
784
+ Attribute option `:format` can be used with name of formatter or with
773
785
  callable instance.
774
786
 
787
+ Formatters can accept up to 2 parameters (formatted object, context)
788
+
775
789
  ```ruby
776
790
  class AppSerializer < Serega
777
791
  plugin :formatters, formatters: {
778
792
  iso8601: ->(value) { time.iso8601.round(6) },
779
793
  on_off: ->(value) { value ? 'ON' : 'OFF' },
780
- money: ->(value) { value.round(2) }
794
+ money: ->(value, ctx) { value / 10**ctx[:digits) }
795
+ date: DateTypeFormatter # callable
781
796
  }
782
797
  end
783
798
 
@@ -888,6 +903,88 @@ Look at [select serialized fields](#selecting-fields) for `:hide` usage examples
888
903
  end
889
904
  ```
890
905
 
906
+ ### Plugin :camel_case
907
+
908
+ By default when we add attribute like `attribute :first_name` this means:
909
+
910
+ - adding a `:first_name` key to resulted hash
911
+ - adding a `#first_name` method call result as value
912
+
913
+ But its often desired to response with *camelCased* keys.
914
+ By default this can be achieved by specifying attribute name and method directly
915
+ for each attribute: `attribute :firstName, method: first_name`
916
+
917
+ This plugin transforms all attribute names automatically.
918
+ We use simple regular expression to replace `_x` to `X` for the whole string.
919
+ We make this transformation only once when attribute is defined.
920
+
921
+ You can provide your own callable transformation when defining plugin,
922
+ for example `plugin :camel_case, transform: ->(name) { name.camelize }`
923
+
924
+ For any attribute camelCase-behavior can be skipped when
925
+ `camel_case: false` attribute option provided.
926
+
927
+ This plugin transforms only attribute keys,
928
+ without affecting `root`, `metadata`, `context_metadata` plugins keys.
929
+
930
+ If you wish to [select serialized fields](#selecting-fields), you should
931
+ provide them camelCased.
932
+
933
+ ```ruby
934
+ class AppSerializer < Serega
935
+ plugin :camel_case
936
+ end
937
+
938
+ class UserSerializer < AppSerializer
939
+ attribute :first_name
940
+ attribute :last_name
941
+ attribute :full_name, camel_case: false,
942
+ value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
943
+ end
944
+
945
+ require "ostruct"
946
+ user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
947
+ UserSerializer.to_h(user)
948
+ # => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
949
+
950
+ UserSerializer.new(only: %i[firstName lastName]).to_h(user)
951
+ # => {firstName: "Bruce", lastName: "Wayne"}
952
+ ```
953
+
954
+ ### Plugin :depth_limit
955
+
956
+ Helps to secure from malicious queries that require to serialize too much
957
+ or from accidental serializing of objects with cyclic relations.
958
+
959
+ Depth limit is checked when constructing a serialization plan, that is when
960
+ `#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
961
+ It can be useful to instantiate serializer before any other business logic
962
+ to get possible errors earlier.
963
+
964
+ Any class-level serialization methods also check depth limit as they also
965
+ instantiate serializer.
966
+
967
+ When depth limit is exceeded `Serega::DepthLimitError` is raised.
968
+ Depth limit error details can be found in additional
969
+ `Serega::DepthLimitError#details` method
970
+
971
+ Limit can be checked or changed with next config options:
972
+
973
+ - `config.depth_limit.limit`
974
+ - `config.depth_limit.limit=`
975
+
976
+ There are no default limit, but it should be set when enabling plugin.
977
+
978
+ ```ruby
979
+ class AppSerializer < Serega
980
+ plugin :depth_limit, limit: 10 # set limit for all child classes
981
+ end
982
+
983
+ class UserSerializer < AppSerializer
984
+ config.depth_limit.limit = 5 # overrides limit for UserSerializer
985
+ end
986
+ ```
987
+
891
988
  ### Plugin :explicit_many_option
892
989
 
893
990
  Plugin requires to add :many option when adding relationships
@@ -941,7 +1038,9 @@ The gem is available as open source under the terms of the [MIT License](https:/
941
1038
 
942
1039
  [activerecord_preloads]: #plugin-activerecord_preloads
943
1040
  [batch]: #plugin-batch
1041
+ [camel_case]: #plugin-camel_case
944
1042
  [context_metadata]: #plugin-context_metadata
1043
+ [depth_limit]: #plugin-depth_limit
945
1044
  [formatters]: #plugin-formatters
946
1045
  [metadata]: #plugin-metadata
947
1046
  [preloads]: #plugin-preloads
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.15.0
1
+ 0.17.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)
@@ -61,10 +61,10 @@ class Serega
61
61
  # Shows specified serializer class
62
62
  # @return [Serega, nil] Attribute serializer if exists
63
63
  def serializer
64
- ser = @serializer
65
- return ser if (ser.is_a?(Class) && (ser < Serega)) || !ser
64
+ serializer = @serializer
65
+ return serializer if (serializer.is_a?(Class) && (serializer < Serega)) || !serializer
66
66
 
67
- @serializer = ser.is_a?(String) ? Object.const_get(ser, false) : ser.call
67
+ @serializer = serializer.is_a?(String) ? Object.const_get(serializer, false) : serializer.call
68
68
  end
69
69
 
70
70
  #
@@ -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
  #
@@ -97,8 +97,8 @@ class Serega
97
97
  # - plugin :formatters (wraps resulted block in formatter block and formats :const values)
98
98
  #
99
99
  def prepare_value_block
100
- init_block ||
101
- init_opts[:value] ||
100
+ prepare_init_block ||
101
+ prepare_value_option_block ||
102
102
  prepare_const_block ||
103
103
  prepare_delegate_block ||
104
104
  prepare_keyword_block
@@ -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,17 +133,36 @@ 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
141
140
  end
142
141
 
142
+ def prepare_init_block
143
+ prepare_callable_proc(init_block)
144
+ end
145
+
146
+ def prepare_value_option_block
147
+ prepare_callable_proc(init_opts[:value])
148
+ end
149
+
150
+ def prepare_callable_proc(callable)
151
+ return unless callable
152
+
153
+ params_count = SeregaUtils::ParamsCount.call(callable, max_count: 2)
154
+ case params_count
155
+ when 0 then proc { |obj, _ctx| callable.call }
156
+ when 1 then proc { |obj, _ctx| callable.call(obj) }
157
+ else callable
158
+ end
159
+ end
160
+
143
161
  def prepare_delegate_block
144
162
  delegate = init_opts[:delegate]
145
163
  return unless delegate
146
164
 
147
- key_method_name = delegate[:key] || key
165
+ key_method_name = delegate[:method] || method_name
148
166
  delegate_to = delegate[:to]
149
167
 
150
168
  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
 
@@ -22,17 +22,20 @@ class Serega
22
22
  #
23
23
  # @return [void]
24
24
  #
25
- def define(loader_name, &block)
26
- unless block
27
- raise SeregaError, "Block must be given to #define method"
25
+ def define(loader_name, callable = nil, &block)
26
+ if (!callable && !block) || (callable && block)
27
+ raise SeregaError, "Batch loader can be specified with one of arguments - callable value or &block"
28
28
  end
29
29
 
30
- params = block.parameters
31
- if params.count > 3 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) }
32
- raise SeregaError, "Block can have maximum 3 regular parameters"
30
+ callable ||= block
31
+ SeregaValidations::Utils::CheckExtraKeywordArg.call(callable, "batch loader `#{loader_name}`")
32
+ params_count = SeregaUtils::ParamsCount.call(callable, max_count: 3)
33
+
34
+ if params_count > 3
35
+ raise SeregaError, "Batch loader can have maximum 3 parameters (keys, context, plan)"
33
36
  end
34
37
 
35
- loaders[loader_name] = block
38
+ loaders[loader_name] = callable
36
39
  end
37
40
 
38
41
  # Shows defined loaders
@@ -48,7 +51,7 @@ class Serega
48
51
  #
49
52
  # @return [Proc] batch loader block
50
53
  def fetch_loader(loader_name)
51
- loaders[loader_name] || (raise SeregaError, "Batch loader with name `#{loader_name.inspect}` was not defined. Define example: config.batch.define(:#{loader_name}) { |keys, ctx, points| ... }")
54
+ loaders[loader_name] || (raise SeregaError, "Batch loader with name `#{loader_name.inspect}` was not defined. Define example: config.batch.define(:#{loader_name}) { |keys| ... }")
52
55
  end
53
56
 
54
57
  # Shows option to auto hide attributes with :batch specified
@@ -42,22 +42,37 @@ class Serega
42
42
  batch = init_opts[:batch]
43
43
  return unless batch
44
44
 
45
- # take loader
46
- loader = batch[:loader]
45
+ loader = prepare_batch_loader(batch[:loader])
47
46
 
48
- # take key
49
47
  key = batch[:key] || self.class.serializer_class.config.batch.default_key
50
- proc_key =
51
- if key.is_a?(Symbol)
52
- proc { |object| object.public_send(key) }
53
- else
54
- key
55
- end
48
+ key = prepare_batch_key(key)
56
49
 
57
- # take default value
58
50
  default = batch.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
59
51
 
60
- {loader: loader, key: proc_key, default: default}
52
+ {loader: loader, key: key, default: default}
53
+ end
54
+
55
+ def prepare_batch_key(key)
56
+ return proc { |object| object.public_send(key) } if key.is_a?(Symbol)
57
+
58
+ params_count = SeregaUtils::ParamsCount.call(key, max_count: 2)
59
+ case params_count
60
+ when 0 then proc { key.call }
61
+ when 1 then proc { |object| key.call(object) }
62
+ else key
63
+ end
64
+ end
65
+
66
+ def prepare_batch_loader(loader)
67
+ return loader if loader.is_a?(Symbol)
68
+
69
+ params_count = SeregaUtils::ParamsCount.call(loader, max_count: 3)
70
+ case params_count
71
+ when 0 then proc { loader.call }
72
+ when 1 then proc { |object| loader.call(object) }
73
+ when 2 then proc { |object, context| loader.call(object, context) }
74
+ else loader
75
+ end
61
76
  end
62
77
  end
63
78
  end