active_interaction 3.8.2 → 4.0.3

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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +192 -0
  3. data/README.md +93 -107
  4. data/lib/active_interaction.rb +1 -7
  5. data/lib/active_interaction/base.rb +44 -67
  6. data/lib/active_interaction/concerns/active_modelable.rb +1 -3
  7. data/lib/active_interaction/concerns/active_recordable.rb +1 -6
  8. data/lib/active_interaction/concerns/hashable.rb +0 -1
  9. data/lib/active_interaction/concerns/missable.rb +0 -1
  10. data/lib/active_interaction/concerns/runnable.rb +7 -14
  11. data/lib/active_interaction/errors.rb +4 -19
  12. data/lib/active_interaction/filter.rb +66 -37
  13. data/lib/active_interaction/filter_column.rb +0 -3
  14. data/lib/active_interaction/filters/abstract_date_time_filter.rb +38 -36
  15. data/lib/active_interaction/filters/abstract_numeric_filter.rb +27 -17
  16. data/lib/active_interaction/filters/array_filter.rb +59 -36
  17. data/lib/active_interaction/filters/boolean_filter.rb +26 -12
  18. data/lib/active_interaction/filters/date_filter.rb +1 -2
  19. data/lib/active_interaction/filters/date_time_filter.rb +1 -2
  20. data/lib/active_interaction/filters/decimal_filter.rb +10 -28
  21. data/lib/active_interaction/filters/file_filter.rb +6 -5
  22. data/lib/active_interaction/filters/float_filter.rb +1 -2
  23. data/lib/active_interaction/filters/hash_filter.rb +37 -27
  24. data/lib/active_interaction/filters/integer_filter.rb +7 -8
  25. data/lib/active_interaction/filters/interface_filter.rb +48 -14
  26. data/lib/active_interaction/filters/object_filter.rb +23 -50
  27. data/lib/active_interaction/filters/record_filter.rb +10 -35
  28. data/lib/active_interaction/filters/string_filter.rb +21 -12
  29. data/lib/active_interaction/filters/symbol_filter.rb +13 -7
  30. data/lib/active_interaction/filters/time_filter.rb +24 -19
  31. data/lib/active_interaction/grouped_input.rb +0 -3
  32. data/lib/active_interaction/inputs.rb +120 -0
  33. data/lib/active_interaction/modules/validation.rb +9 -12
  34. data/lib/active_interaction/version.rb +1 -3
  35. data/spec/active_interaction/base_spec.rb +38 -99
  36. data/spec/active_interaction/concerns/active_modelable_spec.rb +0 -2
  37. data/spec/active_interaction/concerns/active_recordable_spec.rb +0 -2
  38. data/spec/active_interaction/concerns/hashable_spec.rb +0 -2
  39. data/spec/active_interaction/concerns/missable_spec.rb +0 -2
  40. data/spec/active_interaction/concerns/runnable_spec.rb +26 -12
  41. data/spec/active_interaction/errors_spec.rb +4 -25
  42. data/spec/active_interaction/filter_column_spec.rb +0 -2
  43. data/spec/active_interaction/filter_spec.rb +0 -2
  44. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +1 -3
  45. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -3
  46. data/spec/active_interaction/filters/array_filter_spec.rb +51 -14
  47. data/spec/active_interaction/filters/boolean_filter_spec.rb +58 -4
  48. data/spec/active_interaction/filters/date_filter_spec.rb +43 -3
  49. data/spec/active_interaction/filters/date_time_filter_spec.rb +43 -3
  50. data/spec/active_interaction/filters/decimal_filter_spec.rb +57 -3
  51. data/spec/active_interaction/filters/file_filter_spec.rb +1 -3
  52. data/spec/active_interaction/filters/float_filter_spec.rb +60 -4
  53. data/spec/active_interaction/filters/hash_filter_spec.rb +19 -9
  54. data/spec/active_interaction/filters/integer_filter_spec.rb +49 -7
  55. data/spec/active_interaction/filters/interface_filter_spec.rb +397 -24
  56. data/spec/active_interaction/filters/object_filter_spec.rb +23 -59
  57. data/spec/active_interaction/filters/record_filter_spec.rb +23 -49
  58. data/spec/active_interaction/filters/string_filter_spec.rb +15 -3
  59. data/spec/active_interaction/filters/symbol_filter_spec.rb +15 -3
  60. data/spec/active_interaction/filters/time_filter_spec.rb +65 -3
  61. data/spec/active_interaction/grouped_input_spec.rb +0 -2
  62. data/spec/active_interaction/i18n_spec.rb +3 -7
  63. data/spec/active_interaction/{modules/input_processor_spec.rb → inputs_spec.rb} +35 -5
  64. data/spec/active_interaction/integration/array_interaction_spec.rb +18 -22
  65. data/spec/active_interaction/integration/boolean_interaction_spec.rb +0 -2
  66. data/spec/active_interaction/integration/date_interaction_spec.rb +0 -2
  67. data/spec/active_interaction/integration/date_time_interaction_spec.rb +0 -2
  68. data/spec/active_interaction/integration/file_interaction_spec.rb +0 -2
  69. data/spec/active_interaction/integration/float_interaction_spec.rb +0 -2
  70. data/spec/active_interaction/integration/hash_interaction_spec.rb +0 -2
  71. data/spec/active_interaction/integration/integer_interaction_spec.rb +0 -2
  72. data/spec/active_interaction/integration/interface_interaction_spec.rb +1 -3
  73. data/spec/active_interaction/integration/object_interaction_spec.rb +0 -2
  74. data/spec/active_interaction/integration/string_interaction_spec.rb +0 -2
  75. data/spec/active_interaction/integration/symbol_interaction_spec.rb +0 -2
  76. data/spec/active_interaction/integration/time_interaction_spec.rb +14 -18
  77. data/spec/active_interaction/modules/validation_spec.rb +1 -3
  78. data/spec/spec_helper.rb +2 -6
  79. data/spec/support/concerns.rb +0 -2
  80. data/spec/support/filters.rb +13 -9
  81. data/spec/support/interactions.rb +22 -14
  82. metadata +81 -57
  83. data/lib/active_interaction/backports.rb +0 -59
  84. data/lib/active_interaction/filters/abstract_filter.rb +0 -19
  85. data/lib/active_interaction/modules/input_processor.rb +0 -52
  86. data/spec/active_interaction/filters/abstract_filter_spec.rb +0 -8
