active_interaction 3.7.1 → 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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +190 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +96 -90
  5. data/lib/active_interaction.rb +1 -7
  6. data/lib/active_interaction/base.rb +48 -33
  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 +16 -28
  12. data/lib/active_interaction/errors.rb +8 -7
  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 +57 -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 +19 -22
  32. data/lib/active_interaction/grouped_input.rb +0 -3
  33. data/lib/active_interaction/inputs.rb +89 -0
  34. data/lib/active_interaction/locale/ja.yml +24 -0
  35. data/lib/active_interaction/modules/input_processor.rb +9 -6
  36. data/lib/active_interaction/modules/validation.rb +9 -12
  37. data/lib/active_interaction/version.rb +1 -3
  38. data/spec/active_interaction/base_spec.rb +95 -35
  39. data/spec/active_interaction/concerns/active_modelable_spec.rb +0 -2
  40. data/spec/active_interaction/concerns/active_recordable_spec.rb +0 -2
  41. data/spec/active_interaction/concerns/hashable_spec.rb +1 -3
  42. data/spec/active_interaction/concerns/missable_spec.rb +0 -2
  43. data/spec/active_interaction/concerns/runnable_spec.rb +32 -12
  44. data/spec/active_interaction/errors_spec.rb +49 -22
  45. data/spec/active_interaction/filter_column_spec.rb +0 -2
  46. data/spec/active_interaction/filter_spec.rb +0 -2
  47. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +1 -3
  48. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -3
  49. data/spec/active_interaction/filters/array_filter_spec.rb +41 -15
  50. data/spec/active_interaction/filters/boolean_filter_spec.rb +58 -4
  51. data/spec/active_interaction/filters/date_filter_spec.rb +43 -3
  52. data/spec/active_interaction/filters/date_time_filter_spec.rb +43 -3
  53. data/spec/active_interaction/filters/decimal_filter_spec.rb +57 -3
  54. data/spec/active_interaction/filters/file_filter_spec.rb +1 -3
  55. data/spec/active_interaction/filters/float_filter_spec.rb +60 -4
  56. data/spec/active_interaction/filters/hash_filter_spec.rb +19 -9
  57. data/spec/active_interaction/filters/integer_filter_spec.rb +49 -7
  58. data/spec/active_interaction/filters/interface_filter_spec.rb +397 -24
  59. data/spec/active_interaction/filters/object_filter_spec.rb +23 -59
  60. data/spec/active_interaction/filters/record_filter_spec.rb +23 -49
  61. data/spec/active_interaction/filters/string_filter_spec.rb +15 -3
  62. data/spec/active_interaction/filters/symbol_filter_spec.rb +15 -3
  63. data/spec/active_interaction/filters/time_filter_spec.rb +65 -3
  64. data/spec/active_interaction/grouped_input_spec.rb +0 -2
  65. data/spec/active_interaction/i18n_spec.rb +3 -7
  66. data/spec/active_interaction/{modules/input_processor_spec.rb → inputs_spec.rb} +5 -5
  67. data/spec/active_interaction/integration/array_interaction_spec.rb +23 -12
  68. data/spec/active_interaction/integration/boolean_interaction_spec.rb +0 -2
  69. data/spec/active_interaction/integration/date_interaction_spec.rb +0 -2
  70. data/spec/active_interaction/integration/date_time_interaction_spec.rb +0 -2
  71. data/spec/active_interaction/integration/file_interaction_spec.rb +0 -2
  72. data/spec/active_interaction/integration/float_interaction_spec.rb +0 -2
  73. data/spec/active_interaction/integration/hash_interaction_spec.rb +0 -2
  74. data/spec/active_interaction/integration/integer_interaction_spec.rb +0 -2
  75. data/spec/active_interaction/integration/interface_interaction_spec.rb +1 -3
  76. data/spec/active_interaction/integration/object_interaction_spec.rb +0 -2
  77. data/spec/active_interaction/integration/string_interaction_spec.rb +0 -2
  78. data/spec/active_interaction/integration/symbol_interaction_spec.rb +0 -2
  79. data/spec/active_interaction/integration/time_interaction_spec.rb +14 -18
  80. data/spec/active_interaction/modules/validation_spec.rb +1 -3
  81. data/spec/spec_helper.rb +2 -6
  82. data/spec/support/concerns.rb +0 -2
  83. data/spec/support/filters.rb +13 -9
  84. data/spec/support/interactions.rb +22 -14
  85. metadata +106 -52
  86. data/lib/active_interaction/backports.rb +0 -59
  87. data/lib/active_interaction/filters/abstract_filter.rb +0 -19
  88. 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
@@ -0,0 +1,24 @@
1
+ ja:
2
+ active_interaction:
3
+ errors:
4
+ messages:
5
+ invalid: は無効です
6
+ invalid_nested: は無効なネストされた値です (%{name} => %{value})
7
+ invalid_type: は無効な %{type} です
8
+ missing: が必要です
9
+ types:
10
+ array: 配列
11
+ boolean: ブール
12
+ date: 日付
13
+ date_time: 日時
14
+ decimal: 10進数
15
+ file: ファイル
16
+ float: 浮動小数点数
17
+ hash: ハッシュ
18
+ integer: 整数
19
+ interface: インターフェース
20
+ object: オブジェクト
21
+ record: レコード
22
+ string: 文字
23
+ symbol: シンボル
24
+ time: 時間
@@ -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.
@@ -17,8 +14,14 @@ module ActiveInteraction
17
14
  def reserved?(name)
