interactor_with_steroids 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.standard.yml +4 -0
- data/.travis.yml +37 -0
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +49 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +602 -0
- data/Rakefile +7 -0
- data/interactor.gemspec +20 -0
- data/lib/interactor/context.rb +139 -0
- data/lib/interactor/declaration.rb +85 -0
- data/lib/interactor/error.rb +31 -0
- data/lib/interactor/hooks.rb +263 -0
- data/lib/interactor/organizer.rb +69 -0
- data/lib/interactor.rb +158 -0
- data/spec/interactor/context_spec.rb +141 -0
- data/spec/interactor/declaration_spec.rb +116 -0
- data/spec/interactor/hooks_spec.rb +358 -0
- data/spec/interactor/organizer_spec.rb +60 -0
- data/spec/interactor_spec.rb +127 -0
- data/spec/spec_helper.rb +6 -0
- metadata +113 -0
data/lib/interactor.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require "interactor/context"
|
2
|
+
require "interactor/declaration"
|
3
|
+
require "interactor/error"
|
4
|
+
require "interactor/hooks"
|
5
|
+
require "interactor/organizer"
|
6
|
+
|
7
|
+
require "active_support/concern"
|
8
|
+
|
9
|
+
# Public: Interactor methods. Because Interactor is a module, custom Interactor
|
10
|
+
# classes should include Interactor rather than inherit from it.
|
11
|
+
#
|
12
|
+
# Examples
|
13
|
+
#
|
14
|
+
# class MyInteractor
|
15
|
+
# include Interactor
|
16
|
+
#
|
17
|
+
# def call
|
18
|
+
# puts context.foo
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
module Interactor
|
22
|
+
extend ActiveSupport::Concern
|
23
|
+
include Hooks
|
24
|
+
include Declaration
|
25
|
+
|
26
|
+
included do
|
27
|
+
# Public: Gets the Interactor::Context of the Interactor instance.
|
28
|
+
attr_reader :context
|
29
|
+
end
|
30
|
+
|
31
|
+
# Internal: Interactor class methods.
|
32
|
+
class_methods do
|
33
|
+
# Public: Invoke an Interactor. This is the primary public API method to an
|
34
|
+
# interactor.
|
35
|
+
#
|
36
|
+
# context - A Hash whose key/value pairs are used in initializing a new
|
37
|
+
# Interactor::Context object. An existing Interactor::Context may
|
38
|
+
# also be given. (default: {})
|
39
|
+
#
|
40
|
+
# Examples
|
41
|
+
#
|
42
|
+
# MyInteractor.call(foo: "bar")
|
43
|
+
# # => #<Interactor::Context foo="bar">
|
44
|
+
#
|
45
|
+
# MyInteractor.call
|
46
|
+
# # => #<Interactor::Context>
|
47
|
+
#
|
48
|
+
# Returns the resulting Interactor::Context after manipulation by the
|
49
|
+
# interactor.
|
50
|
+
def call(context = {})
|
51
|
+
new(context).tap(&:run).context
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Invoke an Interactor. The "call!" method behaves identically to
|
55
|
+
# the "call" method with one notable exception. If the context is failed
|
56
|
+
# during invocation of the interactor, the Interactor::Failure is raised.
|
57
|
+
#
|
58
|
+
# context - A Hash whose key/value pairs are used in initializing a new
|
59
|
+
# Interactor::Context object. An existing Interactor::Context may
|
60
|
+
# also be given. (default: {})
|
61
|
+
#
|
62
|
+
# Examples
|
63
|
+
#
|
64
|
+
# MyInteractor.call!(foo: "bar")
|
65
|
+
# # => #<Interactor::Context foo="bar">
|
66
|
+
#
|
67
|
+
# MyInteractor.call!
|
68
|
+
# # => #<Interactor::Context>
|
69
|
+
#
|
70
|
+
# MyInteractor.call!(foo: "baz")
|
71
|
+
# # => Interactor::Failure: #<Interactor::Context foo="baz">
|
72
|
+
#
|
73
|
+
# Returns the resulting Interactor::Context after manipulation by the
|
74
|
+
# interactor.
|
75
|
+
# Raises Interactor::Failure if the context is failed.
|
76
|
+
def call!(context = {})
|
77
|
+
new(context).tap(&:run!).context
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Internal: Initialize an Interactor.
|
82
|
+
#
|
83
|
+
# context - A Hash whose key/value pairs are used in initializing the
|
84
|
+
# interactor's context. An existing Interactor::Context may also be
|
85
|
+
# given. (default: {})
|
86
|
+
#
|
87
|
+
# Examples
|
88
|
+
#
|
89
|
+
# MyInteractor.new(foo: "bar")
|
90
|
+
# # => #<MyInteractor @context=#<Interactor::Context foo="bar">>
|
91
|
+
#
|
92
|
+
# MyInteractor.new
|
93
|
+
# # => #<MyInteractor @context=#<Interactor::Context>>
|
94
|
+
def initialize(context = {})
|
95
|
+
@context = self.context_class.build(context)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Internal: Invoke an interactor instance along with all defined hooks. The
|
99
|
+
# "run" method is used internally by the "call" class method. The following
|
100
|
+
# are equivalent:
|
101
|
+
#
|
102
|
+
# MyInteractor.call(foo: "bar")
|
103
|
+
# # => #<Interactor::Context foo="bar">
|
104
|
+
#
|
105
|
+
# interactor = MyInteractor.new(foo: "bar")
|
106
|
+
# interactor.run
|
107
|
+
# interactor.context
|
108
|
+
# # => #<Interactor::Context foo="bar">
|
109
|
+
#
|
110
|
+
# After successful invocation of the interactor, the instance is tracked
|
111
|
+
# within the context. If the context is failed or any error is raised, the
|
112
|
+
# context is rolled back.
|
113
|
+
#
|
114
|
+
# Returns nothing.
|
115
|
+
def run
|
116
|
+
run!
|
117
|
+
rescue Failure
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Invoke an Interactor instance along with all defined hooks. The
|
121
|
+
# "run!" method is used internally by the "call!" class method. The following
|
122
|
+
# are equivalent:
|
123
|
+
#
|
124
|
+
# MyInteractor.call!(foo: "bar")
|
125
|
+
# # => #<Interactor::Context foo="bar">
|
126
|
+
#
|
127
|
+
# interactor = MyInteractor.new(foo: "bar")
|
128
|
+
# interactor.run!
|
129
|
+
# interactor.context
|
130
|
+
# # => #<Interactor::Context foo="bar">
|
131
|
+
#
|
132
|
+
# After successful invocation of the interactor, the instance is tracked
|
133
|
+
# within the context. If the context is failed or any error is raised, the
|
134
|
+
# context is rolled back.
|
135
|
+
#
|
136
|
+
# The "run!" method behaves identically to the "run" method with one notable
|
137
|
+
# exception. If the context is failed during invocation of the interactor,
|
138
|
+
# the Interactor::Failure is raised.
|
139
|
+
#
|
140
|
+
# Returns nothing.
|
141
|
+
# Raises Interactor::Failure if the context is failed.
|
142
|
+
def run!
|
143
|
+
with_hooks do
|
144
|
+
call
|
145
|
+
end
|
146
|
+
rescue Failure => e
|
147
|
+
# Make sure we fail the current context when a call! to another interactor fails
|
148
|
+
context.fail!(error: e.context&.error)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Public: Invoke an Interactor instance without any hooks, tracking, or
|
152
|
+
# rollback. It is expected that the "call" instance method is overwritten for
|
153
|
+
# each interactor class.
|
154
|
+
#
|
155
|
+
# Returns nothing.
|
156
|
+
def call
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Interactor
|
2
|
+
|
3
|
+
describe Context do
|
4
|
+
def build_interactor(&block)
|
5
|
+
Class.new.send(:include, Interactor).tap do |interactor|
|
6
|
+
interactor.class_eval(&block) if block
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:interactor) {
|
11
|
+
build_interactor do
|
12
|
+
receive :foo
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
describe ".build" do
|
17
|
+
it "converts the given hash to a context" do
|
18
|
+
context = interactor.context_class.build(foo: "bar")
|
19
|
+
|
20
|
+
expect(context).to be_a(Context)
|
21
|
+
expect(context.foo).to eq("bar")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises if a required argument is not provided" do
|
25
|
+
expect { interactor.context_class.build }.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "doesn't affect the original hash" do
|
29
|
+
hash = { foo: "bar" }
|
30
|
+
context = interactor.context_class.build(hash)
|
31
|
+
|
32
|
+
expect(context).to be_a(interactor.context_class)
|
33
|
+
expect {
|
34
|
+
context.foo = "baz"
|
35
|
+
}.not_to change {
|
36
|
+
hash[:foo]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#success?" do
|
42
|
+
let(:context) { Context.build }
|
43
|
+
|
44
|
+
it "is true by default" do
|
45
|
+
expect(context.success?).to eq(true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#failure?" do
|
50
|
+
let(:context) { Context.build }
|
51
|
+
|
52
|
+
it "is false by default" do
|
53
|
+
expect(context.failure?).to eq(false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#fail!" do
|
58
|
+
let(:context) { interactor.context_class.build(foo: "bar") }
|
59
|
+
|
60
|
+
it "sets success to false" do
|
61
|
+
expect {
|
62
|
+
begin
|
63
|
+
context.fail!
|
64
|
+
rescue
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
}.to change {
|
68
|
+
context.success?
|
69
|
+
}.from(true).to(false)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "sets failure to true" do
|
73
|
+
expect {
|
74
|
+
begin
|
75
|
+
context.fail!
|
76
|
+
rescue
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
}.to change {
|
80
|
+
context.failure?
|
81
|
+
}.from(false).to(true)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "preserves failure" do
|
85
|
+
begin
|
86
|
+
context.fail!
|
87
|
+
rescue
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
expect {
|
92
|
+
begin
|
93
|
+
context.fail!
|
94
|
+
rescue
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
}.not_to change {
|
98
|
+
context.failure?
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "preserves the context" do
|
103
|
+
expect {
|
104
|
+
begin
|
105
|
+
context.fail!
|
106
|
+
rescue
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
}.not_to change {
|
110
|
+
context.foo
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
it "updates the context" do
|
115
|
+
expect {
|
116
|
+
begin
|
117
|
+
context.fail!(error: "baz")
|
118
|
+
rescue
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
}.to change {
|
122
|
+
context.error
|
123
|
+
}.to("baz")
|
124
|
+
end
|
125
|
+
|
126
|
+
it "raises failure" do
|
127
|
+
expect {
|
128
|
+
context.fail!
|
129
|
+
}.to raise_error(Failure)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "makes the context available from the failure" do
|
133
|
+
begin
|
134
|
+
context.fail!
|
135
|
+
rescue Failure => error
|
136
|
+
expect(error.context).to eq(context)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Interactor
|
2
|
+
describe Declaration do
|
3
|
+
def build_declared(&block)
|
4
|
+
Class.new.send(:include, Declaration).tap do |declared|
|
5
|
+
declared.class_eval(&block)
|
6
|
+
|
7
|
+
declared.class_eval do
|
8
|
+
def context
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { declared.context_class }
|
16
|
+
|
17
|
+
describe "#receive" do
|
18
|
+
context "with a required argument" do
|
19
|
+
let(:declared) {
|
20
|
+
build_declared do
|
21
|
+
receive :foo
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
it "cannot be initialized without foo" do
|
26
|
+
expect { subject.new }.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can be initialized with foo" do
|
30
|
+
expect(subject.new(foo: 'bar').foo).to eq('bar')
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when duplicated in a submodule' do
|
34
|
+
let(:submodule) do
|
35
|
+
Module.new do
|
36
|
+
extend ActiveSupport::Concern
|
37
|
+
|
38
|
+
included do
|
39
|
+
receive :foo
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:declared) {
|
45
|
+
build_declared do
|
46
|
+
include Submodule
|
47
|
+
|
48
|
+
receive :foo
|
49
|
+
end
|
50
|
+
}
|
51
|
+
|
52
|
+
before { stub_const("Submodule", submodule) }
|
53
|
+
|
54
|
+
it "can be initialized with foo" do
|
55
|
+
expect(subject.new(foo: 'bar').foo).to eq('bar')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
context "with an optional argument" do
|
62
|
+
context "with a constant default value" do
|
63
|
+
let(:declared) {
|
64
|
+
build_declared do
|
65
|
+
receive foo: 'bar'
|
66
|
+
end
|
67
|
+
}
|
68
|
+
|
69
|
+
it "can be initialized without foo" do
|
70
|
+
expect(subject.new.foo).to eq('bar')
|
71
|
+
end
|
72
|
+
|
73
|
+
it "can be initialized with foo" do
|
74
|
+
expect(subject.new(foo: 'baz').foo).to eq('baz')
|
75
|
+
end
|
76
|
+
|
77
|
+
it "can be initialized with nil" do
|
78
|
+
expect(subject.new(foo: nil).foo).to be nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with a nil default value" do
|
83
|
+
let(:declared) {
|
84
|
+
build_declared do
|
85
|
+
receive foo: nil
|
86
|
+
end
|
87
|
+
}
|
88
|
+
|
89
|
+
it "can be initialized without foo" do
|
90
|
+
expect(subject.new.foo).to be nil
|
91
|
+
end
|
92
|
+
|
93
|
+
it "can be initialized with foo" do
|
94
|
+
expect(subject.new(foo: 'baz').foo).to eq('baz')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "with a Proc default value" do
|
99
|
+
let(:declared) {
|
100
|
+
build_declared do
|
101
|
+
receive :bar, foo: ->(context) { context.bar }
|
102
|
+
end
|
103
|
+
}
|
104
|
+
|
105
|
+
it "can be initialized without foo" do
|
106
|
+
expect(subject.new(bar: 'bar').foo).to eq('bar')
|
107
|
+
end
|
108
|
+
|
109
|
+
it "can be initialized with foo" do
|
110
|
+
expect(subject.new(bar: 'bar', foo: 'baz').foo).to eq('baz')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|