active_interaction 3.8.3 → 4.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -0
  3. data/README.md +93 -107
  4. data/lib/active_interaction.rb +1 -7
  5. data/lib/active_interaction/base.rb +23 -26
  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 +6 -12
  11. data/lib/active_interaction/errors.rb +3 -6
  12. data/lib/active_interaction/filter.rb +51 -37
  13. data/lib/active_interaction/filter_column.rb +0 -3
  14. data/lib/active_interaction/filters/abstract_date_time_filter.rb +34 -36
  15. data/lib/active_interaction/filters/abstract_numeric_filter.rb +27 -17
  16. data/lib/active_interaction/filters/array_filter.rb +57 -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 +19 -22
  31. data/lib/active_interaction/grouped_input.rb +0 -3
  32. data/lib/active_interaction/inputs.rb +89 -0
  33. data/lib/active_interaction/modules/input_processor.rb +1 -4
  34. data/lib/active_interaction/modules/validation.rb +9 -12
  35. data/lib/active_interaction/version.rb +1 -3
  36. data/spec/active_interaction/base_spec.rb +13 -41
  37. data/spec/active_interaction/concerns/active_modelable_spec.rb +0 -2
  38. data/spec/active_interaction/concerns/active_recordable_spec.rb +0 -2
  39. data/spec/active_interaction/concerns/hashable_spec.rb +1 -3
  40. data/spec/active_interaction/concerns/missable_spec.rb +0 -2
  41. data/spec/active_interaction/concerns/runnable_spec.rb +9 -13
  42. data/spec/active_interaction/errors_spec.rb +4 -25
  43. data/spec/active_interaction/filter_column_spec.rb +0 -2
  44. data/spec/active_interaction/filter_spec.rb +0 -2
  45. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +1 -3
  46. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -3
  47. data/spec/active_interaction/filters/array_filter_spec.rb +41 -15
  48. data/spec/active_interaction/filters/boolean_filter_spec.rb +58 -4
  49. data/spec/active_interaction/filters/date_filter_spec.rb +43 -3
  50. data/spec/active_interaction/filters/date_time_filter_spec.rb +43 -3
  51. data/spec/active_interaction/filters/decimal_filter_spec.rb +57 -3
  52. data/spec/active_interaction/filters/file_filter_spec.rb +1 -3
  53. data/spec/active_interaction/filters/float_filter_spec.rb +60 -4
  54. data/spec/active_interaction/filters/hash_filter_spec.rb +19 -9
  55. data/spec/active_interaction/filters/integer_filter_spec.rb +49 -7
  56. data/spec/active_interaction/filters/interface_filter_spec.rb +397 -24
  57. data/spec/active_interaction/filters/object_filter_spec.rb +23 -59
  58. data/spec/active_interaction/filters/record_filter_spec.rb +23 -49
  59. data/spec/active_interaction/filters/string_filter_spec.rb +15 -3
  60. data/spec/active_interaction/filters/symbol_filter_spec.rb +15 -3
  61. data/spec/active_interaction/filters/time_filter_spec.rb +65 -3
  62. data/spec/active_interaction/grouped_input_spec.rb +0 -2
  63. data/spec/active_interaction/i18n_spec.rb +3 -7
  64. data/spec/active_interaction/{modules/input_processor_spec.rb → inputs_spec.rb} +3 -5
  65. data/spec/active_interaction/integration/array_interaction_spec.rb +18 -22
  66. data/spec/active_interaction/integration/boolean_interaction_spec.rb +0 -2
  67. data/spec/active_interaction/integration/date_interaction_spec.rb +0 -2
  68. data/spec/active_interaction/integration/date_time_interaction_spec.rb +0 -2
  69. data/spec/active_interaction/integration/file_interaction_spec.rb +0 -2
  70. data/spec/active_interaction/integration/float_interaction_spec.rb +0 -2
  71. data/spec/active_interaction/integration/hash_interaction_spec.rb +0 -2
  72. data/spec/active_interaction/integration/integer_interaction_spec.rb +0 -2
  73. data/spec/active_interaction/integration/interface_interaction_spec.rb +1 -3
  74. data/spec/active_interaction/integration/object_interaction_spec.rb +0 -2
  75. data/spec/active_interaction/integration/string_interaction_spec.rb +0 -2
  76. data/spec/active_interaction/integration/symbol_interaction_spec.rb +0 -2
  77. data/spec/active_interaction/integration/time_interaction_spec.rb +14 -18
  78. data/spec/active_interaction/modules/validation_spec.rb +1 -3
  79. data/spec/spec_helper.rb +2 -6
  80. data/spec/support/concerns.rb +0 -2
  81. data/spec/support/filters.rb +13 -9
  82. data/spec/support/interactions.rb +22 -14
  83. metadata +77 -52
  84. data/lib/active_interaction/backports.rb +0 -59
  85. data/lib/active_interaction/filters/abstract_filter.rb +0 -19
  86. data/spec/active_interaction/filters/abstract_filter_spec.rb +0 -8
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -8,61 +7,60 @@ 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
 
32
15
  private
33
16
 
34
- def convert(value, context)
17
+ def klasses
18
+ [klass]
19
+ end
20
+
21
+ def matches?(value)
22
+ klasses.any? { |klass| value.is_a?(klass) }
23
+ rescue NoMethodError # BasicObject
24
+ false
25
+ end
26
+
27
+ def convert(value)
28
+ if value.respond_to?(:to_str)
29
+ value = value.to_str
30
+ value.blank? ? send(__method__, nil) : convert_string(value)
31
+ elsif value.is_a?(GroupedInput)
32
+ convert_grouped_input(value)
33
+ else
34
+ super
35
+ end
36
+ rescue ArgumentError
37
+ value
38
+ rescue NoMethodError # BasicObject
39
+ super
40
+ end
41
+
42
+ def convert_string(value)
35
43
  if format?
