serega 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d467016ac66fcd328919f3d7068dd556ac5a7768f657adb30948c2c316480a1
4
- data.tar.gz: 3ae0c46dc6760cdf94299c0edc34b41a8b195cea92dff13b91a4017bb21653f7
3
+ metadata.gz: 6b6c3672a4fc40557f4fef3e650de56c84bf23d96defabd3f3f04aa5f273aa90
4
+ data.tar.gz: 79bcc2c3526b0ae141de69003e0834c94f9b7bade028cbc85bed20c3f3ba1757
5
5
  SHA512:
6
- metadata.gz: 9f2a9f714be636a26f4ca2de8a5f692455080354f495dc5cc2d69dd874d207ddd0bc3286b47ea600ebf5a0ba2ddfd774d6876da67e82906d35319f4a54614caa
7
- data.tar.gz: 766931dbb8aaa415dcc008a477258ba6f63ca35b664267278ee31ab0becc91f124e16eb1f57bd5968df03e333bb59c98a8396f796be84c254a76e674a472d6e1
6
+ metadata.gz: edcc6856f212848b721af517bb0f77695e6e7f2adbc7bb687e4573a62efe84e02266301cc3e70647b7cd8385b96901436ce1ded9446e9712f3884d8d6348c855
7
+ data.tar.gz: e0fec25bc4471ebbf3b6fbfb1967101af7f8cd7e703cb56eba935fd51bc6de0efe87d802be8257a950648afc633ed3ceb3bfc78f67b3905197ffa0a6e39ada32
data/README.md CHANGED
@@ -20,6 +20,7 @@ It has some great features:
20
20
  - Built-in object presenter ([presenter][presenter] plugin)
21
21
  - Adding custom metadata (via [metadata][metadata] or [context_metadata][context_metadata] plugins)
22
22
  - Attributes formatters ([formatters][formatters] plugin)
23
+ - Conditional attributes ([if][if] plugin)
23
24
 
24
25
  ## Installation
25
26
 
@@ -94,9 +95,11 @@ class UserSerializer < Serega
94
95
  # It allows to specify associations to preload to attribute value
95
96
  attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
96
97
 
97
- # Option `:hide_nil` can be specified when enabled `:hide_nil` plugin
98
- # It is literally hides attribute if its value is nil
99
- attribute :email, hide_nil: true
98
+ # Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified when enabled `:if` plugin
99
+ # They hide attribute key and value from response when conditions satisfied
100
+ # See more usage examples in :if plugin section.
101
+ attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
102
+ attribute :email, if_value: :present?
100
103
 
101
104
  # Option `:format` can be specified when enabled `:formatters` plugin
102
105
  # It changes attribute value
@@ -600,16 +603,45 @@ PostSerializer.new(with: "user(email)").to_h(post)
600
603
  PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
601
604
  ```
602
605
 
603
- ### Plugin :hide_nil
606
+ ### Plugin :if
604
607
 
605
- Allows to hide attributes with `nil` values
608
+ Plugin adds `:if`, `:unless`, `:if_value`, `:unless_value` options to
609
+ attributes so we can remove attributes from response in various ways.
606
610
 
607
- ```ruby
608
- class UserSerializer < Serega
609
- plugin :hide_nil
611
+ Use `:if` and `:unless` when you want to hide attributes before finding attribute value,
612
+ and use `:if_value` and `:unless_value` to hide attributes after we find final value.
610
613
 
611
- attribute :email, hide_nil: true
612
- end
614
+ Options `:if` and `:unless` accept currently serialized object and context as parameters.
615
+ Options `:if_value` and `:unless_value` accept already found serialized value and context as parameters.
616
+
617
+ Options `:if_value` and `:unless_value` cannot be used with :serializer option, as
618
+ serialized objects have no "serialized value". Use `:if` and `:unless` in this case.
619
+
620
+ See also a `:hide` option that is available without any plugins to hide
621
+ attribute without conditions. Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
622
+
623
+ ```ruby
624
+ class UserSerializer < Serega
625
+ attribute :email, if: :active? # if user.active?
626
+ attribute :email, if: proc {|user| user.active?} # same
627
+ attribute :email, if: proc {|user, ctx| user == ctx[:current_user]} # using context
628
+ attribute :email, if: CustomPolicy.method(:view_email?) # You can provide own callable object
629
+
630
+ attribute :email, unless: :hidden? # unless user.hidden?
631
+ attribute :email, unless: proc {|user| user.hidden?} # same
632
+ attribute :email, unless: proc {|user, context| context[:show_emails]} # using context
633
+ attribute :email, unless: CustomPolicy.method(:hide_email?) # You can provide own callable object
634
+
635
+ attribute :email, if_value: :present? # if email.present?
636
+ attribute :email, if_value: proc {|email| email.present?} # same
637
+ attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]} # using context
638
+ attribute :email, if_value: CustomPolicy.method(:view_email?) # You can provide own callable object
639
+
640
+ attribute :email, unless_value: :blank? # unless email.blank?
641
+ attribute :email, unless_value: proc {|email| email.blank?} # same
642
+ attribute :email, unless_value: proc {|email, context| context[:show_emails]} # using context
643
+ attribute :email, unless_value: CustomPolicy.method(:hide_email?) # You can provide own callable object
644
+ end
613
645
  ```
