opera 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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