18
15
  name.to_s.start_with?('_interaction_') ||
19
16
  name == :syscall ||
20
- Base.method_defined?(name) ||
21
- Base.private_method_defined?(name)
17
+ (
18
+ Base.method_defined?(name) &&
19
+ !Object.method_defined?(name)
20
+ ) ||
21
+ (
22
+ Base.private_method_defined?(name) &&
23
+ !Object.private_method_defined?(name)
24
+ )
22
25
  end
23
26
 
24
27
  def process(inputs)
@@ -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.7.1')
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
 
@@ -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
@@ -371,6 +375,13 @@ describe ActiveInteraction::Base do
371
375
  expect(outcome.errors.details)
372
376
  .to eql(x: [{ error: :missing }], base: [{ error: 'Y is required' }])
373
377
  end
378
+
379
+ it 'has the correct backtrace' do
380
+ described_class.run!(inputs)
381
+ rescue ActiveInteraction::InvalidInteractionError => e
382
+ expect(e.backtrace)
383
+ .to include(InterruptInteraction.composition_location)
384
+ end
374
385
  end
375
386
  end
376
387
 
@@ -505,6 +516,79 @@ describe ActiveInteraction::Base do
505
516
  expect(result).to be false
506
517
  end
507
518
  end
519
+
520
+ context 'nested array values' do
521
+ let(:described_class) do
522
+ Class.new(TestInteraction) do
523
+ array :x do
524
+ hash do
525
+ boolean :y, default: true
526
+ end
527
+ end
528
+
529
+ def execute; end
530
+ end
531
+ end
532
+
533
+ context 'has a positive index' do
534
+ it 'returns true if found' do
535
+ described_class.class_exec do
536
+ def execute
537
+ given?(:x, 0, :y)
538
+ end
539
+ end
540
+
541
+ inputs[:x] = [{ y: true }]
542
+ expect(result).to be true
543
+ end
544
+
545
+ it 'returns false if not found' do
546
+ described_class.class_exec do
547
+ def execute
548
+ given?(:x, 0, :y)
549
+ end
550
+ end
551
+
552
+ inputs[:x] = []
553
+ expect(result).to be false
554
+ end
555
+ end
556
+
557
+ context 'has a negative index' do
558
+ it 'returns true if found' do
559
+ described_class.class_exec do
560
+ def execute
561
+ given?(:x, -1, :y)
562
+ end
563
+ end
564
+
565
+ inputs[:x] = [{ y: true }]
566
+ expect(result).to be true
567
+ end
568
+
569
+ it 'returns false if not found' do
570
+ described_class.class_exec do
571
+ def execute
572
+ given?(:x, -1, :y)
573
+ end
574
+ end
575
+
576
+ inputs[:x] = []
577
+ expect(result).to be false
578
+ end
579
+ end
580
+
581
+ it 'returns false if you go too far' do
582
+ described_class.class_exec do
583
+ def execute
584
+ given?(:x, 10, :y)
585
+ end
586
+ end
587
+
588
+ inputs[:x] = [{}]
589
+ expect(result).to be false
590
+ end
591
+ end
508
592
  end
509
593
 
510
594
  context 'inheritance' do
@@ -555,32 +639,6 @@ describe ActiveInteraction::Base do
555
639
  end
556
640
  end
557
641
 
558
- context 'predicates' do
559
- let(:described_class) { InteractionWithFilter }
560
-
561
- it 'responds to the predicate' do
562
- expect(interaction.respond_to?(:thing?)).to be_truthy
563
- end
564
-
565
- context 'without a value' do
566
- it 'returns false' do
567
- expect(interaction.thing?).to be_falsey
568
- end
569
- end
570
-
571
- context 'with a value' do
572
- let(:thing) { rand }
573
-
574
- before do
575
- inputs[:thing] = thing
576
- end
577
-
578
- it 'returns true' do
579
- expect(interaction.thing?).to be_truthy
580
- end
581
- end
582
- end
583
-
584
642
  describe '.import_filters' do
585
643
  shared_context 'import_filters context' do |only, except|
586
644
  let(:klass) { AddInteraction }
@@ -599,9 +657,11 @@ describe ActiveInteraction::Base do
599
657
  include_context 'import_filters context', only, except
600
658
 
601
659
  it 'imports the filters' do
602
- expect(described_class.filters).to eql klass.filters
603
- .select { |k, _| only.nil? ? true : [*only].include?(k) }
604
- .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
+ )
605
665
  end
606
666
 
607
667
  it 'does not modify the source' do
@@ -610,11 +670,11 @@ describe ActiveInteraction::Base do
610
670
  expect(klass.filters).to eql filters
611
671
  end
612
672
 
613
- it 'responds to readers, writers, and predicates' do
673
+ it 'responds to readers and writers' do
614
674
  instance = described_class.new
615
675
 
616
- described_class.filters.keys.each do |name|
617
- [name, "#{name}=", "#{name}?"].each do |method|
676
+ described_class.filters.each_key do |name|
677
+ [name, "#{name}="].each do |method|
618
678
  expect(instance).to respond_to method
619
679
  end
620
680
  end