614
646
 
615
647
  ## Errors
@@ -648,3 +680,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
648
680
  [presenter]: #plugin-presenter
649
681
  [root]: #plugin-root
650
682
  [string_modifiers]: #plugin-string_modifiers
683
+ [if]: #plugin-if
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.2
1
+ 0.9.0
@@ -6,6 +6,9 @@ class Serega
6
6
  # JSON dump adapter for ::Oj
7
7
  #
8
8
  class OjDump
9
+ # Default Oj serialization options
10
+ OPTS = {mode: :compat}.freeze
11
+
9
12
  #
10
13
  # Dumps data to JSON string
11
14
  #
@@ -14,7 +17,7 @@ class Serega
14
17
  # @return [String] Data serialized to JSON
15
18
  #
16
19
  def self.call(data)
17
- ::Oj.dump(data, mode: :compat)
20
+ ::Oj.dump(data, OPTS)
18
21
  end
19
22
  end
20
23
 
data/lib/serega/map.rb CHANGED
@@ -35,7 +35,8 @@ class Serega
35
35
 
36
36
  def cached_map_for(opts, max_cache_size)
37
37
  @cache ||= {}
38
- cache_key = opts.to_s
38
+ cache_key = construct_cache_key(opts)
39
+
39
40
  map = @cache[cache_key] ||= map_for(opts)
40
41
  @cache.shift if @cache.length > max_cache_size
41
42
  map
@@ -50,7 +51,8 @@ class Serega
50
51
  end
51
52
 
52
53
  def construct_map(serializer_class, only:, except:, with:)
53
- serializer_class.attributes.each_with_object([]) do |(name, attribute), map|
54
+ map = []
55
+ serializer_class.attributes.each do |name, attribute|
54
56
  next unless attribute.visible?(only: only, except: except, with: with)
55
57
 
56
58
  nested_points =
@@ -65,6 +67,21 @@ class Serega
65
67
 
66
68
  map << serializer_class::SeregaMapPoint.new(attribute, nested_points)
67
69
  end
70
+ map
71
+ end
72
+
73
+ def construct_cache_key(opts, cache_key = nil)
74
+ return nil if opts.empty?
75
+
76
+ cache_key ||= +""
77
+
78
+ opts.each do |key, nested_opts|
79
+ cache_key.insert(-1, SeregaUtils::SymbolName.call(key))
80
+ cache_key.insert(-1, "-")
81
+ construct_cache_key(nested_opts, cache_key)
82
+ end
83
+
84
+ cache_key
68
85
  end
69
86
  end
70
87
 
@@ -43,10 +43,14 @@ class Serega
43
43
 
44
44
  def serialize_object(object)
45
45
  points.each_with_object({}) do |point, container|
46
- attach_value(object, point, container)
46
+ serialize_point(object, point, container)
47
47
  end
48
48
  end
49
49
 
50
+ def serialize_point(object, point, container)
51
+ attach_value(object, point, container)
52
+ end
53
+
50
54
  def attach_value(object, point, container)
51
55
  value = point.value(object, context)
52
56
  final_value = final_value(value, point)
@@ -264,13 +264,17 @@ class Serega
264
264
  batch = point.batch
265
265
 
266
266
  if batch
267
- key = batch.key.call(object, context)
268
- opts[:batch_loaders].get(point, self).remember(key, container)
269
- container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
267
+ remember_key_for_batch_loading(batch, object, point, container)
270
268
  else
271
269
  super
272
270
  end
273
271
  end
272
+
273
+ def remember_key_for_batch_loading(batch, object, point, container)
274
+ key = batch.key.call(object, context)
275
+ opts[:batch_loaders].get(point, self).remember(key, container)
276
+ container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
277
+ end
274
278
  end
275
279
  end
