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 +4 -4
- data/CHANGELOG.md +49 -3
- data/README.md +6 -11
- data/lib/active_interaction.rb +1 -0
- data/lib/active_interaction/base.rb +34 -54
- data/lib/active_interaction/errors.rb +1 -13
- data/lib/active_interaction/filter.rb +15 -0
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +4 -0
- data/lib/active_interaction/filters/array_filter.rb +7 -5
- data/lib/active_interaction/filters/hash_filter.rb +1 -1
- data/lib/active_interaction/filters/interface_filter.rb +2 -1
- data/lib/active_interaction/filters/time_filter.rb +9 -1
- data/lib/active_interaction/inputs.rb +40 -49
- data/lib/active_interaction/modules/validation.rb +6 -15
- data/lib/active_interaction/version.rb +1 -1
- data/spec/active_interaction/base_spec.rb +26 -59
- data/spec/active_interaction/concerns/hashable_spec.rb +1 -1
- data/spec/active_interaction/filters/array_filter_spec.rb +23 -0
- data/spec/active_interaction/filters/interface_filter_spec.rb +11 -0
- data/spec/active_interaction/inputs_spec.rb +32 -0
- data/spec/active_interaction/integration/hash_interaction_spec.rb +1 -1
- data/spec/active_interaction/integration/time_interaction_spec.rb +4 -4
- data/spec/spec_helper.rb +0 -6
- metadata +23 -18
- data/lib/active_interaction/modules/input_processor.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc966bbcee80b29757efdf5bf0b6b0357b8d24610dca49017f4a8cdca64bfaa3
|
4
|
+
data.tar.gz: fe255f995b77c90c935fb14ae05660fcc973a3e7ef3e26209d5e1e187624ca21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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
|
[](https://rubygems.org/gems/active_interaction)
|
7
7
|
[](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)
|
8
|
-
[](https://coveralls.io/r/orgsync/active_interaction)
|
9
8
|
[](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
|
-
[
|
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:
|
979
|
+
unless: -> { first_name.nil? }
|
981
980
|
validates :last_name,
|
982
981
|
presence: true,
|
983
|
-
unless:
|
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][]
|
1452
|
-
[Taylor Fausak][] and
|
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
|
-
[
|
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
|
data/lib/active_interaction.rb
CHANGED
@@ -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::
|
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
|
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
|
-
|
168
|
-
|
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
|
-
@
|
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 = @
|
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
|
-
|
246
|
-
|
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
|
-
|
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[
|
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
|
256
|
+
break false unless index.between?(-input_level.size, input_level.size - 1)
|
257
257
|
|
258
|
-
input_level = input_level[
|
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
|
-
|
275
|
-
|
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
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
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
|
@@ -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' =>
|
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 (
|
76
|
+
if (keys = FILTER_NAME_OR_OPTION[klass.to_s]) && (keys & options.keys).empty?
|
75
77
|
options.merge(
|
76
|
-
"#{
|
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
|
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
|
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
|
-
|
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
|
-
|
22
|
-
|
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
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
errors <<
|
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}")
|
@@ -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
|
@@ -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
|
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
|
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
|
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.
|
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-
|
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:
|
35
|
+
name: activesupport
|
36
36
|
requirement: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
41
|
-
|
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: '
|
50
|
+
version: '5'
|
51
|
+
- - "<"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '7'
|
48
54
|
- !ruby/object:Gem::Dependency
|
49
|
-
name:
|
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:
|
69
|
+
name: activerecord
|
64
70
|
requirement: !ruby/object:Gem::Requirement
|
65
71
|
requirements:
|
66
|
-
- - "
|
72
|
+
- - ">="
|
67
73
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
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: '
|
81
|
+
version: '0'
|
76
82
|
- !ruby/object:Gem::Dependency
|
77
|
-
name:
|
83
|
+
name: benchmark-ips
|
78
84
|
requirement: !ruby/object:Gem::Requirement
|
79
85
|
requirements:
|
80
86
|
- - "~>"
|
81
87
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
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: '
|
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:
|
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:
|
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
|