active_interaction 4.0.5 → 5.0.0
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 +149 -6
- data/README.md +67 -32
- data/lib/active_interaction/array_input.rb +77 -0
- data/lib/active_interaction/base.rb +14 -98
- data/lib/active_interaction/concerns/active_recordable.rb +3 -3
- data/lib/active_interaction/concerns/missable.rb +2 -2
- data/lib/active_interaction/errors.rb +6 -88
- data/lib/active_interaction/exceptions.rb +47 -0
- data/lib/active_interaction/filter/column.rb +59 -0
- data/lib/active_interaction/filter/error.rb +40 -0
- data/lib/active_interaction/filter.rb +44 -53
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
- data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
- data/lib/active_interaction/filters/array_filter.rb +36 -10
- data/lib/active_interaction/filters/boolean_filter.rb +4 -3
- data/lib/active_interaction/filters/date_filter.rb +1 -1
- data/lib/active_interaction/filters/date_time_filter.rb +1 -1
- data/lib/active_interaction/filters/decimal_filter.rb +1 -1
- data/lib/active_interaction/filters/float_filter.rb +1 -1
- data/lib/active_interaction/filters/hash_filter.rb +23 -15
- data/lib/active_interaction/filters/integer_filter.rb +1 -1
- data/lib/active_interaction/filters/interface_filter.rb +12 -12
- data/lib/active_interaction/filters/object_filter.rb +9 -3
- data/lib/active_interaction/filters/record_filter.rb +21 -11
- data/lib/active_interaction/filters/string_filter.rb +1 -1
- data/lib/active_interaction/filters/symbol_filter.rb +1 -1
- data/lib/active_interaction/filters/time_filter.rb +4 -4
- data/lib/active_interaction/hash_input.rb +43 -0
- data/lib/active_interaction/input.rb +23 -0
- data/lib/active_interaction/inputs.rb +157 -46
- data/lib/active_interaction/locale/en.yml +0 -1
- data/lib/active_interaction/locale/fr.yml +0 -1
- data/lib/active_interaction/locale/it.yml +0 -1
- data/lib/active_interaction/locale/ja.yml +0 -1
- data/lib/active_interaction/locale/pt-BR.yml +0 -1
- data/lib/active_interaction/modules/validation.rb +6 -17
- data/lib/active_interaction/version.rb +1 -1
- data/lib/active_interaction.rb +43 -36
- data/spec/active_interaction/array_input_spec.rb +166 -0
- data/spec/active_interaction/base_spec.rb +15 -240
- data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
- data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
- data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
- data/spec/active_interaction/concerns/missable_spec.rb +9 -9
- data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
- data/spec/active_interaction/errors_spec.rb +60 -43
- data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
- data/spec/active_interaction/filter_spec.rb +6 -6
- data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/array_filter_spec.rb +99 -24
- data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
- data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
- data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
- data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
- data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
- data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
- data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
- data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
- data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
- data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
- data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
- data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
- data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
- data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
- data/spec/active_interaction/hash_input_spec.rb +58 -0
- data/spec/active_interaction/i18n_spec.rb +22 -17
- data/spec/active_interaction/inputs_spec.rb +167 -23
- data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
- data/spec/active_interaction/modules/validation_spec.rb +8 -31
- data/spec/spec_helper.rb +8 -0
- data/spec/support/concerns.rb +2 -2
- data/spec/support/filters.rb +27 -51
- data/spec/support/interactions.rb +4 -4
- metadata +45 -95
- data/lib/active_interaction/filter_column.rb +0 -57
@@ -8,6 +8,8 @@ module ActiveInteraction
|
|
8
8
|
#
|
9
9
|
# @!macro filter_method_params
|
10
10
|
# @param block [Proc] filter method to apply to each element
|
11
|
+
# @option options [Boolean] :index_errors (ActiveRecord.index_nested_attribute_errors) returns errors with an
|
12
|
+
# index
|
11
13
|
#
|
12
14
|
# @example
|
13
15
|
# array :ids
|
@@ -36,8 +38,37 @@ module ActiveInteraction
|
|
36
38
|
|
37
39
|
register :array
|
38
40
|
|
41
|
+
def process(value, context)
|
42
|
+
input = super
|
43
|
+
|
44
|
+
return ArrayInput.new(self, value: input.value, error: input.errors.first) if input.errors.any?
|
45
|
+
return ArrayInput.new(self, value: default(context), error: input.errors.first) if input.value.nil?
|
46
|
+
|
47
|
+
value = input.value
|
48
|
+
error = nil
|
49
|
+
children = []
|
50
|
+
|
51
|
+
unless filters.empty?
|
52
|
+
value.each do |item|
|
53
|
+
children.push(filters[:'0'].process(item, context))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
ArrayInput.new(self, value: value, error: error, children: children, index_errors: index_errors?)
|
58
|
+
end
|
59
|
+
|
39
60
|
private
|
40
61
|
|
62
|
+
def index_errors?
|
63
|
+
default =
|
64
|
+
if ::ActiveRecord.respond_to?(:index_nested_attribute_errors)
|
65
|
+
::ActiveRecord.index_nested_attribute_errors # Moved to here in Rails 7.0
|
66
|
+
else
|
67
|
+
::ActiveRecord::Base.index_nested_attribute_errors
|
68
|
+
end
|
69
|
+
options.fetch(:index_errors, default)
|
70
|
+
end
|
71
|
+
|
41
72
|
def klasses
|
42
73
|
%w[
|
43
74
|
ActiveRecord::Relation
|
@@ -55,16 +86,13 @@ module ActiveInteraction
|
|
55
86
|
false
|
56
87
|
end
|
57
88
|
|
58
|
-
def adjust_output(value,
|
59
|
-
|
60
|
-
|
61
|
-
filter = filters.values.first
|
62
|
-
value.map { |e| filter.clean(e, context) }
|
89
|
+
def adjust_output(value, _context)
|
90
|
+
value.to_a
|
63
91
|
end
|
64
92
|
|
65
93
|
def convert(value)
|
66
94
|
if value.respond_to?(:to_ary)
|
67
|
-
value.to_ary
|
95
|
+
[value.to_ary, nil]
|
68
96
|
else
|
69
97
|
super
|
70
98
|
end
|
@@ -91,7 +119,7 @@ module ActiveInteraction
|
|
91
119
|
|
92
120
|
filters[filters.size.to_s.to_sym] = filter
|
93
121
|
|
94
|
-
validate!(
|
122
|
+
validate!(names)
|
95
123
|
end
|
96
124
|
end
|
97
125
|
# rubocop:enable Style/MissingRespondToMissing
|
@@ -100,13 +128,11 @@ module ActiveInteraction
|
|
100
128
|
# @param names [Array<Symbol>]
|
101
129
|
#
|
102
130
|
# @raise [InvalidFilterError]
|
103
|
-
def validate!(
|
131
|
+
def validate!(names)
|
104
132
|
raise InvalidFilterError, 'multiple filters in array block' if filters.size > 1
|
105
133
|
|
106
134
|
raise InvalidFilterError, 'attribute names in array block' unless names.empty?
|
107
135
|
|
108
|
-
raise InvalidDefaultError, 'default values in array block' if filter.default?
|
109
|
-
|
110
136
|
nil
|
111
137
|
end
|
112
138
|
end
|
@@ -6,7 +6,8 @@ module ActiveInteraction
|
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are Booleans. The strings `"1"`, `"true"`, and `"on"`
|
8
8
|
# (case-insensitive) are converted to `true` while the strings `"0"`,
|
9
|
-
# `"false"`, and `"off"` are converted to `false`.
|
9
|
+
# `"false"`, and `"off"` are converted to `false`. Blank strings are
|
10
|
+
# treated as a `nil` value.
|
10
11
|
#
|
11
12
|
# @!macro filter_method_params
|
12
13
|
#
|
@@ -38,9 +39,9 @@ module ActiveInteraction
|
|
38
39
|
|
39
40
|
case value
|
40
41
|
when /\A(?:0|false|off)\z/i
|
41
|
-
false
|
42
|
+
[false, nil]
|
42
43
|
when /\A(?:1|true|on)\z/i
|
43
|
-
true
|
44
|
+
[true, nil]
|
44
45
|
else
|
45
46
|
super
|
46
47
|
end
|
@@ -6,7 +6,7 @@ module ActiveInteraction
|
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are Dates. String values are processed using `parse`
|
8
8
|
# unless the format option is given, in which case they will be
|
9
|
-
# processed with `strptime`.
|
9
|
+
# processed with `strptime`. Blank strings are treated as a `nil` value.
|
10
10
|
#
|
11
11
|
# @!macro filter_method_params
|
12
12
|
# @option options [String] :format parse strings using this format string
|
@@ -6,7 +6,7 @@ module ActiveInteraction
|
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are DateTimes. String values are processed using
|
8
8
|
# `parse` unless the format option is given, in which case they will be
|
9
|
-
# processed with `strptime`.
|
9
|
+
# processed with `strptime`. Blank strings are treated as a `nil` value.
|
10
10
|
#
|
11
11
|
# @!macro filter_method_params
|
12
12
|
# @option options [String] :format parse strings using this format string
|
@@ -7,7 +7,7 @@ module ActiveInteraction
|
|
7
7
|
# @!method self.decimal(*attributes, options = {})
|
8
8
|
# Creates accessors for the attributes and ensures that values passed to
|
9
9
|
# the attributes are BigDecimals. Numerics and String values are
|
10
|
-
# converted into BigDecimals.
|
10
|
+
# converted into BigDecimals. Blank strings are treated as a `nil` value.
|
11
11
|
#
|
12
12
|
# @!macro filter_method_params
|
13
13
|
#
|
@@ -5,7 +5,7 @@ module ActiveInteraction
|
|
5
5
|
# @!method self.float(*attributes, options = {})
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are Floats. Integer and String values are converted
|
8
|
-
# into Floats.
|
8
|
+
# into Floats. Blank strings are treated as a `nil` value.
|
9
9
|
#
|
10
10
|
# @!macro filter_method_params
|
11
11
|
#
|
@@ -25,6 +25,26 @@ module ActiveInteraction
|
|
25
25
|
|
26
26
|
register :hash
|
27
27
|
|
28
|
+
def process(value, context)
|
29
|
+
input = super
|
30
|
+
|
31
|
+
return HashInput.new(self, value: input.value, error: input.errors.first) if input.errors.first
|
32
|
+
return HashInput.new(self, value: default(context), error: input.errors.first) if input.value.nil?
|
33
|
+
|
34
|
+
value = strip? ? HashWithIndifferentAccess.new : input.value
|
35
|
+
error = nil
|
36
|
+
children = {}
|
37
|
+
|
38
|
+
filters.each do |name, filter|
|
39
|
+
filter.process(input.value[name], context).tap do |result|
|
40
|
+
value[name] = result.value
|
41
|
+
children[name.to_sym] = result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
HashInput.new(self, value: value, error: error, children: children)
|
46
|
+
end
|
47
|
+
|
28
48
|
private
|
29
49
|
|
30
50
|
def matches?(value)
|
@@ -33,29 +53,17 @@ module ActiveInteraction
|
|
33
53
|
false
|
34
54
|
end
|
35
55
|
|
36
|
-
def clean_value(hash, name, filter, value, context)
|
37
|
-
hash[name] = filter.clean(value[name], context)
|
38
|
-
rescue InvalidValueError, MissingValueError
|
39
|
-
raise InvalidNestedValueError.new(name, value[name])
|
40
|
-
end
|
41
|
-
|
42
56
|
def strip?
|
43
57
|
options.fetch(:strip, true)
|
44
58
|
end
|
45
59
|
|
46
|
-
def adjust_output(value,
|
47
|
-
|
48
|
-
|
49
|
-
initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
|
50
|
-
|
51
|
-
filters.each_with_object(initial) do |(name, filter), hash|
|
52
|
-
clean_value(hash, name.to_s, filter, value, context)
|
53
|
-
end
|
60
|
+
def adjust_output(value, _context)
|
61
|
+
ActiveSupport::HashWithIndifferentAccess.new(value)
|
54
62
|
end
|
55
63
|
|
56
64
|
def convert(value)
|
57
65
|
if value.respond_to?(:to_hash)
|
58
|
-
value.to_hash
|
66
|
+
[value.to_hash, nil]
|
59
67
|
else
|
60
68
|
super
|
61
69
|
end
|
@@ -5,7 +5,7 @@ module ActiveInteraction
|
|
5
5
|
# @!method self.integer(*attributes, options = {})
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are Integers. String values are converted into
|
8
|
-
# Integers.
|
8
|
+
# Integers. Blank strings are treated as a `nil` value.
|
9
9
|
#
|
10
10
|
# @!macro filter_method_params
|
11
11
|
# @option options [Integer] :base (10) The base used to convert strings
|
@@ -47,34 +47,34 @@ module ActiveInteraction
|
|
47
47
|
"constant #{const_name.inspect} does not exist"
|
48
48
|
end
|
49
49
|
|
50
|
-
def matches?(
|
51
|
-
return false if
|
52
|
-
return matches_methods?(
|
50
|
+
def matches?(value)
|
51
|
+
return false if value == nil # rubocop:disable Style/NilComparison
|
52
|
+
return matches_methods?(value) if options.key?(:methods)
|
53
53
|
|
54
54
|
const = from
|
55
|
-
if checking_class_inheritance?(
|
56
|
-
class_inherits_from?(
|
55
|
+
if checking_class_inheritance?(value, const)
|
56
|
+
class_inherits_from?(value, const)
|
57
57
|
else
|
58
|
-
singleton_ancestor?(
|
58
|
+
singleton_ancestor?(value, const)
|
59
59
|
end
|
60
60
|
rescue NoMethodError
|
61
61
|
false
|
62
62
|
end
|
63
63
|
|
64
|
-
def matches_methods?(
|
65
|
-
options[:methods].all? { |method|
|
64
|
+
def matches_methods?(value)
|
65
|
+
options[:methods].all? { |method| value.respond_to?(method) }
|
66
66
|
end
|
67
67
|
|
68
|
-
def checking_class_inheritance?(
|
69
|
-
|
68
|
+
def checking_class_inheritance?(value, from)
|
69
|
+
value.is_a?(Class) && from.is_a?(Class)
|
70
70
|
end
|
71
71
|
|
72
72
|
def class_inherits_from?(klass, inherits_from)
|
73
73
|
klass != inherits_from && klass.ancestors.include?(inherits_from)
|
74
74
|
end
|
75
75
|
|
76
|
-
def singleton_ancestor?(
|
77
|
-
|
76
|
+
def singleton_ancestor?(value, from)
|
77
|
+
value.class != from && value.singleton_class.ancestors.include?(from)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
end
|
@@ -38,19 +38,25 @@ module ActiveInteraction
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def matches?(value)
|
41
|
+
return false if value == nil # rubocop:disable Style/NilComparison
|
42
|
+
|
41
43
|
value.class <= klass
|
42
44
|
rescue NoMethodError
|
43
45
|
false
|
44
46
|
end
|
45
47
|
|
46
48
|
def convert(value)
|
47
|
-
converter(value)
|
48
|
-
|
49
|
+
result = converter(value)
|
50
|
+
|
51
|
+
if result.nil?
|
52
|
+
[value, Filter::Error.new(self, :invalid_type)]
|
53
|
+
else
|
54
|
+
[result, nil]
|
49
55
|
end
|
50
56
|
rescue StandardError => e
|
51
57
|
raise e if e.is_a?(InvalidConverterError)
|
52
58
|
|
53
|
-
|
59
|
+
[value, Filter::Error.new(self, :invalid_type)]
|
54
60
|
end
|
55
61
|
|
56
62
|
def converter(value)
|
@@ -4,7 +4,9 @@ module ActiveInteraction
|
|
4
4
|
class Base # rubocop:disable Lint/EmptyClass
|
5
5
|
# @!method self.record(*attributes, options = {})
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
|
-
# the attributes are the correct class.
|
7
|
+
# the attributes are the correct class. Blank strings passed in will be
|
8
|
+
# treated as `nil` and the `finder` will not be called.
|
9
|
+
|
8
10
|
#
|
9
11
|
# @!macro filter_method_params
|
10
12
|
# @option options [Class, String, Symbol] :class (use the attribute name)
|
@@ -44,20 +46,28 @@ module ActiveInteraction
|
|
44
46
|
end
|
45
47
|
|
46
48
|
def convert(value)
|
47
|
-
|
48
|
-
find(klass, value, finder)
|
49
|
-
end
|
49
|
+
return [nil, nil] if blank_string?(value)
|
50
50
|
|
51
|
-
|
52
|
-
result = klass
|
51
|
+
finder = options.fetch(:finder, :find)
|
52
|
+
result = find(klass, value, finder)
|
53
53
|
|
54
|
-
|
54
|
+
if result.nil?
|
55
|
+
[value, Filter::Error.new(self, :invalid_type)]
|
56
|
+
else
|
57
|
+
[result, nil]
|
58
|
+
end
|
59
|
+
end
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
def blank_string?(value)
|
62
|
+
value.is_a?(String) && value.blank?
|
63
|
+
rescue NoMethodError # BasicObject
|
64
|
+
false
|
65
|
+
end
|
59
66
|
|
60
|
-
|
67
|
+
def find(klass, value, finder)
|
68
|
+
klass.public_send(finder, value)
|
69
|
+
rescue StandardError
|
70
|
+
nil
|
61
71
|
end
|
62
72
|
end
|
63
73
|
end
|
@@ -6,9 +6,9 @@ module ActiveInteraction
|
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are Times. Numeric values are processed using `at`.
|
8
8
|
# Strings are processed using `parse` unless the format option is
|
9
|
-
# given, in which case they will be processed with `strptime`.
|
10
|
-
# `Time.zone` is available it
|
11
|
-
# zone aware.
|
9
|
+
# given, in which case they will be processed with `strptime`. Blank
|
10
|
+
# strings are treated as a `nil` value. If `Time.zone` is available it
|
11
|
+
# will be used so that the values are time zone aware.
|
12
12
|
#
|
13
13
|
# @!macro filter_method_params
|
14
14
|
# @option options [String] :format parse strings using this format string
|
@@ -59,7 +59,7 @@ module ActiveInteraction
|
|
59
59
|
value = value.to_int if value.respond_to?(:to_int)
|
60
60
|
|
61
61
|
if value.is_a?(Numeric)
|
62
|
-
klass.at(value)
|
62
|
+
[klass.at(value), nil]
|
63
63
|
else
|
64
64
|
super
|
65
65
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInteraction
|
4
|
+
# Represents a processed hash input.
|
5
|
+
class HashInput < Input
|
6
|
+
# @private
|
7
|
+
def initialize(filter, value: nil, error: nil, children: {})
|
8
|
+
super(filter, value: value, error: error)
|
9
|
+
|
10
|
+
@children = children
|
11
|
+
end
|
12
|
+
|
13
|
+
# @overload children
|
14
|
+
# Child inputs if nested filters are used.
|
15
|
+
#
|
16
|
+
# @return [Hash{ Symbol => Input, ArrayInput, HashInput }]
|
17
|
+
attr_reader :children
|
18
|
+
|
19
|
+
# Any errors that occurred during processing.
|
20
|
+
#
|
21
|
+
# @return [Filter::Error]
|
22
|
+
def errors
|
23
|
+
return @errors if defined?(@errors)
|
24
|
+
|
25
|
+
return @errors = super if @error
|
26
|
+
|
27
|
+
child_errors = get_errors(children)
|
28
|
+
|
29
|
+
return @errors = super if child_errors.empty?
|
30
|
+
|
31
|
+
@errors ||=
|
32
|
+
child_errors.map do |error|
|
33
|
+
Filter::Error.new(error.filter, error.type, name: :"#{@filter.name}.#{error.name}")
|
34
|
+
end.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def get_errors(children)
|
40
|
+
children.values.flat_map(&:errors)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInteraction
|
4
|
+
# Represents a processed input.
|
5
|
+
class Input
|
6
|
+
# @private
|
7
|
+
def initialize(filter, value: nil, error: nil)
|
8
|
+
@filter = filter
|
9
|
+
@value = value
|
10
|
+
@error = error
|
11
|
+
end
|
12
|
+
|
13
|
+
# The processed input value.
|
14
|
+
attr_reader :value
|
15
|
+
|
16
|
+
# Any errors that occurred during processing.
|
17
|
+
#
|
18
|
+
# @return [Filter::Error]
|
19
|
+
def errors
|
20
|
+
@errors ||= Array(@error)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|