276
280
 
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ #
6
+ # Plugin adds `:if`, `:unless`, `:if_value`, `:unless_value` options to
7
+ # attributes so we can remove attributes from response in various ways.
8
+ #
9
+ # Use `:if` and `:unless` when you want to hide attributes before finding attribute value,
10
+ # and use `:if_value` and `:unless_value` to hide attributes after we find final value.
11
+ #
12
+ # Options `:if` and `:unless` accept currently serialized object and context as parameters.
13
+ # Options `:if_value` and `:unless_value` accept already found serialized value and context as parameters.
14
+ #
15
+ # Options `:if_value` and `:unless_value` cannot be used with :serializer option, as
16
+ # serialized objects have no "serialized value". Use `:if` and `:unless` in this case.
17
+ #
18
+ # See also a `:hide` option that is available without any plugins to hide
19
+ # attribute without conditions. Look at README.md#selecting-fields for `:hide` usage examples.
20
+ #
21
+ # Examples:
22
+ # class UserSerializer < Serega
23
+ # attribute :email, if: :active? # if user.active?
24
+ # attribute :email, if: proc {|user| user.active?} # same
25
+ # attribute :email, if: proc {|user, ctx| user == ctx[:current_user]} # using context
26
+ # attribute :email, if: CustomPolicy.method(:view_email?) # You can provide own callable object
27
+ #
28
+ # attribute :email, unless: :hidden? # unless user.hidden?
29
+ # attribute :email, unless: proc {|user| user.hidden?} # same
30
+ # attribute :email, unless: proc {|user, context| context[:show_emails]} # using context
31
+ # attribute :email, unless: CustomPolicy.method(:hide_email?) # You can provide own callable object
32
+ #
33
+ # attribute :email, if_value: :present? # if email.present?
34
+ # attribute :email, if_value: proc {|email| email.present?} # same
35
+ # attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]} # using context
36
+ # attribute :email, if_value: CustomPolicy.method(:view_email?) # You can provide own callable object
37
+ #
38
+ # attribute :email, unless_value: :blank? # unless email.blank?
39
+ # attribute :email, unless_value: proc {|email| email.blank?} # same
40
+ # attribute :email, unless_value: proc {|email, context| context[:show_emails]} # using context
41
+ # attribute :email, unless_value: CustomPolicy.method(:hide_email?) # You can provide own callable object
42
+ # end
43
+ #
44
+ module If
45
+ # @return [Symbol] Plugin name
46
+ def self.plugin_name
47
+ :if
48
+ end
49
+
50
+ #
51
+ # Applies plugin code to specific serializer
52
+ #
53
+ # @param serializer_class [Class<Serega>] Current serializer class
54
+ # @param _opts [Hash] Loaded plugins options
55
+ #
56
+ # @return [void]
57
+ #
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"
63
+
64
+ serializer_class::SeregaMapPoint.include(MapPointInstanceMethods)
65
+ serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
66
+ serializer_class::SeregaObjectSerializer.include(SeregaObjectSerializerInstanceMethods)
67
+ end
68
+
69
+ # Checks requirements and loads additional plugins
70
+ #
71
+ # @param serializer_class [Class<Serega>] Current serializer class
72
+ # @param opts [Hash] loaded plugins opts
73
+ #
74
+ # @return [void]
75
+ #
76
+ def self.before_load_plugin(serializer_class, **opts)
77
+ if serializer_class.plugin_used?(:batch)
78
+ raise SeregaError, "Plugin `#{plugin_name}` must be loaded before `batch`"
79
+ end
80
+ end
81
+
82
+ #
83
+ # Adds config options and runs other callbacks after plugin was loaded
84
+ #
85
+ # @param serializer_class [Class<Serega>] Current serializer class
86
+ # @param opts [Hash] loaded plugins opts
87
+ #
88
+ # @return [void]
89
+ #
90
+ def self.after_load_plugin(serializer_class, **opts)
91
+ serializer_class.config.attribute_keys << :if << :if_value << :unless << :unless_value
92
+ end
93
+
94
+ #
95
+ # Serega::SeregaMapPoint additional/patched instance methods
96
+ #
97
+ # @see Serega::SeregaMapPoint::InstanceMethods
98
+ #
99
+ module MapPointInstanceMethods
100
+ #
101
+ # @return [Boolean] Should we show attribute or not
102
+ # Conditions for this checks are specified by :if and :unless attribute options.
103
+ #
104
+ def satisfy_if_conditions?(obj, ctx)
105
+ check_if_unless(obj, ctx, :if, :unless)
106
+ end
107
+
108
+ #
109
+ # @return [Boolean] Should we show attribute with specific value or not.
110
+ # Conditions for this checks are specified by :if_value and :unless_value attribute options.
111
+ #
112
+ def satisfy_if_value_conditions?(value, ctx)
113
+ check_if_unless(value, ctx, :if_value, :unless_value)
114
+ end
115
+
116
+ private
117
+
118
+ def check_if_unless(obj, ctx, opt_if_name, opt_unless_name)
119
+ opt_if = attribute.opts[opt_if_name]
120
+ opt_unless = attribute.opts[opt_unless_name]
121
+ return true if opt_if.nil? && opt_unless.nil?
122
+
123
+ res_if =
124
+ case opt_if
125
+ when NilClass then true
126
+ when Symbol then obj.public_send(opt_if)
127
+ else opt_if.call(obj, ctx)
128
+ end
129
+
130
+ res_unless =
131
+ case opt_unless
132
+ when NilClass then true
133
+ when Symbol then !obj.public_send(opt_unless)
134
+ else !opt_unless.call(obj, ctx)
135
+ end
136
+
137
+ res_if && res_unless
138
+ end
139
+ end
140
+
141
+ #
142
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
143
+ #
144
+ # @see Serega::SeregaValidations::CheckAttributeParams
145
+ #
146
+ module CheckAttributeParamsInstanceMethods
147
+ private
148
+
149
+ def check_opts
150
+ super
151
+
152
+ CheckOptIf.call(opts)
153
+ CheckOptUnless.call(opts)
154
+ CheckOptIfValue.call(opts)
155
+ CheckOptUnlessValue.call(opts)
156
+ end
157
+ end
158
+
159
+ #
160
+ # SeregaObjectSerializer additional/patched class methods
161
+ #
162
+ # @see Serega::SeregaObjectSerializer
163
+ #
164
+ module SeregaObjectSerializerInstanceMethods
165
+ private
166
+
167
+ def serialize_point(object, point, _container)
168
+ return unless point.satisfy_if_conditions?(object, context)
169
+ super
170
+ end
171
+
172
+ def attach_final_value(value, point, _container)
173
+ return unless point.satisfy_if_value_conditions?(value, context)
174
+ super
175
+ end
176
+ end
177
+ end
178
+
179
+ register_plugin(If.plugin_name, If)
180
+ end
181
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :if option
8
+ #
9
+ class CheckOptIf
10
+ class << self
11
+ #
12
+ # Checks attribute :if option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:if)
22
+
23
+ check_type(opts[:if])
24
+ end
25
+
26
+ private
27
+
28
+ def check_type(value)
29
+ return if value.is_a?(Symbol)
30
+
31
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
32
+
33
+ if value.is_a?(Proc)
34
+ check_block(value)
35
+ else
36
+ check_callable(value)
37
+ end
38
+ end
39
+
40
+ def check_block(block)
41
+ return if valid_parameters?(block, accepted_count: 0..2)
42
+
43
+ raise SeregaError, block_parameters_error
44
+ end
45
+
46
+ def check_callable(callable)
47
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
48
+
49
+ raise SeregaError, callable_parameters_error
50
+ end
51
+
52
+ def valid_parameters?(data, accepted_count:)
53
+ params = data.parameters
54
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
55
+ end
56
+
57
+ def valid_parameters_types?(params)
58
+ params.all? do |param|
59
+ type = param[0]
60
+ (type == :req) || (type == :opt)
61
+ end
62
+ end
63
+
64
+ def block_parameters_error
65
+ "Invalid attribute option :if. When it is a Proc it can have maximum two regular parameters (object, context)"
66
+ end
67
+
68
+ def callable_parameters_error
69
+ "Invalid attribute option :if. When it is a callable object it must have two regular parameters (object, context)"
70
+ end
71
+
72
+ def must_be_callable
73
+ "Invalid attribute option :if. It must be a Symbol, a Proc or respond to :call"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :if_value option
8
+ #
9
+ class CheckOptIfValue
10
+ class << self
11
+ #
12
+ # Checks attribute :if_value option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:if_value)
22
+
23
+ check_usage_with_other_params(opts)
24
+ check_type(opts[:if_value])
25
+ end
26
+
27
+ private
28
+
29
+ def check_type(value)
30
+ return if value.is_a?(Symbol)
31
+
32
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
33
+
34
+ if value.is_a?(Proc)
35
+ check_block(value)
36
+ else
37
+ check_callable(value)
38
+ end
39
+ end
40
+
41
+ def check_usage_with_other_params(opts)
42
+ raise SeregaError, "Option :if_value can not be used together with option :serializer" if opts.key?(:serializer)
43
+ end
44
+
45
+ def check_block(block)
46
+ return if valid_parameters?(block, accepted_count: 0..2)
47
+
48
+ raise SeregaError, block_parameters_error
49
+ end
50
+
51
+ def check_callable(callable)
52
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
53
+
54
+ raise SeregaError, callable_parameters_error
55
+ end
56
+
57
+ def valid_parameters?(data, accepted_count:)
58
+ params = data.parameters
59
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
60
+ end
61
+
62
+ def valid_parameters_types?(params)
63
+ params.all? do |param|
64
+ type = param[0]
65
+ (type == :req) || (type == :opt)
66
+ end
67
+ end
68
+
69
+ def block_parameters_error
70
+ "Invalid attribute option :if_value. When it is a Proc it can have maximum two regular parameters (object, context)"
71
+ end
72
+
73
+ def callable_parameters_error
74
+ "Invalid attribute option :if_value. When it is a callable object it must have two regular parameters (object, context)"
75
+ end
76
+
77
+ def must_be_callable
78
+ "Invalid attribute option :if_value. It must be a Symbol, a Proc or respond to :call"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :unless option
8
+ #
9
+ class CheckOptUnless
10
+ class << self
11
+ #
12
+ # Checks attribute :unless option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:unless)
22
+
23
+ check_type(opts[:unless])
24
+ end
25
+
26
+ private
27
+
28
+ #
29
+ # Checks attribute :unless option
30
+ #
31
+ # @param value [nil, Symbol, Proc, #call] Attribute :unless option
32
+ #
33
+ # @raise [SeregaError] validation error
34
+ #
35
+ # @return [void]
36
+ #
37
+ def check_type(value)
38
+ return if value.is_a?(Symbol)
39
+
40
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
41
+
42
+ if value.is_a?(Proc)
43
+ check_block(value)
44
+ else
45
+ check_callable(value)
46
+ end
47
+ end
48
+
49
+ def check_block(block)
50
+ return if valid_parameters?(block, accepted_count: 0..2)
51
+
52
+ raise SeregaError, block_parameters_error
53
+ end
54
+
55
+ def check_callable(callable)
56
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
57
+
58
+ raise SeregaError, callable_parameters_error
59
+ end
60
+
61
+ def valid_parameters?(data, accepted_count:)
62
+ params = data.parameters
63
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
64
+ end
65
+
66
+ def valid_parameters_types?(params)
67
+ params.all? do |param|
68
+ type = param[0]
69
+ (type == :req) || (type == :opt)
70
+ end
71
+ end
72
+
73
+ def block_parameters_error
74
+ "Invalid attribute option :unless. When it is a Proc it can have maximum two regular parameters (object, context)"
75
+ end
76
+
77
+ def callable_parameters_error
78
+ "Invalid attribute option :unless. When it is a callable object it must have two regular parameters (object, context)"
79
+ end
80
+
81
+ def must_be_callable
82
+ "Invalid attribute option :unless. It must be a Symbol, a Proc or respond to :call"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :unless_value option
8
+ #
9
+ class CheckOptUnlessValue
10
+ class << self
11
+ #
12
+ # Checks attribute :unless_value option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:unless_value)
22
+
23
+ check_usage_with_other_params(opts)
24
+ check_type(opts[:unless_value])
25
+ end
26
+
27
+ private
28
+
29
+ def check_type(value)
30
+ return if value.is_a?(Symbol)
31
+
32
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
33
+
34
+ if value.is_a?(Proc)
35
+ check_block(value)
36
+ else
37
+ check_callable(value)
38
+ end
39
+ end
40
+
41
+ def check_usage_with_other_params(opts)
42
+ raise SeregaError, "Option :unless_value can not be used together with option :serializer" if opts.key?(:serializer)
43
+ end
44
+
45
+ def check_block(block)
46
+ return if valid_parameters?(block, accepted_count: 0..2)
47
+
48
+ raise SeregaError, block_parameters_error
49
+ end
50
+
51
+ def check_callable(callable)
52
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
53
+
54
+ raise SeregaError, callable_parameters_error
55
+ end
56
+
57
+ def valid_parameters?(data, accepted_count:)
58
+ params = data.parameters
59
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
60
+ end
61
+
62
+ def valid_parameters_types?(params)
63
+ params.all? do |param|
64
+ type = param[0]
65
+ (type == :req) || (type == :opt)
66
+ end
67
+ end
68
+
69
+ def block_parameters_error
70
+ "Invalid attribute option :unless_value. When it is a Proc it can have maximum two regular parameters (object, context)"
71
+ end
72
+
73
+ def callable_parameters_error
74
+ "Invalid attribute option :unless_value. When it is a callable object it must have two regular parameters (object, context)"
75
+ end
76
+
77
+ def must_be_callable
78
+ "Invalid attribute option :unless_value. It must be a Symbol, a Proc or respond to :call"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
3
5
  class Serega
