active_interaction 3.8.3 → 4.0.0

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 +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.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,42 +23,40 @@ 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
 
53
- def klass
54
- if Time.respond_to?(:zone) && !Time.zone.nil?
55
- Time.zone
38
+ def time_with_zone?
39
+ Time.respond_to?(:zone) && !Time.zone.nil?
40
+ end
41
+
42
+ def klasses
43
+ if time_with_zone?
44
+ super + [Time.zone.class]
56
45
  else
57
46
  super
58
47
  end
59
48
  end
60
49
 
61
- def klasses
62
- [_klass, klass.at(0).class]
50
+ def convert(value)
51
+ value = value.to_int if value.respond_to?(:to_int)
52
+
53
+ if value.is_a?(Numeric)
54
+ klass.at(value)
55
+ else
56
+ super
57
+ end
58
+ rescue NoMethodError # BasicObject
59
+ super
63
60
  end
64
61
  end
65
62
  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,89 @@
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
+ Base.method_defined?(name) ||
22
+ Base.private_method_defined?(name)
23
+ end
24
+
25
+ def process(inputs)
26
+ inputs.stringify_keys.sort.each_with_object({}) do |(k, v), h|
27
+ next if reserved?(k)
28
+
29
+ if (group = GROUPED_INPUT_PATTERN.match(k))
30
+ assign_to_grouped_input!(h, group[:key], group[:index], v)
31
+ else
32
+ h[k.to_sym] = v
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def assign_to_grouped_input!(inputs, key, index, value)
40
+ key = key.to_sym
41
+
42
+ inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
43
+ inputs[key][index] = value
44
+ end
45
+ end
46
+
47
+ def initialize
48
+ @groups = {}
49
+ @groups.default_proc = ->(hash, key) { hash[key] = [] }
50
+
51
+ super(@inputs = {})
52
+ end
53
+
54
+ # Associates the `value` with the `key`. Allows the `key`/`value` pair to
55
+ # be associated with one or more groups.
56
+ #
57
+ # @example
58
+ # inputs.store(:key, :value)
59
+ # # => :value
60
+ # inputs.store(:key, :value, %i[a b])
61
+ # # => :value
62
+ #
63
+ # @param key [Object] The key to store the value under.
64
+ # @param value [Object] The value to store.
65
+ # @param groups [Array<Object>] The groups to store the pair under.
66
+ #
67
+ # @return [Object] value
68
+ def store(key, value, groups = [])
69
+ groups.each do |group|
70
+ @groups[group] << key
71
+ end
72
+
73
+ super(key, value)
74
+ end
75
+
76
+ # Returns inputs from the group name given.
77
+ #
78
+ # @example
79
+ # inputs.group(:a)
80
+ # # => {key: :value}
81
+ #
82
+ # @param name [Object] Name of the group to return.
83
+ #
84
+ # @return [Hash] Inputs from the group name given.
85
+ def group(name)
86
+ @inputs.select { |k, _| @groups[name].include?(k) }
87
+ end
88
+ end
89
+ end
@@ -1,15 +1,12 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
4
  # Groups inputs ending in "(*N*i)" into {GroupedInput}.
6
5
  #
7
- # @since 1.2.0
8
- #
9
6
  # @private
10
7
  module InputProcessor
11
8
  class << self
12
- GROUPED_INPUT_PATTERN = /\A(.+)\((\d+)i\)\z/
9
+ GROUPED_INPUT_PATTERN = /\A(.+)\((\d+)i\)\z/.freeze
13
10
  private_constant :GROUPED_INPUT_PATTERN
14
11
 
15
12
  # Checking `syscall` is the result of what appears to be a bug in Ruby.
@@ -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.3')
7
+ VERSION = Gem::Version.new('4.0.0')
10
8
  end
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
  require 'action_controller'
5
3
 
@@ -57,7 +55,7 @@ describe ActiveInteraction::Base do
57
55
  end
58
56
 
59
57
  context 'with non-hash inputs' do
60
- let(:inputs) { [[:k, :v]] }
58
+ let(:inputs) { [%i[k v]] }
61
59
 
62
60
  it 'raises an error' do
63
61
  expect { interaction }.to raise_error ArgumentError
@@ -379,12 +377,10 @@ describe ActiveInteraction::Base do
379
377
  end
380
378
 
381
379
  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
380
+ described_class.run!(inputs)
381
+ rescue ActiveInteraction::InvalidInteractionError => e
382
+ expect(e.backtrace)
383
+ .to include(InterruptInteraction.composition_location)
388
384
  end
389
385
  end
390
386
  end
@@ -643,32 +639,6 @@ describe ActiveInteraction::Base do
643
639
  end
644
640
  end
645
641
 
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
642
  describe '.import_filters' do
673
643
  shared_context 'import_filters context' do |only, except|
674
644
  let(:klass) { AddInteraction }
@@ -687,9 +657,11 @@ describe ActiveInteraction::Base do
687
657
  include_context 'import_filters context', only, except
