serega 0.15.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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