4
6
  module SeregaPlugins
5
7
  #
@@ -14,7 +16,7 @@ class Serega
14
16
  # PostSerializer.new(only: "id,user(id,username)").to_h(post)
15
17
  # PostSerializer.new(except: "user(username,email)").to_h(post)
16
18
  # PostSerializer.new(with: "user(email)").to_h(post)
17
-
19
+ #
18
20
  # # Modifiers can still be provided old way with nested hashes or arrays.
19
21
  # PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
20
22
  #
@@ -52,9 +54,11 @@ class Serega
52
54
  def parse(fields)
53
55
  res = {}
54
56
  attribute = +""
57
+ char = +""
55
58
  path_stack = nil
59
+ fields = StringIO.new(fields)
56
60
 
57
- fields.each_char do |char|
61
+ while fields.read(1, char)
58
62
  case char
59
63
  when ","
60
64
  add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaUtils
5
+ #
6
+ # Utility to get frozen string from symbol in any ruby version
7
+ #
8
+ class SymbolName
9
+ class << self
10
+ #
11
+ # Returns frozen string corresponding to provided symbol
12
+ #
13
+ # @param key [Symbol]
14
+ #
15
+ # @return [String] frozen string corresponding to provided symbol
16
+ #
17
+ def call(key)
18
+ key.is_a?(String) ? key : to_frozen_string(key)
19
+ end
20
+
21
+ private
22
+
23
+ # :nocov:
24
+ if RUBY_VERSION < "3"
25
+ def to_frozen_string(key)
26
+ key.to_s.freeze
27
+ end
28
+ else
29
+ def to_frozen_string(key)
30
+ key.name
31
+ end
32
+ end
33
+ # :nocov:
34
+ end
35
+ end
36
+ end
37
+ end
@@ -27,7 +27,7 @@ class Serega
27
27
  # @return [void]
