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
data/README.md
CHANGED
|
@@ -3,673 +3,30 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/opera)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
A lightweight DSL for building operations, services and interactions in Ruby. Zero runtime dependencies.
|
|
7
7
|
|
|
8
|
-
Opera
|
|
8
|
+
Opera gives developers a consistent way to structure business logic as a pipeline of steps -- validate, execute, handle errors -- with a declarative DSL at the top of each class that makes the flow immediately readable.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
Add this line to your application's Gemfile:
|
|
15
|
-
|
|
16
|
-
```ruby
|
|
17
|
-
gem 'opera'
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
And then execute:
|
|
21
|
-
|
|
22
|
-
$ bundle install
|
|
23
|
-
|
|
24
|
-
Or install it yourself as:
|
|
25
|
-
|
|
26
|
-
$ gem install opera
|
|
27
|
-
|
|
28
|
-
Note. If you are using Ruby 2.x please use Opera 0.2.x
|
|
29
|
-
|
|
30
|
-
## Configuration
|
|
31
|
-
|
|
32
|
-
Opera is built to be used with or without Rails.
|
|
33
|
-
Simply initialize the configuration and choose a custom logger and which library to use for implementing transactions.
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
Opera::Operation::Config.configure do |config|
|
|
37
|
-
config.transaction_class = ActiveRecord::Base
|
|
38
|
-
config.transaction_method = :transaction
|
|
39
|
-
config.transaction_options = { requires_new: true, level: :step } # or level: :operation - default
|
|
40
|
-
config.instrumentation_class = Datadog::Tracing
|
|
41
|
-
config.instrumentation_method = :trace
|
|
42
|
-
config.instrumentation_options = { service: :operation }
|
|
43
|
-
config.mode = :development # Can be set to production too
|
|
44
|
-
config.reporter = defined?(Rollbar) ? Rollbar : Rails.logger
|
|
45
|
-
end
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
You can later override this configuration in each Operation to have more granularity
|
|
49
|
-
|
|
50
|
-
## Usage
|
|
51
|
-
|
|
52
|
-
Once opera gem is in your project you can start to build Operations
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
class A < Opera::Operation::Base
|
|
56
|
-
configure do |config|
|
|
57
|
-
config.transaction_class = Profile
|
|
58
|
-
config.reporter = Rails.logger
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
success :populate
|
|
62
|
-
|
|
63
|
-
operation :inner_operation
|
|
64
|
-
|
|
65
|
-
validate :profile_schema
|
|
66
|
-
|
|
67
|
-
transaction do
|
|
68
|
-
step :create
|
|
69
|
-
step :update
|
|
70
|
-
step :destroy
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
validate do
|
|
74
|
-
step :validate_object
|
|
75
|
-
step :validate_relationships
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
success do
|
|
79
|
-
step :send_mail
|
|
80
|
-
step :report_to_audit_log
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
step :output
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Start developing your business logic, services and interactions as Opera::Operations and benefit of code that is documented, self-explanatory, easy to maintain and debug.
|
|
88
|
-
|
|
89
|
-
### Specs
|
|
90
|
-
|
|
91
|
-
When using Opera::Operation inside an engine add the following
|
|
92
|
-
configuration to your spec_helper.rb or rails_helper.rb:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
Opera::Operation::Config.configure do |config|
|
|
96
|
-
config.transaction_class = ActiveRecord::Base
|
|
97
|
-
end
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Without this extra configuration you will receive:
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
NoMethodError:
|
|
104
|
-
undefined method `transaction' for nil:NilClass
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Instrumentation
|
|
108
|
-
|
|
109
|
-
When you want to easily instrument your operations you can add this to the opera config:
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
Rails.application.configure do
|
|
113
|
-
config.x.instrumentation_class = Datadog::Tracing
|
|
114
|
-
config.x.instrumentation_method = :trace
|
|
115
|
-
config.x.instrumentation_options = { service: :opera }
|
|
116
|
-
end
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
You can also instrument individual operations by adding this to the operation config:
|
|
120
|
-
|
|
121
|
-
```ruby
|
|
122
|
-
class A < Opera::Operation::Base
|
|
123
|
-
configure do |config|
|
|
124
|
-
config.instrumentation_class = Datadog::Tracing
|
|
125
|
-
config.instrumentation_method = :trace
|
|
126
|
-
config.instrumentation_options = { service: :opera, level: :step }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# steps
|
|
130
|
-
end
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Content
|
|
134
|
-
|
|
135
|
-
[Basic operation](#user-content-basic-operation)
|
|
136
|
-
|
|
137
|
-
[Example with sanitizing parameters](#user-content-example-with-sanitizing-parameters)
|
|
138
|
-
|
|
139
|
-
[Example operation with old validations](#user-content-example-operation-with-old-validations)
|
|
140
|
-
|
|
141
|
-
[Failing transaction](#user-content-failing-transaction)
|
|
142
|
-
|
|
143
|
-
[Passing transaction](#user-content-passing-transaction)
|
|
144
|
-
|
|
145
|
-
[Success](#user-content-success)
|
|
146
|
-
|
|
147
|
-
[Finish if](#user-content-finish-if)
|
|
148
|
-
|
|
149
|
-
[Inner Operation](#user-content-inner-operation)
|
|
150
|
-
|
|
151
|
-
[Inner Operations](#user-content-inner-operations)
|
|
152
|
-
|
|
153
|
-
[Within](#user-content-within)
|
|
154
|
-
|
|
155
|
-
## Usage examples
|
|
156
|
-
|
|
157
|
-
Some cases and example how to use new operations
|
|
158
|
-
|
|
159
|
-
### Basic operation
|
|
160
|
-
|
|
161
|
-
```ruby
|
|
162
|
-
class Profile::Create < Opera::Operation::Base
|
|
163
|
-
# DEPRECATED
|
|
164
|
-
# context_accessor :profile
|
|
165
|
-
context do
|
|
166
|
-
attr_accessor :profile
|
|
167
|
-
end
|
|
168
|
-
# DEPRECATED
|
|
169
|
-
# dependencies_reader :current_account, :mailer
|
|
170
|
-
dependencies do
|
|
171
|
-
attr_reader :current_account, :mailer
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
validate :profile_schema
|
|
175
|
-
|
|
176
|
-
step :create
|
|
177
|
-
step :send_email
|
|
178
|
-
step :output
|
|
179
|
-
|
|
180
|
-
def profile_schema
|
|
181
|
-
Dry::Validation.Schema do
|
|
182
|
-
required(:first_name).filled
|
|
183
|
-
end.call(params)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def create
|
|
187
|
-
self.profile = current_account.profiles.create(params)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def send_email
|
|
191
|
-
mailer&.send_mail(profile: profile)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def output
|
|
195
|
-
result.output = { model: profile }
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
#### Call with valid parameters
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
Profile::Create.call(params: {
|
|
204
|
-
first_name: :foo,
|
|
205
|
-
last_name: :bar
|
|
206
|
-
}, dependencies: {
|
|
207
|
-
mailer: MyMailer,
|
|
208
|
-
current_account: Account.find(1)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
#<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: []>}>
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
#### Call with INVALID parameters - missing first_name
|
|
215
|
-
|
|
216
|
-
```ruby
|
|
217
|
-
Profile::Create.call(params: {
|
|
218
|
-
last_name: :bar
|
|
219
|
-
}, dependencies: {
|
|
220
|
-
mailer: MyMailer,
|
|
221
|
-
current_account: Account.find(1)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
#<Opera::Operation::Result:0x0000562d3f635390 @errors={:first_name=>["is missing"]}, @information={}, @executions=[:profile_schema]>
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
#### Call with MISSING dependencies
|
|
228
|
-
|
|
229
|
-
```ruby
|
|
230
|
-
Profile::Create.call(params: {
|
|
231
|
-
first_name: :foo,
|
|
232
|
-
last_name: :bar
|
|
233
|
-
}, dependencies: {
|
|
234
|
-
current_account: Account.find(1)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
#<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: []>}>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Example with sanitizing parameters
|
|
241
|
-
|
|
242
|
-
```ruby
|
|
243
|
-
class Profile::Create < Opera::Operation::Base
|
|
244
|
-
# DEPRECATED
|
|
245
|
-
# context_accessor :profile
|
|
246
|
-
context do
|
|
247
|
-
attr_accessor :profile
|
|
248
|
-
end
|
|
249
|
-
# DEPRECATED
|
|
250
|
-
# dependencies_reader :current_account, :mailer
|
|
251
|
-
dependencies do
|
|
252
|
-
attr_reader :current_account, :mailer
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
validate :profile_schema
|
|
257
|
-
|
|
258
|
-
step :create
|
|
259
|
-
step :send_email
|
|
260
|
-
step :output
|
|
261
|
-
|
|
262
|
-
def profile_schema
|
|
263
|
-
Dry::Validation.Schema do
|
|
264
|
-
configure { config.input_processor = :sanitizer }
|
|
265
|
-
|
|
266
|
-
required(:first_name).filled
|
|
267
|
-
end.call(params)
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def create
|
|
271
|
-
self.profile = current_account.profiles.create(context[:profile_schema_output])
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def send_email
|
|
275
|
-
return true unless mailer
|
|
276
|
-
|
|
277
|
-
mailer.send_mail(profile: profile)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def output
|
|
281
|
-
result.output = { model: profile }
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
```ruby
|
|
287
|
-
Profile::Create.call(params: {
|
|
288
|
-
first_name: :foo,
|
|
289
|
-
last_name: :bar
|
|
290
|
-
}, dependencies: {
|
|
291
|
-
mailer: MyMailer,
|
|
292
|
-
current_account: Account.find(1)
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
# NOTE: Last name is missing in output model
|
|
296
|
-
#<Opera::Operation::Result:0x000055e36a1fab78 @errors={}, @information={}, @executions=[:profile_schema, :create, :send_email, :output], @output={:model=>#<Profile id: 44, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: nil, created_at: "2020-08-17 11:07:08", updated_at: "2020-08-17 11:07: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: []>}>
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### Example operation with old validations
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
class Profile::Create < Opera::Operation::Base
|
|
303
|
-
# DEPRECATED
|
|
304
|
-
# context_accessor :profile
|
|
305
|
-
context do
|
|
306
|
-
attr_accessor :profile
|
|
307
|
-
end
|
|
308
|
-
# DEPRECATED
|
|
309
|
-
# dependencies_reader :current_account, :mailer
|
|
310
|
-
dependencies do
|
|
311
|
-
attr_reader :current_account, :mailer
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
validate :profile_schema
|
|
315
|
-
|
|
316
|
-
step :build_record
|
|
317
|
-
step :old_validation
|
|
318
|
-
step :create
|
|
319
|
-
step :send_email
|
|
320
|
-
step :output
|
|
321
|
-
|
|
322
|
-
def profile_schema
|
|
323
|
-
Dry::Validation.Schema do
|
|
324
|
-
required(:first_name).filled
|
|
325
|
-
end.call(params)
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def build_record
|
|
329
|
-
self.profile = current_account.profiles.build(params)
|
|
330
|
-
self.profile.force_name_validation = true
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def old_validation
|
|
334
|
-
return true if profile.valid?
|
|
335
|
-
|
|
336
|
-
result.add_information(missing_validations: "Please check dry validations")
|
|
337
|
-
result.add_errors(profile.errors.messages)
|
|
338
|
-
|
|
339
|
-
false
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def create
|
|
343
|
-
profile.save
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def send_email
|
|
347
|
-
mailer.send_mail(profile: profile)
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
def output
|
|
351
|
-
result.output = { model: profile }
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
#### Call with valid parameters
|
|
357
|
-
|
|
358
|
-
```ruby
|
|
359
|
-
Profile::Create.call(params: {
|
|
360
|
-
first_name: :foo,
|
|
361
|
-
last_name: :bar
|
|
362
|
-
}, dependencies: {
|
|
363
|
-
mailer: MyMailer,
|
|
364
|
-
current_account: Account.find(1)
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
#<Opera::Operation::Result:0x0000560ebc9e7a98 @errors={}, @information={}, @executions=[:profile_schema, :build_record, :old_validation, :create, :send_email, :output], @output={:model=>#<Profile id: 41, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2020-08-14 19:15:12", updated_at: "2020-08-14 19:15:12", 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: []>}>
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
#### Call with INVALID parameters
|
|
371
|
-
|
|
372
|
-
```ruby
|
|
373
|
-
Profile::Create.call(params: {
|
|
374
|
-
first_name: :foo
|
|
375
|
-
}, dependencies: {
|
|
376
|
-
mailer: MyMailer,
|
|
377
|
-
current_account: Account.find(1)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
#<Opera::Operation::Result:0x0000560ef76ba588 @errors={:last_name=>["can't be blank"]}, @information={:missing_validations=>"Please check dry validations"}, @executions=[:build_record, :old_validation]>
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Example with step that finishes execution
|
|
384
|
-
|
|
385
|
-
```ruby
|
|
386
|
-
class Profile::Create < Opera::Operation::Base
|
|
387
|
-
# DEPRECATED
|
|
388
|
-
# context_accessor :profile
|
|
389
|
-
context do
|
|
390
|
-
attr_accessor :profile
|
|
391
|
-
end
|
|
392
|
-
# DEPRECATED
|
|
393
|
-
# dependencies_reader :current_account, :mailer
|
|
394
|
-
dependencies do
|
|
395
|
-
attr_reader :current_account, :mailer
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
validate :profile_schema
|
|
399
|
-
|
|
400
|
-
step :build_record
|
|
401
|
-
step :create
|
|
402
|
-
step :send_email
|
|
403
|
-
step :output
|
|
404
|
-
|
|
405
|
-
def profile_schema
|
|
406
|
-
Dry::Validation.Schema do
|
|
407
|
-
required(:first_name).filled
|
|
408
|
-
end.call(params)
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def build_record
|
|
412
|
-
self.profile = current_account.profiles.build(params)
|
|
413
|
-
self.profile.force_name_validation = true
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def create
|
|
417
|
-
self.profile = profile.save
|
|
418
|
-
finish!
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def send_email
|
|
422
|
-
return true unless mailer
|
|
423
|
-
|
|
424
|
-
mailer.send_mail(profile: profile)
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def output
|
|
428
|
-
result.output(model: profile)
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
##### Call
|
|
434
|
-
|
|
435
|
-
```ruby
|
|
436
|
-
result = Profile::Create.call(params: {
|
|
437
|
-
first_name: :foo,
|
|
438
|
-
last_name: :bar
|
|
439
|
-
}, dependencies: {
|
|
440
|
-
current_account: Account.find(1)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
#<Opera::Operation::Result:0x007fc2c59a8460 @errors={}, @information={}, @executions=[:profile_schema, :build_record, :create]>
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### Failing transaction
|
|
447
|
-
|
|
448
|
-
```ruby
|
|
449
|
-
class Profile::Create < Opera::Operation::Base
|
|
450
|
-
configure do |config|
|
|
451
|
-
config.transaction_class = Profile
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
# DEPRECATED
|
|
455
|
-
# context_accessor :profile
|
|
456
|
-
context do
|
|
457
|
-
attr_accessor :profile
|
|
458
|
-
end
|
|
459
|
-
# DEPRECATED
|
|
460
|
-
# dependencies_reader :current_account, :mailer
|
|
461
|
-
dependencies do
|
|
462
|
-
attr_reader :current_account, :mailer
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
validate :profile_schema
|
|
466
|
-
|
|
467
|
-
transaction do
|
|
468
|
-
step :create
|
|
469
|
-
step :update
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
step :send_email
|
|
473
|
-
step :output
|
|
474
|
-
|
|
475
|
-
def profile_schema
|
|
476
|
-
Dry::Validation.Schema do
|
|
477
|
-
required(:first_name).filled
|
|
478
|
-
end.call(params)
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def create
|
|
482
|
-
self.profile = current_account.profiles.create(params)
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def update
|
|
486
|
-
profile.update(example_attr: :Example)
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
def send_email
|
|
490
|
-
return true unless mailer
|
|
491
|
-
|
|
492
|
-
mailer.send_mail(profile: profile)
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
def output
|
|
496
|
-
result.output = { model: profile }
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
#### Example with non-existing attribute
|
|
502
|
-
|
|
503
|
-
```ruby
|
|
504
|
-
Profile::Create.call(params: {
|
|
505
|
-
first_name: :foo,
|
|
506
|
-
last_name: :bar
|
|
507
|
-
}, dependencies: {
|
|
508
|
-
mailer: MyMailer,
|
|
509
|
-
current_account: Account.find(1)
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
D, [2020-08-14T16:13:30.946466 #2504] DEBUG -- : Account Load (0.5ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
|
|
513
|
-
D, [2020-08-14T16:13:30.960254 #2504] DEBUG -- : (0.2ms) BEGIN
|
|
514
|
-
D, [2020-08-14T16:13:30.983981 #2504] DEBUG -- : SQL (0.7ms) INSERT INTO "profiles" ("first_name", "last_name", "created_at", "updated_at", "account_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["first_name", "foo"], ["last_name", "bar"], ["created_at", "2020-08-14 16:13:30.982289"], ["updated_at", "2020-08-14 16:13:30.982289"], ["account_id", 1]]
|
|
515
|
-
D, [2020-08-14T16:13:30.986233 #2504] DEBUG -- : (0.2ms) ROLLBACK
|
|
516
|
-
D, [2020-08-14T16:13:30.988231 #2504] DEBUG -- : unknown attribute 'example_attr' for Profile. (ActiveModel::UnknownAttributeError)
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Passing transaction
|
|
520
|
-
|
|
521
|
-
```ruby
|
|
522
|
-
class Profile::Create < Opera::Operation::Base
|
|
523
|
-
configure do |config|
|
|
524
|
-
config.transaction_class = Profile
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# DEPRECATED
|
|
528
|
-
# context_accessor :profile
|
|
529
|
-
context do
|
|
530
|
-
attr_accessor :profile
|
|
531
|
-
end
|
|
532
|
-
# DEPRECATED
|
|
533
|
-
# dependencies_reader :current_account, :mailer
|
|
534
|
-
dependencies do
|
|
535
|
-
attr_reader :current_account, :mailer
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
validate :profile_schema
|
|
539
|
-
|
|
540
|
-
transaction do
|
|
541
|
-
step :create
|
|
542
|
-
step :update
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
step :send_email
|
|
546
|
-
step :output
|
|
547
|
-
|
|
548
|
-
def profile_schema
|
|
549
|
-
Dry::Validation.Schema do
|
|
550
|
-
required(:first_name).filled
|
|
551
|
-
end.call(params)
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def create
|
|
555
|
-
self.profile = current_account.profiles.create(params)
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def update
|
|
559
|
-
profile.update(updated_at: 1.day.ago)
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def send_email
|
|
563
|
-
return true unless mailer
|
|
564
|
-
|
|
565
|
-
mailer.send_mail(profile: profile)
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
def output
|
|
569
|
-
result.output = { model: profile }
|
|
570
|
-
end
|
|
571
|
-
end
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
#### Example with updating timestamp
|
|
575
|
-
|
|
576
|
-
```ruby
|
|
577
|
-
Profile::Create.call(params: {
|
|
578
|
-
first_name: :foo,
|
|
579
|
-
last_name: :bar
|
|
580
|
-
}, dependencies: {
|
|
581
|
-
mailer: MyMailer,
|
|
582
|
-
current_account: Account.find(1)
|
|
583
|
-
})
|
|
584
|
-
D, [2020-08-17T12:10:44.842392 #2741] DEBUG -- : Account Load (0.7ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
|
|
585
|
-
D, [2020-08-17T12:10:44.856964 #2741] DEBUG -- : (0.2ms) BEGIN
|
|
586
|
-
D, [2020-08-17T12:10:44.881332 #2741] DEBUG -- : SQL (0.7ms) INSERT INTO "profiles" ("first_name", "last_name", "created_at", "updated_at", "account_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["first_name", "foo"], ["last_name", "bar"], ["created_at", "2020-08-17 12:10:44.879684"], ["updated_at", "2020-08-17 12:10:44.879684"], ["account_id", 1]]
|
|
587
|
-
D, [2020-08-17T12:10:44.886168 #2741] DEBUG -- : SQL (0.6ms) UPDATE "profiles" SET "updated_at" = $1 WHERE "profiles"."id" = $2 [["updated_at", "2020-08-16 12:10:44.883164"], ["id", 47]]
|
|
588
|
-
D, [2020-08-17T12:10:44.898132 #2741] DEBUG -- : (10.3ms) COMMIT
|
|
589
|
-
#<Opera::Operation::Result:0x0000556528f29058 @errors={}, @information={}, @executions=[:profile_schema, :create, :update, :send_email, :output], @output={:model=>#<Profile id: 47, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2020-08-17 12:10:44", updated_at: "2020-08-16 12:10:44", 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: []>}>
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### Success
|
|
593
|
-
|
|
594
|
-
```ruby
|
|
595
|
-
class Profile::Create < Opera::Operation::Base
|
|
596
|
-
# DEPRECATED
|
|
597
|
-
# context_accessor :profile
|
|
598
|
-
context do
|
|
599
|
-
attr_accessor :profile
|
|
600
|
-
end
|
|
601
|
-
# DEPRECATED
|
|
602
|
-
# dependencies_reader :current_account, :mailer
|
|
603
|
-
dependencies do
|
|
604
|
-
attr_reader :current_account, :mailer
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
validate :profile_schema
|
|
608
|
-
|
|
609
|
-
success :populate
|
|
610
|
-
|
|
611
|
-
step :create
|
|
612
|
-
step :update
|
|
613
|
-
|
|
614
|
-
success do
|
|
615
|
-
step :send_email
|
|
616
|
-
step :output
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
def profile_schema
|
|
620
|
-
Dry::Validation.Schema do
|
|
621
|
-
required(:first_name).filled
|
|
622
|
-
end.call(params)
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
def populate
|
|
626
|
-
context[:attributes] = {}
|
|
627
|
-
context[:valid] = false
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
def create
|
|
631
|
-
self.profile = current_account.profiles.create(params)
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
def update
|
|
635
|
-
profile.update(updated_at: 1.day.ago)
|
|
636
|
-
end
|
|
10
|
+
## Installation
|
|
637
11
|
|
|
638
|
-
|
|
639
|
-
def send_email
|
|
640
|
-
result.add_error('mailer', 'Missing dependency')
|
|
641
|
-
mailer&.send_mail(profile: profile)
|
|
642
|
-
end
|
|
12
|
+
Add to your Gemfile:
|
|
643
13
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
end
|
|
647
|
-
end
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'opera'
|
|
648
16
|
```
|
|
649
17
|
|
|
650
|
-
|
|
18
|
+
Then run `bundle install`.
|
|
651
19
|
|
|
652
|
-
|
|
653
|
-
Profile::Create.call(params: {
|
|
654
|
-
first_name: :foo,
|
|
655
|
-
last_name: :bar
|
|
656
|
-
}, dependencies: {
|
|
657
|
-
current_account: Account.find(1)
|
|
658
|
-
})
|
|
659
|
-
#<Opera::Operation::Result:0x007fd0248e5638 @errors={"mailer"=>["Missing dependency"]}, @information={}, @executions=[:profile_schema, :populate, :create, :update, :send_email, :output], @output={:model=>#<Profile id: 40, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2019-01-03 12:21:35", updated_at: "2019-01-02 12:21:35", 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: []>}>
|
|
660
|
-
```
|
|
20
|
+
> Requires Ruby >= 3.1. For Ruby 2.x use Opera 0.2.x.
|
|
661
21
|
|
|
662
|
-
|
|
22
|
+
## Quick Start
|
|
663
23
|
|
|
664
24
|
```ruby
|
|
665
25
|
class Profile::Create < Opera::Operation::Base
|
|
666
|
-
# DEPRECATED
|
|
667
|
-
# context_accessor :profile
|
|
668
26
|
context do
|
|
669
27
|
attr_accessor :profile
|
|
670
28
|
end
|
|
671
|
-
|
|
672
|
-
# dependencies_reader :current_account, :mailer
|
|
29
|
+
|
|
673
30
|
dependencies do
|
|
674
31
|
attr_reader :current_account, :mailer
|
|
675
32
|
end
|
|
@@ -677,13 +34,8 @@ class Profile::Create < Opera::Operation::Base
|
|
|
677
34
|
validate :profile_schema
|
|
678
35
|
|
|
679
36
|
step :create
|
|
680
|
-
|
|
681
|
-
step :
|
|
682
|
-
|
|
683
|
-
success do
|
|
684
|
-
step :send_email
|
|
685
|
-
step :output
|
|
686
|
-
end
|
|
37
|
+
step :send_email
|
|
38
|
+
step :output
|
|
687
39
|
|
|
688
40
|
def profile_schema
|
|
689
41
|
Dry::Validation.Schema do
|
|
@@ -695,466 +47,164 @@ class Profile::Create < Opera::Operation::Base
|
|
|
695
47
|
self.profile = current_account.profiles.create(params)
|
|
696
48
|
end
|
|
697
49
|
|
|
698
|
-
def profile_create_only
|
|
699
|
-
dependencies[:create_only].present?
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
def update
|
|
703
|
-
profile.update(updated_at: 1.day.ago)
|
|
704
|
-
end
|
|
705
|
-
|
|
706
|
-
# NOTE: We can add an error in this step and it won't break the execution
|
|
707
50
|
def send_email
|
|
708
|
-
result.add_error('mailer', 'Missing dependency')
|
|
709
51
|
mailer&.send_mail(profile: profile)
|
|
710
52
|
end
|
|
711
53
|
|
|
712
54
|
def output
|
|
713
|
-
result.output = { model:
|
|
55
|
+
result.output = { model: profile }
|
|
714
56
|
end
|
|
715
57
|
end
|
|
716
58
|
```
|
|
717
59
|
|
|
718
60
|
```ruby
|
|
719
|
-
Profile::Create.call(
|
|
720
|
-
first_name: :
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
create_only: true,
|
|
724
|
-
current_account: Account.find(1)
|
|
725
|
-
})
|
|
726
|
-
#<Opera::Operation::Result:0x007fd0248e5638 @errors={}, @information={}, @executions=[:profile_schema, :create, :profile_create_only], @output={}>
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
### Inner Operation
|
|
730
|
-
|
|
731
|
-
```ruby
|
|
732
|
-
class Profile::Find < Opera::Operation::Base
|
|
733
|
-
step :find
|
|
734
|
-
|
|
735
|
-
def find
|
|
736
|
-
result.output = Profile.find(params[:id])
|
|
737
|
-
end
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
class Profile::Create < Opera::Operation::Base
|
|
741
|
-
validate :profile_schema
|
|
742
|
-
|
|
743
|
-
operation :find
|
|
744
|
-
|
|
745
|
-
step :create
|
|
746
|
-
|
|
747
|
-
step :output
|
|
748
|
-
|
|
749
|
-
def profile_schema
|
|
750
|
-
Dry::Validation.Schema do
|
|
751
|
-
optional(:id).filled
|
|
752
|
-
end.call(params)
|
|
753
|
-
end
|
|
61
|
+
result = Profile::Create.call(
|
|
62
|
+
params: { first_name: "Jane", last_name: "Doe" },
|
|
63
|
+
dependencies: { current_account: Account.find(1), mailer: MyMailer }
|
|
64
|
+
)
|
|
754
65
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
def create
|
|
760
|
-
return if context[:find_output]
|
|
761
|
-
puts 'not found'
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
def output
|
|
765
|
-
result.output = { model: context[:find_output] }
|
|
766
|
-
end
|
|
767
|
-
end
|
|
66
|
+
result.success? # => true
|
|
67
|
+
result.output # => { model: #<Profile ...> }
|
|
768
68
|
```
|
|
769
69
|
|
|
770
|
-
|
|
70
|
+
## Configuration
|
|
771
71
|
|
|
772
72
|
```ruby
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
73
|
+
Opera::Operation::Config.configure do |config|
|
|
74
|
+
config.transaction_class = ActiveRecord::Base
|
|
75
|
+
config.transaction_method = :transaction # default
|
|
76
|
+
config.transaction_options = { requires_new: true } # optional
|
|
77
|
+
config.instrumentation_class = MyInstrumentationAdapter # optional
|
|
78
|
+
config.mode = :development # or :production
|
|
79
|
+
config.reporter = Rails.logger # optional
|
|
80
|
+
end
|
|
779
81
|
```
|
|
780
82
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
Expects that method returns array of `Opera::Operation::Result`
|
|
83
|
+
Override per operation:
|
|
784
84
|
|
|
785
85
|
```ruby
|
|
786
|
-
class
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
def validate; end
|
|
791
|
-
|
|
792
|
-
def create
|
|
793
|
-
result.output = { model: "Profile #{Kernel.rand(100)}" }
|
|
794
|
-
end
|
|
795
|
-
end
|
|
796
|
-
|
|
797
|
-
class Profile::CreateMultiple < Opera::Operation::Base
|
|
798
|
-
operations :create_multiple
|
|
799
|
-
|
|
800
|
-
step :output
|
|
801
|
-
|
|
802
|
-
def create_multiple
|
|
803
|
-
(0..params[:number]).map do
|
|
804
|
-
Profile::Create.call
|
|
805
|
-
end
|
|
806
|
-
end
|
|
807
|
-
|
|
808
|
-
def output
|
|
809
|
-
result.output = context[:create_multiple_output]
|
|
86
|
+
class MyOperation < Opera::Operation::Base
|
|
87
|
+
configure do |config|
|
|
88
|
+
config.transaction_class = Profile
|
|
89
|
+
config.reporter = Rollbar
|
|
810
90
|
end
|
|
811
91
|
end
|
|
812
92
|
```
|
|
813
93
|
|
|
814
|
-
|
|
815
|
-
Profile::CreateMultiple.call(params: { number: 3 })
|
|
816
|
-
|
|
817
|
-
#<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"}]>
|
|
818
|
-
```
|
|
94
|
+
Setting `mode: :production` skips storing execution traces for lower memory usage.
|
|
819
95
|
|
|
820
|
-
|
|
96
|
+
## Instrumentation
|
|
821
97
|
|
|
822
|
-
|
|
98
|
+
To instrument operations, create an adapter inheriting from `Opera::Operation::Instrumentation::Base`:
|
|
823
99
|
|
|
824
100
|
```ruby
|
|
825
|
-
class
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
dependencies do
|
|
831
|
-
attr_reader :current_account
|
|
832
|
-
end
|
|
833
|
-
|
|
834
|
-
step :build
|
|
835
|
-
|
|
836
|
-
within :read_from_replica do
|
|
837
|
-
step :check_duplicate
|
|
838
|
-
step :validate_quota
|
|
839
|
-
end
|
|
840
|
-
|
|
841
|
-
step :create
|
|
842
|
-
step :output
|
|
843
|
-
|
|
844
|
-
def build
|
|
845
|
-
self.profile = current_account.profiles.build(params)
|
|
846
|
-
end
|
|
847
|
-
|
|
848
|
-
def check_duplicate
|
|
849
|
-
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
850
|
-
end
|
|
851
|
-
|
|
852
|
-
def validate_quota
|
|
853
|
-
result.add_error(:base, 'quota exceeded') if current_account.profiles.count >= 100
|
|
854
|
-
end
|
|
855
|
-
|
|
856
|
-
def create
|
|
857
|
-
profile.save!
|
|
858
|
-
end
|
|
859
|
-
|
|
860
|
-
def output
|
|
861
|
-
result.output = { model: profile }
|
|
862
|
-
end
|
|
863
|
-
|
|
864
|
-
private
|
|
865
|
-
|
|
866
|
-
def read_from_replica(&block)
|
|
867
|
-
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
101
|
+
class MyInstrumentation < Opera::Operation::Instrumentation::Base
|
|
102
|
+
def self.instrument(operation, name:, level:)
|
|
103
|
+
# level is :operation or :step
|
|
104
|
+
Datadog::Tracing.trace(name, service: :opera) { yield }
|
|
868
105
|
end
|
|
869
106
|
end
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
`within`-method can also be used inline inside any step method when you need the wrapper for only part of that method's logic:
|
|
873
|
-
|
|
874
|
-
```ruby
|
|
875
|
-
def some_step
|
|
876
|
-
value = read_from_replica { Profile.count }
|
|
877
|
-
result.output = { count: value }
|
|
878
|
-
end
|
|
879
107
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
def read_from_replica(&block)
|
|
883
|
-
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
108
|
+
Opera::Operation::Config.configure do |config|
|
|
109
|
+
config.instrumentation_class = MyInstrumentation
|
|
884
110
|
end
|
|
885
111
|
```
|
|
886
112
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
`within` can wrap any combination of `step` and `operation` instructions. All of them execute inside the wrapper, and their outputs are available in context afterwards as usual.
|
|
890
|
-
|
|
891
|
-
```ruby
|
|
892
|
-
class Profile::Create < Opera::Operation::Base
|
|
893
|
-
context do
|
|
894
|
-
attr_accessor :profile
|
|
895
|
-
end
|
|
896
|
-
|
|
897
|
-
dependencies do
|
|
898
|
-
attr_reader :current_account, :quota_checker
|
|
899
|
-
end
|
|
900
|
-
|
|
901
|
-
within :read_from_replica do
|
|
902
|
-
step :check_duplicate
|
|
903
|
-
operation :fetch_quota
|
|
904
|
-
end
|
|
905
|
-
|
|
906
|
-
step :create
|
|
907
|
-
step :output
|
|
908
|
-
|
|
909
|
-
def check_duplicate
|
|
910
|
-
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
911
|
-
end
|
|
912
|
-
|
|
913
|
-
def fetch_quota
|
|
914
|
-
quota_checker.call(params: params)
|
|
915
|
-
end
|
|
916
|
-
|
|
917
|
-
def create
|
|
918
|
-
self.profile = current_account.profiles.create(params)
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
def output
|
|
922
|
-
result.output = { model: profile, quota: context[:fetch_quota_output] }
|
|
923
|
-
end
|
|
924
|
-
|
|
925
|
-
private
|
|
926
|
-
|
|
927
|
-
def read_from_replica(&block)
|
|
928
|
-
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
929
|
-
end
|
|
930
|
-
end
|
|
931
|
-
```
|
|
113
|
+
## DSL Reference
|
|
932
114
|
|
|
933
|
-
|
|
115
|
+
| Instruction | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `step :method` | Executes a method. Returns falsy to stop execution. |
|
|
118
|
+
| `validate :method` | Executes a method that must return `Dry::Validation::Result` or `Opera::Operation::Result`. Errors are accumulated -- all validations run even if some fail. |
|
|
119
|
+
| `transaction do ... end` | Wraps steps in a database transaction. Rolls back on error. |
|
|
120
|
+
| `success :method` or `success do ... end` | Like `step`, but a falsy return does **not** stop execution. Use for side effects. |
|
|
121
|
+
| `finish_if :method` | Stops execution (successfully) if the method returns truthy. |
|
|
122
|
+
| `operation :method` | Calls an inner operation. Must return `Opera::Operation::Result`. Propagates errors on failure. Output stored in `context[:<method>_output]`. |
|
|
123
|
+
| `operations :method` | Like `operation`, but the method must return an array of `Opera::Operation::Result`. |
|
|
124
|
+
| `within :method do ... end` | Wraps nested steps with a custom method that must `yield`. If it doesn't yield, nested steps are skipped. |
|
|
934
125
|
|
|
935
|
-
|
|
126
|
+
### Combining instructions
|
|
936
127
|
|
|
937
128
|
```ruby
|
|
938
|
-
class
|
|
939
|
-
|
|
940
|
-
config.transaction_class = ActiveRecord::Base
|
|
941
|
-
end
|
|
942
|
-
|
|
943
|
-
context do
|
|
944
|
-
attr_accessor :profile
|
|
945
|
-
end
|
|
129
|
+
class MyOperation < Opera::Operation::Base
|
|
130
|
+
validate :schema
|
|
946
131
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
end
|
|
132
|
+
step :prepare
|
|
133
|
+
finish_if :already_done?
|
|
950
134
|
|
|
951
135
|
transaction do
|
|
136
|
+
step :create
|
|
137
|
+
step :update
|
|
138
|
+
|
|
952
139
|
within :read_from_replica do
|
|
953
140
|
step :check_duplicate
|
|
954
|
-
operation :fetch_quota
|
|
955
141
|
end
|
|
956
|
-
operation :write_audit_log
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
step :output
|
|
960
|
-
|
|
961
|
-
def check_duplicate
|
|
962
|
-
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
963
|
-
end
|
|
964
|
-
|
|
965
|
-
def fetch_quota
|
|
966
|
-
quota_checker.call(params: params)
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
def write_audit_log
|
|
970
|
-
audit_logger.call(params: params)
|
|
971
|
-
end
|
|
972
|
-
|
|
973
|
-
def output
|
|
974
|
-
result.output = { quota: context[:fetch_quota_output] }
|
|
975
142
|
end
|
|
976
143
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
144
|
+
success do
|
|
145
|
+
step :send_notification
|
|
146
|
+
step :log_audit
|
|
981
147
|
end
|
|
982
|
-
end
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
## Opera::Operation::Result - Instance Methods
|
|
986
|
-
|
|
987
|
-
Sometimes it may be useful to be able to create an instance of the `Result` with preset `output`.
|
|
988
|
-
It can be handy especially in specs. Then just include it in the initializer:
|
|
989
148
|
|
|
990
|
-
|
|
991
|
-
Opera::Operation::Result.new(output: 'success')
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
>
|
|
995
|
-
|
|
996
|
-
- success? - [true, false] - Return true if no errors
|
|
997
|
-
- failure? - [true, false] - Return true if any error
|
|
998
|
-
- output - [Anything] - Return Anything
|
|
999
|
-
- output=(Anything) - Sets content of operation output
|
|
1000
|
-
- output! - Return Anything if Success, raise exception if Failure
|
|
1001
|
-
- add_error(key, value) - Adds new error message
|
|
1002
|
-
- add_errors(Hash) - Adds multiple error messages
|
|
1003
|
-
- add_information(Hash) - Adss new information - Useful informations for developers
|
|
1004
|
-
|
|
1005
|
-
## Opera::Operation::Base - Instance Methods
|
|
1006
|
-
|
|
1007
|
-
>
|
|
1008
|
-
|
|
1009
|
-
- context [Hash] - used to pass information between steps - only for internal usage
|
|
1010
|
-
- params [Hash] - immutable and received in call method
|
|
1011
|
-
- dependencies [Hash] - immutable and received in call method
|
|
1012
|
-
- finish! - this method interrupts the execution of steps after is invoked
|
|
1013
|
-
|
|
1014
|
-
## Opera::Operation::Base - Class Methods
|
|
1015
|
-
|
|
1016
|
-
#### `context_reader`
|
|
1017
|
-
|
|
1018
|
-
The `context_reader` helper method is designed to facilitate easy access to specified keys within a `context` hash. It dynamically defines a method that acts as a getter for the value associated with a specified key, simplifying data retrieval.
|
|
1019
|
-
|
|
1020
|
-
#### Parameters
|
|
1021
|
-
|
|
1022
|
-
**key (Symbol):** The key(s) for which the getter and setter methods are to be created. These symbols should correspond to keys in the context hash.
|
|
1023
|
-
|
|
1024
|
-
**default (Proc, optional):** A lambda or proc that returns a default value for the key if it is not present in the context hash. This proc is lazily evaluated only when the getter is invoked and the key is not present in the hash.
|
|
1025
|
-
|
|
1026
|
-
#### Usage
|
|
1027
|
-
|
|
1028
|
-
**GOOD**
|
|
1029
|
-
|
|
1030
|
-
```ruby
|
|
1031
|
-
# USE context_reader to read steps outputs from the context hash
|
|
1032
|
-
|
|
1033
|
-
context_reader :schema_output
|
|
1034
|
-
|
|
1035
|
-
validate :schema # context = { schema_output: { id: 1 } }
|
|
1036
|
-
step :do_something
|
|
1037
|
-
|
|
1038
|
-
def do_something
|
|
1039
|
-
puts schema_output # outputs: { id: 1 }
|
|
149
|
+
step :output
|
|
1040
150
|
end
|
|
1041
151
|
```
|
|
1042
152
|
|
|
1043
|
-
|
|
1044
|
-
# USE context_reader with 'default' option to provide default value when key is missing in the context hash
|
|
1045
|
-
|
|
1046
|
-
context_reader :profile, default: -> { Profile.new }
|
|
153
|
+
## Result API
|
|
1047
154
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
**BAD**
|
|
155
|
+
| Method | Returns | Description |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `success?` | `Boolean` | `true` if no errors |
|
|
158
|
+
| `failure?` | `Boolean` | `true` if any errors |
|
|
159
|
+
| `output` | `Object` | The operation's return value |
|
|
160
|
+
| `output!` | `Object` | Returns output if success, raises `OutputError` if failure |
|
|
161
|
+
| `output=` | | Sets the output |
|
|
162
|
+
| `errors` | `Hash` | Accumulated error messages |
|
|
163
|
+
| `failures` | `Hash` | Alias for `errors` |
|
|
164
|
+
| `information` | `Hash` | Developer-facing metadata |
|
|
165
|
+
| `executions` | `Array` | Ordered list of executed steps (development mode only) |
|
|
166
|
+
| `add_error(key, value)` | | Adds a single error |
|
|
167
|
+
| `add_errors(hash)` | | Merges multiple errors |
|
|
168
|
+
| `add_information(hash)` | | Merges metadata |
|
|
1064
169
|
|
|
1065
170
|
```ruby
|
|
1066
|
-
#
|
|
1067
|
-
|
|
1068
|
-
# This approach can lead to confusion and misuse of the context hash,
|
|
1069
|
-
# as it suggests that the object might be part of the persistent state.
|
|
1070
|
-
context_reader :serializer, default: -> { ProfileSerializer.new }
|
|
1071
|
-
|
|
1072
|
-
step :output
|
|
1073
|
-
|
|
1074
|
-
def output
|
|
1075
|
-
self.result = serializer.to_json({...})
|
|
1076
|
-
end
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
# A better practice is to use private methods to define read-only access to resources
|
|
1080
|
-
# that are instantiated on the fly and not intended for storage in any state context.
|
|
1081
|
-
|
|
1082
|
-
step :output
|
|
1083
|
-
|
|
1084
|
-
def output
|
|
1085
|
-
self.result = serializer.to_json({...})
|
|
1086
|
-
end
|
|
1087
|
-
|
|
1088
|
-
private
|
|
1089
|
-
|
|
1090
|
-
def serializer
|
|
1091
|
-
ProfileSerializer.new
|
|
1092
|
-
end
|
|
171
|
+
# Pre-set output (useful in specs)
|
|
172
|
+
Opera::Operation::Result.new(output: 'success')
|
|
1093
173
|
```
|
|
1094
174
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
For creating instance methods that are meant to be read-only and not stored within a context hash, defining these methods as private is a more suitable and clear approach compared to using context_reader with a default. This method ensures that transient dependencies remain well-encapsulated and are not confused with persistent application state.
|
|
1098
|
-
|
|
1099
|
-
### `context|params|depenencies`
|
|
1100
|
-
|
|
1101
|
-
The `context|params|depenencies` helper method is designed to enable easy access to and modification of values for specified keys within a `context` hash. This method dynamically defines both getter and setter methods for the designated keys, facilitating straightforward retrieval and update of values.
|
|
1102
|
-
|
|
1103
|
-
#### attr_reader, attr_accessor Parameters
|
|
1104
|
-
|
|
1105
|
-
**key (Symbol):** The key(s) for which the getter and setter methods are to be created. These symbols will correspond to keys in the context hash.
|
|
175
|
+
## Operation Instance Methods
|
|
1106
176
|
|
|
1107
|
-
|
|
177
|
+
| Method | Description |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `context` | Mutable `Hash` for passing data between steps |
|
|
180
|
+
| `params` | Immutable `Hash` received via `call` |
|
|
181
|
+
| `dependencies` | Immutable `Hash` received via `call` |
|
|
182
|
+
| `result` | The `Opera::Operation::Result` instance |
|
|
183
|
+
| `finish!` | Halts step execution (operation is still successful) |
|
|
1108
184
|
|
|
1109
|
-
|
|
185
|
+
## Testing
|
|
1110
186
|
|
|
1111
|
-
|
|
1112
|
-
context do
|
|
1113
|
-
attr_accessor :profile
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
|
-
step :fetch_profile
|
|
1117
|
-
step :update_profile
|
|
1118
|
-
|
|
1119
|
-
def fetch_profile
|
|
1120
|
-
self.profile = ProfileFetcher.call # sets context[:profile]
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
|
-
def update_profile
|
|
1124
|
-
profile.update!(name: 'John') # reads profile from context[:profile]
|
|
1125
|
-
end
|
|
1126
|
-
```
|
|
187
|
+
When using Opera inside a Rails engine, configure the transaction class in your test helper:
|
|
1127
188
|
|
|
1128
189
|
```ruby
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
```ruby
|
|
1135
|
-
context do
|
|
1136
|
-
attr_accessor :profile, :account
|
|
190
|
+
# spec_helper.rb or rails_helper.rb
|
|
191
|
+
Opera::Operation::Config.configure do |config|
|
|
192
|
+
config.transaction_class = ActiveRecord::Base
|
|
1137
193
|
end
|
|
1138
194
|
```
|
|
1139
195
|
|
|
1140
|
-
|
|
196
|
+
## Examples
|
|
1141
197
|
|
|
1142
|
-
|
|
198
|
+
Detailed examples with full input/output are available in the [`docs/examples/`](docs/examples/) directory:
|
|
1143
199
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
- return [Truthly] - continue operation execution
|
|
1153
|
-
- return [False] - stops operation execution and breaks transaction/do rollback
|
|
1154
|
-
- within(Symbol, &block) - wraps nested steps with a custom method that must yield
|
|
1155
|
-
- the named method receives a block and must yield to execute the nested steps
|
|
1156
|
-
- call(params: Hash, dependencies: Hash?)
|
|
1157
|
-
- return [Opera::Operation::Result]
|
|
200
|
+
- [Basic Operation](docs/examples/basic-operation.md)
|
|
201
|
+
- [Validations](docs/examples/validations.md)
|
|
202
|
+
- [Transactions](docs/examples/transactions.md)
|
|
203
|
+
- [Success Blocks](docs/examples/success-blocks.md)
|
|
204
|
+
- [Finish If](docs/examples/finish-if.md)
|
|
205
|
+
- [Inner Operations](docs/examples/inner-operations.md)
|
|
206
|
+
- [Within](docs/examples/within.md)
|
|
207
|
+
- [Context, Params & Dependencies](docs/examples/context-params-dependencies.md)
|
|
1158
208
|
|
|
1159
209
|
## Development
|
|
1160
210
|
|
|
@@ -1164,12 +214,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
1164
214
|
|
|
1165
215
|
## Contributing
|
|
1166
216
|
|
|
1167
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/profinda/opera. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/
|
|
217
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/profinda/opera. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/profinda/opera/blob/master/CODE_OF_CONDUCT.md).
|
|
1168
218
|
|
|
1169
219
|
## License
|
|
1170
220
|
|
|
1171
221
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
1172
|
-
|
|
1173
|
-
## Code of Conduct
|
|
1174
|
-
|
|
1175
|
-
Everyone interacting in the Opera project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/profinda/opera/blob/master/CODE_OF_CONDUCT.md).
|