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.symbol(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Symbols. Strings will be converted to Symbols.
@@ -17,15 +16,22 @@ module ActiveInteraction
17
16
  class SymbolFilter < Filter
18
17
  register :symbol
19
18
 
20
- def cast(value, _interaction)
21
- case value
22
- when Symbol
23
- value
24
- when String
19
+ private
20
+
21
+ def matches?(value)
22
+ value.is_a?(Symbol)
23
+ rescue NoMethodError # BasicObject
24
+ false
25
+ end
26
+
27
+ def convert(value)
28
+ if value.respond_to?(:to_sym)
25
29
  value.to_sym
26
30
  else
27
31
  super
28
32
  end
33
+ rescue NoMethodError # BasicObject
34
+ super
29
35
  end
30
36
  end
31
37
  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.time(*attributes, options = {})
7
6
  # Creates accessors for the attributes and ensures that values passed to
8
7
  # the attributes are Times. Numeric values are processed using `at`.
@@ -24,34 +23,24 @@ module ActiveInteraction
24
23
  class TimeFilter < AbstractDateTimeFilter
25
24
  register :time
26
25
 
27
- alias _klass klass
28
- private :_klass
29
-
30
26
  def initialize(name, options = {}, &block)
31
- if options.key?(:format) && klass != Time
32
- raise InvalidFilterError, 'format option unsupported with time zones'
33
- end
27
+ raise InvalidFilterError, 'format option unsupported with time zones' if options.key?(:format) && time_with_zone?
34
28
 
35
29
  super
36
30
  end
37
31
 
38
- def cast(value, _interaction)
39
- case value
40
- when Numeric
41
- klass.at(value)
42
- else
43
- super
44
- end
45
- end
46
-
47
32
  def database_column_type
48
33
  :datetime
49
34
  end
50
35
 
51
36
  private
52
37
 
38
+ def time_with_zone?
39
+ Time.respond_to?(:zone) && !Time.zone.nil?
40
+ end
41
+
53
42
  def klass
54
- if Time.respond_to?(:zone) && !Time.zone.nil?
43
+ if time_with_zone?
55
44
  Time.zone
56
45
  else
57
46
  super
@@ -59,7 +48,23 @@ module ActiveInteraction
59
48
  end
60
49
 
61
50
  def klasses
62
- [_klass, klass.at(0).class]
51
+ if time_with_zone?
52
+ [Time.zone.at(0).class, Time]
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def convert(value)
59
+ value = value.to_int if value.respond_to?(:to_int)
60
+
61
+ if value.is_a?(Numeric)
62
+ klass.at(value)
63
+ else
64
+ super
65
+ end
66
+ rescue NoMethodError # BasicObject
67
+ super
63
68
  end
64
69
  end
65
70
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'ostruct'
@@ -6,8 +5,6 @@ require 'ostruct'
6
5
  module ActiveInteraction
7
6
  # Holds a group of inputs together for passing from {Base} to {Filter}s.
8
7
  #
9
- # @since 1.2.0
10
- #
11
8
  # @private
12
9
  class GroupedInput < OpenStruct
13
10
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteraction
4
+ # Holds inputs passed to the interaction.
5
+ class Inputs < DelegateClass(Hash)
6
+ class << self
7
+ # matches inputs like "key(1i)"
8
+ GROUPED_INPUT_PATTERN = /
9
+ \A
10
+ (?<key>.+) # extracts "key"
11
+ \((?<index>\d+)i\) # extracts "1"
12
+ \z
13
+ /x.freeze
14
+ private_constant :GROUPED_INPUT_PATTERN
15
+
16
+ # @private
17
+ def keys_for_group?(keys, group_key)
18
+ search_key = /\A#{group_key}\(\d+i\)\z/
19
+ keys.any? { |key| search_key.match?(key) }
20
+ end
21
+
22
+ # Checking `syscall` is the result of what appears to be a bug in Ruby.
23
+ # https://bugs.ruby-lang.org/issues/15597
24
+ # @private
25
+ def reserved?(name)
26
+ name.to_s.start_with?('_interaction_') ||
27
+ name == :syscall ||
28
+ (
29
+ Base.method_defined?(name) &&
30
+ !Object.method_defined?(name)
31
+ ) ||
32
+ (
33
+ Base.private_method_defined?(name) &&
34
+ !Object.private_method_defined?(name)
35
+ )
36
+ end
37
+
38
+ # @param inputs [Hash, ActionController::Parameters, ActiveInteraction::Inputs] Attribute values to set.
39
+ #
40
+ # @private
41
+ def process(inputs)
42
+ normalize_inputs!(inputs)
43
+ .stringify_keys
44
+ .sort
45
+ .each_with_object({}) do |(k, v), h|
46
+ next if reserved?(k)
47
+
48
+ if (group = GROUPED_INPUT_PATTERN.match(k))
49
+ assign_to_grouped_input!(h, group[:key], group[:index], v)
50
+ else
51
+ h[k.to_sym] = v
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def normalize_inputs!(inputs)
59
+ return inputs if inputs.is_a?(Hash) || inputs.is_a?(Inputs)
60
+
61
+ parameters = 'ActionController::Parameters'
62
+ klass = parameters.safe_constantize
63
+ return inputs.to_unsafe_h if klass && inputs.is_a?(klass)
64
+
65
+ raise ArgumentError, "inputs must be a hash or #{parameters}"
66
+ end
67
+
68
+ def assign_to_grouped_input!(inputs, key, index, value)
69
+ key = key.to_sym
70
+
71
+ inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
72
+ inputs[key][index] = value
73
+ end
74
+ end
75
+
76
+ def initialize
77
+ @groups = {}
78
+ @groups.default_proc = ->(hash, key) { hash[key] = [] }
79
+
80
+ super(@inputs = {})
81
+ end
82
+
83
+ # Associates the `value` with the `key`. Allows the `key`/`value` pair to
84
+ # be associated with one or more groups.
85
+ #
86
+ # @example
87
+ # inputs.store(:key, :value)
88
+ # # => :value
89
+ # inputs.store(:key, :value, %i[a b])
90
+ # # => :value
91
+ #
92
+ # @param key [Object] The key to store the value under.
93
+ # @param value [Object] The value to store.
94
+ # @param groups [Array<Object>] The groups to store the pair under.
95
+ #
96
+ # @return [Object] value
97
+ # @private
98
+ def store(key, value, groups = [])
99
+ groups.each do |group|
100
+ @groups[group] << key
101
+ end
102
+
103
+ super(key, value)
104
+ end
105
+
106
+ # Returns inputs from the group name given.
107
+ #
108
+ # @example
109
+ # inputs.group(:a)
110
+ # # => {key: :value}
111
+ #
112
+ # @param name [Object] Name of the group to return.
113
+ #
114
+ # @return [Hash] Inputs from the group name given.
115
+ # @private
116
+ def group(name)
117
+ @inputs.select { |k, _| @groups[name].include?(k) }
118
+ end
119
+ end
120
+ end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -12,14 +11,13 @@ module ActiveInteraction
12
11
  # @param inputs [Hash{Symbol => Object}]
13
12
  def validate(context, filters, inputs)
14
13
  filters.each_with_object([]) do |(name, filter), errors|
15
- begin
16
- filter.clean(inputs[name], context)
17
- rescue NoDefaultError
18
- nil
19
- rescue InvalidNestedValueError,
20
- InvalidValueError, MissingValueError => e
21
- errors << error_args(filter, e)
22
- end
14
+ filter.clean(inputs[name], context)
15
+ rescue NoDefaultError
16
+ nil
17
+ rescue InvalidNestedValueError,
18
+ InvalidValueError,
19
+ MissingValueError => e
20
+ errors << error_args(filter, e)
23
21
  end
24
22
  end
25
23
 
@@ -28,10 +26,9 @@ module ActiveInteraction
28
26
  def error_args(filter, error)
29
27
  case error
30
28
  when InvalidNestedValueError
31
- [filter.name, :invalid_nested,
32
- name: error.filter_name.inspect, value: error.input_value.inspect]
29
+ [filter.name, :invalid_nested, { name: error.filter_name.inspect, value: error.input_value.inspect }]
33
30
  when InvalidValueError
34
- [filter.name, :invalid_type, type: type(filter)]
31
+ [filter.name, :invalid_type, { type: type(filter) }]
35
32
  when MissingValueError
36
33
  [filter.name, :missing]
37
34
  end
@@ -1,10 +1,8 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- #
5
3
  module ActiveInteraction
6
4
  # The version number.
7
5
  #
8
6
  # @return [Gem::Version]
9
- VERSION = Gem::Version.new('3.8.2')
7
+ VERSION = Gem::Version.new('4.0.3')
10
8
  end
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
  require 'action_controller'
5
3
 
@@ -48,65 +46,6 @@ describe ActiveInteraction::Base do
48
46
  expect(interaction.instance_variable_defined?(:"@#{key}")).to be false
49
47
  end
50
48
 
51
- context 'with invalid inputs' do
52
- let(:inputs) { nil }
53
-
54
- it 'raises an error' do
55
- expect { interaction }.to raise_error ArgumentError
56
- end
57
- end
58
-
59
- context 'with non-hash inputs' do
60
- let(:inputs) { [[:k, :v]] }
61
-
62
- it 'raises an error' do
63
- expect { interaction }.to raise_error ArgumentError
64
- end
65
- end
66
-
67
- context 'with ActionController::Parameters inputs' do
68
- let(:inputs) { ActionController::Parameters.new }
69
-
70
- it 'does not raise an error' do
71
- expect { interaction }.to_not raise_error
72
- end
73
- end
74
-
75
- context 'with a reader' do
76
- let(:described_class) do
77
- Class.new(TestInteraction) do
78
- attr_reader :thing
79
-
80
- validates :thing, presence: true
81
- end
82
- end
83
-
84
- context 'validation' do
85
- context 'failing' do
86
- it 'returns an invalid outcome' do
87
- expect(interaction).to be_invalid
88
- end
89
- end
90
-
91
- context 'passing' do
92
- before { inputs[:thing] = SecureRandom.hex }
93
-
94
- it 'returns a valid outcome' do
95
- expect(interaction).to be_valid
96
- end
97
- end
98
- end
99
-
100
- context 'with a single input' do
101
- let(:thing) { SecureRandom.hex }
102
- before { inputs[:thing] = thing }
103
-
104
- it 'sets the attribute' do
105
- expect(interaction.thing).to eql thing
106
- end
107
- end
108
- end
109
-
110
49
  context 'with a filter' do
111
50
  let(:described_class) { InteractionWithFilter }
112
51
 
@@ -379,12 +318,10 @@ describe ActiveInteraction::Base do
379
318
  end
380
319
 
381
320
  it 'has the correct backtrace' do
382
- begin
383
- described_class.run!(inputs)
384
- rescue ActiveInteraction::InvalidInteractionError => e
385
- expect(e.backtrace)
386
- .to include(InterruptInteraction.composition_location)
387
- end
321
+ described_class.run!(inputs)
322
+ rescue ActiveInteraction::InvalidInteractionError => e
323
+ expect(e.backtrace)
324
+ .to include(InterruptInteraction.composition_location)
388
325
  end
389
326
  end
390
327
  end
@@ -593,6 +530,32 @@ describe ActiveInteraction::Base do
593
530
  expect(result).to be false
594
531
  end
595
532
  end
533
+
534
+ context 'multi-part date values' do
535
+ let(:described_class) do
536
+ Class.new(TestInteraction) do
537
+ date :thing,
538
+ default: nil
539
+
540
+ def execute
541
+ given?(:thing)
542
+ end
543
+ end
544
+ end
545
+
546
+ it 'returns true when the input is given' do
547
+ inputs.merge!(
548
+ 'thing(1i)' => '2020',
549
+ 'thing(2i)' => '12',
550
+ 'thing(3i)' => '31'
551
+ )
552
+ expect(result).to be true
553
+ end
554
+
555
+ it 'returns false if not found' do
556
+ expect(result).to be false
557
+ end
558
+ end
596
559
  end
597
560
 
598
561
  context 'inheritance' do
@@ -643,32 +606,6 @@ describe ActiveInteraction::Base do
643
606
  end
644
607
  end
645
608
 
646
- context 'predicates' do
647
- let(:described_class) { InteractionWithFilter }
648
-
649
- it 'responds to the predicate' do
650
- expect(interaction.respond_to?(:thing?)).to be_truthy
651
- end
652
-
653
- context 'without a value' do
654
- it 'returns false' do
655
- expect(interaction.thing?).to be_falsey
656
- end
657
- end
658
-
659
- context 'with a value' do
660
- let(:thing) { rand }
661
-
662
- before do
663
- inputs[:thing] = thing
664
- end
665
-
666
- it 'returns true' do
667
- expect(interaction.thing?).to be_truthy
668
- end
669
- end
670
- end
671
-
672
609
  describe '.import_filters' do
673
610
  shared_context 'import_filters context' do |only, except|
674
611
  let(:klass) { AddInteraction }
@@ -687,9 +624,11 @@ describe ActiveInteraction::Base do
687
624
  include_context 'import_filters context', only, except
688
625
 
689
626
  it 'imports the filters' do
690
- expect(described_class.filters).to eql klass.filters
691
- .select { |k, _| only.nil? ? true : [*only].include?(k) }
692
- .reject { |k, _| except.nil? ? false : [*except].include?(k) }
627
+ expect(described_class.filters).to eql(
628
+ klass.filters
629
+ .select { |k, _| only.nil? ? true : [*only].include?(k) }
630
+ .reject { |k, _| except.nil? ? false : [*except].include?(k) }
631
+ )
693
632
  end
694
633
 
695
634
  it 'does not modify the source' do
@@ -698,11 +637,11 @@ describe ActiveInteraction::Base do
698
637
  expect(klass.filters).to eql filters
699
638
  end
700
639
 
701
- it 'responds to readers, writers, and predicates' do
640
+ it 'responds to readers and writers' do
702
641
  instance = described_class.new
703
642
 
704
- described_class.filters.keys.each do |name|
705
- [name, "#{name}=", "#{name}?"].each do |method|
643
+ described_class.filters.each_key do |name|
644
+ [name, "#{name}="].each do |method|
706
645
  expect(instance).to respond_to method
707
646
  end
708
647
  end