28
28
  #
29
29
  def call(name)
30
- name = name.to_s
30
+ name = SeregaUtils::SymbolName.call(name)
31
31
 
32
32
  valid =
33
33
  case name.size
data/lib/serega.rb CHANGED
@@ -16,6 +16,7 @@ end
16
16
  require_relative "serega/errors"
17
17
  require_relative "serega/helpers/serializer_class_helper"
18
18
  require_relative "serega/utils/enum_deep_dup"
19
+ require_relative "serega/utils/symbol_name"
19
20
  require_relative "serega/utils/to_hash"
20
21
  require_relative "serega/json/adapter"
21
22
 
@@ -184,7 +185,7 @@ class Serega
184
185
  # Serializes provided object to Hash
185
186
  #
186
187
  # @param object [Object] Serialized object
187
- # @param opts [Hash] Serializer modifiers and other instantiating options
188
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
188
189
  # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
189
190
  # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
190
191
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
@@ -194,13 +195,23 @@ class Serega
194
195
  #
195
196
  # @return [Hash] Serialization result
196
197
  #
197
- def call(object, opts = FROZEN_EMPTY_HASH)
198
+ def call(object, opts = nil)
199
+ opts ||= FROZEN_EMPTY_HASH
198
200
  initiate_keys = config.initiate_keys
