opera 0.4.1 → 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/.rubocop.yml +1223 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +45 -1
- data/README.md +106 -957
- data/Rakefile +5 -3
- data/benchmarks/operation_benchmark.rb +330 -0
- data/bin/console +4 -3
- 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/errors.rb +1 -0
- data/lib/opera/operation/attributes_dsl.rb +16 -7
- data/lib/opera/operation/base.rb +7 -6
- data/lib/opera/operation/builder.rb +2 -2
- data/lib/opera/operation/config.rb +3 -7
- data/lib/opera/operation/executor.rb +16 -7
- 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 +2 -3
- 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 +23 -0
- data/lib/opera/operation.rb +1 -1
- data/lib/opera/version.rb +1 -1
- data/opera.gemspec +12 -10
- metadata +13 -3
- data/lib/opera/operation/instructions/executors/benchmark.rb +0 -26
data/README.md
CHANGED
|
@@ -3,550 +3,37 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/opera)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
+
A lightweight DSL for building operations, services and interactions in Ruby. Zero runtime dependencies.
|
|
6
7
|
|
|
7
|
-
|
|
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.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Our aim was and is to write as many Operations, Services and Interactions using this fun and intuitive DSL to help developers have consistent code, easy to understand and maintain.
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
Add this line to your application's Gemfile:
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
gem 'opera'
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
And then execute:
|
|
22
|
-
|
|
23
|
-
$ bundle install
|
|
24
|
-
|
|
25
|
-
Or install it yourself as:
|
|
26
|
-
|
|
27
|
-
$ gem install opera
|
|
28
|
-
|
|
29
|
-
Note. If you are using Ruby 2.x please use Opera 0.2.x
|
|
30
|
-
|
|
31
|
-
## Configuration
|
|
32
|
-
|
|
33
|
-
Opera is built to be used with or without Rails.
|
|
34
|
-
Simply initialize the configuration and choose a custom logger and which library to use for implementing transactions.
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
Opera::Operation::Config.configure do |config|
|
|
38
|
-
config.transaction_class = ActiveRecord::Base
|
|
39
|
-
config.transaction_method = :transaction
|
|
40
|
-
config.transaction_options = { requires_new: true, level: :step } # or level: :operation - default
|
|
41
|
-
config.instrumentation_class = Datadog::Tracing
|
|
42
|
-
config.instrumentation_method = :trace
|
|
43
|
-
config.instrumentation_options = { service: :operation }
|
|
44
|
-
config.mode = :development # Can be set to production too
|
|
45
|
-
config.reporter = defined?(Rollbar) ? Rollbar : Rails.logger
|
|
46
|
-
end
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
You can later override this configuration in each Operation to have more granularity
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## Usage
|
|
53
|
-
|
|
54
|
-
Once opera gem is in your project you can start to build Operations
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
class A < Opera::Operation::Base
|
|
58
|
-
configure do |config|
|
|
59
|
-
config.transaction_class = Profile
|
|
60
|
-
config.reporter = Rails.logger
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
success :populate
|
|
64
|
-
|
|
65
|
-
operation :inner_operation
|
|
66
|
-
|
|
67
|
-
validate :profile_schema
|
|
68
|
-
|
|
69
|
-
transaction do
|
|
70
|
-
step :create
|
|
71
|
-
step :update
|
|
72
|
-
step :destroy
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
validate do
|
|
76
|
-
step :validate_object
|
|
77
|
-
step :validate_relationships
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
benchmark do
|
|
81
|
-
success :hal_sync
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
success do
|
|
85
|
-
step :send_mail
|
|
86
|
-
step :report_to_audit_log
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
step :output
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
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.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
### Specs
|
|
97
|
-
|
|
98
|
-
When using Opera::Operation inside an engine add the following
|
|
99
|
-
configuration to your spec_helper.rb or rails_helper.rb:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
Opera::Operation::Config.configure do |config|
|
|
103
|
-
config.transaction_class = ActiveRecord::Base
|
|
104
|
-
end
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Without this extra configuration you will receive:
|
|
108
|
-
```ruby
|
|
109
|
-
NoMethodError:
|
|
110
|
-
undefined method `transaction' for nil:NilClass
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Instrumentation
|
|
114
|
-
|
|
115
|
-
When you want to easily instrument your operations you can add this to the opera config:
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
Rails.application.configure do
|
|
119
|
-
config.x.instrumentation_class = Datadog::Tracing
|
|
120
|
-
config.x.instrumentation_method = :trace
|
|
121
|
-
config.x.instrumentation_options = { service: :opera }
|
|
122
|
-
end
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
You can also instrument individual operations by adding this to the operation config:
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
class A < Opera::Operation::Base
|
|
129
|
-
configure do |config|
|
|
130
|
-
config.instrumentation_class = Datadog::Tracing
|
|
131
|
-
config.instrumentation_method = :trace
|
|
132
|
-
config.instrumentation_options = { service: :opera, level: :step }
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# steps
|
|
136
|
-
end
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Content
|
|
140
|
-
[Basic operation](#user-content-basic-operation)
|
|
141
|
-
|
|
142
|
-
[Example with sanitizing parameters](#user-content-example-with-sanitizing-parameters)
|
|
143
|
-
|
|
144
|
-
[Example operation with old validations](#user-content-example-operation-with-old-validations)
|
|
145
|
-
|
|
146
|
-
[Failing transaction](#user-content-failing-transaction)
|
|
147
|
-
|
|
148
|
-
[Passing transaction](#user-content-passing-transaction)
|
|
149
|
-
|
|
150
|
-
[Benchmark](#user-content-benchmark)
|
|
151
|
-
|
|
152
|
-
[Success](#user-content-success)
|
|
153
|
-
|
|
154
|
-
[Finish if](#user-content-finish-if)
|
|
155
|
-
|
|
156
|
-
[Inner Operation](#user-content-inner-operation)
|
|
157
|
-
|
|
158
|
-
[Inner Operations](#user-content-inner-operations)
|
|
159
|
-
|
|
160
|
-
## Usage examples
|
|
161
|
-
|
|
162
|
-
Some cases and example how to use new operations
|
|
163
|
-
|
|
164
|
-
### Basic operation
|
|
165
|
-
|
|
166
|
-
```ruby
|
|
167
|
-
class Profile::Create < Opera::Operation::Base
|
|
168
|
-
# DEPRECATED
|
|
169
|
-
# context_accessor :profile
|
|
170
|
-
context do
|
|
171
|
-
attr_accessor :profile
|
|
172
|
-
end
|
|
173
|
-
# DEPRECATED
|
|
174
|
-
# dependencies_reader :current_account, :mailer
|
|
175
|
-
dependencies do
|
|
176
|
-
attr_reader :current_account, :mailer
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
validate :profile_schema
|
|
180
|
-
|
|
181
|
-
step :create
|
|
182
|
-
step :send_email
|
|
183
|
-
step :output
|
|
184
|
-
|
|
185
|
-
def profile_schema
|
|
186
|
-
Dry::Validation.Schema do
|
|
187
|
-
required(:first_name).filled
|
|
188
|
-
end.call(params)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def create
|
|
192
|
-
self.profile = current_account.profiles.create(params)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def send_email
|
|
196
|
-
mailer&.send_mail(profile: profile)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def output
|
|
200
|
-
result.output = { model: profile }
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
#### Call with valid parameters
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
Profile::Create.call(params: {
|
|
209
|
-
first_name: :foo,
|
|
210
|
-
last_name: :bar
|
|
211
|
-
}, dependencies: {
|
|
212
|
-
mailer: MyMailer,
|
|
213
|
-
current_account: Account.find(1)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
#<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: []>}>
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
#### Call with INVALID parameters - missing first_name
|
|
220
|
-
|
|
221
|
-
```ruby
|
|
222
|
-
Profile::Create.call(params: {
|
|
223
|
-
last_name: :bar
|
|
224
|
-
}, dependencies: {
|
|
225
|
-
mailer: MyMailer,
|
|
226
|
-
current_account: Account.find(1)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
#<Opera::Operation::Result:0x0000562d3f635390 @errors={:first_name=>["is missing"]}, @information={}, @executions=[:profile_schema]>
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
#### Call with MISSING dependencies
|
|
233
|
-
|
|
234
|
-
```ruby
|
|
235
|
-
Profile::Create.call(params: {
|
|
236
|
-
first_name: :foo,
|
|
237
|
-
last_name: :bar
|
|
238
|
-
}, dependencies: {
|
|
239
|
-
current_account: Account.find(1)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
#<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: []>}>
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Example with sanitizing parameters
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
class Profile::Create < Opera::Operation::Base
|
|
249
|
-
# DEPRECATED
|
|
250
|
-
# context_accessor :profile
|
|
251
|
-
context do
|
|
252
|
-
attr_accessor :profile
|
|
253
|
-
end
|
|
254
|
-
# DEPRECATED
|
|
255
|
-
# dependencies_reader :current_account, :mailer
|
|
256
|
-
dependencies do
|
|
257
|
-
attr_reader :current_account, :mailer
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
validate :profile_schema
|
|
262
|
-
|
|
263
|
-
step :create
|
|
264
|
-
step :send_email
|
|
265
|
-
step :output
|
|
266
|
-
|
|
267
|
-
def profile_schema
|
|
268
|
-
Dry::Validation.Schema do
|
|
269
|
-
configure { config.input_processor = :sanitizer }
|
|
270
|
-
|
|
271
|
-
required(:first_name).filled
|
|
272
|
-
end.call(params)
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def create
|
|
276
|
-
self.profile = current_account.profiles.create(context[:profile_schema_output])
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def send_email
|
|
280
|
-
return true unless mailer
|
|
281
|
-
|
|
282
|
-
mailer.send_mail(profile: profile)
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def output
|
|
286
|
-
result.output = { model: profile }
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
```ruby
|
|
292
|
-
Profile::Create.call(params: {
|
|
293
|
-
first_name: :foo,
|
|
294
|
-
last_name: :bar
|
|
295
|
-
}, dependencies: {
|
|
296
|
-
mailer: MyMailer,
|
|
297
|
-
current_account: Account.find(1)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
# NOTE: Last name is missing in output model
|
|
301
|
-
#<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: []>}>
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Example operation with old validations
|
|
305
|
-
|
|
306
|
-
```ruby
|
|
307
|
-
class Profile::Create < Opera::Operation::Base
|
|
308
|
-
# DEPRECATED
|
|
309
|
-
# context_accessor :profile
|
|
310
|
-
context do
|
|
311
|
-
attr_accessor :profile
|
|
312
|
-
end
|
|
313
|
-
# DEPRECATED
|
|
314
|
-
# dependencies_reader :current_account, :mailer
|
|
315
|
-
dependencies do
|
|
316
|
-
attr_reader :current_account, :mailer
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
validate :profile_schema
|
|
320
|
-
|
|
321
|
-
step :build_record
|
|
322
|
-
step :old_validation
|
|
323
|
-
step :create
|
|
324
|
-
step :send_email
|
|
325
|
-
step :output
|
|
326
|
-
|
|
327
|
-
def profile_schema
|
|
328
|
-
Dry::Validation.Schema do
|
|
329
|
-
required(:first_name).filled
|
|
330
|
-
end.call(params)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def build_record
|
|
334
|
-
self.profile = current_account.profiles.build(params)
|
|
335
|
-
self.profile.force_name_validation = true
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def old_validation
|
|
339
|
-
return true if profile.valid?
|
|
340
|
-
|
|
341
|
-
result.add_information(missing_validations: "Please check dry validations")
|
|
342
|
-
result.add_errors(profile.errors.messages)
|
|
343
|
-
|
|
344
|
-
false
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def create
|
|
348
|
-
profile.save
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def send_email
|
|
352
|
-
mailer.send_mail(profile: profile)
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def output
|
|
356
|
-
result.output = { model: profile }
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
#### Call with valid parameters
|
|
362
|
-
|
|
363
|
-
```ruby
|
|
364
|
-
Profile::Create.call(params: {
|
|
365
|
-
first_name: :foo,
|
|
366
|
-
last_name: :bar
|
|
367
|
-
}, dependencies: {
|
|
368
|
-
mailer: MyMailer,
|
|
369
|
-
current_account: Account.find(1)
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
#<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: []>}>
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
#### Call with INVALID parameters
|
|
376
|
-
|
|
377
|
-
```ruby
|
|
378
|
-
Profile::Create.call(params: {
|
|
379
|
-
first_name: :foo
|
|
380
|
-
}, dependencies: {
|
|
381
|
-
mailer: MyMailer,
|
|
382
|
-
current_account: Account.find(1)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
#<Opera::Operation::Result:0x0000560ef76ba588 @errors={:last_name=>["can't be blank"]}, @information={:missing_validations=>"Please check dry validations"}, @executions=[:build_record, :old_validation]>
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### Example with step that finishes execution
|
|
389
|
-
|
|
390
|
-
```ruby
|
|
391
|
-
class Profile::Create < Opera::Operation::Base
|
|
392
|
-
# DEPRECATED
|
|
393
|
-
# context_accessor :profile
|
|
394
|
-
context do
|
|
395
|
-
attr_accessor :profile
|
|
396
|
-
end
|
|
397
|
-
# DEPRECATED
|
|
398
|
-
# dependencies_reader :current_account, :mailer
|
|
399
|
-
dependencies do
|
|
400
|
-
attr_reader :current_account, :mailer
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
validate :profile_schema
|
|
404
|
-
|
|
405
|
-
step :build_record
|
|
406
|
-
step :create
|
|
407
|
-
step :send_email
|
|
408
|
-
step :output
|
|
409
|
-
|
|
410
|
-
def profile_schema
|
|
411
|
-
Dry::Validation.Schema do
|
|
412
|
-
required(:first_name).filled
|
|
413
|
-
end.call(params)
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def build_record
|
|
417
|
-
self.profile = current_account.profiles.build(params)
|
|
418
|
-
self.profile.force_name_validation = true
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def create
|
|
422
|
-
self.profile = profile.save
|
|
423
|
-
finish!
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
def send_email
|
|
427
|
-
return true unless mailer
|
|
428
|
-
|
|
429
|
-
mailer.send_mail(profile: profile)
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
def output
|
|
433
|
-
result.output(model: profile)
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
##### Call
|
|
439
|
-
|
|
440
|
-
```ruby
|
|
441
|
-
result = Profile::Create.call(params: {
|
|
442
|
-
first_name: :foo,
|
|
443
|
-
last_name: :bar
|
|
444
|
-
}, dependencies: {
|
|
445
|
-
current_account: Account.find(1)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
#<Opera::Operation::Result:0x007fc2c59a8460 @errors={}, @information={}, @executions=[:profile_schema, :build_record, :create]>
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### Failing transaction
|
|
452
|
-
|
|
453
|
-
```ruby
|
|
454
|
-
class Profile::Create < Opera::Operation::Base
|
|
455
|
-
configure do |config|
|
|
456
|
-
config.transaction_class = Profile
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
# DEPRECATED
|
|
460
|
-
# context_accessor :profile
|
|
461
|
-
context do
|
|
462
|
-
attr_accessor :profile
|
|
463
|
-
end
|
|
464
|
-
# DEPRECATED
|
|
465
|
-
# dependencies_reader :current_account, :mailer
|
|
466
|
-
dependencies do
|
|
467
|
-
attr_reader :current_account, :mailer
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
validate :profile_schema
|
|
471
|
-
|
|
472
|
-
transaction do
|
|
473
|
-
step :create
|
|
474
|
-
step :update
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
step :send_email
|
|
478
|
-
step :output
|
|
479
|
-
|
|
480
|
-
def profile_schema
|
|
481
|
-
Dry::Validation.Schema do
|
|
482
|
-
required(:first_name).filled
|
|
483
|
-
end.call(params)
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def create
|
|
487
|
-
self.profile = current_account.profiles.create(params)
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
def update
|
|
491
|
-
profile.update(example_attr: :Example)
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def send_email
|
|
495
|
-
return true unless mailer
|
|
10
|
+
## Installation
|
|
496
11
|
|
|
497
|
-
|
|
498
|
-
end
|
|
12
|
+
Add to your Gemfile:
|
|
499
13
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
end
|
|
503
|
-
end
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'opera'
|
|
504
16
|
```
|
|
505
17
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
```ruby
|
|
509
|
-
Profile::Create.call(params: {
|
|
510
|
-
first_name: :foo,
|
|
511
|
-
last_name: :bar
|
|
512
|
-
}, dependencies: {
|
|
513
|
-
mailer: MyMailer,
|
|
514
|
-
current_account: Account.find(1)
|
|
515
|
-
})
|
|
18
|
+
Then run `bundle install`.
|
|
516
19
|
|
|
517
|
-
|
|
518
|
-
D, [2020-08-14T16:13:30.960254 #2504] DEBUG -- : (0.2ms) BEGIN
|
|
519
|
-
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]]
|
|
520
|
-
D, [2020-08-14T16:13:30.986233 #2504] DEBUG -- : (0.2ms) ROLLBACK
|
|
521
|
-
D, [2020-08-14T16:13:30.988231 #2504] DEBUG -- : unknown attribute 'example_attr' for Profile. (ActiveModel::UnknownAttributeError)
|
|
522
|
-
```
|
|
20
|
+
> Requires Ruby >= 3.1. For Ruby 2.x use Opera 0.2.x.
|
|
523
21
|
|
|
524
|
-
|
|
22
|
+
## Quick Start
|
|
525
23
|
|
|
526
24
|
```ruby
|
|
527
25
|
class Profile::Create < Opera::Operation::Base
|
|
528
|
-
configure do |config|
|
|
529
|
-
config.transaction_class = Profile
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
# DEPRECATED
|
|
533
|
-
# context_accessor :profile
|
|
534
26
|
context do
|
|
535
27
|
attr_accessor :profile
|
|
536
28
|
end
|
|
537
|
-
|
|
538
|
-
# dependencies_reader :current_account, :mailer
|
|
29
|
+
|
|
539
30
|
dependencies do
|
|
540
31
|
attr_reader :current_account, :mailer
|
|
541
32
|
end
|
|
542
33
|
|
|
543
34
|
validate :profile_schema
|
|
544
35
|
|
|
545
|
-
|
|
546
|
-
step :create
|
|
547
|
-
step :update
|
|
548
|
-
end
|
|
549
|
-
|
|
36
|
+
step :create
|
|
550
37
|
step :send_email
|
|
551
38
|
step :output
|
|
552
39
|
|
|
@@ -560,14 +47,8 @@ class Profile::Create < Opera::Operation::Base
|
|
|
560
47
|
self.profile = current_account.profiles.create(params)
|
|
561
48
|
end
|
|
562
49
|
|
|
563
|
-
def update
|
|
564
|
-
profile.update(updated_at: 1.day.ago)
|
|
565
|
-
end
|
|
566
|
-
|
|
567
50
|
def send_email
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
mailer.send_mail(profile: profile)
|
|
51
|
+
mailer&.send_mail(profile: profile)
|
|
571
52
|
end
|
|
572
53
|
|
|
573
54
|
def output
|
|
@@ -576,481 +57,154 @@ class Profile::Create < Opera::Operation::Base
|
|
|
576
57
|
end
|
|
577
58
|
```
|
|
578
59
|
|
|
579
|
-
#### Example with updating timestamp
|
|
580
|
-
|
|
581
|
-
```ruby
|
|
582
|
-
Profile::Create.call(params: {
|
|
583
|
-
first_name: :foo,
|
|
584
|
-
last_name: :bar
|
|
585
|
-
}, dependencies: {
|
|
586
|
-
mailer: MyMailer,
|
|
587
|
-
current_account: Account.find(1)
|
|
588
|
-
})
|
|
589
|
-
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]]
|
|
590
|
-
D, [2020-08-17T12:10:44.856964 #2741] DEBUG -- : (0.2ms) BEGIN
|
|
591
|
-
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]]
|
|
592
|
-
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]]
|
|
593
|
-
D, [2020-08-17T12:10:44.898132 #2741] DEBUG -- : (10.3ms) COMMIT
|
|
594
|
-
#<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: []>}>
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### Benchmark
|
|
598
|
-
|
|
599
60
|
```ruby
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
attr_accessor :profile
|
|
605
|
-
end
|
|
606
|
-
# DEPRECATED
|
|
607
|
-
# dependencies_reader :current_account, :mailer
|
|
608
|
-
dependencies do
|
|
609
|
-
attr_reader :current_account, :mailer
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
validate :profile_schema
|
|
613
|
-
|
|
614
|
-
benchmark :fast_section do
|
|
615
|
-
step :create
|
|
616
|
-
step :update
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
benchmark :slow_section do
|
|
620
|
-
step :send_email
|
|
621
|
-
step :output
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
def profile_schema
|
|
625
|
-
Dry::Validation.Schema do
|
|
626
|
-
required(:first_name).filled
|
|
627
|
-
end.call(params)
|
|
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
|
|
637
|
-
|
|
638
|
-
def send_email
|
|
639
|
-
return true unless mailer
|
|
640
|
-
|
|
641
|
-
mailer.send_mail(profile: profile)
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
def output
|
|
645
|
-
result.output = { model: profile }
|
|
646
|
-
end
|
|
647
|
-
end
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
#### Example with information (real and total) from benchmark
|
|
61
|
+
result = Profile::Create.call(
|
|
62
|
+
params: { first_name: "Jane", last_name: "Doe" },
|
|
63
|
+
dependencies: { current_account: Account.find(1), mailer: MyMailer }
|
|
64
|
+
)
|
|
651
65
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
first_name: :foo,
|
|
655
|
-
last_name: :bar
|
|
656
|
-
}, dependencies: {
|
|
657
|
-
current_account: Account.find(1)
|
|
658
|
-
})
|
|
659
|
-
#<Opera::Operation::Result:0x007ff414a01238 @errors={}, @information={fast_section: {:real=>0.300013706088066e-05, :total=>0.0}, slow_section: {:real=>1.800013706088066e-05, :total=>0.0}}, @executions=[:profile_schema, :create, :update, :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-19 10:46:00", updated_at: "2020-08-18 10:46:00", 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: []>}>
|
|
66
|
+
result.success? # => true
|
|
67
|
+
result.output # => { model: #<Profile ...> }
|
|
660
68
|
```
|
|
661
69
|
|
|
662
|
-
|
|
70
|
+
## Configuration
|
|
663
71
|
|
|
664
72
|
```ruby
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
#
|
|
672
|
-
# dependencies_reader :current_account, :mailer
|
|
673
|
-
dependencies do
|
|
674
|
-
attr_reader :current_account, :mailer
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
validate :profile_schema
|
|
678
|
-
|
|
679
|
-
success :populate
|
|
680
|
-
|
|
681
|
-
step :create
|
|
682
|
-
step :update
|
|
683
|
-
|
|
684
|
-
success do
|
|
685
|
-
step :send_email
|
|
686
|
-
step :output
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
def profile_schema
|
|
690
|
-
Dry::Validation.Schema do
|
|
691
|
-
required(:first_name).filled
|
|
692
|
-
end.call(params)
|
|
693
|
-
end
|
|
694
|
-
|
|
695
|
-
def populate
|
|
696
|
-
context[:attributes] = {}
|
|
697
|
-
context[:valid] = false
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
def create
|
|
701
|
-
self.profile = current_account.profiles.create(params)
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
def update
|
|
705
|
-
profile.update(updated_at: 1.day.ago)
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
# NOTE: We can add an error in this step and it won't break the execution
|
|
709
|
-
def send_email
|
|
710
|
-
result.add_error('mailer', 'Missing dependency')
|
|
711
|
-
mailer&.send_mail(profile: profile)
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
def output
|
|
715
|
-
result.output = { model: context[:profile] }
|
|
716
|
-
end
|
|
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
|
|
717
80
|
end
|
|
718
81
|
```
|
|
719
82
|
|
|
720
|
-
|
|
83
|
+
Override per operation:
|
|
721
84
|
|
|
722
85
|
```ruby
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
current_account: Account.find(1)
|
|
728
|
-
})
|
|
729
|
-
#<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: []>}>
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### Finish If
|
|
733
|
-
|
|
734
|
-
```ruby
|
|
735
|
-
class Profile::Create < Opera::Operation::Base
|
|
736
|
-
# DEPRECATED
|
|
737
|
-
# context_accessor :profile
|
|
738
|
-
context do
|
|
739
|
-
attr_accessor :profile
|
|
740
|
-
end
|
|
741
|
-
# DEPRECATED
|
|
742
|
-
# dependencies_reader :current_account, :mailer
|
|
743
|
-
dependencies do
|
|
744
|
-
attr_reader :current_account, :mailer
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
validate :profile_schema
|
|
748
|
-
|
|
749
|
-
step :create
|
|
750
|
-
finish_if :profile_create_only
|
|
751
|
-
step :update
|
|
752
|
-
|
|
753
|
-
success do
|
|
754
|
-
step :send_email
|
|
755
|
-
step :output
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
def profile_schema
|
|
759
|
-
Dry::Validation.Schema do
|
|
760
|
-
required(:first_name).filled
|
|
761
|
-
end.call(params)
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
def create
|
|
765
|
-
self.profile = current_account.profiles.create(params)
|
|
766
|
-
end
|
|
767
|
-
|
|
768
|
-
def profile_create_only
|
|
769
|
-
dependencies[:create_only].present?
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
def update
|
|
773
|
-
profile.update(updated_at: 1.day.ago)
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
# NOTE: We can add an error in this step and it won't break the execution
|
|
777
|
-
def send_email
|
|
778
|
-
result.add_error('mailer', 'Missing dependency')
|
|
779
|
-
mailer&.send_mail(profile: profile)
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
def output
|
|
783
|
-
result.output = { model: context[:profile] }
|
|
86
|
+
class MyOperation < Opera::Operation::Base
|
|
87
|
+
configure do |config|
|
|
88
|
+
config.transaction_class = Profile
|
|
89
|
+
config.reporter = Rollbar
|
|
784
90
|
end
|
|
785
91
|
end
|
|
786
92
|
```
|
|
787
93
|
|
|
788
|
-
|
|
94
|
+
Setting `mode: :production` skips storing execution traces for lower memory usage.
|
|
789
95
|
|
|
790
|
-
|
|
791
|
-
Profile::Create.call(params: {
|
|
792
|
-
first_name: :foo,
|
|
793
|
-
last_name: :bar
|
|
794
|
-
}, dependencies: {
|
|
795
|
-
create_only: true,
|
|
796
|
-
current_account: Account.find(1)
|
|
797
|
-
})
|
|
798
|
-
#<Opera::Operation::Result:0x007fd0248e5638 @errors={}, @information={}, @executions=[:profile_schema, :create, :profile_create_only], @output={}>
|
|
799
|
-
```
|
|
96
|
+
## Instrumentation
|
|
800
97
|
|
|
801
|
-
|
|
98
|
+
To instrument operations, create an adapter inheriting from `Opera::Operation::Instrumentation::Base`:
|
|
802
99
|
|
|
803
100
|
```ruby
|
|
804
|
-
class
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
result.output = Profile.find(params[:id])
|
|
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 }
|
|
809
105
|
end
|
|
810
106
|
end
|
|
811
107
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
operation :find
|
|
816
|
-
|
|
817
|
-
step :create
|
|
818
|
-
|
|
819
|
-
step :output
|
|
820
|
-
|
|
821
|
-
def profile_schema
|
|
822
|
-
Dry::Validation.Schema do
|
|
823
|
-
optional(:id).filled
|
|
824
|
-
end.call(params)
|
|
825
|
-
end
|
|
826
|
-
|
|
827
|
-
def find
|
|
828
|
-
Profile::Find.call(params: params, dependencies: dependencies)
|
|
829
|
-
end
|
|
830
|
-
|
|
831
|
-
def create
|
|
832
|
-
return if context[:find_output]
|
|
833
|
-
puts 'not found'
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
def output
|
|
837
|
-
result.output = { model: context[:find_output] }
|
|
838
|
-
end
|
|
108
|
+
Opera::Operation::Config.configure do |config|
|
|
109
|
+
config.instrumentation_class = MyInstrumentation
|
|
839
110
|
end
|
|
840
111
|
```
|
|
841
112
|
|
|
842
|
-
|
|
113
|
+
## DSL Reference
|
|
843
114
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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. |
|
|
852
125
|
|
|
853
|
-
###
|
|
854
|
-
Expects that method returns array of `Opera::Operation::Result`
|
|
126
|
+
### Combining instructions
|
|
855
127
|
|
|
856
128
|
```ruby
|
|
857
|
-
class
|
|
858
|
-
|
|
859
|
-
step :create
|
|
860
|
-
|
|
861
|
-
def validate; end
|
|
862
|
-
|
|
863
|
-
def create
|
|
864
|
-
result.output = { model: "Profile #{Kernel.rand(100)}" }
|
|
865
|
-
end
|
|
866
|
-
end
|
|
129
|
+
class MyOperation < Opera::Operation::Base
|
|
130
|
+
validate :schema
|
|
867
131
|
|
|
868
|
-
|
|
869
|
-
|
|
132
|
+
step :prepare
|
|
133
|
+
finish_if :already_done?
|
|
870
134
|
|
|
871
|
-
|
|
135
|
+
transaction do
|
|
136
|
+
step :create
|
|
137
|
+
step :update
|
|
872
138
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
Profile::Create.call
|
|
139
|
+
within :read_from_replica do
|
|
140
|
+
step :check_duplicate
|
|
876
141
|
end
|
|
877
142
|
end
|
|
878
143
|
|
|
879
|
-
|
|
880
|
-
|
|
144
|
+
success do
|
|
145
|
+
step :send_notification
|
|
146
|
+
step :log_audit
|
|
881
147
|
end
|
|
882
|
-
end
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
```ruby
|
|
886
|
-
Profile::CreateMultiple.call(params: { number: 3 })
|
|
887
|
-
|
|
888
|
-
#<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"}]>
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
## Opera::Operation::Result - Instance Methods
|
|
892
|
-
|
|
893
|
-
Sometimes it may be useful to be able to create an instance of the `Result` with preset `output`.
|
|
894
|
-
It can be handy especially in specs. Then just include it in the initializer:
|
|
895
|
-
|
|
896
|
-
```
|
|
897
|
-
Opera::Operation::Result.new(output: 'success')
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
>
|
|
901
|
-
- success? - [true, false] - Return true if no errors
|
|
902
|
-
- failure? - [true, false] - Return true if any error
|
|
903
|
-
- output - [Anything] - Return Anything
|
|
904
|
-
- output=(Anything) - Sets content of operation output
|
|
905
|
-
- output! - Return Anything if Success, raise exception if Failure
|
|
906
|
-
- add_error(key, value) - Adds new error message
|
|
907
|
-
- add_errors(Hash) - Adds multiple error messages
|
|
908
|
-
- add_information(Hash) - Adss new information - Useful informations for developers
|
|
909
|
-
|
|
910
|
-
## Opera::Operation::Base - Instance Methods
|
|
911
|
-
>
|
|
912
|
-
- context [Hash] - used to pass information between steps - only for internal usage
|
|
913
|
-
- params [Hash] - immutable and received in call method
|
|
914
|
-
- dependencies [Hash] - immutable and received in call method
|
|
915
|
-
- finish! - this method interrupts the execution of steps after is invoked
|
|
916
|
-
|
|
917
|
-
## Opera::Operation::Base - Class Methods
|
|
918
|
-
|
|
919
|
-
#### `context_reader`
|
|
920
|
-
|
|
921
|
-
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.
|
|
922
148
|
|
|
923
|
-
|
|
924
|
-
**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.
|
|
925
|
-
|
|
926
|
-
**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.
|
|
927
|
-
|
|
928
|
-
#### Usage
|
|
929
|
-
|
|
930
|
-
**GOOD**
|
|
931
|
-
|
|
932
|
-
```ruby
|
|
933
|
-
# USE context_reader to read steps outputs from the context hash
|
|
934
|
-
|
|
935
|
-
context_reader :schema_output
|
|
936
|
-
|
|
937
|
-
validate :schema # context = { schema_output: { id: 1 } }
|
|
938
|
-
step :do_something
|
|
939
|
-
|
|
940
|
-
def do_something
|
|
941
|
-
puts schema_output # outputs: { id: 1 }
|
|
149
|
+
step :output
|
|
942
150
|
end
|
|
943
151
|
```
|
|
944
152
|
|
|
945
|
-
|
|
946
|
-
# USE context_reader with 'default' option to provide default value when key is missing in the context hash
|
|
947
|
-
|
|
948
|
-
context_reader :profile, default: -> { Profile.new }
|
|
949
|
-
|
|
950
|
-
step :fetch_profile
|
|
951
|
-
step :do_something
|
|
952
|
-
|
|
953
|
-
def fetch_profile
|
|
954
|
-
return if App.http_disabled?
|
|
955
|
-
|
|
956
|
-
context[:profile] = ProfileFetcher.call
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
def update_profile
|
|
960
|
-
profile.name = 'John'
|
|
961
|
-
profile.save!
|
|
962
|
-
end
|
|
963
|
-
```
|
|
153
|
+
## Result API
|
|
964
154
|
|
|
965
|
-
|
|
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 |
|
|
966
169
|
|
|
967
170
|
```ruby
|
|
968
|
-
#
|
|
969
|
-
|
|
970
|
-
# This approach can lead to confusion and misuse of the context hash,
|
|
971
|
-
# as it suggests that the object might be part of the persistent state.
|
|
972
|
-
context_reader :serializer, default: -> { ProfileSerializer.new }
|
|
973
|
-
|
|
974
|
-
step :output
|
|
975
|
-
|
|
976
|
-
def output
|
|
977
|
-
self.result = serializer.to_json({...})
|
|
978
|
-
end
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
# A better practice is to use private methods to define read-only access to resources
|
|
982
|
-
# that are instantiated on the fly and not intended for storage in any state context.
|
|
983
|
-
|
|
984
|
-
step :output
|
|
985
|
-
|
|
986
|
-
def output
|
|
987
|
-
self.result = serializer.to_json({...})
|
|
988
|
-
end
|
|
989
|
-
|
|
990
|
-
private
|
|
991
|
-
|
|
992
|
-
def serializer
|
|
993
|
-
ProfileSerializer.new
|
|
994
|
-
end
|
|
171
|
+
# Pre-set output (useful in specs)
|
|
172
|
+
Opera::Operation::Result.new(output: 'success')
|
|
995
173
|
```
|
|
996
|
-
**Conclusion**
|
|
997
|
-
|
|
998
|
-
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.
|
|
999
|
-
|
|
1000
|
-
### `context|params|depenencies`
|
|
1001
174
|
|
|
1002
|
-
|
|
175
|
+
## Operation Instance Methods
|
|
1003
176
|
|
|
1004
|
-
|
|
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) |
|
|
1005
184
|
|
|
1006
|
-
|
|
185
|
+
## Testing
|
|
1007
186
|
|
|
1008
|
-
|
|
187
|
+
When using Opera inside a Rails engine, configure the transaction class in your test helper:
|
|
1009
188
|
|
|
1010
|
-
#### Usage
|
|
1011
189
|
```ruby
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
step :fetch_profile
|
|
1017
|
-
step :update_profile
|
|
1018
|
-
|
|
1019
|
-
def fetch_profile
|
|
1020
|
-
self.profile = ProfileFetcher.call # sets context[:profile]
|
|
1021
|
-
end
|
|
1022
|
-
|
|
1023
|
-
def update_profile
|
|
1024
|
-
profile.update!(name: 'John') # reads profile from context[:profile]
|
|
190
|
+
# spec_helper.rb or rails_helper.rb
|
|
191
|
+
Opera::Operation::Config.configure do |config|
|
|
192
|
+
config.transaction_class = ActiveRecord::Base
|
|
1025
193
|
end
|
|
1026
194
|
```
|
|
1027
195
|
|
|
1028
|
-
|
|
1029
|
-
context do
|
|
1030
|
-
attr_accessor :profile, default: -> { Profile.new }
|
|
1031
|
-
end
|
|
1032
|
-
```
|
|
196
|
+
## Examples
|
|
1033
197
|
|
|
1034
|
-
|
|
1035
|
-
context do
|
|
1036
|
-
attr_accessor :profile, :account
|
|
1037
|
-
end
|
|
1038
|
-
```
|
|
198
|
+
Detailed examples with full input/output are available in the [`docs/examples/`](docs/examples/) directory:
|
|
1039
199
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
- return [Dry::Validation::Result] - stops operation STEPS execution if any error but continue with other validations
|
|
1049
|
-
- transaction(*Symbols) - list of instructions to be wrapped in transaction
|
|
1050
|
-
- return [Truthly] - continue operation execution
|
|
1051
|
-
- return [False] - stops operation execution and breaks transaction/do rollback
|
|
1052
|
-
- call(params: Hash, dependencies: Hash?)
|
|
1053
|
-
- 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)
|
|
1054
208
|
|
|
1055
209
|
## Development
|
|
1056
210
|
|
|
@@ -1060,13 +214,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
1060
214
|
|
|
1061
215
|
## Contributing
|
|
1062
216
|
|
|
1063
|
-
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/
|
|
1064
|
-
|
|
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).
|
|
1065
218
|
|
|
1066
219
|
## License
|
|
1067
220
|
|
|
1068
221
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
1069
|
-
|
|
1070
|
-
## Code of Conduct
|
|
1071
|
-
|
|
1072
|
-
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).
|