@@ -1,10 +1,7 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
4
  # A minimal implementation of an `ActiveRecord::ConnectionAdapters::Column`.
6
- #
7
- # @since 1.2.0
8
5
  class FilterColumn
9
6
  # @return [nil]
10
7
  attr_reader :limit
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -8,61 +7,64 @@ module ActiveInteraction
8
7
  # objects.
9
8
  #
10
9
  # @private
11
- class AbstractDateTimeFilter < AbstractFilter
12
- alias _cast cast
13
- private :_cast
14
-
15
- def cast(value, context)
16
- case value
17
- when String
18
- convert(value, context)
19
- when GroupedInput
20
- convert(stringify(value), context)
21
- when *klasses
22
- value
23
- else
24
- super
25
- end
26
- end
27
-
10
+ class AbstractDateTimeFilter < Filter
28
11
  def database_column_type
29
12
  self.class.slug
30
13
  end
31
14
 
15
+ def accepts_grouped_inputs?
16
+ true
17
+ end
18
+
32
19
  private
33
20
 
34
- def convert(value, context)
21
+ def klasses
22
+ [klass]
23
+ end
24
+
25
+ def matches?(value)
26
+ klasses.any? { |klass| value.is_a?(klass) }
27
+ rescue NoMethodError # BasicObject
28
+ false
29
+ end
30
+
31
+ def convert(value)
32
+ if value.respond_to?(:to_str)
33
+ value = value.to_str
34
+ value.blank? ? send(__method__, nil) : convert_string(value)
35
+ elsif value.is_a?(GroupedInput)
36
+ convert_grouped_input(value)
37
+ else
38
+ super
39
+ end
40
+ rescue ArgumentError
41
+ value
42
+ rescue NoMethodError # BasicObject
43
+ super
44
+ end
45
+
46
+ def convert_string(value)
35
47
  if format?