199
- new(opts.slice(*initiate_keys)).to_h(object, opts.except(*initiate_keys))
201
+
202
+ if opts.empty?
203
+ modifiers_opts = FROZEN_EMPTY_HASH
204
+ serialize_opts = nil
205
+ else
206
+ serialize_opts = opts.except(*initiate_keys)
207
+ modifiers_opts = opts.slice(*initiate_keys)
208
+ end
209
+
210
+ new(modifiers_opts).to_h(object, serialize_opts)
200
211
  end
201
212
 
202
213
  # @see #call
203
- def to_h(object, opts = FROZEN_EMPTY_HASH)
214
+ def to_h(object, opts = nil)
204
215
  call(object, opts)
205
216
  end
206
217
 
@@ -208,7 +219,7 @@ class Serega
208
219
  # Serializes provided object to JSON string
209
220
  #
210
221
  # @param object [Object] Serialized object
211
- # @param opts [Hash] Serializer modifiers and other instantiating options
222
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
212
223
  # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
213
224
  # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
214
225
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
@@ -218,16 +229,15 @@ class Serega
218
229
  #
219
230
  # @return [String] Serialization result
220
231
  #
221
- def to_json(object, opts = FROZEN_EMPTY_HASH)
222
- initiate_keys = config.initiate_keys
223
- new(opts.slice(*initiate_keys)).to_json(object, opts.except(*initiate_keys))
232
+ def to_json(object, opts = nil)
233
+ config.to_json.call(to_h(object, opts))
224
234
  end
