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