36
48
  klass.strptime(value, format)
37
49
  else
38
50
  klass.parse(value) ||
39
51
  (raise ArgumentError, "no time information in #{value.inspect}")
40
52
  end
41
- rescue ArgumentError
42
- _cast(value, context)
43
53
  end
44
54
 
45
- # @return [String]
55
+ def convert_grouped_input(value)
56
+ date = %w[1 2 3].map { |key| value[key] }.join('-')
57
+ time = %w[4 5 6].map { |key| value[key] }.join(':')
58
+
59
+ convert_string("#{date} #{time}")
60
+ end
61
+
46
62
  def format
47
63
  options.fetch(:format)
48
64
  end
49
65
 
50
- # @return [Boolean]
51
66
  def format?
52
67
  options.key?(:format)
53
68
  end
54
-
55
- # @return [Array<Class>]
56
- def klasses
57
- [klass]
58
- end
59
-
60
- # @return [String]
61
- def stringify(value)
62
- date = %w[1 2 3].map { |key| value[key] }.join('-')
63
- time = %w[4 5 6].map { |key| value[key] }.join(':')
64
-
65
- "#{date} #{time}"
66
- end
67
69
  end
68
70
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -7,31 +6,42 @@ module ActiveInteraction
7
6
  # Common logic for filters that handle numeric objects.
8
7
  #
9
8
  # @private
10
- class AbstractNumericFilter < AbstractFilter
11
- alias _cast cast
12
- private :_cast
9
+ class AbstractNumericFilter < Filter
10
+ def database_column_type
11
+ self.class.slug
12
+ end
13
+
14
+ private
15
+
16
+ def matches?(value)
17
+ value.is_a?(klass)
18
+ rescue NoMethodError # BasicObject
19
+ false
20
+ end
13
21
 
14
- def cast(value, context)
15
- case value
16
- when klass
17
- value
18
- when Numeric, String
19
- convert(value, context)
22
+ def convert(value)
23
+ if value.is_a?(Numeric)
24
+ safe_converter(value)
25
+ elsif value.respond_to?(:to_int)
26
+ safe_converter(value.to_int)
27
+ elsif value.respond_to?(:to_str)
28
+ value = value.to_str
29
+ value.blank? ? send(__method__, nil) : safe_converter(value)
20
30
  else
21
31
  super
22
32
  end
33
+ rescue NoMethodError # BasicObject
34
+ super
23
35
  end
24
36
 
25
- def database_column_type
26
- self.class.slug
37
+ def converter(value)
38
+ Kernel.public_send(klass.name, value)
27
39
  end
28
40
 
29
- private
30
-
31
- def convert(value, context)
32
- Kernel.public_send(klass.name, value)
41
+ def safe_converter(value)
42
+ converter(value)
33
43
  rescue ArgumentError
34
- _cast(value, context)
44
+ value
35
45
  end
36
46
  end
37
47
  end
@@ -1,8 +1,7 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
- class Base
4
+ class Base # rubocop:disable Lint/EmptyClass
6
5
  # @!method self.array(*attributes, options = {}, &block)
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Arrays.
@@ -26,63 +25,87 @@ module ActiveInteraction
26
25
  class ArrayFilter < Filter
27
26
  include Missable
28
27
 
28
+ # The array starts with the class override key and then contains any
29
+ # additional options which halt explicit setting of the class.
30
+ FILTER_NAME_OR_OPTION = {
31
+ 'ActiveInteraction::ObjectFilter' => [:class].freeze,
32
+ 'ActiveInteraction::RecordFilter' => [:class].freeze,
33
+ 'ActiveInteraction::InterfaceFilter' => %i[from methods].freeze
34
+ }.freeze
35
+ private_constant :FILTER_NAME_OR_OPTION
36
+
29
37
  register :array
