active_interaction 3.8.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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