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