30
38
 
31
- def cast(value, context)
32
- case value
33
- when *classes
34
- return value if filters.empty?
39
+ private
35
40
 
36
- filter = filters.values.first
37
- value.map { |e| filter.clean(e, context) }
38
- else
39
- super
41
+ def klasses
42
+ %w[
43
+ ActiveRecord::Relation
44
+ ActiveRecord::Associations::CollectionProxy
45
+ ].each_with_object([Array]) do |name, result|
46
+ next unless (klass = name.safe_constantize)
47
+
48
+ result.push(klass)
40
49
  end
41
50
  end
42
51
 
43
- def method_missing(*, &block) # rubocop:disable Style/MethodMissing
44
- super do |klass, names, options|
45
- filter = klass.new(name.to_s.singularize.to_sym, options, &block)
52
+ def matches?(value)
53
+ klasses.any? { |klass| value.is_a?(klass) }
54
+ rescue NoMethodError # BasicObject
55
+ false
56
+ end
46
57
 
47
- validate!(filter, names)
58
+ def adjust_output(value, context)
59
+ return value if filters.empty?
60
+
61
+ filter = filters.values.first
62
+ value.map { |e| filter.clean(e, context) }
63
+ end
64
+
65
+ def convert(value)
66
+ if value.respond_to?(:to_ary)
67
+ value.to_ary
68
+ else
69
+ super
70
+ end
71
+ rescue NoMethodError # BasicObject
72
+ super
73
+ end
48
74
 
49
- filters[filter.name] = filter
75
+ def add_option_in_place_of_name(klass, options)
76
+ if (keys = FILTER_NAME_OR_OPTION[klass.to_s]) && (keys && options.keys).empty?
77
+ options.merge(
78
+ "#{keys.first}": name.to_s.singularize.camelize.to_sym
79
+ )
80
+ else
81
+ options
50
82
  end
51
83
  end
52
84
 
53
- private
85
+ # rubocop:disable Style/MissingRespondToMissing
86
+ def method_missing(*, &block)
87
+ super do |klass, names, options|
88
+ options = add_option_in_place_of_name(klass, options)
54
89
 
55
- # @return [Array<Class>]
56
- def classes
57
- result = [Array]
90
+ filter = klass.new(names.first || '', options, &block)
58
91
 
59
- %w[
60
- ActiveRecord::Relation
61
- ActiveRecord::Associations::CollectionProxy
62
- ].each do |name|
63
- next unless (klass = name.safe_constantize)
64
- result.push(klass)
65
- end
92
+ filters[filters.size.to_s.to_sym] = filter
66
93
 
67
- result
94
+ validate!(filter, names)
95
+ end
68
96
  end
97
+ # rubocop:enable Style/MissingRespondToMissing
69
98
 
70
99
  # @param filter [Filter]
71
100
  # @param names [Array<Symbol>]
72
101
  #
73
102
  # @raise [InvalidFilterError]
74
103
  def validate!(filter, names)
75
- unless filters.empty?
76
- raise InvalidFilterError, 'multiple filters in array block'
77
- end
104
+ raise InvalidFilterError, 'multiple filters in array block' if filters.size > 1
78
105
 
79
- unless names.empty?
80
- raise InvalidFilterError, 'attribute names in array block'
81
- end
106
+ raise InvalidFilterError, 'attribute names in array block' unless names.empty?
82
107
 
83
- if filter.default?
84
- raise InvalidDefaultError, 'default values in array block'
85
- end
108
+ raise InvalidDefaultError, 'default values in array block' if filter.default?
86
109
 
87
110
  nil
88
111
  end
@@ -1,13 +1,12 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
- class Base
4
+ class Base # rubocop:disable Lint/EmptyClass
6
5
  # @!method self.boolean(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
