active_interaction 3.8.1 → 4.0.2

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 (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,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,95 @@
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
+ # Checking `syscall` is the result of what appears to be a bug in Ruby.
17
+ # https://bugs.ruby-lang.org/issues/15597
18
+ def reserved?(name)
19
+ name.to_s.start_with?('_interaction_') ||
20
+ name == :syscall ||
21
+ (
22
+ Base.method_defined?(name) &&
23
+ !Object.method_defined?(name)
24
+ ) ||
25
+ (
26
+ Base.private_method_defined?(name) &&
27
+ !Object.private_method_defined?(name)
28
+ )
29
+ end
30
+
31
+ def process(inputs)
32
+ inputs.stringify_keys.sort.each_with_object({}) do |(k, v), h|
33
+ next if reserved?(k)
34
+
35
+ if (group = GROUPED_INPUT_PATTERN.match(k))
36
+ assign_to_grouped_input!(h, group[:key], group[:index], v)
37
+ else
38
+ h[k.to_sym] = v
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def assign_to_grouped_input!(inputs, key, index, value)
46
+ key = key.to_sym
47
+
48
+ inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
49
+ inputs[key][index] = value
50
+ end
51
+ end
52
+
53
+ def initialize
54
+ @groups = {}
55
+ @groups.default_proc = ->(hash, key) { hash[key] = [] }
56
+
57
+ super(@inputs = {})
58
+ end
59
+
60
+ # Associates the `value` with the `key`. Allows the `key`/`value` pair to
61
+ # be associated with one or more groups.
62
+ #
63
+ # @example
64
+ # inputs.store(:key, :value)
65
+ # # => :value
66
+ # inputs.store(:key, :value, %i[a b])
67
+ # # => :value
68
+ #
69
+ # @param key [Object] The key to store the value under.
70
+ # @param value [Object] The value to store.
71
+ # @param groups [Array<Object>] The groups to store the pair under.
72
+ #
73
+ # @return [Object] value
74
+ def store(key, value, groups = [])
75
+ groups.each do |group|
76
+ @groups[group] << key
77
+ end
78
+
79
+ super(key, value)
80
+ end
81
+
82
+ # Returns inputs from the group name given.
83
+ #
84
+ # @example
85
+ # inputs.group(:a)
86
+ # # => {key: :value}
87
+ #
88
+ # @param name [Object] Name of the group to return.
89
+ #
90
+ # @return [Hash] Inputs from the group name given.
91
+ def group(name)
92
+ @inputs.select { |k, _| @groups[name].include?(k) }
93
+ end
94
+ end
95
+ 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.1')
7
+ VERSION = Gem::Version.new('4.0.2')
10
8
  end
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
  require 'action_controller'
5
3
 
@@ -24,6 +22,12 @@ InterruptInteraction = Class.new(TestInteraction) do
24
22
  class: Object,
25
23
  default: nil
26
24
 
25
+ # NOTE: the relative position between this method
26
+ # and the compose line should be preserved.
27
+ def self.composition_location
28
+ "#{__FILE__}:#{__LINE__ + 4}:in `execute'"
29
+ end
30
+
27
31
  def execute
28
32
  compose(AddInteraction, x: x, y: z)
29
33
  end
@@ -51,7 +55,7 @@ describe ActiveInteraction::Base do
51
55
  end
52
56
 
53
57
  context 'with non-hash inputs' do
54
- let(:inputs) { [[:k, :v]] }
58
+ let(:inputs) { [%i[k v]] }
55
59
 
56
60
  it 'raises an error' do
57
61
  expect { interaction }.to raise_error ArgumentError
@@ -66,41 +70,6 @@ describe ActiveInteraction::Base do
66
70
  end
67
71
  end
68
72
 
69
- context 'with a reader' do
70
- let(:described_class) do
71
- Class.new(TestInteraction) do
72
- attr_reader :thing
73
-
74
- validates :thing, presence: true
75
- end
76
- end
77
-
78
- context 'validation' do
79
- context 'failing' do
80
- it 'returns an invalid outcome' do
81
- expect(interaction).to be_invalid
82
- end
83
- end
84
-
85
- context 'passing' do
86
- before { inputs[:thing] = SecureRandom.hex }
87
-
88
- it 'returns a valid outcome' do
89
- expect(interaction).to be_valid
90
- end
91
- end
92
- end
93
-
94
- context 'with a single input' do
95
- let(:thing) { SecureRandom.hex }
96
- before { inputs[:thing] = thing }
97
-
98
- it 'sets the attribute' do
99
- expect(interaction.thing).to eql thing
100
- end
101
- end
102
- end
103
-
104
73
  context 'with a filter' do
105
74
  let(:described_class) { InteractionWithFilter }
106
75
 
@@ -371,6 +340,13 @@ describe ActiveInteraction::Base do
371
340
  expect(outcome.errors.details)
372
341
  .to eql(x: [{ error: :missing }], base: [{ error: 'Y is required' }])
373
342
  end
343
+
344
+ it 'has the correct backtrace' do
345
+ described_class.run!(inputs)
346
+ rescue ActiveInteraction::InvalidInteractionError => e
347
+ expect(e.backtrace)
348
+ .to include(InterruptInteraction.composition_location)
349
+ end
374
350
  end
375
351
  end
376
352
 
@@ -628,32 +604,6 @@ describe ActiveInteraction::Base do
628
604
  end
629
605
  end
630
606
 
631
- context 'predicates' do
632
- let(:described_class) { InteractionWithFilter }
633
-
634
- it 'responds to the predicate' do
635
- expect(interaction.respond_to?(:thing?)).to be_truthy
636
- end
637
-
638
- context 'without a value' do
639
- it 'returns false' do
640
- expect(interaction.thing?).to be_falsey
641
- end
642
- end
643
-
644
- context 'with a value' do
645
- let(:thing) { rand }
646
-
647
- before do
648
- inputs[:thing] = thing
649
- end
650
-
651
- it 'returns true' do
652
- expect(interaction.thing?).to be_truthy
653
- end
654
- end
655
- end
656
-
657
607
  describe '.import_filters' do
658
608
  shared_context 'import_filters context' do |only, except|
659
609
  let(:klass) { AddInteraction }
@@ -672,9 +622,11 @@ describe ActiveInteraction::Base do
672
622
  include_context 'import_filters context', only, except
673
623
 
674
624
  it 'imports the filters' do
675
- expect(described_class.filters).to eql klass.filters
676
- .select { |k, _| only.nil? ? true : [*only].include?(k) }
677
- .reject { |k, _| except.nil? ? false : [*except].include?(k) }
625
+ expect(described_class.filters).to eql(
626
+ klass.filters
627
+ .select { |k, _| only.nil? ? true : [*only].include?(k) }
628
+ .reject { |k, _| except.nil? ? false : [*except].include?(k) }
629
+ )
678
630
  end
679
631
 
680
632
  it 'does not modify the source' do
@@ -683,11 +635,11 @@ describe ActiveInteraction::Base do
683
635
  expect(klass.filters).to eql filters
684
636
  end
685
637
 
686
- it 'responds to readers, writers, and predicates' do
638
+ it 'responds to readers and writers' do
687
639
  instance = described_class.new
688
640
 
689
- described_class.filters.keys.each do |name|
690
- [name, "#{name}=", "#{name}?"].each do |method|
641
+ described_class.filters.each_key do |name|
642
+ [name, "#{name}="].each do |method|
691
643
  expect(instance).to respond_to method
692
644
  end
693
645
  end
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  shared_examples_for 'ActiveModel' do
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  InteractionWithFloatFilter = Class.new(TestInteraction) do
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe ActiveInteraction::Hashable do
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe ActiveInteraction::Missable do