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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +190 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +93 -107
- data/lib/active_interaction.rb +1 -7
- data/lib/active_interaction/base.rb +31 -35
- data/lib/active_interaction/concerns/active_modelable.rb +1 -3
- data/lib/active_interaction/concerns/active_recordable.rb +1 -6
- data/lib/active_interaction/concerns/hashable.rb +0 -1
- data/lib/active_interaction/concerns/missable.rb +0 -1
- data/lib/active_interaction/concerns/runnable.rb +12 -24
- data/lib/active_interaction/errors.rb +6 -19
- data/lib/active_interaction/filter.rb +51 -37
- data/lib/active_interaction/filter_column.rb +0 -3
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +34 -36
- data/lib/active_interaction/filters/abstract_numeric_filter.rb +27 -17
- data/lib/active_interaction/filters/array_filter.rb +59 -36
- data/lib/active_interaction/filters/boolean_filter.rb +26 -12
- data/lib/active_interaction/filters/date_filter.rb +1 -2
- data/lib/active_interaction/filters/date_time_filter.rb +1 -2
- data/lib/active_interaction/filters/decimal_filter.rb +10 -28
- data/lib/active_interaction/filters/file_filter.rb +6 -5
- data/lib/active_interaction/filters/float_filter.rb +1 -2
- data/lib/active_interaction/filters/hash_filter.rb +37 -27
- data/lib/active_interaction/filters/integer_filter.rb +7 -8
- data/lib/active_interaction/filters/interface_filter.rb +48 -14
- data/lib/active_interaction/filters/object_filter.rb +23 -50
- data/lib/active_interaction/filters/record_filter.rb +10 -35
- data/lib/active_interaction/filters/string_filter.rb +21 -12
- data/lib/active_interaction/filters/symbol_filter.rb +13 -7
- data/lib/active_interaction/filters/time_filter.rb +24 -19
- data/lib/active_interaction/grouped_input.rb +0 -3
- data/lib/active_interaction/inputs.rb +95 -0
- data/lib/active_interaction/modules/validation.rb +9 -12
- data/lib/active_interaction/version.rb +1 -3
- data/spec/active_interaction/base_spec.rb +22 -70
- data/spec/active_interaction/concerns/active_modelable_spec.rb +0 -2
- data/spec/active_interaction/concerns/active_recordable_spec.rb +0 -2
- data/spec/active_interaction/concerns/hashable_spec.rb +0 -2
- data/spec/active_interaction/concerns/missable_spec.rb +0 -2
- data/spec/active_interaction/concerns/runnable_spec.rb +26 -12
- data/spec/active_interaction/errors_spec.rb +4 -25
- data/spec/active_interaction/filter_column_spec.rb +0 -2
- data/spec/active_interaction/filter_spec.rb +0 -2
- data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +1 -3
- data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -3
- data/spec/active_interaction/filters/array_filter_spec.rb +51 -14
- data/spec/active_interaction/filters/boolean_filter_spec.rb +58 -4
- data/spec/active_interaction/filters/date_filter_spec.rb +43 -3
- data/spec/active_interaction/filters/date_time_filter_spec.rb +43 -3
- data/spec/active_interaction/filters/decimal_filter_spec.rb +57 -3
- data/spec/active_interaction/filters/file_filter_spec.rb +1 -3
- data/spec/active_interaction/filters/float_filter_spec.rb +60 -4
- data/spec/active_interaction/filters/hash_filter_spec.rb +19 -9
- data/spec/active_interaction/filters/integer_filter_spec.rb +49 -7
- data/spec/active_interaction/filters/interface_filter_spec.rb +397 -24
- data/spec/active_interaction/filters/object_filter_spec.rb +23 -59
- data/spec/active_interaction/filters/record_filter_spec.rb +23 -49
- data/spec/active_interaction/filters/string_filter_spec.rb +15 -3
- data/spec/active_interaction/filters/symbol_filter_spec.rb +15 -3
- data/spec/active_interaction/filters/time_filter_spec.rb +65 -3
- data/spec/active_interaction/grouped_input_spec.rb +0 -2
- data/spec/active_interaction/i18n_spec.rb +3 -7
- data/spec/active_interaction/{modules/input_processor_spec.rb → inputs_spec.rb} +3 -5
- data/spec/active_interaction/integration/array_interaction_spec.rb +18 -22
- data/spec/active_interaction/integration/boolean_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/date_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/date_time_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/file_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/float_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/hash_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/integer_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/interface_interaction_spec.rb +1 -3
- data/spec/active_interaction/integration/object_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/string_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/symbol_interaction_spec.rb +0 -2
- data/spec/active_interaction/integration/time_interaction_spec.rb +14 -18
- data/spec/active_interaction/modules/validation_spec.rb +1 -3
- data/spec/spec_helper.rb +2 -6
- data/spec/support/concerns.rb +0 -2
- data/spec/support/filters.rb +13 -9
- data/spec/support/interactions.rb +22 -14
- metadata +81 -57
- data/lib/active_interaction/backports.rb +0 -59
- data/lib/active_interaction/filters/abstract_filter.rb +0 -19
- data/lib/active_interaction/modules/input_processor.rb +0 -52
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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) &&
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,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) { [[
|
|
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
|
|
676
|
-
.
|
|
677
|
-
|
|
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
|
|
638
|
+
it 'responds to readers and writers' do
|
|
687
639
|
instance = described_class.new
|
|
688
640
|
|
|
689
|
-
described_class.filters.
|
|
690
|
-
[name, "#{name}="
|
|
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
|