- # the attributes are Booleans. The strings `"1"` and `"true"`
9
- # (case-insensitive) are converted to `true` while the strings `"0"`
10
- # and `"false"` are converted to `false`.
7
+ # the attributes are Booleans. The strings `"1"`, `"true"`, and `"on"`
8
+ # (case-insensitive) are converted to `true` while the strings `"0"`,
9
+ # `"false"`, and `"off"` are converted to `false`.
11
10
  #
12
11
  # @!macro filter_method_params
13
12
  #
@@ -19,19 +18,34 @@ module ActiveInteraction
19
18
  class BooleanFilter < Filter
20
19
  register :boolean
21
20
 
22
- def cast(value, _interaction)
21
+ def database_column_type
22
+ self.class.slug
23
+ end
24
+
25
+ private
26
+
27
+ def matches?(value)
28
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
29
+ rescue NoMethodError # BasicObject
30
+ false
31
+ end
32
+
33
+ def convert(value)
34
+ if value.respond_to?(:to_str)
35
+ value = value.to_str
36
+ value = nil if value.blank?
37
+ end
38
+
23
39
  case value
24
- when FalseClass, /\A(?:0|false|off)\z/i
40
+ when /\A(?:0|false|off)\z/i
25
41
  false
26
- when TrueClass, /\A(?:1|true|on)\z/i
42
+ when /\A(?:1|true|on)\z/i
27
43
  true
28
44
  else
29
45
  super
30
46
  end
31
- end
32
-
33
- def database_column_type
34
- self.class.slug
47
+ rescue NoMethodError # BasicObject
48
+ super
35
49
  end
36
50
  end
37
51
  end
@@ -1,8 +1,7 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
- class Base
4
+ class Base # rubocop:disable Lint/EmptyClass
6
5
  # @!method self.date(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Dates. String values are processed using `parse`
@@ -1,8 +1,7 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
- class Base
4
+ class Base # rubocop:disable Lint/EmptyClass
6
5
  # @!method self.date_time(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are DateTimes. String values are processed using
@@ -1,10 +1,9 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'bigdecimal'
5
4
 
6
5
  module ActiveInteraction
7
- class Base
6
+ class Base # rubocop:disable Lint/EmptyClass
8
7
  # @!method self.decimal(*attributes, options = {})
9
8
  # Creates accessors for the attributes and ensures that values passed to
10
9
  # the attributes are BigDecimals. Numerics and String values are
@@ -14,46 +13,29 @@ module ActiveInteraction
14
13
  #
15
14
  # @example
16
15
  # decimal :amount, digits: 4
17
- #
18
- # @since 1.2.0
19
16
  end
20
17
 
21
18
  # @private
22
19
  class DecimalFilter < AbstractNumericFilter
23
20
  register :decimal
24
21
 
25
- def cast(value, _interaction)
26
- case value
27
- when Numeric
28
- BigDecimal(value, digits)
29
- when String
30
- decimal_from_string(value)
31
- else
32
- super
33
- end
34
- end
35
-
36
22
  private
37
23
 
38
- # @return [Integer]
39
24
  def digits
40
25
  options.fetch(:digits, 0)
41
26
  end
42
27
 
43
- # @param value [String] string that has to be converted
44
- #
45
- # @return [BigDecimal]
46
- #
47
- # @raise [InvalidValueError] if given value can not be converted
48
- def decimal_from_string(value)
49
- Float(value)
50
- BigDecimal(value, digits)
51
- rescue ArgumentError
52
- raise InvalidValueError, "Given value: #{value.inspect}"
53
- end
54
-
55
28
  def klass
56
29
  BigDecimal
57
30
  end
31
+
32
+ def converter(value)
33
+ # Ruby < 2.4 does not throw an error in BigDecimal
34
+ # for invalid strings. We'll simulate the error by
35
+ # calling Float.
36
+ Float(value) if value.is_a?(String)
37
+
38
+ BigDecimal(value, digits)
39
+ end
58
40
  end
59
41
  end