688
658
 
689
659
  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) }
660
+ expect(described_class.filters).to eql(
661
+ klass.filters
662
+ .select { |k, _| only.nil? ? true : [*only].include?(k) }
663
+ .reject { |k, _| except.nil? ? false : [*except].include?(k) }
664
+ )
693
665
  end
694
666
 
695
667
  it 'does not modify the source' do
@@ -698,11 +670,11 @@ describe ActiveInteraction::Base do
698
670
  expect(klass.filters).to eql filters
699
671
  end
700
672
 
701
- it 'responds to readers, writers, and predicates' do
673
+ it 'responds to readers and writers' do
702
674
  instance = described_class.new
703
675
 
704
- described_class.filters.keys.each do |name|
705
- [name, "#{name}=", "#{name}?"].each do |method|
676
+ described_class.filters.each_key do |name|
677
+ [name, "#{name}="].each do |method|
706
678
  expect(instance).to respond_to method
707
679
  end
708
680
  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
@@ -27,7 +25,7 @@ describe ActiveInteraction::Hashable do
27
25
  end
28
26
 
29
27
  context 'with a block' do
30
- let(:block) { proc {} }
28
+ let(:block) { proc {} } # rubocop:disable Lint/EmptyBlock
31
29
  let(:hash) { subject.hash(*arguments, &block) }
32
30
 
33
31
  it 'calls method_missing' do
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe ActiveInteraction::Missable do
@@ -1,11 +1,9 @@
1
- # coding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe ActiveInteraction::Runnable do
6
4
  include_context 'concerns', ActiveInteraction::Runnable
7
5
 
8
- class WrappableFailingInteraction
6
+ class WrappableFailingInteraction # rubocop:disable Lint/ConstantDefinitionInBlock
9
7
  include ActiveInteraction::Runnable
10
8
 
11
9
  def execute
@@ -72,7 +70,7 @@ describe ActiveInteraction::Runnable do
72
70
  include_examples 'set_callback examples', :execute
73
71
 
74
72
  context 'execute with composed interaction' do
75
- class WithFailingCompose
73
+ class WithFailingCompose # rubocop:disable Lint/ConstantDefinitionInBlock
76
74
  include ActiveInteraction::Runnable
77
75
 
78
76
  def execute
@@ -107,11 +105,9 @@ describe ActiveInteraction::Runnable do
107
105
  context 'using if' do
108
106
  it 'yields errors to the if' do
109
107
  has_run = false
110
- # rubocop:disable Metrics/LineLength
111
108
  WithFailingCompose.set_callback :execute, :after, if: -> { errors.any? } do
112
109
  has_run = true
113
110
  end
114
- # rubocop:enable Metrics/LineLength
115
111
 
116
112
  WithFailingCompose.run
117
113
  expect(has_run).to be_truthy
@@ -237,10 +233,10 @@ describe ActiveInteraction::Runnable do
237
233
  context 'caches the validity and result of the run' do
238
234
  let(:klass) do
239
235
  Class.new(ActiveInteraction::Base) do
240
- INVALID = [false, true].cycle
236
+ invalid = [false, true].cycle
241
237
 
242
238
  validate do |interaction|
243
- interaction.errors.add(:base, 'failed') unless INVALID.next
239
+ interaction.errors.add(:base, 'failed') unless invalid.next
244
240
  end
245
241
 
246
242
  def execute
@@ -260,10 +256,10 @@ describe ActiveInteraction::Runnable do
260
256
  context 'caches the validity and result of the run' do
261
257
  let(:klass) do
262
258
  Class.new(ActiveInteraction::Base) do
263
- VALID = [true, false].cycle
259
+ valid = [true, false].cycle
264
260
 
265
261
  validate do |interaction|
266
- interaction.errors.add(:base, 'failed') unless VALID.next
262
+ interaction.errors.add(:base, 'failed') unless valid.next
267
263
  end
268
264
 
269
265
  def execute
@@ -327,14 +323,14 @@ describe ActiveInteraction::Runnable do
327
323
  end
328
324
 
329
325
  context 'with failing composition' do
330
- class CheckInnerForFailure
326
+ class CheckInnerForFailure # rubocop:disable Lint/ConstantDefinitionInBlock
331
327
  include ActiveInteraction::Runnable
332
328
 
333
329
  attr_reader :caught_error
334
330
 
335
331
  def execute
336
332
  compose(WrappableFailingInteraction)
337
- rescue
333
+ rescue StandardError
338
334
  @caught_error = true
339
335
  raise
340
336
  end
@@ -347,7 +343,7 @@ describe ActiveInteraction::Runnable do
347
343
  end
348
344
 
349
345
  context 'with block not called and error in execute around callback' do
350
- class CheckExecuteAroundCallbackForFailure
346
+ class CheckExecuteAroundCallbackForFailure # rubocop:disable Lint/ConstantDefinitionInBlock
351
347
  include ActiveInteraction::ActiveModelable
352
348
  include ActiveInteraction::Runnable
353
349