opera 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +828 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/opera.rb +7 -0
- data/lib/opera/errors.rb +6 -0
- data/lib/opera/operation.rb +20 -0
- data/lib/opera/operation/README.md +786 -0
- data/lib/opera/operation/base.rb +60 -0
- data/lib/opera/operation/builder.rb +50 -0
- data/lib/opera/operation/config.rb +31 -0
- data/lib/opera/operation/executor.rb +80 -0
- data/lib/opera/operation/instructions/executors/benchmark.rb +19 -0
- data/lib/opera/operation/instructions/executors/operation.rb +25 -0
- data/lib/opera/operation/instructions/executors/operations.rb +58 -0
- data/lib/opera/operation/instructions/executors/step.rb +21 -0
- data/lib/opera/operation/instructions/executors/success.rb +20 -0
- data/lib/opera/operation/instructions/executors/transaction.rb +33 -0
- data/lib/opera/operation/instructions/executors/validate.rb +24 -0
- data/lib/opera/operation/result.rb +72 -0
- data/lib/opera/version.rb +3 -0
- data/opera.gemspec +28 -0
- metadata +90 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "opera"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/opera.rb
ADDED
data/lib/opera/errors.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'opera/operation/builder'
|
4
|
+
require 'opera/operation/base'
|
5
|
+
require 'opera/operation/executor'
|
6
|
+
require 'opera/operation/result'
|
7
|
+
require 'opera/operation/config'
|
8
|
+
require 'opera/operation/instructions/executors/success'
|
9
|
+
require 'opera/operation/instructions/executors/transaction'
|
10
|
+
require 'opera/operation/instructions/executors/benchmark'
|
11
|
+
require 'opera/operation/instructions/executors/validate'
|
12
|
+
require 'opera/operation/instructions/executors/operation'
|
13
|
+
require 'opera/operation/instructions/executors/operations'
|
14
|
+
require 'opera/operation/instructions/executors/step'
|
15
|
+
|
16
|
+
module Opera
|
17
|
+
module Operation
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,786 @@
|
|
1
|
+
# Opera::Operation
|
2
|
+
|
3
|
+
Simple DSL for services/interactions classes.
|
4
|
+
|
5
|
+
# Installation
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install pro_finda-operation
|
9
|
+
```
|
10
|
+
|
11
|
+
or in Gemfile:
|
12
|
+
|
13
|
+
```
|
14
|
+
gem 'pro_finda-operation', path: 'vendor/pro_finda-operation'
|
15
|
+
```
|
16
|
+
|
17
|
+
# Configuration
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
Opera::Operation::Config.configure do |config|
|
21
|
+
config.transaction_class = ActiveRecord::Base
|
22
|
+
config.transaction_method = :transaction
|
23
|
+
config.reporter = if defined?(Rollbar) then Rollbar else Rails.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
class A < Opera::Operation::Base
|
27
|
+
|
28
|
+
configure do |config|
|
29
|
+
config.transaction_class = Profile
|
30
|
+
config.reporter = Rails.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
success :populate
|
34
|
+
|
35
|
+
operation :inner_operation
|
36
|
+
|
37
|
+
validate :profile_schema
|
38
|
+
|
39
|
+
transaction do
|
40
|
+
step :create
|
41
|
+
step :update
|
42
|
+
step :destroy
|
43
|
+
end
|
44
|
+
|
45
|
+
validate do
|
46
|
+
step :validate_object
|
47
|
+
step :validate_relationships
|
48
|
+
end
|
49
|
+
|
50
|
+
benchmark do
|
51
|
+
success :hal_sync
|
52
|
+
end
|
53
|
+
|
54
|
+
success do
|
55
|
+
step :send_mail
|
56
|
+
step :report_to_audit_log
|
57
|
+
end
|
58
|
+
|
59
|
+
step :output
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
# Specs
|
64
|
+
|
65
|
+
When using Opera::Operation inside an engine add the following
|
66
|
+
configuration to your spec_helper.rb or rails_helper.rb:
|
67
|
+
|
68
|
+
```
|
69
|
+
Opera::Operation::Config.configure do |config|
|
70
|
+
config.transaction_class = ActiveRecord::Base
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Without this extra configuration you will receive:
|
75
|
+
```
|
76
|
+
NoMethodError:
|
77
|
+
undefined method `transaction' for nil:NilClass
|
78
|
+
```
|
79
|
+
|
80
|
+
# Debugging
|
81
|
+
|
82
|
+
When you want to easily debug exceptions you can add this
|
83
|
+
to your dummy.rb:
|
84
|
+
|
85
|
+
```
|
86
|
+
Rails.application.configure do
|
87
|
+
config.x.reporter = Logger.new(STDERR)
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
This should display exceptions captured inside operations.
|
92
|
+
|
93
|
+
You can also do it in Opera::Operation configuration block:
|
94
|
+
|
95
|
+
```
|
96
|
+
Opera::Operation::Config.configure do |config|
|
97
|
+
config.transaction_class = ActiveRecord::Base
|
98
|
+
config.reporter = Logger.new(STDERR)
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
# Content
|
103
|
+
[Basic operation](#user-content-basic-operation)
|
104
|
+
|
105
|
+
[Example with sanitizing parameters](#user-content-example-with-sanitizing-parameters)
|
106
|
+
|
107
|
+
[Example operation with old validations](#user-content-example-operation-with-old-validations)
|
108
|
+
|
109
|
+
[Example with step that raises exception](#user-content-example-with-step-that-raises-exception)
|
110
|
+
|
111
|
+
[Failing transaction](#user-content-failing-transaction)
|
112
|
+
|
113
|
+
[Passing transaction](#user-content-passing-transaction)
|
114
|
+
|
115
|
+
[Benchmark](#user-content-benchmark)
|
116
|
+
|
117
|
+
[Success](#user-content-success)
|
118
|
+
|
119
|
+
[Inner Operation](#user-content-inner-operation)
|
120
|
+
|
121
|
+
[Inner Operations](#user-content-inner-operations)
|
122
|
+
|
123
|
+
# Usage examples
|
124
|
+
|
125
|
+
Some cases and example how to use new operations
|
126
|
+
|
127
|
+
## Basic operation
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class Profile::Create < Opera::Operation::Base
|
131
|
+
validate :profile_schema
|
132
|
+
|
133
|
+
step :create
|
134
|
+
step :send_email
|
135
|
+
step :output
|
136
|
+
|
137
|
+
def profile_schema
|
138
|
+
Dry::Validation.Schema do
|
139
|
+
required(:first_name).filled
|
140
|
+
end.call(params)
|
141
|
+
end
|
142
|
+
|
143
|
+
def create
|
144
|
+
context[:profile] = dependencies[:current_account].profiles.create(params)
|
145
|
+
end
|
146
|
+
|
147
|
+
def send_email
|
148
|
+
dependencies[:mailer]&.send_mail(profile: context[:profile])
|
149
|
+
end
|
150
|
+
|
151
|
+
def output
|
152
|
+
result.output = { model: context[:profile] }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
#### Call with valid parameters
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
Profile::Create.call(params: {
|
161
|
+
first_name: :foo,
|
162
|
+
last_name: :bar
|
163
|
+
}, dependencies: {
|
164
|
+
mailer: ProfindaMailer,
|
165
|
+
current_account: Account.find(1)
|
166
|
+
})
|
167
|
+
|
168
|
+
#<Opera::Operation::Result:0x0000561636dced60 @errors={}, @exceptions={}, @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: "2018-12-14 16:04:08", updated_at: "2018-12-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: []>}>
|
169
|
+
```
|
170
|
+
|
171
|
+
#### Call with INVALID parameters - missing first_name
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
Profile::Create.call(params: {
|
175
|
+
last_name: :bar
|
176
|
+
}, dependencies: {
|
177
|
+
mailer: ProfindaMailer,
|
178
|
+
current_account: Account.find(1)
|
179
|
+
})
|
180
|
+
|
181
|
+
#<Opera::Operation::Result:0x0000562d3f635390 @errors={:first_name=>["is missing"]}, @exceptions={}, @information={}, @executions=[:profile_schema]>
|
182
|
+
```
|
183
|
+
|
184
|
+
#### Call with MISSING dependencies
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
Profile::Create.call(params: {
|
188
|
+
first_name: :foo,
|
189
|
+
last_name: :bar
|
190
|
+
}, dependencies: {
|
191
|
+
current_account: Account.find(1)
|
192
|
+
})
|
193
|
+
|
194
|
+
#<Opera::Operation::Result:0x007f87ba2c8f00 @errors={}, @exceptions={}, @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: []>}>
|
195
|
+
```
|
196
|
+
|
197
|
+
## Example with sanitizing parameters
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
class Profile::Create < Opera::Operation::Base
|
201
|
+
validate :profile_schema
|
202
|
+
|
203
|
+
step :create
|
204
|
+
step :send_email
|
205
|
+
step :output
|
206
|
+
|
207
|
+
def profile_schema
|
208
|
+
Dry::Validation.Schema do
|
209
|
+
configure { config.input_processor = :sanitizer }
|
210
|
+
|
211
|
+
required(:first_name).filled
|
212
|
+
end.call(params)
|
213
|
+
end
|
214
|
+
|
215
|
+
def create
|
216
|
+
context[:profile] = dependencies[:current_account].profiles.create(context[:profile_schema_output])
|
217
|
+
end
|
218
|
+
|
219
|
+
def send_email
|
220
|
+
return true unless dependencies[:mailer]
|
221
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
222
|
+
end
|
223
|
+
|
224
|
+
def output
|
225
|
+
result.output = { model: context[:profile] }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Profile::Create.call(params: {
|
232
|
+
first_name: :foo,
|
233
|
+
last_name: :bar
|
234
|
+
}, dependencies: {
|
235
|
+
mailer: ProfindaMailer,
|
236
|
+
current_account: Account.find(1)
|
237
|
+
})
|
238
|
+
|
239
|
+
# NOTE: Last name is missing in output model
|
240
|
+
#<Opera::Operation::Result:0x000055e36a1fab78 @errors={}, @exceptions={}, @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: "2018-12-17 11:07:08", updated_at: "2018-12-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: []>}>
|
241
|
+
```
|
242
|
+
|
243
|
+
## Example operation with old validations
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class Profile::Create < Opera::Operation::Base
|
247
|
+
validate :profile_schema
|
248
|
+
|
249
|
+
step :build_record
|
250
|
+
step :old_validation
|
251
|
+
step :create
|
252
|
+
step :send_email
|
253
|
+
step :output
|
254
|
+
|
255
|
+
def profile_schema
|
256
|
+
Dry::Validation.Schema do
|
257
|
+
required(:first_name).filled
|
258
|
+
end.call(params)
|
259
|
+
end
|
260
|
+
|
261
|
+
def build_record
|
262
|
+
context[:profile] = dependencies[:current_account].profiles.build(params)
|
263
|
+
context[:profile].force_name_validation = true
|
264
|
+
end
|
265
|
+
|
266
|
+
def old_validation
|
267
|
+
return true if context[:profile].valid?
|
268
|
+
|
269
|
+
result.add_information(missing_validations: "Please check dry validations")
|
270
|
+
result.add_errors(context[:profile].errors.messages)
|
271
|
+
|
272
|
+
false
|
273
|
+
end
|
274
|
+
|
275
|
+
def create
|
276
|
+
context[:profile].save
|
277
|
+
end
|
278
|
+
|
279
|
+
def send_email
|
280
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
281
|
+
end
|
282
|
+
|
283
|
+
def output
|
284
|
+
result.output = { model: context[:profile] }
|
285
|
+
end
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
#### Call with valid parameters
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
Profile::Create.call(params: {
|
293
|
+
first_name: :foo,
|
294
|
+
last_name: :bar
|
295
|
+
}, dependencies: {
|
296
|
+
mailer: ProfindaMailer,
|
297
|
+
current_account: Account.find(1)
|
298
|
+
})
|
299
|
+
|
300
|
+
#<Opera::Operation::Result:0x0000560ebc9e7a98 @errors={}, @exceptions={}, @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: "2018-12-14 19:15:12", updated_at: "2018-12-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: []>}>
|
301
|
+
```
|
302
|
+
|
303
|
+
#### Call with INVALID parameters
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
Profile::Create.call(params: {
|
307
|
+
first_name: :foo
|
308
|
+
}, dependencies: {
|
309
|
+
mailer: ProfindaMailer,
|
310
|
+
current_account: Account.find(1)
|
311
|
+
})
|
312
|
+
|
313
|
+
#<Opera::Operation::Result:0x0000560ef76ba588 @errors={:last_name=>["can't be blank"]}, @exceptions={}, @information={:missing_validations=>"Please check dry validations"}, @executions=[:build_record, :old_validation]>
|
314
|
+
```
|
315
|
+
|
316
|
+
## Example with step that raises exception
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
class Profile::Create < Opera::Operation::Base
|
320
|
+
validate :profile_schema
|
321
|
+
|
322
|
+
step :build_record
|
323
|
+
step :exception
|
324
|
+
step :create
|
325
|
+
step :send_email
|
326
|
+
step :output
|
327
|
+
|
328
|
+
def profile_schema
|
329
|
+
Dry::Validation.Schema do
|
330
|
+
required(:first_name).filled
|
331
|
+
end.call(params)
|
332
|
+
end
|
333
|
+
|
334
|
+
def build_record
|
335
|
+
context[:profile] = dependencies[:current_account].profiles.build(params)
|
336
|
+
context[:profile].force_name_validation = true
|
337
|
+
end
|
338
|
+
|
339
|
+
def exception
|
340
|
+
raise StandardError, 'Example'
|
341
|
+
end
|
342
|
+
|
343
|
+
def create
|
344
|
+
context[:profile] = context[:profile].save
|
345
|
+
end
|
346
|
+
|
347
|
+
def send_email
|
348
|
+
return true unless dependencies[:mailer]
|
349
|
+
|
350
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
351
|
+
end
|
352
|
+
|
353
|
+
def output
|
354
|
+
result.output(model: context[:profile])
|
355
|
+
end
|
356
|
+
end
|
357
|
+
```
|
358
|
+
##### Call with step throwing exception
|
359
|
+
```ruby
|
360
|
+
result = Profile::Create.call(params: {
|
361
|
+
first_name: :foo,
|
362
|
+
last_name: :bar
|
363
|
+
}, dependencies: {
|
364
|
+
current_account: Account.find(1)
|
365
|
+
})
|
366
|
+
|
367
|
+
#<Opera::Operation::Result:0x0000562ad0f897c8 @errors={}, @exceptions={"Profile::Create#exception"=>["Example"]}, @information={}, @executions=[:profile_schema, :build_record, :exception]>
|
368
|
+
```
|
369
|
+
|
370
|
+
## Example with step that finishes execution
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
class Profile::Create < Opera::Operation::Base
|
374
|
+
validate :profile_schema
|
375
|
+
|
376
|
+
step :build_record
|
377
|
+
step :create
|
378
|
+
step :send_email
|
379
|
+
step :output
|
380
|
+
|
381
|
+
def profile_schema
|
382
|
+
Dry::Validation.Schema do
|
383
|
+
required(:first_name).filled
|
384
|
+
end.call(params)
|
385
|
+
end
|
386
|
+
|
387
|
+
def build_record
|
388
|
+
context[:profile] = dependencies[:current_account].profiles.build(params)
|
389
|
+
context[:profile].force_name_validation = true
|
390
|
+
end
|
391
|
+
|
392
|
+
def create
|
393
|
+
context[:profile] = context[:profile].save
|
394
|
+
finish
|
395
|
+
end
|
396
|
+
|
397
|
+
def send_email
|
398
|
+
return true unless dependencies[:mailer]
|
399
|
+
|
400
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
401
|
+
end
|
402
|
+
|
403
|
+
def output
|
404
|
+
result.output(model: context[:profile])
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
408
|
+
##### Call
|
409
|
+
```ruby
|
410
|
+
result = Profile::Create.call(params: {
|
411
|
+
first_name: :foo,
|
412
|
+
last_name: :bar
|
413
|
+
}, dependencies: {
|
414
|
+
current_account: Account.find(1)
|
415
|
+
})
|
416
|
+
|
417
|
+
#<Opera::Operation::Result:0x007fc2c59a8460 @errors={}, @exceptions={}, @information={}, @executions=[:profile_schema, :build_record, :create]>
|
418
|
+
```
|
419
|
+
|
420
|
+
## Failing transaction
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
class Profile::Create < Opera::Operation::Base
|
424
|
+
configure do |config|
|
425
|
+
config.transaction_class = Profile
|
426
|
+
end
|
427
|
+
|
428
|
+
validate :profile_schema
|
429
|
+
|
430
|
+
transaction do
|
431
|
+
step :create
|
432
|
+
step :update
|
433
|
+
end
|
434
|
+
|
435
|
+
step :send_email
|
436
|
+
step :output
|
437
|
+
|
438
|
+
def profile_schema
|
439
|
+
Dry::Validation.Schema do
|
440
|
+
required(:first_name).filled
|
441
|
+
end.call(params)
|
442
|
+
end
|
443
|
+
|
444
|
+
def create
|
445
|
+
context[:profile] = dependencies[:current_account].profiles.create(params)
|
446
|
+
end
|
447
|
+
|
448
|
+
def update
|
449
|
+
context[:profile].update(example_attr: :Example)
|
450
|
+
end
|
451
|
+
|
452
|
+
def send_email
|
453
|
+
return true unless dependencies[:mailer]
|
454
|
+
|
455
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
456
|
+
end
|
457
|
+
|
458
|
+
def output
|
459
|
+
result.output = { model: context[:profile] }
|
460
|
+
end
|
461
|
+
end
|
462
|
+
```
|
463
|
+
|
464
|
+
#### Example with non-existing attribute
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
Profile::Create.call(params: {
|
468
|
+
first_name: :foo,
|
469
|
+
last_name: :bar
|
470
|
+
}, dependencies: {
|
471
|
+
mailer: ProfindaMailer,
|
472
|
+
current_account: Account.find(1)
|
473
|
+
})
|
474
|
+
|
475
|
+
D, [2018-12-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]]
|
476
|
+
D, [2018-12-14T16:13:30.960254 #2504] DEBUG -- : (0.2ms) BEGIN
|
477
|
+
D, [2018-12-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", "2018-12-14 16:13:30.982289"], ["updated_at", "2018-12-14 16:13:30.982289"], ["account_id", 1]]
|
478
|
+
D, [2018-12-14T16:13:30.986233 #2504] DEBUG -- : (0.2ms) ROLLBACK
|
479
|
+
#<Opera::Operation::Result:0x00005650e89b7708 @errors={}, @exceptions={"Profile::Create#update"=>["unknown attribute 'example_attr' for Profile."], "Profile::Create#transaction"=>["Opera::Operation::Base::RollbackTransactionError"]}, @information={}, @executions=[:profile_schema, :create, :update]>
|
480
|
+
```
|
481
|
+
|
482
|
+
## Passing transaction
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
class Profile::Create < Opera::Operation::Base
|
486
|
+
configure do |config|
|
487
|
+
config.transaction_class = Profile
|
488
|
+
end
|
489
|
+
|
490
|
+
validate :profile_schema
|
491
|
+
|
492
|
+
transaction do
|
493
|
+
step :create
|
494
|
+
step :update
|
495
|
+
end
|
496
|
+
|
497
|
+
step :send_email
|
498
|
+
step :output
|
499
|
+
|
500
|
+
def profile_schema
|
501
|
+
Dry::Validation.Schema do
|
502
|
+
required(:first_name).filled
|
503
|
+
end.call(params)
|
504
|
+
end
|
505
|
+
|
506
|
+
def create
|
507
|
+
context[:profile] = dependencies[:current_account].profiles.create(params)
|
508
|
+
end
|
509
|
+
|
510
|
+
def update
|
511
|
+
context[:profile].update(updated_at: 1.day.ago)
|
512
|
+
end
|
513
|
+
|
514
|
+
def send_email
|
515
|
+
return true unless dependencies[:mailer]
|
516
|
+
|
517
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
518
|
+
end
|
519
|
+
|
520
|
+
def output
|
521
|
+
result.output = { model: context[:profile] }
|
522
|
+
end
|
523
|
+
end
|
524
|
+
```
|
525
|
+
|
526
|
+
#### Example with updating timestamp
|
527
|
+
|
528
|
+
```ruby
|
529
|
+
Profile::Create.call(params: {
|
530
|
+
first_name: :foo,
|
531
|
+
last_name: :bar
|
532
|
+
}, dependencies: {
|
533
|
+
mailer: ProfindaMailer,
|
534
|
+
current_account: Account.find(1)
|
535
|
+
})
|
536
|
+
D, [2018-12-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]]
|
537
|
+
D, [2018-12-17T12:10:44.856964 #2741] DEBUG -- : (0.2ms) BEGIN
|
538
|
+
D, [2018-12-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", "2018-12-17 12:10:44.879684"], ["updated_at", "2018-12-17 12:10:44.879684"], ["account_id", 1]]
|
539
|
+
D, [2018-12-17T12:10:44.886168 #2741] DEBUG -- : SQL (0.6ms) UPDATE "profiles" SET "updated_at" = $1 WHERE "profiles"."id" = $2 [["updated_at", "2018-12-16 12:10:44.883164"], ["id", 47]]
|
540
|
+
D, [2018-12-17T12:10:44.898132 #2741] DEBUG -- : (10.3ms) COMMIT
|
541
|
+
#<Opera::Operation::Result:0x0000556528f29058 @errors={}, @exceptions={}, @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: "2018-12-17 12:10:44", updated_at: "2018-12-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: []>}>
|
542
|
+
```
|
543
|
+
|
544
|
+
## Benchmark
|
545
|
+
|
546
|
+
```ruby
|
547
|
+
class Profile::Create < Opera::Operation::Base
|
548
|
+
validate :profile_schema
|
549
|
+
|
550
|
+
step :create
|
551
|
+
step :update
|
552
|
+
|
553
|
+
benchmark do
|
554
|
+
step :send_email
|
555
|
+
step :output
|
556
|
+
end
|
557
|
+
|
558
|
+
def profile_schema
|
559
|
+
Dry::Validation.Schema do
|
560
|
+
required(:first_name).filled
|
561
|
+
end.call(params)
|
562
|
+
end
|
563
|
+
|
564
|
+
def create
|
565
|
+
context[:profile] = dependencies[:current_account].profiles.create(params)
|
566
|
+
end
|
567
|
+
|
568
|
+
def update
|
569
|
+
context[:profile].update(updated_at: 1.day.ago)
|
570
|
+
end
|
571
|
+
|
572
|
+
def send_email
|
573
|
+
return true unless dependencies[:mailer]
|
574
|
+
|
575
|
+
dependencies[:mailer].send_mail(profile: context[:profile])
|
576
|
+
end
|
577
|
+
|
578
|
+
def output
|
579
|
+
result.output = { model: context[:profile] }
|
580
|
+
end
|
581
|
+
end
|
582
|
+
```
|
583
|
+
|
584
|
+
#### Example with information (real and total) from benchmark
|
585
|
+
|
586
|
+
```ruby
|
587
|
+
Profile::Create.call(params: {
|
588
|
+
first_name: :foo,
|
589
|
+
last_name: :bar
|
590
|
+
}, dependencies: {
|
591
|
+
current_account: Account.find(1)
|
592
|
+
})
|
593
|
+
#<Opera::Operation::Result:0x007ff414a01238 @errors={}, @exceptions={}, @information={: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: "2018-12-19 10:46:00", updated_at: "2018-12-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: []>}>
|
594
|
+
```
|
595
|
+
|
596
|
+
## Success
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
class Profile::Create < Opera::Operation::Base
|
600
|
+
validate :profile_schema
|
601
|
+
|
602
|
+
success :populate
|
603
|
+
|
604
|
+
step :create
|
605
|
+
step :update
|
606
|
+
|
607
|
+
success do
|
608
|
+
step :send_email
|
609
|
+
step :output
|
610
|
+
end
|
611
|
+
|
612
|
+
def profile_schema
|
613
|
+
Dry::Validation.Schema do
|
614
|
+
required(:first_name).filled
|
615
|
+
end.call(params)
|
616
|
+
end
|
617
|
+
|
618
|
+
def populate
|
619
|
+
context[:attributes] = {}
|
620
|
+
context[:valid] = false
|
621
|
+
end
|
622
|
+
|
623
|
+
def create
|
624
|
+
context[:profile] = dependencies[:current_account].profiles.create(params)
|
625
|
+
end
|
626
|
+
|
627
|
+
def update
|
628
|
+
context[:profile].update(updated_at: 1.day.ago)
|
629
|
+
end
|
630
|
+
|
631
|
+
# NOTE: We can add an error in this step and it won't break the execution
|
632
|
+
def send_email
|
633
|
+
result.add_error('mailer', 'Missing dependency')
|
634
|
+
dependencies[:mailer]&.send_mail(profile: context[:profile])
|
635
|
+
end
|
636
|
+
|
637
|
+
def output
|
638
|
+
result.output = { model: context[:profile] }
|
639
|
+
end
|
640
|
+
end
|
641
|
+
```
|
642
|
+
|
643
|
+
#### Example with information (real and total) from benchmark
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
Profile::Create.call(params: {
|
647
|
+
first_name: :foo,
|
648
|
+
last_name: :bar
|
649
|
+
}, dependencies: {
|
650
|
+
current_account: Account.find(1)
|
651
|
+
})
|
652
|
+
#<Opera::Operation::Result:0x007fd0248e5638 @errors={"mailer"=>["Missing dependency"]}, @exceptions={}, @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: []>}>
|
653
|
+
```
|
654
|
+
|
655
|
+
## Inner Operation
|
656
|
+
|
657
|
+
```ruby
|
658
|
+
class Profile::Find < Opera::Operation::Base
|
659
|
+
step :find
|
660
|
+
|
661
|
+
def find
|
662
|
+
result.output = Profile.find(params[:id])
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
class Profile::Create < Opera::Operation::Base
|
667
|
+
validate :profile_schema
|
668
|
+
|
669
|
+
operation :find
|
670
|
+
|
671
|
+
step :create
|
672
|
+
|
673
|
+
step :output
|
674
|
+
|
675
|
+
def profile_schema
|
676
|
+
Dry::Validation.Schema do
|
677
|
+
optional(:id).filled
|
678
|
+
end.call(params)
|
679
|
+
end
|
680
|
+
|
681
|
+
def find
|
682
|
+
Profile::Find.call(params: params, dependencies: dependencies)
|
683
|
+
end
|
684
|
+
|
685
|
+
def create
|
686
|
+
return if context[:find_output]
|
687
|
+
puts 'not found'
|
688
|
+
end
|
689
|
+
|
690
|
+
def output
|
691
|
+
result.output = { model: context[:find_output] }
|
692
|
+
end
|
693
|
+
end
|
694
|
+
```
|
695
|
+
|
696
|
+
#### Example with inner operation doing the find
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
Profile::Create.call(params: {
|
700
|
+
id: 1
|
701
|
+
}, dependencies: {
|
702
|
+
current_account: Account.find(1)
|
703
|
+
})
|
704
|
+
#<Opera::Operation::Result:0x007f99b25f0f20 @errors={}, @exceptions={}, @information={}, @executions=[:profile_schema, :find, :create, :output], @output={:model=>{:id=>1, :user_id=>1, :linkedin_uid=>nil, ...}}>
|
705
|
+
```
|
706
|
+
|
707
|
+
## Inner Operations
|
708
|
+
Expects that method returns array of `Opera::Operation::Result`
|
709
|
+
|
710
|
+
```ruby
|
711
|
+
class Profile::Create < Opera::Operation::Base
|
712
|
+
step :validate
|
713
|
+
step :create
|
714
|
+
|
715
|
+
def validate; end
|
716
|
+
|
717
|
+
def create
|
718
|
+
result.output = { model: "Profile #{Kernel.rand(100)}" }
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
class Profile::CreateMultiple < Opera::Operation::Base
|
723
|
+
operations :create_multiple
|
724
|
+
|
725
|
+
step :output
|
726
|
+
|
727
|
+
def create_multiple
|
728
|
+
(0..params[:number]).map do
|
729
|
+
Profile::Create.call
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
def output
|
734
|
+
result.output = context[:create_multiple_output]
|
735
|
+
end
|
736
|
+
end
|
737
|
+
```
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
Profile::CreateMultiple.call(params: { number: 3 })
|
741
|
+
|
742
|
+
#<Opera::Operation::Result:0x0000564189f38c90 @errors={}, @exceptions={}, @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"}]>
|
743
|
+
```
|
744
|
+
|
745
|
+
## Opera::Operation::Result - Instance Methods
|
746
|
+
|
747
|
+
Sometimes it may be useful to be able to create an instance of the `Result` with preset `output`.
|
748
|
+
It can be handy especially in specs. Then just include it in the initializer:
|
749
|
+
|
750
|
+
```
|
751
|
+
Opera::Operation::Result.new(output: 'success')
|
752
|
+
```
|
753
|
+
|
754
|
+
>
|
755
|
+
- success? - [true, false] - Return true if no errors and no exceptions
|
756
|
+
- failure? - [true, false] - Return true if any error or exception
|
757
|
+
- output - [Anything] - Return Anything
|
758
|
+
- output=(Anything) - Sets content of operation output
|
759
|
+
- add_error(key, value) - Adds new error message
|
760
|
+
- add_errors(Hash) - Adds multiple error messages
|
761
|
+
- add_exception(method, message, classname: nil) - Adds new exception
|
762
|
+
- add_exceptions(Hash) - Adds multiple exceptions
|
763
|
+
- add_information(Hash) - Adss new information - Useful informations for developers
|
764
|
+
|
765
|
+
## Opera::Operation::Base - Class Methods
|
766
|
+
>
|
767
|
+
- step(Symbol) - single instruction
|
768
|
+
- return [Truthly] - continue operation execution
|
769
|
+
- return [False] - stops operation execution
|
770
|
+
- raise Exception - exception gets captured and stops operation execution
|
771
|
+
- operation(Symbol) - single instruction - requires to return Opera::Operation::Result object
|
772
|
+
- return [Opera::Operation::Result] - stops operation STEPS execution if any error, exception
|
773
|
+
- validate(Symbol) - single dry-validations - requires to return Dry::Validation::Result object
|
774
|
+
- return [Dry::Validation::Result] - stops operation STEPS execution if any error but continue with other validations
|
775
|
+
- transaction(*Symbols) - list of instructions to be wrapped in transaction
|
776
|
+
- return [Truthly] - continue operation execution
|
777
|
+
- return [False|Exception] - stops operation execution and breaks transaction/do rollback
|
778
|
+
- call(params: Hash, dependencies: Hash?)
|
779
|
+
- return [Opera::Operation::Result] - never raises an exception
|
780
|
+
|
781
|
+
## Opera::Operation::Base - Instance Methods
|
782
|
+
>
|
783
|
+
- context [Hash] - used to pass information between steps - only for internal usage
|
784
|
+
- params [Hash] - immutable and received in call method
|
785
|
+
- dependencies [Hash] - immutable and received in call method
|
786
|
+
- finish - this method interrupts the execution of steps after is invoked
|