225
235
 
226
236
  #
227
237
  # Serializes provided object as JSON
228
238
  #
229
239
  # @param object [Object] Serialized object
230
- # @param opts [Hash] Serializer modifiers and other instantiating options
240
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
231
241
  # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
232
242
  # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
233
243
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
@@ -237,7 +247,7 @@ class Serega
237
247
  #
238
248
  # @return [Hash] Serialization result
239
249
  #
240
- def as_json(object, opts = FROZEN_EMPTY_HASH)
250
+ def as_json(object, opts = nil)
241
251
  config.from_json.call(to_json(object, opts))
242
252
  end
243
253
  end
@@ -249,36 +259,37 @@ class Serega
249
259
  #
250
260
  # Instantiates new Serega class
251
261
  #
252
- # @param opts [Hash] Serializer modifiers and other instantiating options
262
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
253
263
  # @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
254
264
  # @option opts [Array, Hash, String, Symbol] :except Attributes to hide
255
265
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
256
266
  # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
257
267
  #
258
- def initialize(opts = FROZEN_EMPTY_HASH)
259
- @opts = (opts == FROZEN_EMPTY_HASH) ? opts : prepare_modifiers(opts)
260
- self.class::CheckInitiateParams.new(@opts).validate if opts.fetch(:check_initiate_params) { config.check_initiate_params }
268
+ def initialize(opts = nil)
269
+ @opts = (opts.nil? || opts.empty?) ? FROZEN_EMPTY_HASH : prepare_modifiers(opts)
270
+ self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
261
271
  end
262
272
 
263
273
  #
264
274
  # Serializes provided object to Hash
265
275
  #
266
276
  # @param object [Object] Serialized object
267
- # @param opts [Hash] Serializer modifiers and other instantiating options
277
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
268
278
  # @option opts [Hash] :context Serialization context
269
279
  # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
270
280
  #
271
281
  # @return [Hash] Serialization result
272
282
  #
273
- def call(object, opts = {})
274
- self.class::CheckSerializeParams.new(opts).validate
283
+ def call(object, opts = nil)
284
+ self.class::CheckSerializeParams.new(opts).validate if opts&.any?
285
+ opts ||= {}
275
286
  opts[:context] ||= {}
276
287
 
277
288
  serialize(object, opts)
278
289
  end
279
290
 
280
291
  # @see #call
281
- def to_h(object, opts = {})
292
+ def to_h(object, opts = nil)
282
293
  call(object, opts)
283
294
  end
284
295
 
@@ -286,13 +297,13 @@ class Serega
286
297
  # Serializes provided object to JSON string
287
298
  #
288
299
  # @param object [Object] Serialized object
289
- # @param opts [Hash] Serializer modifiers and other instantiating options
300
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
290
301
  # @option opts [Hash] :context Serialization context
291
302
  # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
292
303
  #
293
304
  # @return [Hash] Serialization result
294
305
  #
295
- def to_json(object, opts = {})
306
+ def to_json(object, opts = nil)
296
307
  hash = to_h(object, opts)
297
308
  config.to_json.call(hash)
298
309
  end
@@ -301,13 +312,13 @@ class Serega
301
312
  # Serializes provided object as JSON
302
313
  #
303
314
  # @param object [Object] Serialized object
304
- # @param opts [Hash] Serializer modifiers and other instantiating options
315
+ # @param opts [Hash, nil] Serializer modifiers and other instantiating options
305
316
  # @option opts [Hash] :context Serialization context
306
317
  # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
307
318
  #
308
319
  # @return [Hash] Serialization result
309
320
  #
310
- def as_json(object, opts = {})
321
+ def as_json(object, opts = nil)
311
322
  json = to_json(object, opts)
312
323
  config.from_json.call(json)
313
324
  end
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.8.2
4
+ version: 0.9.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: 2022-12-20 00:00:00.000000000 Z
11
+ date: 2023-03-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  JSON Serializer
@@ -53,7 +53,11 @@ files:
53
53
  - lib/serega/plugins/batch/lib/validations/check_opt_batch.rb
54
54
  - lib/serega/plugins/context_metadata/context_metadata.rb
55
55
  - lib/serega/plugins/formatters/formatters.rb
