active_interaction 4.0.0 → 4.0.5

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: acc5f59cdd588429a58556cea7b70aa0c43a6a13c523a682cac266cfc7ab76a1
4
- data.tar.gz: 9a58db1735b436ae91a70baf9c2eab9ef4851a20a9e51c05c6f243af6e3c282b
3
+ metadata.gz: cc966bbcee80b29757efdf5bf0b6b0357b8d24610dca49017f4a8cdca64bfaa3
4
+ data.tar.gz: fe255f995b77c90c935fb14ae05660fcc973a3e7ef3e26209d5e1e187624ca21
5
5
  SHA512:
6
- metadata.gz: c78b1f82d8593a238a2076f7351146e51079d163d963cc830d9e086bbc82bd791fbc95527d4fc829e3c38039bb77ce06fefe82520f5b6f2148a64384a8e3a339
7
- data.tar.gz: 362a8b86512c8ef05d9582a377000e06e2b8b0855c112458020911fa77477c3c98cd10555e81d259cfc767f79c1eb8b03e9ec12124cfca38c552208006e1a70a
6
+ metadata.gz: 233b8b4334a5f4d34d12607c054a47aa4839f9c734cd251b682f30173aaa5115e0d8292c0074efd21137a966831b635d2a4e4f3c55f6cb282bb78ed6f795450e
7
+ data.tar.gz: a5fef4055b48c4c72f3f297f52260f0fe4055a2299961d0fd70ebfd7fcfadc2814628a01cec947259e4b99620fc9a2ee313baaa373449aadeba62829eea7be00
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ # [4.0.5][] (2021-07-11)
2
+
3
+ ## Fix
4
+
5
+ - [#480][] - Interfaces used inside hashes failed to recognize `nil` as a non-value.
6
+
7
+ # [4.0.4][] (2021-07-03)
8
+
9
+ ## Fix
10
+
11
+ - [#510][] - Hash parameters failed when working outside of Rails.
12
+ - [#511][] - Nested filters with options but no `:class` failed to have `:class` automatically added.
13
+
14
+ # [4.0.3][] (2021-06-24)
15
+
16
+ ## Fix
17
+
18
+ - [#499][] - `given?` now recognizes multi-part date inputs by their primary key name
19
+ - [#493][] - `compose` now properly accepts `Inputs`
20
+
21
+ # [4.0.2][] (2021-06-22)
22
+
23
+ ## Fix
24
+
25
+ - [#505][] - Nested Interface filters using the `:methods` option threw an error.
26
+
27
+ # [4.0.1][] (2021-05-26)
28
+
29
+ ## Fix
30
+
31
+ - Fix regression of filter name relaxing.
32
+ - [#495][] - Fix time filter ignoring time zones
33
+
1
34
  # [4.0.0][] (2021-01-10)
2
35
 
3
36
  ## Changed
@@ -57,7 +90,7 @@ class Example < ActiveInteraction::Base
57
90
 
58
91
  validates :first_name,
59
92
  presence: true,
60
- unless: 'first_name.nil?'
93
+ unless: -> { first_name.nil? }
61
94
 
62
95
  def execute
63
96
  # ...
@@ -156,13 +189,13 @@ has a particular module included, you'll need to use the newly expanded
156
189
 
157
190
  ## Fixed
158
191
 
159
- - [486][] `valid?` returns true if block not called and error added in execute around callback.
192
+ - [#486][] `valid?` returns true if block not called and error added in execute around callback.
160
193
 
161
194
  # [3.8.2][] (2020-04-22)
162
195
 
163
196
  ## Fixed
164
197
 
165
- - [479][] Composed interactions that throw errors now show a complete backtrace instead of ending at the `run!` of the outermost interaction.
198
+ - [#479][] Composed interactions that throw errors now show a complete backtrace instead of ending at the `run!` of the outermost interaction.
166
199
 
167
200
  # [3.8.1][] (2020-04-04)
168
201
 
@@ -931,6 +964,11 @@ Example.run
931
964
 
932
965
  - Initial release.
933
966
 
967
+ [4.0.5]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.4...v4.0.5
968
+ [4.0.4]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.3...v4.0.4
969
+ [4.0.3]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.2...v4.0.3
970
+ [4.0.2]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.1...v4.0.2
971
+ [4.0.1]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.0...v4.0.1
934
972
  [4.0.0]: https://github.com/AaronLasseigne/active_interaction/compare/v3.8.3...v4.0.0
935
973
  [3.8.3]: https://github.com/AaronLasseigne/active_interaction/compare/v3.8.2...v3.8.3
936
974
  [3.8.2]: https://github.com/AaronLasseigne/active_interaction/compare/v3.8.1...v3.8.2
@@ -1136,3 +1174,11 @@ Example.run
1136
1174
  [#486]: https://github.com/AaronLasseigne/active_interaction/issues/486
1137
1175
  [#392]: https://github.com/AaronLasseigne/active_interaction/issues/392
1138
1176
  [#398]: https://github.com/AaronLasseigne/active_interaction/issues/398
1177
+ [#495]: https://github.com/AaronLasseigne/active_interaction/issues/495
1178
+ [#505]: https://github.com/AaronLasseigne/active_interaction/issues/505
1179
+ [#499]: https://github.com/AaronLasseigne/active_interaction/issues/499
1180
+ [#493]: https://github.com/AaronLasseigne/active_interaction/issues/493
1181
+ [#510]: https://github.com/AaronLasseigne/active_interaction/issues/510
1182
+ [#511]: https://github.com/AaronLasseigne/active_interaction/issues/511
1183
+ [#412]: https://github.com/AaronLasseigne/active_interaction/issues/412
1184
+ [#480]: https://github.com/AaronLasseigne/active_interaction/issues/480
data/README.md CHANGED
@@ -5,7 +5,6 @@ It's an implementation of the command pattern in Ruby.
5
5
 
6
6
  [![Version](https://img.shields.io/gem/v/active_interaction.svg?style=flat-square)](https://rubygems.org/gems/active_interaction)
7
7
  [![Test](https://img.shields.io/github/workflow/status/AaronLasseigne/active_interaction/Test?label=Test&style=flat-square)](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)
8
- [![Coverage](https://img.shields.io/coveralls/github/AaronLasseigne/active_interaction.svg?style=flat-square)](https://coveralls.io/r/orgsync/active_interaction)
9
8
  [![Climate](https://img.shields.io/codeclimate/maintainability/orgsync/active_interaction.svg?style=flat-square)](https://codeclimate.com/github/orgsync/active_interaction)
10
9
 
11
10
  ---
@@ -58,7 +57,7 @@ handles your verbs.
58
57
  - [Translations](#translations)
59
58
  - [Credits](#credits)
60
59
 
61
- [Full Documentation][]
60
+ [API Documentation][]
62
61
 
63
62
  ## Installation
64
63
 
@@ -977,10 +976,10 @@ class UpdateAccount < ActiveInteraction::Base
977
976
 
978
977
  validates :first_name,
979
978
  presence: true,
980
- unless: 'first_name.nil?'
979
+ unless: -> { first_name.nil? }
981
980
  validates :last_name,
982
981
  presence: true,
983
- unless: 'last_name.nil?'
982
+ unless: -> { last_name.nil? }
984
983
 
985
984
  def execute
986
985
  account.first_name = first_name if first_name.present?
@@ -1448,8 +1447,8 @@ I18nInteraction.run(name: false).errors.messages[:name]
1448
1447
 
1449
1448
  ## Credits
1450
1449
 
1451
- ActiveInteraction is brought to you by [Aaron Lasseigne][] and
1452
- [Taylor Fausak][] and was originally built at [OrgSync][].
1450
+ ActiveInteraction is brought to you by [Aaron Lasseigne][].
1451
+ Along with Aaron, [Taylor Fausak][] helped create and maintain ActiveInteraction but has since moved on.
1453
1452
 
1454
1453
  If you want to contribute to ActiveInteraction, please read
1455
1454
  [our contribution guidelines][]. A [complete list of contributors][] is
@@ -1458,14 +1457,11 @@ available on GitHub.
1458
1457
  ActiveInteraction is licensed under [the MIT License][].
1459
1458
 
1460
1459
  [activeinteraction]: https://github.com/AaronLasseigne/active_interaction
1461
- [Full Documentation]: http://rubydoc.info/github/orgsync/active_interaction
1460
+ [API Documentation]: http://rubydoc.info/github/AaronLasseigne/active_interaction
1462
1461
  [semantic versioning]: http://semver.org/spec/v2.0.0.html
1463
1462
  [GitHub releases]: https://github.com/AaronLasseigne/active_interaction/releases
1464
- [the announcement post]: http://devblog.orgsync.com/2015/05/06/announcing-active-interaction-2/
1465
- [active_model-errors_details]: https://github.com/cowbell/active_model-errors_details
1466
1463
  [aaron lasseigne]: https://github.com/AaronLasseigne
1467
1464
  [taylor fausak]: https://github.com/tfausak
1468
- [orgsync]: https://github.com/orgsync
1469
1465
  [our contribution guidelines]: CONTRIBUTING.md
1470
1466
  [complete list of contributors]: https://github.com/AaronLasseigne/active_interaction/graphs/contributors
1471
1467
  [the mit license]: LICENSE.md
@@ -1474,5 +1470,4 @@ ActiveInteraction is licensed under [the MIT License][].
1474
1470
  [the filters section]: #filters
1475
1471
  [the errors section]: #errors
1476
1472
  [the optional inputs section]: #optional-inputs
1477
- [aire]: example
1478
1473
  [`with_options`]: http://api.rubyonrails.org/classes/Object.html#method-i-with_options
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
+ require 'active_support/hash_with_indifferent_access'
4
5
 
5
6
  # Manage application specific business logic.
6
7
  #
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/hash/indifferent_access'
4
-
5
3
  module ActiveInteraction
6
4
  # @abstract Subclass and override {#execute} to implement a custom
7
5
  # ActiveInteraction::Base class.
@@ -45,7 +43,7 @@ module ActiveInteraction
45
43
  #
46
44
  # Runs validations and if there are no errors it will call {#execute}.
47
45
  #
48
- # @param (see ActiveInteraction::Base#initialize)
46
+ # @param (see ActiveInteraction::Inputs.process)
49
47
  #
50
48
  # @return [Base]
51
49
 
@@ -107,7 +105,7 @@ module ActiveInteraction
107
105
  # @param name [Symbol]
108
106
  # @param options [Hash]
109
107
  def add_filter(klass, name, options, &block)
110
- raise InvalidFilterError, %("#{name}" is a reserved name) if ActiveInteraction::Inputs.reserved?(name)
108
+ raise InvalidFilterError, %("#{name}" is a reserved name) if Inputs.reserved?(name)
111
109
 
112
110
  initialize_filter(klass.new(name, options, &block))
113
111
  end
@@ -160,12 +158,11 @@ module ActiveInteraction
160
158
  end
161
159
  end
162
160
 
163
- # @param inputs [Hash{Symbol => Object}] Attribute values to set.
164
- #
165
161
  # @private
166
162
  def initialize(inputs = {})
167
- inputs = normalize_inputs!(inputs)
168
- process_inputs(inputs.symbolize_keys)
163
+ @_interaction_raw_inputs = inputs
164
+
165
+ populate_filters_and_inputs(Inputs.process(inputs))
169
166
  end
170
167
 
171
168
  # @!method compose(other, inputs = {})
@@ -191,10 +188,7 @@ module ActiveInteraction
191
188
  #
192
189
  # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
193
190
  def inputs
194
- @inputs ||= self.class.filters
195
- .each_key.with_object(ActiveInteraction::Inputs.new) do |name, h|
196
- h[name] = public_send(name)
197
- end.freeze
191
+ @_interaction_inputs
198
192
  end
199
193
 
200
194
  # Returns `true` if the given key was in the hash passed to {.run}.
@@ -238,24 +232,30 @@ module ActiveInteraction
238
232
  # rubocop:disable all
239
233
  def given?(input, *rest)
240
234
  filter_level = self.class
241
- input_level = @_interaction_inputs
235
+ input_level = @_interaction_raw_inputs
242
236
 
243
237
  [input, *rest].each do |key_or_index|
244
238
  if key_or_index.is_a?(Symbol) || key_or_index.is_a?(String)
245
- key_or_index = key_or_index.to_sym
246
- filter_level = filter_level.filters[key_or_index]
239
+ key = key_or_index.to_sym
240
+ key_to_s = key_or_index.to_s
241
+ filter_level = filter_level.filters[key]
247
242
 
248
243
  break false if filter_level.nil? || input_level.nil?
249
- break false unless input_level.key?(key_or_index) || input_level.key?(key_or_index.to_s)
244
+ if filter_level.accepts_grouped_inputs?
245
+ break false unless input_level.key?(key) || input_level.key?(key_to_s) || Inputs.keys_for_group?(input_level.keys, key)
246
+ else
247
+ break false unless input_level.key?(key) || input_level.key?(key_to_s)
248
+ end
250
249
 
251
- input_level = input_level[key_or_index] || input_level[key_or_index.to_s]
250
+ input_level = input_level[key] || input_level[key_to_s]
252
251
  else
252
+ index = key_or_index
253
253
  filter_level = filter_level.filters.first.last
254
254
 
255
255
  break false if filter_level.nil? || input_level.nil?
256
- break false unless key_or_index.between?(-input_level.size, input_level.size - 1)
256
+ break false unless index.between?(-input_level.size, input_level.size - 1)
257
257
 
258
- input_level = input_level[key_or_index]
258
+ input_level = input_level[index]
259
259
  end
260
260
  end && true
261
261
  end
@@ -271,44 +271,24 @@ module ActiveInteraction
271
271
 
272
272
  private
273
273
 
274
- # We want to allow both `Hash` objects and `ActionController::Parameters`
275
- # objects. In Rails < 5, parameters are a subclass of hash and calling
276
- # `#symbolize_keys` returns the entire hash, not just permitted values. In
277
- # Rails >= 5, parameters are not a subclass of hash but calling
278
- # `#to_unsafe_h` returns the entire hash.
279
- def normalize_inputs!(inputs)
280
- return inputs if inputs.is_a?(Hash)
281
-
282
- parameters = 'ActionController::Parameters'
283
- klass = parameters.safe_constantize
284
- return inputs.to_unsafe_h if klass && inputs.is_a?(klass)
285
-
286
- raise ArgumentError, "inputs must be a hash or #{parameters}"
287
- end
288
-
289
- # @param inputs [Hash{Symbol => Object}]
290
- def process_inputs(inputs)
291
- @_interaction_inputs = inputs
292
-
293
- inputs.each do |key, value|
294
- next if ActiveInteraction::Inputs.reserved?(key)
274
+ def populate_filters_and_inputs(inputs)
275
+ @_interaction_inputs = Inputs.new
295
276
 
296
- populate_reader(key, value)
297
- end
298
-
299
- populate_filters(ActiveInteraction::Inputs.process(inputs))
300
- end
301
-
302
- def populate_reader(key, value)
303
- instance_variable_set("@#{key}", value) if respond_to?(key)
304
- end
305
-
306
- def populate_filters(inputs)
307
277
  self.class.filters.each do |name, filter|
308
- public_send("#{name}=", filter.clean(inputs[name], self))
309
- rescue InvalidValueError, MissingValueError, NoDefaultError
310
- nil # #type_check will add errors if appropriate.
278
+ value =
279
+ begin
280
+ filter.clean(inputs[name], self)
281
+ rescue InvalidValueError, MissingValueError, NoDefaultError
282
+ # #type_check will add errors if appropriate.
283
+ # We'll get the original value for the error.
284
+ inputs[name]
285
+ end
286
+
287
+ @_interaction_inputs[name] = value
288
+ public_send("#{name}=", value)
311
289
  end
290
+
291
+ @_interaction_inputs.freeze
312
292
  end
313
293
 
314
294
  def type_check
@@ -98,11 +98,7 @@ module ActiveInteraction
98
98
  #
99
99
  # @return [Errors]
100
100
  def merge!(other)
101
- if other.respond_to?(:details)
102
- merge_details!(other)
103
- else
104
- merge_messages!(other)
105
- end
101
+ merge_details!(other)
106
102
 
107
103
  self
108
104
  end
@@ -117,14 +113,6 @@ module ActiveInteraction
117
113
  detail[:error].is_a?(Symbol)
118
114
  end
119
115
 
120
- def merge_messages!(other)
121
- other.messages.each do |attribute, messages|
122
- messages.each do |message|
123
- merge_message!(attribute, message)
124
- end
125
- end
126
- end
127
-
128
116
  def merge_message!(attribute, message)
129
117
  unless attribute?(attribute)
130
118
  message = full_message(attribute, message)
@@ -178,6 +178,21 @@ module ActiveInteraction
178
178
  :string
179
179
  end
180
180
 
181
+ # Tells whether or not the filter accepts a group of parameters to form a
182
+ # single input.
183
+ #
184
+ # @example
185
+ # ActiveInteraction::TimeFilter.new(Time.now).accepts_grouped_inputs?
186
+ # # => true
187
+ # @example
188
+ # ActiveInteraction::Filter.new(:example).accepts_grouped_inputs?
189
+ # # => false
190
+ #
191
+ # @return [Boolean]
192
+ def accepts_grouped_inputs?
193
+ false
194
+ end
195
+
181
196
  private
182
197
 
183
198
  # rubocop:disable Metrics/MethodLength
@@ -12,6 +12,10 @@ module ActiveInteraction
12
12
  self.class.slug
13
13
  end
14
14
 
15
+ def accepts_grouped_inputs?
16
+ true
17
+ end
18
+
15
19
  private
16
20
 
17
21
  def klasses
@@ -25,10 +25,12 @@ module ActiveInteraction
25
25
  class ArrayFilter < Filter
26
26
  include Missable
27
27
 
28
+ # The array starts with the class override key and then contains any
29
+ # additional options which halt explicit setting of the class.
28
30
  FILTER_NAME_OR_OPTION = {
29
- 'ActiveInteraction::ObjectFilter' => :class,
30
- 'ActiveInteraction::RecordFilter' => :class,
31
- 'ActiveInteraction::InterfaceFilter' => :from
31
+ 'ActiveInteraction::ObjectFilter' => [:class].freeze,
32
+ 'ActiveInteraction::RecordFilter' => [:class].freeze,
33
+ 'ActiveInteraction::InterfaceFilter' => %i[from methods].freeze
32
34
  }.freeze
33
35
  private_constant :FILTER_NAME_OR_OPTION
34
36
 
@@ -71,9 +73,9 @@ module ActiveInteraction
71
73
  end
72
74
 
73
75
  def add_option_in_place_of_name(klass, options)
74
- if (key = FILTER_NAME_OR_OPTION[klass.to_s]) && !options.key?(key)
76
+ if (keys = FILTER_NAME_OR_OPTION[klass.to_s]) && (keys & options.keys).empty?
75
77
  options.merge(
76
- "#{key}": name.to_s.singularize.camelize.to_sym
78
+ "#{keys.first}": name.to_s.singularize.camelize.to_sym
77
79
  )
78
80
  else
79
81
  options
@@ -44,7 +44,7 @@ module ActiveInteraction
44
44
  end
45
45
 
46
46
  def adjust_output(value, context)
47
- value = value.to_hash.with_indifferent_access
47
+ value = ActiveSupport::HashWithIndifferentAccess.new(value.to_hash)
48
48
 
49
49
  initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
50
50
 
@@ -48,6 +48,7 @@ module ActiveInteraction
48
48
  end
49
49
 
50
50
  def matches?(object)
51
+ return false if object.nil?
51
52
  return matches_methods?(object) if options.key?(:methods)
52
53
 
53
54
  const = from
@@ -61,7 +62,7 @@ module ActiveInteraction
61
62
  end
62
63
 
63
64
  def matches_methods?(object)
64
- options.fetch(:methods, []).all? { |method| object.respond_to?(method) }
65
+ options[:methods].all? { |method| object.respond_to?(method) }
65
66
  end
66
67
 
67
68
  def checking_class_inheritance?(object, from)
@@ -39,9 +39,17 @@ module ActiveInteraction
39
39
  Time.respond_to?(:zone) && !Time.zone.nil?
40
40
  end
41
41
 
42
+ def klass
43
+ if time_with_zone?
44
+ Time.zone
45
+ else
46
+ super
47
+ end
48
+ end
49
+
42
50
  def klasses
43
51
  if time_with_zone?
44
- super + [Time.zone.class]
52
+ [Time.zone.at(0).class, Time]
45
53
  else
46
54
  super
47
55
  end
@@ -13,29 +13,58 @@ module ActiveInteraction
13
13
  /x.freeze
14
14
  private_constant :GROUPED_INPUT_PATTERN
15
15
 
16
+ # @private
17
+ def keys_for_group?(keys, group_key)
18
+ search_key = /\A#{group_key}\(\d+i\)\z/
19
+ keys.any? { |key| search_key.match?(key) }
20
+ end
21
+
16
22
  # Checking `syscall` is the result of what appears to be a bug in Ruby.
17
23
  # https://bugs.ruby-lang.org/issues/15597
24
+ # @private
18
25
  def reserved?(name)
19
26
  name.to_s.start_with?('_interaction_') ||
20
27
  name == :syscall ||
21
- Base.method_defined?(name) ||
22
- Base.private_method_defined?(name)
28
+ (
29
+ Base.method_defined?(name) &&
30
+ !Object.method_defined?(name)
31
+ ) ||
32
+ (
33
+ Base.private_method_defined?(name) &&
34
+ !Object.private_method_defined?(name)
35
+ )
23
36
  end
24
37
 
38
+ # @param inputs [Hash, ActionController::Parameters, ActiveInteraction::Inputs] Attribute values to set.
39
+ #
40
+ # @private
25
41
  def process(inputs)
26
- inputs.stringify_keys.sort.each_with_object({}) do |(k, v), h|
27
- next if reserved?(k)
42
+ normalize_inputs!(inputs)
43
+ .stringify_keys
44
+ .sort
45
+ .each_with_object({}) do |(k, v), h|
46
+ next if reserved?(k)
28
47
 
29
- if (group = GROUPED_INPUT_PATTERN.match(k))
30
- assign_to_grouped_input!(h, group[:key], group[:index], v)
31
- else
32
- h[k.to_sym] = v
48
+ if (group = GROUPED_INPUT_PATTERN.match(k))
49
+ assign_to_grouped_input!(h, group[:key], group[:index], v)
50
+ else
51
+ h[k.to_sym] = v
52
+ end
33
53
  end
34
- end
35
54
  end
36
55
 
37
56
  private
38
57
 
58
+ def normalize_inputs!(inputs)
59
+ return inputs if inputs.is_a?(Hash) || inputs.is_a?(Inputs)
60
+
61
+ parameters = 'ActionController::Parameters'
62
+ klass = parameters.safe_constantize
63
+ return inputs.to_unsafe_h if klass && inputs.is_a?(klass)
64
+
65
+ raise ArgumentError, "inputs must be a hash or #{parameters}"
66
+ end
67
+
39
68
  def assign_to_grouped_input!(inputs, key, index, value)
40
69
  key = key.to_sym
41
70
 
@@ -44,46 +73,8 @@ module ActiveInteraction
44
73
  end
45
74
  end
46
75
 
47
- def initialize
48
- @groups = {}
49
- @groups.default_proc = ->(hash, key) { hash[key] = [] }
50
-
51
- super(@inputs = {})
52
- end
53
-
54
- # Associates the `value` with the `key`. Allows the `key`/`value` pair to
55
- # be associated with one or more groups.
56
- #
57
- # @example
58
- # inputs.store(:key, :value)
59
- # # => :value
60
- # inputs.store(:key, :value, %i[a b])
61
- # # => :value
62
- #
63
- # @param key [Object] The key to store the value under.
64
- # @param value [Object] The value to store.
65
- # @param groups [Array<Object>] The groups to store the pair under.
66
- #
67
- # @return [Object] value
68
- def store(key, value, groups = [])
69
- groups.each do |group|
70
- @groups[group] << key
71
- end
72
-
73
- super(key, value)
74
- end
75
-
76
- # Returns inputs from the group name given.
77
- #
78
- # @example
79
- # inputs.group(:a)
80
- # # => {key: :value}
81
- #
82
- # @param name [Object] Name of the group to return.
83
- #
84
- # @return [Hash] Inputs from the group name given.
85
- def group(name)
86
- @inputs.select { |k, _| @groups[name].include?(k) }
76
+ def initialize(inputs = {})
77
+ super(inputs)
87
78
  end
88
79
  end
89
80
  end
@@ -14,26 +14,17 @@ module ActiveInteraction
14
14
  filter.clean(inputs[name], context)
15
15
  rescue NoDefaultError
16
16
  nil
17
- rescue InvalidNestedValueError,
18
- InvalidValueError,
19
- MissingValueError => e
20
- errors << error_args(filter, e)
17
+ rescue InvalidNestedValueError => e
18
+ errors << [filter.name, :invalid_nested, { name: e.filter_name.inspect, value: e.input_value.inspect }]
19
+ rescue InvalidValueError
20
+ errors << [filter.name, :invalid_type, { type: type(filter) }]
21
+ rescue MissingValueError
22
+ errors << [filter.name, :missing]
21
23
  end
22
24
  end
23
25
 
24
26
  private
25
27
 
26
- def error_args(filter, error)
27
- case error
28
- when InvalidNestedValueError
29
- [filter.name, :invalid_nested, { name: error.filter_name.inspect, value: error.input_value.inspect }]
30
- when InvalidValueError
31
- [filter.name, :invalid_type, { type: type(filter) }]
32
- when MissingValueError
33
- [filter.name, :missing]
34
- end
35
- end
36
-
37
28
  # @param filter [Filter]
38
29
  def type(filter)
39
30
  I18n.translate("#{Base.i18n_scope}.types.#{filter.class.slug}")
@@ -4,5 +4,5 @@ module ActiveInteraction
4
4
  # The version number.
5
5
  #
6
6
  # @return [Gem::Version]
7
- VERSION = Gem::Version.new('4.0.0')
7
+ VERSION = Gem::Version.new('4.0.5')
8
8
  end
@@ -46,65 +46,6 @@ describe ActiveInteraction::Base do
46
46
  expect(interaction.instance_variable_defined?(:"@#{key}")).to be false
47
47
  end
48
48
 
49
- context 'with invalid inputs' do
50
- let(:inputs) { nil }
51
-
52
- it 'raises an error' do
53
- expect { interaction }.to raise_error ArgumentError
54
- end
55
- end
56
-
57
- context 'with non-hash inputs' do
58
- let(:inputs) { [%i[k v]] }
59
-
60
- it 'raises an error' do
61
- expect { interaction }.to raise_error ArgumentError
62
- end
63
- end
64
-
65
- context 'with ActionController::Parameters inputs' do
66
- let(:inputs) { ActionController::Parameters.new }
67
-
68
- it 'does not raise an error' do
69
- expect { interaction }.to_not raise_error
70
- end
71
- end
72
-
73
- context 'with a reader' do
74
- let(:described_class) do
75
- Class.new(TestInteraction) do
76
- attr_reader :thing
77
-
78
- validates :thing, presence: true
79
- end
80
- end
81
-
82
- context 'validation' do
83
- context 'failing' do
84
- it 'returns an invalid outcome' do
85
- expect(interaction).to be_invalid
86
- end
87
- end
88
-
89
- context 'passing' do
90
- before { inputs[:thing] = SecureRandom.hex }
91
-
92
- it 'returns a valid outcome' do
93
- expect(interaction).to be_valid
94
- end
95
- end
96
- end
97
-
98
- context 'with a single input' do
99
- let(:thing) { SecureRandom.hex }
100
- before { inputs[:thing] = thing }
101
-
102
- it 'sets the attribute' do
103
- expect(interaction.thing).to eql thing
104
- end
105
- end
106
- end
107
-
108
49
  context 'with a filter' do
109
50
  let(:described_class) { InteractionWithFilter }
110
51
 
@@ -589,6 +530,32 @@ describe ActiveInteraction::Base do
589
530
  expect(result).to be false
590
531
  end
591
532
  end
533
+
534
+ context 'multi-part date values' do
535
+ let(:described_class) do
536
+ Class.new(TestInteraction) do
537
+ date :thing,
538
+ default: nil
539
+
540
+ def execute
541
+ given?(:thing)
542
+ end
543
+ end
544
+ end
545
+
546
+ it 'returns true when the input is given' do
547
+ inputs.merge!(
548
+ 'thing(1i)' => '2020',
549
+ 'thing(2i)' => '12',
550
+ 'thing(3i)' => '31'
551
+ )
552
+ expect(result).to be true
553
+ end
554
+
555
+ it 'returns false if not found' do
556
+ expect(result).to be false
557
+ end
558
+ end
592
559
  end
593
560
 
594
561
  context 'inheritance' do
@@ -25,7 +25,7 @@ describe ActiveInteraction::Hashable do
25
25
  end
26
26
 
27
27
  context 'with a block' do
28
- let(:block) { proc {} } # rubocop:disable Lint/EmptyBlock
28
+ let(:block) { proc {} }
29
29
  let(:hash) { subject.hash(*arguments, &block) }
30
30
 
31
31
  it 'calls method_missing' do
@@ -127,6 +127,29 @@ describe ActiveInteraction::ArrayFilter, :filter do
127
127
  end
128
128
  end
129
129
  end
130
+
131
+ context 'with a nested interface type' do
132
+ context 'with the methods option set' do
133
+ let(:block) { proc { public_send(:interface, methods: %i[to_s]) } }
134
+
135
+ it 'has a filter with the right option' do
136
+ expect(filter.filters[:'0'].options).to have_key(:methods)
137
+ expect(filter.filters[:'0'].options[:methods]).to eql %i[to_s]
138
+ end
139
+ end
140
+
141
+ context 'with another option set' do
142
+ let(:block) { proc { public_send(:object, converter: :new) } }
143
+ let(:name) { :objects }
144
+
145
+ it 'has a filter with the right options' do
146
+ expect(filter.filters[:'0'].options).to have_key(:class)
147
+ expect(filter.filters[:'0'].options[:class]).to eql :Object
148
+ expect(filter.filters[:'0'].options).to have_key(:converter)
149
+ expect(filter.filters[:'0'].options[:converter]).to eql :new
150
+ end
151
+ end
152
+ end
130
153
  end
131
154
 
132
155
  describe '#database_column_type' do
@@ -61,6 +61,17 @@ describe ActiveInteraction::InterfaceFilter, :filter do
61
61
  end
62
62
  end
63
63
 
64
+ context 'that is nil' do
65
+ let(:name) { :interface_module }
66
+ let(:value) { nil }
67
+
68
+ it 'raises an error' do
69
+ expect do
70
+ result
71
+ end.to raise_error ActiveInteraction::MissingValueError
72
+ end
73
+ end
74
+
64
75
  context 'with the class itself' do
65
76
  let(:name) { :interface_class }
66
77
  let(:value) do
@@ -26,6 +26,38 @@ describe ActiveInteraction::Inputs do
26
26
  let(:inputs) { {} }
27
27
  let(:result) { described_class.process(inputs) }
28
28
 
29
+ context 'with invalid inputs' do
30
+ let(:inputs) { nil }
31
+
32
+ it 'raises an error' do
33
+ expect { result }.to raise_error ArgumentError
34
+ end
35
+ end
36
+
37
+ context 'with non-hash inputs' do
38
+ let(:inputs) { [%i[k v]] }
39
+
40
+ it 'raises an error' do
41
+ expect { result }.to raise_error ArgumentError
42
+ end
43
+ end
44
+
45
+ context 'with ActionController::Parameters inputs' do
46
+ let(:inputs) { ActionController::Parameters.new }
47
+
48
+ it 'does not raise an error' do
49
+ expect { result }.to_not raise_error
50
+ end
51
+ end
52
+
53
+ context 'with Inputs inputs' do
54
+ let(:inputs) { ActiveInteraction::Inputs.new }
55
+
56
+ it 'does not raise an error' do
57
+ expect { result }.to_not raise_error
58
+ end
59
+ end
60
+
29
61
  context 'with simple inputs' do
30
62
  before { inputs[:key] = :value }
31
63
 
@@ -19,7 +19,7 @@ describe HashInteraction do
19
19
  before { inputs[:a] = a }
20
20
 
21
21
  it 'returns the correct value for :a' do
22
- expect(result[:a]).to eql a.with_indifferent_access
22
+ expect(result[:a]).to eql ActiveSupport::HashWithIndifferentAccess.new(a)
23
23
  end
24
24
 
25
25
  it 'returns the correct value for :b' do
@@ -12,11 +12,11 @@ TimeWithZone = Class.new do
12
12
  end
13
13
 
14
14
  def at(*args)
15
- Time.at(*args)
15
+ TimeWithZone.new(Time.at(*args) + 1)
16
16
  end
17
17
 
18
18
  def parse(*args)
19
- Time.parse(*args)
19
+ TimeWithZone.new(Time.parse(*args) + 1)
20
20
  rescue ArgumentError
21
21
  nil
22
22
  end
@@ -43,7 +43,7 @@ describe TimeInteraction do
43
43
  let(:a) { rand(1 << 16) }
44
44
 
45
45
  it 'returns the correct value' do
46
- expect(result[:a]).to eq Time.zone.at(a)
46
+ expect(result[:a]).to eq TimeWithZone.new(0).at(a)
47
47
  end
48
48
  end
49
49
 
@@ -51,7 +51,7 @@ describe TimeInteraction do
51
51
  let(:a) { '2011-12-13T14:15:16Z' }
52
52
 
53
53
  it 'returns the correct value' do
54
- expect(result[:a]).to eq Time.zone.parse(a)
54
+ expect(result[:a]).to eq TimeWithZone.new(0).parse(a)
55
55
  end
56
56
  end
57
57
 
data/spec/spec_helper.rb CHANGED
@@ -1,9 +1,3 @@
1
- # Disable code coverage for JRuby because it always reports 0% coverage.
2
- if RUBY_ENGINE != 'jruby'
3
- require 'coveralls'
4
- Coveralls.wear!
5
- end
6
-
7
1
  require 'i18n'
8
2
  I18n.config.enforce_available_locales = true if I18n.config.respond_to?(:enforce_available_locales)
9
3
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_interaction
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Lasseigne
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-01-10 00:00:00.000000000 Z
12
+ date: 2021-07-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel
@@ -32,21 +32,27 @@ dependencies:
32
32
  - !ruby/object:Gem::Version
33
33
  version: '7'
34
34
  - !ruby/object:Gem::Dependency
35
- name: actionpack
35
+ name: activesupport
36
36
  requirement: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
41
- type: :development
40
+ version: '5'
41
+ - - "<"
42
+ - !ruby/object:Gem::Version
43
+ version: '7'
44
+ type: :runtime
42
45
  prerelease: false
43
46
  version_requirements: !ruby/object:Gem::Requirement
44
47
  requirements:
45
48
  - - ">="
46
49
  - !ruby/object:Gem::Version
47
- version: '0'
50
+ version: '5'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '7'
48
54
  - !ruby/object:Gem::Dependency
49
- name: activerecord
55
+ name: actionpack
50
56
  requirement: !ruby/object:Gem::Requirement
51
57
  requirements:
52
58
  - - ">="
@@ -60,33 +66,33 @@ dependencies:
60
66
  - !ruby/object:Gem::Version
61
67
  version: '0'
62
68
  - !ruby/object:Gem::Dependency
63
- name: benchmark-ips
69
+ name: activerecord
64
70
  requirement: !ruby/object:Gem::Requirement
65
71
  requirements:
66
- - - "~>"
72
+ - - ">="
67
73
  - !ruby/object:Gem::Version
68
- version: '2.7'
74
+ version: '0'
69
75
  type: :development
70
76
  prerelease: false
71
77
  version_requirements: !ruby/object:Gem::Requirement
72
78
  requirements:
73
- - - "~>"
79
+ - - ">="
74
80
  - !ruby/object:Gem::Version
75
- version: '2.7'
81
+ version: '0'
76
82
  - !ruby/object:Gem::Dependency
77
- name: coveralls
83
+ name: benchmark-ips
78
84
  requirement: !ruby/object:Gem::Requirement
79
85
  requirements:
80
86
  - - "~>"
81
87
  - !ruby/object:Gem::Version
82
- version: '0.8'
88
+ version: '2.7'
83
89
  type: :development
84
90
  prerelease: false
85
91
  version_requirements: !ruby/object:Gem::Requirement
86
92
  requirements:
87
93
  - - "~>"
88
94
  - !ruby/object:Gem::Version
89
- version: '0.8'
95
+ version: '2.7'
90
96
  - !ruby/object:Gem::Dependency
91
97
  name: kramdown
92
98
  requirement: !ruby/object:Gem::Requirement
@@ -135,14 +141,14 @@ dependencies:
135
141
  requirements:
136
142
  - - "~>"
137
143
  - !ruby/object:Gem::Version
138
- version: '1.8'
144
+ version: 1.17.0
139
145
  type: :development
140
146
  prerelease: false
141
147
  version_requirements: !ruby/object:Gem::Requirement
142
148
  requirements:
143
149
  - - "~>"
144
150
  - !ruby/object:Gem::Version
145
- version: '1.8'
151
+ version: 1.17.0
146
152
  - !ruby/object:Gem::Dependency
147
153
  name: rubocop-rake
148
154
  requirement: !ruby/object:Gem::Requirement
@@ -247,7 +253,6 @@ files:
247
253
  - lib/active_interaction/locale/it.yml
248
254
  - lib/active_interaction/locale/ja.yml
249
255
  - lib/active_interaction/locale/pt-BR.yml
250
- - lib/active_interaction/modules/input_processor.rb
251
256
  - lib/active_interaction/modules/validation.rb
252
257
  - lib/active_interaction/version.rb
253
258
  - spec/active_interaction/base_spec.rb
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteraction
4
- # Groups inputs ending in "(*N*i)" into {GroupedInput}.
5
- #
6
- # @private
7
- module InputProcessor
8
- class << self
9
- GROUPED_INPUT_PATTERN = /\A(.+)\((\d+)i\)\z/.freeze
10
- private_constant :GROUPED_INPUT_PATTERN
11
-
12
- # Checking `syscall` is the result of what appears to be a bug in Ruby.
13
- # https://bugs.ruby-lang.org/issues/15597
14
- def reserved?(name)
15
- name.to_s.start_with?('_interaction_') ||
16
- name == :syscall ||
17
- (
18
- Base.method_defined?(name) &&
19
- !Object.method_defined?(name)
20
- ) ||
21
- (
22
- Base.private_method_defined?(name) &&
23
- !Object.private_method_defined?(name)
24
- )
25
- end
26
-
27
- def process(inputs)
28
- inputs.stringify_keys.sort.each_with_object({}) do |(k, v), h|
29
- next if reserved?(k)
30
-
31
- if (match = GROUPED_INPUT_PATTERN.match(k))
32
- assign_to_group!(h, *match.captures, v)
33
- else
34
- h[k.to_sym] = v
35
- end
36
- end
37
- end
38
-
39
- private
40
-
41
- def assign_to_group!(inputs, key, index, value)
42
- key = key.to_sym
43
-
44
- inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
45
- inputs[key][index] = value
46
- end
47
- end
48
- end
49
- end