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,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