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,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
@@ -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.float(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Floats. Integer and String values are converted
@@ -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.hash(*attributes, options = {}, &block)
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Hashes.
@@ -26,21 +25,46 @@ module ActiveInteraction
26
25
 
27
26
  register :hash
28
27
 
29
- def cast(value, context)
30
- case value
31
- when Hash
32
- value = value.with_indifferent_access
33
- initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
28
+ private
34
29
 
35
- filters.each_with_object(initial) do |(name, filter), h|
36
- clean_value(h, name.to_s, filter, value, context)
37
- end
30
+ def matches?(value)
31
+ value.is_a?(Hash)
32
+ rescue NoMethodError # BasicObject
33
+ false
34
+ end
35
+
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
+ def strip?
43
+ options.fetch(:strip, true)
44
+ end
45
+
46
+ def adjust_output(value, context)
47
+ value = value.to_hash.with_indifferent_access
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
54
+ end
55
+
56
+ def convert(value)
57
+ if value.respond_to?(:to_hash)
58
+ value.to_hash
38
59
  else
39
60
  super
40
61
  end
62
+ rescue NoMethodError # BasicObject
63
+ super
41
64
  end
42
65
 
43
- def method_missing(*args, &block) # rubocop:disable Style/MethodMissing
66
+ # rubocop:disable Style/MissingRespondToMissing
67
+ def method_missing(*args, &block)
44
68
  super(*args) do |klass, names, options|
45
69
  raise InvalidFilterError, 'missing attribute name' if names.empty?
46
70
 
@@ -49,28 +73,14 @@ module ActiveInteraction
49
73
  end
50
74
  end
51
75
  end
52
-
53
- private
54
-
55
- def clean_value(h, name, filter, value, context)
56
- h[name] = filter.clean(value[name], context)
57
- rescue InvalidValueError, MissingValueError
58
- raise InvalidNestedValueError.new(name, value[name])
59
- end
76
+ # rubocop:enable Style/MissingRespondToMissing
60
77
 
61
78
  def raw_default(*)
62
79
  value = super
63
80
 
64
- if value.is_a?(Hash) && !value.empty?
65
- raise InvalidDefaultError, "#{name}: #{value.inspect}"
66
- end
81
+ raise InvalidDefaultError, "#{name}: #{value.inspect}" if value.is_a?(Hash) && !value.empty?
67
82
 
68
83
  value
69
84
  end
70
-
71
- # @return [Boolean]
72
- def strip?
73
- options.fetch(:strip, true)
74
- end
75
85
  end
76
86
  end
@@ -1,14 +1,16 @@
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.integer(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Integers. String values are converted into
9
8
  # Integers.
10
9
  #
11
10
  # @!macro filter_method_params
11
+ # @option options [Integer] :base (10) The base used to convert strings
12
+ # into integers. When set to `0` it will honor radix indicators (i.e.
13
+ # 0, 0b, and 0x).
12
14
  #
13
15
  # @example
14
16
  # integer :quantity
@@ -20,15 +22,12 @@ module ActiveInteraction
20
22
 
21
23
  private
22
24
 
23
- # @return [Integer]
24
25
  def base
25
- options.fetch(:base, 0)
26
+ options.fetch(:base, 10)
26
27
  end
27
28
 
28
- def convert(value, context)
29
- Integer(value, base)
30
- rescue ArgumentError
31
- _cast(value, context)
29
+ def converter(value)
30
+ Integer(value, value.is_a?(String) ? base : 0)
32
31
  end
33
32
  end
34
33
  end
@@ -1,18 +1,24 @@
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.interface(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
- # the attributes implement an interface.
7
+ # the attributes implement an interface. An interface can be based on a
8
+ # set of methods or the existance of a class or module in the ancestors
9
+ # of the passed value.
9
10
  #
10
11
  # @!macro filter_method_params
11
- # @option options [Array<String,Symbol>] :methods ([]) the methods that
12
+ # @option options [Constant, String, Symbol] :from (use the attribute
13
+ # name) The class or module representing the interface to check for.
14
+ # @option options [Array<String, Symbol>] :methods ([]) the methods that
12
15
  # objects conforming to this interface should respond to
13
16
  #
14
17
  # @example
15
- # interface :anything
18
+ # interface :concern
19
+ # @example
20
+ # interface :person,
21
+ # from: Manageable
16
22
  # @example
17
23
  # interface :serializer,
18
24
  # methods: %i[dump load]
@@ -22,24 +28,52 @@ module ActiveInteraction
22
28
  class InterfaceFilter < Filter
23
29
  register :interface
24
30
 
25
- def cast(value, _interaction)
26
- matches?(value) ? value : super
31
+ def initialize(name, options = {}, &block)
32
+ if options.key?(:methods) && options.key?(:from)
33
+ raise InvalidFilterError,
34
+ 'method and from options cannot both be passed'
35
+ end
36
+
37
+ super
27
38
  end
28
39
 
29
40
  private
30
41
 
31
- # @param object [Object]
32
- #
33
- # @return [Boolean]
42
+ def from
43
+ const_name = options.fetch(:from, name).to_s.camelize
44
+ Object.const_get(const_name)
45
+ rescue NameError
46
+ raise InvalidNameError,
47
+ "constant #{const_name.inspect} does not exist"
48
+ end
49
+
34
50
  def matches?(object)
35
- methods.all? { |method| object.respond_to?(method) }
51
+ return matches_methods?(object) if options.key?(:methods)
52
+
53
+ const = from
54
+ if checking_class_inheritance?(object, const)
55
+ class_inherits_from?(object, const)
56
+ else
57
+ singleton_ancestor?(object, const)
58
+ end
36
59
  rescue NoMethodError
37
60
  false
38
61
  end
39
62
 
40
- # @return [Array<Symbol>]
41
- def methods
42
- options.fetch(:methods, [])
63
+ def matches_methods?(object)
64
+ options.fetch(:methods, []).all? { |method| object.respond_to?(method) }
65
+ end
66
+
67
+ def checking_class_inheritance?(object, from)
68
+ object.is_a?(Class) && from.is_a?(Class)
69
+ end
70
+
71
+ def class_inherits_from?(klass, inherits_from)
72
+ klass != inherits_from && klass.ancestors.include?(inherits_from)
73
+ end
74
+
75
+ def singleton_ancestor?(object, from)
76
+ object.class != from && object.singleton_class.ancestors.include?(from)
43
77
  end
44
78
  end
45
79
  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.object(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are the correct class.
@@ -29,69 +28,43 @@ module ActiveInteraction
29
28
  class ObjectFilter < Filter
30
29
  register :object
31
30
 
32
- # rubocop:disable Metrics/MethodLength
33
- def cast(value, context, reconstantize: true, convert: true)
34
- @klass ||= klass
35
-
36
- if matches?(value)
37
- value
38
- elsif reconstantize
39
- @klass = klass
40
- public_send(__method__, value, context,
41
- reconstantize: false,
42
- convert: convert
43
- )
44
- elsif !value.nil? && convert && (converter = options[:converter])
45
- value = convert(klass, value, converter)
46
- public_send(__method__, value, context,
47
- reconstantize: reconstantize,
48
- convert: false
49
- )
50
- else
51
- super(value, context)
52
- end
53
- end
54
- # rubocop:enable Metrics/MethodLength
55
-
56
31
  private
57
32
 
58
- # @return [Class]
59
- #
60
- # @raise [InvalidClassError]
61
33
  def klass
62
34
  klass_name = options.fetch(:class, name).to_s.camelize
63
35
  Object.const_get(klass_name)
64
36
  rescue NameError
65
- raise InvalidClassError, "class #{klass_name.inspect} does not exist"
37
+ raise InvalidNameError, "class #{klass_name.inspect} does not exist"
66
38
  end
67
39
 
68
- # @param value [Object]
69
- #
70
- # @return [Boolean]
71
40
  def matches?(value)
72
- @klass === value || # rubocop:disable Style/CaseEquality
73
- value.is_a?(@klass)
41
+ value.class <= klass
42
+ rescue NoMethodError
43
+ false
74
44
  end
75
45
 
76
- def convert(klass, value, converter) # rubocop:disable Metrics/MethodLength
77
- result =
78
- case converter
79
- when Proc
80
- converter.call(value)
81
- when Symbol
82
- klass.public_send(converter, value)
83
- else
84
- raise InvalidConverterError,
85
- "#{converter.inspect} is not a valid converter"
86
- end
87
-
88
- raise InvalidValueError if result.nil?
89
-
90
- result
46
+ def convert(value)
47
+ converter(value).tap do |result|
48
+ raise InvalidValueError if result.nil?
49
+ end
91
50
  rescue StandardError => e
92
51
  raise e if e.is_a?(InvalidConverterError)
93
52
 
94
53
  raise InvalidValueError
95
54
  end
55
+
56
+ def converter(value)
57
+ return value unless (converter = options[:converter])
58
+
59
+ case converter
60
+ when Proc
61
+ converter.call(value)
62
+ when Symbol
63
+ klass.public_send(converter, value)
64
+ else
65
+ raise InvalidConverterError,
66
+ "#{converter.inspect} is not a valid converter"
67
+ end
68
+ end
96
69
  end
97
70
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveInteraction
4
- class Base
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
7
  # the attributes are the correct class.
@@ -28,49 +28,24 @@ module ActiveInteraction
28
28
  class RecordFilter < Filter
29
29
  register :record
30
30
 
31
- # rubocop:disable Metrics/MethodLength
32
- def cast(value, context, reconstantize: true, convert: true)
33
- @klass ||= klass
34
-
35
- if matches?(value)
36
- value
37
- elsif reconstantize
38
- @klass = klass
39
- public_send(__method__, value, context,
40
- reconstantize: false,
41
- convert: convert
42
- )
43
- elsif !value.nil? && convert
44
- finder = options.fetch(:finder, :find)
45
- value = find(klass, value, finder)
46
- public_send(__method__, value, context,
47
- reconstantize: reconstantize,
48
- convert: false
49
- )
50
- else
51
- super(value, context)
52
- end
53
- end
54
- # rubocop:enable Metrics/MethodLength
55
-
56
31
  private
57
32
 
58
- # @return [Class]
59
- #
60
- # @raise [InvalidClassError]
61
33
  def klass
62
34
  klass_name = options.fetch(:class, name).to_s.camelize
63
35
  Object.const_get(klass_name)
64
36
  rescue NameError
65
- raise InvalidClassError, "class #{klass_name.inspect} does not exist"
37
+ raise InvalidNameError, "class #{klass_name.inspect} does not exist"
66
38
  end
67
39
 
68
- # @param value [Object]
69
- #
70
- # @return [Boolean]
71
40
  def matches?(value)
72
- @klass === value || # rubocop:disable Style/CaseEquality
73
- value.is_a?(@klass)
41
+ value.class <= klass
42
+ rescue NoMethodError
43
+ false
44
+ end
45
+
46
+ def convert(value)
47
+ finder = options.fetch(:finder, :find)
48
+ find(klass, value, finder)
74
49
  end
75
50
 
76
51
  def find(klass, value, finder)
@@ -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.string(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Strings.
@@ -21,20 +20,30 @@ module ActiveInteraction
21
20
  class StringFilter < Filter
22
21
  register :string
23
22
 
24
- def cast(value, _interaction)
25
- case value
26
- when String
27
- strip? ? value.strip : value
28
- else
29
- super
30
- end
31
- end
32
-
33
23
  private
34
24
 
35
- # @return [Boolean]
36
25
  def strip?
37
26
  options.fetch(:strip, true)
38
27
  end
28
+
29
+ def matches?(value)
30
+ value.is_a?(String)
31
+ rescue NoMethodError # BasicObject
32
+ false
33
+ end
34
+
35
+ def adjust_output(value, _context)
36
+ strip? ? value.strip : value
37
+ end
38
+
39
+ def convert(value)
40
+ if value.respond_to?(:to_str)
41
+ value.to_str
42
+ else
43
+ super
44
+ end
45
+ rescue NoMethodError # BasicObject
46
+ super
47
+ end
39
48
  end
40
49
  end