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 +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
|
[![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
|
-
[
|
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
|