active_interaction 4.0.0 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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