active_interaction 3.8.3 → 4.0.4

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 +193 -0
  3. data/README.md +97 -116
  4. data/lib/active_interaction.rb +2 -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 +6 -12
  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 -21
  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 +9 -13
  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 +62 -13
  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 +1 -3
  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 -12
  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 +92 -62
  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,31 +11,20 @@ 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 => e
18
+ errors << [filter.name, :invalid_nested, { name: e.filter_name.inspect, value: e.input_value.inspect }]
19
+ rescue InvalidValueError
20
+ errors << [filter.name, :invalid_type, { type: type(filter) }]
21
+ rescue MissingValueError
22
+ errors << [filter.name, :missing]
23
23
  end
24
24
  end
25
25
 
26
26
  private
27
27
 
28
- def error_args(filter, error)
29
- case error
30
- when InvalidNestedValueError
31
- [filter.name, :invalid_nested,
32
- name: error.filter_name.inspect, value: error.input_value.inspect]
33
- when InvalidValueError
34
- [filter.name, :invalid_type, type: type(filter)]
35
- when MissingValueError
36
- [filter.name, :missing]
37
- end
38
- end
39
-
40
28
  # @param filter [Filter]
41
29
  def type(filter)
42
30
  I18n.translate("#{Base.i18n_scope}.types.#{filter.class.slug}")
@@ -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.3')
7
+ VERSION = Gem::Version.new('4.0.4')
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