opera 0.5.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +106 -1060
- data/benchmarks/operation_benchmark.rb +330 -0
- data/docs/examples/basic-operation.md +79 -0
- data/docs/examples/context-params-dependencies.md +122 -0
- data/docs/examples/finish-if.md +67 -0
- data/docs/examples/inner-operations.md +94 -0
- data/docs/examples/success-blocks.md +68 -0
- data/docs/examples/transactions.md +227 -0
- data/docs/examples/validations.md +139 -0
- data/docs/examples/within.md +166 -0
- data/lib/opera/operation/config.rb +2 -6
- data/lib/opera/operation/executor.rb +13 -4
- data/lib/opera/operation/instructions/executors/finish_if.rb +1 -2
- data/lib/opera/operation/instructions/executors/operation.rb +1 -2
- data/lib/opera/operation/instructions/executors/operations.rb +1 -2
- data/lib/opera/operation/instructions/executors/step.rb +1 -6
- data/lib/opera/operation/instructions/executors/success.rb +5 -2
- data/lib/opera/operation/instructions/executors/validate.rb +1 -2
- data/lib/opera/operation/instructions/executors/within.rb +1 -1
- data/lib/opera/version.rb +1 -1
- metadata +11 -2
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Performance benchmark for Opera operations.
|
|
4
|
+
#
|
|
5
|
+
# Exercises the full execution path: step dispatch, instruction iteration,
|
|
6
|
+
# context accessors, validate, transaction, success, finish_if, operation,
|
|
7
|
+
# operations, and within -- with nested inner operations and loops to
|
|
8
|
+
# simulate realistic workloads.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ruby benchmarks/operation_benchmark.rb
|
|
12
|
+
|
|
13
|
+
require 'bundler/setup'
|
|
14
|
+
require 'benchmark'
|
|
15
|
+
require 'opera'
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Fake transaction class (no DB, just yields)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
FakeTransaction = Class.new do
|
|
21
|
+
def self.transaction
|
|
22
|
+
yield
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Opera::Operation::Config.configure do |config|
|
|
27
|
+
config.transaction_class = FakeTransaction
|
|
28
|
+
config.mode = :production # skip execution traces, like real production
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Leaf operation — called many times from within loops
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
LeafOperation = Class.new(Opera::Operation::Base) do
|
|
35
|
+
step :compute
|
|
36
|
+
step :output
|
|
37
|
+
|
|
38
|
+
def compute
|
|
39
|
+
context[:value] = params.fetch(:n, 1) * 2
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def output
|
|
43
|
+
result.output = { value: context[:value] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Inner operation — calls LeafOperation in a loop
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
InnerOperation = Class.new(Opera::Operation::Base) do
|
|
51
|
+
context do
|
|
52
|
+
attr_accessor :results
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
step :process_batch
|
|
56
|
+
step :output
|
|
57
|
+
|
|
58
|
+
def process_batch
|
|
59
|
+
self.results = (1..params.fetch(:batch_size, 5)).map do |n|
|
|
60
|
+
LeafOperation.call(params: { n: n })
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def output
|
|
65
|
+
result.output = { batch: results.map(&:output) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Validation-heavy operation
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
ValidationOperation = Class.new(Opera::Operation::Base) do
|
|
73
|
+
validate :schema
|
|
74
|
+
|
|
75
|
+
step :transform
|
|
76
|
+
step :output
|
|
77
|
+
|
|
78
|
+
def schema
|
|
79
|
+
# Return a successful Opera::Operation::Result (simulates dry-validation)
|
|
80
|
+
Opera::Operation::Result.new(output: params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def transform
|
|
84
|
+
context[:transformed] = params.transform_values { |v| v.to_s.upcase }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def output
|
|
88
|
+
result.output = context[:transformed]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Complex operation — combines everything
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
ComplexOperation = Class.new(Opera::Operation::Base) do
|
|
96
|
+
configure do |config|
|
|
97
|
+
config.transaction_class = FakeTransaction
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context do
|
|
101
|
+
attr_accessor :profile, :batch_results, :validated
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
validate :schema
|
|
105
|
+
|
|
106
|
+
step :prepare
|
|
107
|
+
finish_if :skip_processing?
|
|
108
|
+
|
|
109
|
+
transaction do
|
|
110
|
+
step :create_record
|
|
111
|
+
step :update_record
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
operation :run_inner
|
|
115
|
+
|
|
116
|
+
within :with_timing do
|
|
117
|
+
step :heavy_computation
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
success do
|
|
121
|
+
step :notify
|
|
122
|
+
step :log_audit
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
step :output
|
|
126
|
+
|
|
127
|
+
def schema
|
|
128
|
+
Opera::Operation::Result.new(output: params)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def prepare
|
|
132
|
+
self.validated = context[:schema_output]
|
|
133
|
+
context[:counter] = 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def skip_processing?
|
|
137
|
+
params[:skip] == true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def create_record
|
|
141
|
+
self.profile = { id: rand(1000), name: validated[:name] }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def update_record
|
|
145
|
+
profile[:updated_at] = Time.now.to_i
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def run_inner
|
|
149
|
+
InnerOperation.call(params: { batch_size: params.fetch(:batch_size, 5) })
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def heavy_computation
|
|
153
|
+
# Simulate CPU work: string operations in a loop
|
|
154
|
+
50.times do |i|
|
|
155
|
+
context[:counter] += i
|
|
156
|
+
"operation-#{i}-#{context[:counter]}".hash
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def notify
|
|
161
|
+
context[:notified] = true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def log_audit
|
|
165
|
+
context[:audited] = true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def output
|
|
169
|
+
result.output = {
|
|
170
|
+
profile: profile,
|
|
171
|
+
counter: context[:counter],
|
|
172
|
+
batch: context[:run_inner_output]
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def with_timing
|
|
177
|
+
yield
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Within operation — wraps steps and inner operations with a custom method
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
WithinOperation = Class.new(Opera::Operation::Base) do
|
|
185
|
+
configure do |config|
|
|
186
|
+
config.transaction_class = FakeTransaction
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
context do
|
|
190
|
+
attr_accessor :log, default: -> { [] }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
step :prepare
|
|
194
|
+
|
|
195
|
+
within :with_connection do
|
|
196
|
+
step :query_one
|
|
197
|
+
step :query_two
|
|
198
|
+
operation :fetch_leaf
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
transaction do
|
|
202
|
+
within :with_lock do
|
|
203
|
+
step :write_one
|
|
204
|
+
step :write_two
|
|
205
|
+
end
|
|
206
|
+
step :write_three
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
step :output
|
|
210
|
+
|
|
211
|
+
def prepare
|
|
212
|
+
context[:counter] = 0
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def query_one
|
|
216
|
+
context[:counter] += 1
|
|
217
|
+
log << :query_one
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def query_two
|
|
221
|
+
context[:counter] += 1
|
|
222
|
+
log << :query_two
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def fetch_leaf
|
|
226
|
+
LeafOperation.call(params: { n: context[:counter] })
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def write_one
|
|
230
|
+
context[:counter] += 10
|
|
231
|
+
log << :write_one
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def write_two
|
|
235
|
+
context[:counter] += 10
|
|
236
|
+
log << :write_two
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def write_three
|
|
240
|
+
context[:counter] += 1
|
|
241
|
+
log << :write_three
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def output
|
|
245
|
+
result.output = {
|
|
246
|
+
counter: context[:counter],
|
|
247
|
+
log: log,
|
|
248
|
+
leaf: context[:fetch_leaf_output]
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def with_connection
|
|
253
|
+
log << :connect
|
|
254
|
+
yield
|
|
255
|
+
log << :disconnect
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def with_lock
|
|
259
|
+
log << :lock
|
|
260
|
+
yield
|
|
261
|
+
log << :unlock
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Operations (plural) consumer — calls multiple inner operations
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
BatchOperation = Class.new(Opera::Operation::Base) do
|
|
269
|
+
operations :run_all
|
|
270
|
+
step :output
|
|
271
|
+
|
|
272
|
+
def run_all
|
|
273
|
+
(1..params.fetch(:count, 3)).map do |n|
|
|
274
|
+
LeafOperation.call(params: { n: n })
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def output
|
|
279
|
+
result.output = context[:run_all_output]
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Benchmark
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
ITERATIONS = 1000
|
|
287
|
+
PARAMS = { name: 'benchmark', batch_size: 5 }.freeze
|
|
288
|
+
BATCH_PARAMS = { count: 5 }.freeze
|
|
289
|
+
VALIDATION_PARAMS = { first_name: 'Jane', last_name: 'Doe', email: 'jane@example.com' }.freeze
|
|
290
|
+
WITHIN_PARAMS = {}.freeze
|
|
291
|
+
|
|
292
|
+
# Warm up
|
|
293
|
+
3.times do
|
|
294
|
+
ComplexOperation.call(params: PARAMS)
|
|
295
|
+
BatchOperation.call(params: BATCH_PARAMS)
|
|
296
|
+
ValidationOperation.call(params: VALIDATION_PARAMS)
|
|
297
|
+
WithinOperation.call(params: WITHIN_PARAMS)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
puts "Opera v#{Opera::VERSION} — #{ITERATIONS} iterations each"
|
|
301
|
+
puts "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
|
302
|
+
puts '-' * 60
|
|
303
|
+
|
|
304
|
+
Benchmark.bm(35) do |x|
|
|
305
|
+
x.report('ComplexOperation (nested + tx):') do
|
|
306
|
+
ITERATIONS.times { ComplexOperation.call(params: PARAMS) }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
x.report('BatchOperation (operations):') do
|
|
310
|
+
ITERATIONS.times { BatchOperation.call(params: BATCH_PARAMS) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
x.report('ValidationOperation (validate):') do
|
|
314
|
+
ITERATIONS.times { ValidationOperation.call(params: VALIDATION_PARAMS) }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
x.report('WithinOperation (within + tx):') do
|
|
318
|
+
ITERATIONS.times { WithinOperation.call(params: WITHIN_PARAMS) }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
x.report('LeafOperation (minimal):') do
|
|
322
|
+
ITERATIONS.times { LeafOperation.call(params: { n: 42 }) }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Total operations executed in ComplexOperation run:
|
|
326
|
+
# 1 complex + 1 inner + 5 leaf = 7 operations per iteration
|
|
327
|
+
# = 7000 total operation instantiations for ComplexOperation alone
|
|
328
|
+
total_ops = ITERATIONS * 7
|
|
329
|
+
puts "\nComplexOperation spawns ~#{total_ops} total operation instances across #{ITERATIONS} calls"
|
|
330
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Basic Operation
|
|
2
|
+
|
|
3
|
+
A simple operation that validates input, creates a record, sends an email, and returns output.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Profile::Create < Opera::Operation::Base
|
|
7
|
+
context do
|
|
8
|
+
attr_accessor :profile
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
dependencies do
|
|
12
|
+
attr_reader :current_account, :mailer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
validate :profile_schema
|
|
16
|
+
|
|
17
|
+
step :create
|
|
18
|
+
step :send_email
|
|
19
|
+
step :output
|
|
20
|
+
|
|
21
|
+
def profile_schema
|
|
22
|
+
Dry::Validation.Schema do
|
|
23
|
+
required(:first_name).filled
|
|
24
|
+
end.call(params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create
|
|
28
|
+
self.profile = current_account.profiles.create(params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def send_email
|
|
32
|
+
mailer&.send_mail(profile: profile)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def output
|
|
36
|
+
result.output = { model: profile }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Call with valid parameters
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
Profile::Create.call(params: {
|
|
45
|
+
first_name: :foo,
|
|
46
|
+
last_name: :bar
|
|
47
|
+
}, dependencies: {
|
|
48
|
+
mailer: MyMailer,
|
|
49
|
+
current_account: Account.find(1)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
#<Opera::Operation::Result:0x0000561636dced60 @errors={}, @information={}, @executions=[:profile_schema, :create, :send_email, :output], @output={:model=>#<Profile id: 30, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2020-08-14 16:04:08", updated_at: "2020-08-14 16:04:08", agree_to_terms_and_conditions: nil, registration_status: "", account_id: 1, start_date: nil, supervisor_id: nil, picture_processing: false, statistics: {}, data: {}, notification_timestamps: {}, suggestions: {}, notification_settings: {}, contact_information: []>}>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Call with INVALID parameters - missing first_name
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Profile::Create.call(params: {
|
|
59
|
+
last_name: :bar
|
|
60
|
+
}, dependencies: {
|
|
61
|
+
mailer: MyMailer,
|
|
62
|
+
current_account: Account.find(1)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
#<Opera::Operation::Result:0x0000562d3f635390 @errors={:first_name=>["is missing"]}, @information={}, @executions=[:profile_schema]>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Call with MISSING dependencies
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Profile::Create.call(params: {
|
|
72
|
+
first_name: :foo,
|
|
73
|
+
last_name: :bar
|
|
74
|
+
}, dependencies: {
|
|
75
|
+
current_account: Account.find(1)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
#<Opera::Operation::Result:0x007f87ba2c8f00 @errors={}, @information={}, @executions=[:profile_schema, :create, :send_email, :output], @output={:model=>#<Profile id: 33, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2019-01-03 12:04:25", updated_at: "2019-01-03 12:04:25", agree_to_terms_and_conditions: nil, registration_status: "", account_id: 1, start_date: nil, supervisor_id: nil, picture_processing: false, statistics: {}, data: {}, notification_timestamps: {}, suggestions: {}, notification_settings: {}, contact_information: []>}>
|
|
79
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Context, Params & Dependencies
|
|
2
|
+
|
|
3
|
+
Opera provides typed accessor blocks for managing state within an operation.
|
|
4
|
+
|
|
5
|
+
## context
|
|
6
|
+
|
|
7
|
+
Mutable hash for passing data between steps. Supports `attr_reader`, `attr_writer`, and `attr_accessor`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
context do
|
|
11
|
+
attr_accessor :profile
|
|
12
|
+
attr_accessor :account, default: -> { Account.new }
|
|
13
|
+
attr_reader :schema_output
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `attr_accessor` defines getter and setter methods that read/write to the `context` hash
|
|
18
|
+
- `attr_reader` defines only a getter
|
|
19
|
+
- `default` accepts a lambda, evaluated lazily on first access when the key is missing
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
context do
|
|
23
|
+
attr_accessor :profile, :account
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
step :fetch_profile
|
|
27
|
+
step :update_profile
|
|
28
|
+
|
|
29
|
+
def fetch_profile
|
|
30
|
+
self.profile = ProfileFetcher.call # sets context[:profile]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_profile
|
|
34
|
+
profile.update!(name: 'John') # reads profile from context[:profile]
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## params
|
|
39
|
+
|
|
40
|
+
Immutable hash received in the `call` method. Only supports `attr_reader`.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
params do
|
|
44
|
+
attr_reader :activity, :requester
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## dependencies
|
|
49
|
+
|
|
50
|
+
Immutable hash received in the `call` method. Only supports `attr_reader`.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
dependencies do
|
|
54
|
+
attr_reader :current_account, :mailer
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## context_reader with defaults
|
|
59
|
+
|
|
60
|
+
Use `context_reader` to read step outputs from the context hash:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
context_reader :schema_output
|
|
64
|
+
|
|
65
|
+
validate :schema # context = { schema_output: { id: 1 } }
|
|
66
|
+
step :do_something
|
|
67
|
+
|
|
68
|
+
def do_something
|
|
69
|
+
puts schema_output # outputs: { id: 1 }
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `default` to provide a fallback value when the key is missing:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
context_reader :profile, default: -> { Profile.new }
|
|
77
|
+
|
|
78
|
+
step :fetch_profile
|
|
79
|
+
step :do_something
|
|
80
|
+
|
|
81
|
+
def fetch_profile
|
|
82
|
+
return if App.http_disabled?
|
|
83
|
+
|
|
84
|
+
context[:profile] = ProfileFetcher.call
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_profile
|
|
88
|
+
profile.name = 'John'
|
|
89
|
+
profile.save!
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Best practices
|
|
94
|
+
|
|
95
|
+
**Good** -- Use `context_reader` for step outputs and shared state:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
context_reader :schema_output
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Bad** -- Don't use `context_reader` with `default` for transient objects that aren't stored in context:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# BAD: suggests serializer is part of persistent state
|
|
105
|
+
context_reader :serializer, default: -> { ProfileSerializer.new }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Better** -- Use private methods for transient dependencies:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
step :output
|
|
112
|
+
|
|
113
|
+
def output
|
|
114
|
+
self.result = serializer.to_json({...})
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def serializer
|
|
120
|
+
ProfileSerializer.new
|
|
121
|
+
end
|
|
122
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Finish If
|
|
2
|
+
|
|
3
|
+
`finish_if` evaluates a method and stops execution (successfully) if the method returns a truthy value. Subsequent steps are skipped, but the operation is considered successful.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Profile::Create < Opera::Operation::Base
|
|
7
|
+
context do
|
|
8
|
+
attr_accessor :profile
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
dependencies do
|
|
12
|
+
attr_reader :current_account, :mailer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
validate :profile_schema
|
|
16
|
+
|
|
17
|
+
step :create
|
|
18
|
+
finish_if :profile_create_only
|
|
19
|
+
step :update
|
|
20
|
+
|
|
21
|
+
success do
|
|
22
|
+
step :send_email
|
|
23
|
+
step :output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def profile_schema
|
|
27
|
+
Dry::Validation.Schema do
|
|
28
|
+
required(:first_name).filled
|
|
29
|
+
end.call(params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create
|
|
33
|
+
self.profile = current_account.profiles.create(params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def profile_create_only
|
|
37
|
+
dependencies[:create_only].present?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update
|
|
41
|
+
profile.update(updated_at: 1.day.ago)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# NOTE: We can add an error in this step and it won't break the execution
|
|
45
|
+
def send_email
|
|
46
|
+
result.add_error('mailer', 'Missing dependency')
|
|
47
|
+
mailer&.send_mail(profile: profile)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output
|
|
51
|
+
result.output = { model: context[:profile] }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Profile::Create.call(params: {
|
|
60
|
+
first_name: :foo,
|
|
61
|
+
last_name: :bar
|
|
62
|
+
}, dependencies: {
|
|
63
|
+
create_only: true,
|
|
64
|
+
current_account: Account.find(1)
|
|
65
|
+
})
|
|
66
|
+
#<Opera::Operation::Result:0x007fd0248e5638 @errors={}, @information={}, @executions=[:profile_schema, :create, :profile_create_only], @output={}>
|
|
67
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Inner Operations
|
|
2
|
+
|
|
3
|
+
## Single operation
|
|
4
|
+
|
|
5
|
+
Use `operation` to call another Opera operation from within a step. The method must return an `Opera::Operation::Result`. If the inner operation fails, errors are propagated and execution stops. If it succeeds, its output is stored in context as `:<method_name>_output`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Profile::Find < Opera::Operation::Base
|
|
9
|
+
step :find
|
|
10
|
+
|
|
11
|
+
def find
|
|
12
|
+
result.output = Profile.find(params[:id])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Profile::Create < Opera::Operation::Base
|
|
17
|
+
validate :profile_schema
|
|
18
|
+
|
|
19
|
+
operation :find
|
|
20
|
+
|
|
21
|
+
step :create
|
|
22
|
+
|
|
23
|
+
step :output
|
|
24
|
+
|
|
25
|
+
def profile_schema
|
|
26
|
+
Dry::Validation.Schema do
|
|
27
|
+
optional(:id).filled
|
|
28
|
+
end.call(params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find
|
|
32
|
+
Profile::Find.call(params: params, dependencies: dependencies)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
return if context[:find_output]
|
|
37
|
+
puts 'not found'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output
|
|
41
|
+
result.output = { model: context[:find_output] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Example with inner operation doing the find
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Profile::Create.call(params: {
|
|
50
|
+
id: 1
|
|
51
|
+
}, dependencies: {
|
|
52
|
+
current_account: Account.find(1)
|
|
53
|
+
})
|
|
54
|
+
#<Opera::Operation::Result:0x007f99b25f0f20 @errors={}, @information={}, @executions=[:profile_schema, :find, :create, :output], @output={:model=>{:id=>1, :user_id=>1, :linkedin_uid=>nil, ...}}>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Multiple operations
|
|
58
|
+
|
|
59
|
+
Use `operations` when a method returns an array of `Opera::Operation::Result` objects. If any of the inner operations fail, all their errors are collected and execution stops.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class Profile::Create < Opera::Operation::Base
|
|
63
|
+
step :validate
|
|
64
|
+
step :create
|
|
65
|
+
|
|
66
|
+
def validate; end
|
|
67
|
+
|
|
68
|
+
def create
|
|
69
|
+
result.output = { model: "Profile #{Kernel.rand(100)}" }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Profile::CreateMultiple < Opera::Operation::Base
|
|
74
|
+
operations :create_multiple
|
|
75
|
+
|
|
76
|
+
step :output
|
|
77
|
+
|
|
78
|
+
def create_multiple
|
|
79
|
+
(0..params[:number]).map do
|
|
80
|
+
Profile::Create.call
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def output
|
|
85
|
+
result.output = context[:create_multiple_output]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Profile::CreateMultiple.call(params: { number: 3 })
|
|
92
|
+
|
|
93
|
+
#<Opera::Operation::Result:0x0000564189f38c90 @errors={}, @information={}, @executions=[{:create_multiple=>[[:validate, :create], [:validate, :create], [:validate, :create], [:validate, :create]]}, :output], @output=[{:model=>"Profile 1"}, {:model=>"Profile 7"}, {:model=>"Profile 69"}, {:model=>"Profile 92"}]>
|
|
94
|
+
```
|