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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ require 'opera/version'
2
+
3
+ require 'opera/errors'
4
+ require 'opera/operation'
5
+
6
+ module Opera
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ class Error < StandardError; end
5
+ class UnknownInstructionError < Error; end
6
+ end
@@ -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