active_interaction 3.8.1 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +190 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +93 -107
  5. data/lib/active_interaction.rb +1 -7
  6. data/lib/active_interaction/base.rb +31 -35
  7. data/lib/active_interaction/concerns/active_modelable.rb +1 -3
  8. data/lib/active_interaction/concerns/active_recordable.rb +1 -6
  9. data/lib/active_interaction/concerns/hashable.rb +0 -1
  10. data/lib/active_interaction/concerns/missable.rb +0 -1
  11. data/lib/active_interaction/concerns/runnable.rb +12 -24
  12. data/lib/active_interaction/errors.rb +6 -19
  13. data/lib/active_interaction/filter.rb +51 -37
  14. data/lib/active_interaction/filter_column.rb +0 -3
  15. data/lib/active_interaction/filters/abstract_date_time_filter.rb +34 -36
  16. data/lib/active_interaction/filters/abstract_numeric_filter.rb +27 -17
  17. data/lib/active_interaction/filters/array_filter.rb +59 -36
  18. data/lib/active_interaction/filters/boolean_filter.rb +26 -12
  19. data/lib/active_interaction/filters/date_filter.rb +1 -2
  20. data/lib/active_interaction/filters/date_time_filter.rb +1 -2
  21. data/lib/active_interaction/filters/decimal_filter.rb +10 -28
  22. data/lib/active_interaction/filters/file_filter.rb +6 -5
  23. data/lib/active_interaction/filters/float_filter.rb +1 -2
  24. data/lib/active_interaction/filters/hash_filter.rb +37 -27
  25. data/lib/active_interaction/filters/integer_filter.rb +7 -8
  26. data/lib/active_interaction/filters/interface_filter.rb +48 -14
  27. data/lib/active_interaction/filters/object_filter.rb +23 -50
  28. data/lib/active_interaction/filters/record_filter.rb +10 -35
  29. data/lib/active_interaction/filters/string_filter.rb +21 -12
  30. data/lib/active_interaction/filters/symbol_filter.rb +13 -7
  31. data/lib/active_interaction/filters/time_filter.rb +24 -19
  32. data/lib/active_interaction/grouped_input.rb +0 -3
  33. data/lib/active_interaction/inputs.rb +95 -0
  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 +22 -70
  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 +0 -2
  40. data/spec/active_interaction/concerns/missable_spec.rb +0 -2
  41. data/spec/active_interaction/concerns/runnable_spec.rb +26 -12
  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 +51 -14
  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 +81 -57
  84. data/lib/active_interaction/backports.rb +0 -59
  85. data/lib/active_interaction/filters/abstract_filter.rb +0 -19
  86. data/lib/active_interaction/modules/input_processor.rb +0 -52
  87. 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,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,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