36
44
  klass.strptime(value, format)
37
45
  else
38
46
  klass.parse(value) ||
39
47
  (raise ArgumentError, "no time information in #{value.inspect}")
40
48
  end
41
- rescue ArgumentError
42
- _cast(value, context)
43
49
  end
44
50
 
45
- # @return [String]
51
+ def convert_grouped_input(value)
52
+ date = %w[1 2 3].map { |key| value[key] }.join('-')
53
+ time = %w[4 5 6].map { |key| value[key] }.join(':')
54
+
55
+ convert_string("#{date} #{time}")
56
+ end
57
+
46
58
  def format
47
59
  options.fetch(:format)
48
60
  end
49
61
 
50
- # @return [Boolean]
51
62
  def format?
52
63
  options.key?(:format)
53
64
  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
65
  end
68
66
  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,85 @@ module ActiveInteraction
26
25
  class ArrayFilter < Filter
27
26
  include Missable
28
27
 
28
+ FILTER_NAME_OR_OPTION = {
29
+ 'ActiveInteraction::ObjectFilter' => :class,
30
+ 'ActiveInteraction::RecordFilter' => :class,
31
+ 'ActiveInteraction::InterfaceFilter' => :from
32
+ }.freeze
33
+ private_constant :FILTER_NAME_OR_OPTION
34
+
29
35
  register :array
30
36
 
31
- def cast(value, context)
32
- case value
33
- when *classes
34
- return value if filters.empty?
37
+ private
35
38
 
36
- filter = filters.values.first
37
- value.map { |e| filter.clean(e, context) }
38
- else
39
- super
39
+ def klasses
40
+ %w[
41
+ ActiveRecord::Relation
42
+ ActiveRecord::Associations::CollectionProxy
43
+ ].each_with_object([Array]) do |name, result|
44
+ next unless (klass = name.safe_constantize)
45
+
46
+ result.push(klass)
40
47
  end
41
48
  end
42
49
 
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)
50
+ def matches?(value)
51
+ klasses.any? { |klass| value.is_a?(klass) }
52
+ rescue NoMethodError # BasicObject
53
+ false
54
+ end
46
55
 
47
- validate!(filter, names)
56
+ def adjust_output(value, context)
57
+ return value if filters.empty?
58
+
59
+ filter = filters.values.first
60
+ value.map { |e| filter.clean(e, context) }
61
+ end
62
+
63
+ def convert(value)
64
+ if value.respond_to?(:to_ary)
65
+ value.to_ary
66
+ else
67
+ super
68
+ end
69
+ rescue NoMethodError # BasicObject
70
+ super
71
+ end
48
72
 
49
- filters[filter.name] = filter
73
+ def add_option_in_place_of_name(klass, options)
74
+ if (key = FILTER_NAME_OR_OPTION[klass.to_s]) && !options.key?(key)
75
+ options.merge(
76
+ "#{key}": name.to_s.singularize.camelize.to_sym
77
+ )
78
+ else
79
+ options
50
80
  end
51
81
  end
52
82
 
53
- private
83
+ # rubocop:disable Style/MissingRespondToMissing
84
+ def method_missing(*, &block)
85
+ super do |klass, names, options|
86
+ options = add_option_in_place_of_name(klass, options)
54
87
 
55
- # @return [Array<Class>]
56
- def classes
57
- result = [Array]
88
+ filter = klass.new(names.first || '', options, &block)
58
89
 
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
90
+ filters[filters.size.to_s.to_sym] = filter
66
91
 
67
- result
92
+ validate!(filter, names)
93
+ end
68
94
  end
95
+ # rubocop:enable Style/MissingRespondToMissing
69
96
 
70
97
  # @param filter [Filter]
71
98
  # @param names [Array<Symbol>]
72
99
  #
73
100
  # @raise [InvalidFilterError]
74
101
  def validate!(filter, names)
75
- unless filters.empty?
76
- raise InvalidFilterError, 'multiple filters in array block'
77
- end
102
+ raise InvalidFilterError, 'multiple filters in array block' if filters.size > 1
78
103
 
79
- unless names.empty?
80
- raise InvalidFilterError, 'attribute names in array block'
81
- end
104
+ raise InvalidFilterError, 'attribute names in array block' unless names.empty?
82
105
 
83
- if filter.default?
84
- raise InvalidDefaultError, 'default values in array block'
85
- end
106
+ raise InvalidDefaultError, 'default values in array block' if filter.default?
86
107
 
87
108
  nil
88
109
  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
@@ -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.file(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes respond to the `rewind` method. This is useful when
@@ -16,7 +15,7 @@ module ActiveInteraction
16
15
  end
17
16
 
18
17
  # @private
19
- class FileFilter < InterfaceFilter
18
+ class FileFilter < Filter
20
19
  register :file
21
20
 
22
21
  def database_column_type
@@ -25,8 +24,10 @@ module ActiveInteraction
25
24
 
26
25
  private
27
26
 
28
- def methods
29
- [:rewind]
27
+ def matches?(object)
28
+ object.respond_to?(:rewind)
29
+ rescue NoMethodError
30
+ false
30
31
  end
31
32
  end
32
33
  end