56
- - lib/serega/plugins/hide_nil/hide_nil.rb
56
+ - lib/serega/plugins/if/if.rb
57
+ - lib/serega/plugins/if/validations/check_opt_if.rb
58
+ - lib/serega/plugins/if/validations/check_opt_if_value.rb
59
+ - lib/serega/plugins/if/validations/check_opt_unless.rb
60
+ - lib/serega/plugins/if/validations/check_opt_unless_value.rb
57
61
  - lib/serega/plugins/metadata/meta_attribute.rb
58
62
  - lib/serega/plugins/metadata/metadata.rb
59
63
  - lib/serega/plugins/metadata/validations/check_block.rb
@@ -73,6 +77,7 @@ files:
73
77
  - lib/serega/plugins/string_modifiers/parse_string_modifiers.rb
74
78
  - lib/serega/plugins/string_modifiers/string_modifiers.rb
75
79
  - lib/serega/utils/enum_deep_dup.rb
80
+ - lib/serega/utils/symbol_name.rb
76
81
  - lib/serega/utils/to_hash.rb
77
82
  - lib/serega/validations/attribute/check_block.rb
78
83
  - lib/serega/validations/attribute/check_name.rb
@@ -114,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
119
  - !ruby/object:Gem::Version
115
120
  version: '0'
116
121
  requirements: []
117
- rubygems_version: 3.3.15
122
+ rubygems_version: 3.4.7
118
123
  signing_key:
119
124
  specification_version: 4
120
125
  summary: JSON Serializer
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- #
6
- # Plugin adds `:hide_nil` option to attributes to delete them from final result
7
- # if value is nil
8
- #
9
- module HideNil
10
- # @return [Symbol] Plugin name
11
- def self.plugin_name
12
- :hide_nil
13
- end
14
-
15
- #
16
- # Applies plugin code to specific serializer
17
- #
18
- # @param serializer_class [Class<Serega>] Current serializer class
19
- # @param _opts [Hash] Loaded plugins options
20
- #
21
- # @return [void]
22
- #
23
- def self.load_plugin(serializer_class, **_opts)
24
- serializer_class::SeregaAttribute.include(AttributeInstanceMethods)
25
- serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
26
- serializer_class::SeregaObjectSerializer.include(SeregaObjectSerializerInstanceMethods)
27
- end
28
-
29
- #
30
- # Adds config options and runs other callbacks after plugin was loaded
31
- #
32
- # @param serializer_class [Class<Serega>] Current serializer class
33
- # @param opts [Hash] loaded plugins opts
34
- #
35
- # @return [void]
36
- #
37
- def self.after_load_plugin(serializer_class, **opts)
38
- serializer_class.config.attribute_keys << :hide_nil
39
- end
40
-
41
- #
42
- # Serega::SeregaAttribute additional/patched instance methods
43
- #
44
- # @see Serega::SeregaValidations::CheckAttributeParams
45
- #
46
- module AttributeInstanceMethods
47
- # Check hide_nil is specified
48
- def hide_nil?
49
- !!opts[:hide_nil]
50
- end
51
- end
52
-
53
- #
54
- # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
55
- #
56
- # @see Serega::SeregaValidations::CheckAttributeParams
57
- #
58
- module CheckAttributeParamsInstanceMethods
59
- private
60
-
61
- def check_opts
62
- super
63
- CheckOptHideNil.call(opts)
64
- end
65
- end
66
-
67
- #
68
- # Validator class for :hide_nil attribute option
69
- #
70
- class CheckOptHideNil
71
- #
72
- # Checks attribute :hide_nil option
73
- #
74
- # @param opts [Hash] Attribute options
75
- #
76
- # @raise [Serega::SeregaError] SeregaError that option has invalid value
77
- #
78
- # @return [void]
79
- #
80
- def self.call(opts)
81
- return unless opts.key?(:hide_nil)
82
-
83
- value = opts[:hide_nil]
84
- return if (value == true) || (value == false)
85
-
86
- raise SeregaError, "Invalid option :hide_nil => #{value.inspect}. Must have a boolean value"
87
- end
88
- end
89
-
90
- #
91
- # SeregaObjectSerializer additional/patched class methods
92
- #
93
- # @see Serega::SeregaObjectSerializer
94
- #
95
- module SeregaObjectSerializerInstanceMethods
96
- private
97
-
98
- def attach_final_value(final_value, *)
99
- super unless final_value.nil?
100
- end
101
- end
102
- end
103
-
104
- register_plugin(HideNil.plugin_name, HideNil)
105
- end
106
- end