interactor_with_steroids 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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