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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a62cea42daf06dd2b7b751e109c25c4d04cec6f8a3369e96556e1474304962ae
4
+ data.tar.gz: b5b698a1a6c55ef91198a7613fc0568e3f4b56f1ace013d7e0b1b2ec7dc54e2d
5
+ SHA512:
6
+ metadata.gz: 38121aebabefe9c8bde3fdebfbd10f1e28a15c6c99962ae340746c23862729d78d3efb0c70ddac1a0148008416b6ebf677688c7c4caa2ac5bdba99f1f783e40c
7
+ data.tar.gz: 164c6cc6915f9ed56fde54d25824cd3794636269df436143f45d6eee86d69e8419a9732b34670b4baae879014b91def0145f915e627dbbb239f029ebb34927fe
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,6 @@
1
+ # Opera Changelog
2
+
3
+ ## 0.1.0 - September 12, 2020
4
+
5
+ - Initial release
6
+
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at francisco.ruiz@profinda.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in opera.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 12.0'
7
+ gem 'rspec', '~> 3.0'
8
+
9
+ group :test, :development do
10
+ gem 'dry-validation'
11
+ end
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ opera (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ concurrent-ruby (1.1.7)
10
+ diff-lcs (1.4.4)
11
+ dry-configurable (0.11.6)
12
+ concurrent-ruby (~> 1.0)
13
+ dry-core (~> 0.4, >= 0.4.7)
14
+ dry-equalizer (~> 0.2)
15
+ dry-container (0.7.2)
16
+ concurrent-ruby (~> 1.0)
17
+ dry-configurable (~> 0.1, >= 0.1.3)
18
+ dry-core (0.4.9)
19
+ concurrent-ruby (~> 1.0)
20
+ dry-equalizer (0.3.0)
21
+ dry-inflector (0.2.0)
22
+ dry-logic (0.6.1)
23
+ concurrent-ruby (~> 1.0)
24
+ dry-core (~> 0.2)
25
+ dry-equalizer (~> 0.2)
26
+ dry-types (0.14.0)
27
+ concurrent-ruby (~> 1.0)
28
+ dry-container (~> 0.3)
29
+ dry-core (~> 0.4, >= 0.4.4)
30
+ dry-equalizer (~> 0.2)
31
+ dry-inflector (~> 0.1, >= 0.1.2)
32
+ dry-logic (~> 0.5, >= 0.5)
33
+ dry-validation (0.13.0)
34
+ concurrent-ruby (~> 1.0)
35
+ dry-configurable (~> 0.1, >= 0.1.3)
36
+ dry-core (~> 0.2, >= 0.2.1)
37
+ dry-equalizer (~> 0.2)
38
+ dry-logic (~> 0.5, >= 0.5.0)
39
+ dry-types (~> 0.14, >= 0.14)
40
+ rake (12.3.3)
41
+ rspec (3.9.0)
42
+ rspec-core (~> 3.9.0)
43
+ rspec-expectations (~> 3.9.0)
44
+ rspec-mocks (~> 3.9.0)
45
+ rspec-core (3.9.2)
46
+ rspec-support (~> 3.9.3)
47
+ rspec-expectations (3.9.2)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.9.0)
50
+ rspec-mocks (3.9.1)
51
+ diff-lcs (>= 1.2.0, < 2.0)
52
+ rspec-support (~> 3.9.0)
53
+ rspec-support (3.9.3)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ dry-validation
60
+ opera!
61
+ rake (~> 12.0)
62
+ rspec (~> 3.0)
63
+
64
+ BUNDLED WITH
65
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Francisco Ruiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,828 @@
1
+ # Opera
2
+
3
+ Simple DSL for services/interactions classes.
4
+
5
+ Opera was born to mimic some of the philosophy of the dry gems but keeping the DSL simple.
6
+
7
+ 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.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'opera'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install opera
24
+
25
+ ## Configuration
26
+
27
+ Opera is built to be used with or without Rails.
28
+ Simply initialise the configuration and chose what method you want to use to report errors and what library you want to use to implement transactions
29
+
30
+ ```ruby
31
+ Opera::Operation::Config.configure do |config|
32
+ config.transaction_class = ActiveRecord::Base
33
+ config.transaction_method = :transaction
34
+ config.reporter = if defined?(Rollbar) then Rollbar else Rails.logger
35
+ end
36
+ ```
37
+
38
+ You can later override this configuration in each Operation to have more granularity
39
+
40
+
41
+ ## Usage
42
+
43
+ Once opera gem is in your project you can start to build Operations
44
+
45
+ ```ruby
46
+ class A < Opera::Operation::Base
47
+
48
+ configure do |config|
49
+ config.transaction_class = Profile
50
+ config.reporter = Rails.logger
51
+ end
52
+
53
+ success :populate
54
+
55
+ operation :inner_operation
56
+
57
+ validate :profile_schema
58
+
59
+ transaction do
60
+ step :create
61
+ step :update
62
+ step :destroy
63
+ end
64
+
65
+ validate do
66
+ step :validate_object
67
+ step :validate_relationships
68
+ end
69
+
70
+ benchmark do
71
+ success :hal_sync
72
+ end
73
+
74
+ success do
75
+ step :send_mail
76
+ step :report_to_audit_log
77
+ end
78
+
79
+ step :output
80
+ end
81
+ ```
82
+
83
+ 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.
84
+
85
+
86
+ ### Specs
87
+
88
+ When using Opera::Operation inside an engine add the following
89
+ configuration to your spec_helper.rb or rails_helper.rb:
90
+
91
+ ```
92
+ Opera::Operation::Config.configure do |config|
93
+ config.transaction_class = ActiveRecord::Base
94
+ end
95
+ ```
96
+
97
+ Without this extra configuration you will receive:
98
+ ```
99
+ NoMethodError:
100
+ undefined method `transaction' for nil:NilClass
101
+ ```
102
+
103
+ ### Debugging
104
+
105
+ When you want to easily debug exceptions you can add this
106
+ to your dummy.rb:
107
+
108
+ ```
109
+ Rails.application.configure do
110
+ config.x.reporter = Logger.new(STDERR)
111
+ end
112
+ ```
113
+
114
+ This should display exceptions captured inside operations.
115
+
116
+ You can also do it in Opera::Operation configuration block:
117
+
118
+ ```
119
+ Opera::Operation::Config.configure do |config|
120
+ config.transaction_class = ActiveRecord::Base
121
+ config.reporter = Logger.new(STDERR)
122
+ end
123
+ ```
124
+
125
+ ### Content
126
+ [Basic operation](#user-content-basic-operation)
127
+
128
+ [Example with sanitizing parameters](#user-content-example-with-sanitizing-parameters)
129
+
130
+ [Example operation with old validations](#user-content-example-operation-with-old-validations)
131
+
132
+ [Example with step that raises exception](#user-content-example-with-step-that-raises-exception)
133
+
134
+ [Failing transaction](#user-content-failing-transaction)
135
+
136
+ [Passing transaction](#user-content-passing-transaction)
137
+
138
+ [Benchmark](#user-content-benchmark)
139
+
140
+ [Success](#user-content-success)
141
+
142
+ [Inner Operation](#user-content-inner-operation)
143
+
144
+ [Inner Operations](#user-content-inner-operations)
145
+
146
+ ## Usage examples
147
+
148
+ Some cases and example how to use new operations
149
+
150
+ ### Basic operation
151
+
152
+ ```ruby
153
+ class Profile::Create < Opera::Operation::Base
154
+ validate :profile_schema
155
+
156
+ step :create
157
+ step :send_email
158
+ step :output
159
+
160
+ def profile_schema
161
+ Dry::Validation.Schema do
162
+ required(:first_name).filled
163
+ end.call(params)
164
+ end
165
+
166
+ def create
167
+ context[:profile] = dependencies[:current_account].profiles.create(params)
168
+ end
169
+
170
+ def send_email
171
+ dependencies[:mailer]&.send_mail(profile: context[:profile])
172
+ end
173
+
174
+ def output
175
+ result.output = { model: context[:profile] }
176
+ end
177
+ end
178
+ ```
179
+
180
+ #### Call with valid parameters
181
+
182
+ ```ruby
183
+ Profile::Create.call(params: {
184
+ first_name: :foo,
185
+ last_name: :bar
186
+ }, dependencies: {
187
+ mailer: MyMailer,
188
+ current_account: Account.find(1)
189
+ })
190
+
191
+ #<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: "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: []>}>
192
+ ```
193
+
194
+ #### Call with INVALID parameters - missing first_name
195
+
196
+ ```ruby
197
+ Profile::Create.call(params: {
198
+ last_name: :bar
199
+ }, dependencies: {
200
+ mailer: MyMailer,
201
+ current_account: Account.find(1)
202
+ })
203
+
204
+ #<Opera::Operation::Result:0x0000562d3f635390 @errors={:first_name=>["is missing"]}, @exceptions={}, @information={}, @executions=[:profile_schema]>
205
+ ```
206
+
207
+ #### Call with MISSING dependencies
208
+
209
+ ```ruby
210
+ Profile::Create.call(params: {
211
+ first_name: :foo,
212
+ last_name: :bar
213
+ }, dependencies: {
214
+ current_account: Account.find(1)
215
+ })
216
+
217
+ #<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: []>}>
218
+ ```
219
+
220
+ ### Example with sanitizing parameters
221
+
222
+ ```ruby
223
+ class Profile::Create < Opera::Operation::Base
224
+ validate :profile_schema
225
+
226
+ step :create
227
+ step :send_email
228
+ step :output
229
+
230
+ def profile_schema
231
+ Dry::Validation.Schema do
232
+ configure { config.input_processor = :sanitizer }
233
+
234
+ required(:first_name).filled
235
+ end.call(params)
236
+ end
237
+
238
+ def create
239
+ context[:profile] = dependencies[:current_account].profiles.create(context[:profile_schema_output])
240
+ end
241
+
242
+ def send_email
243
+ return true unless dependencies[:mailer]
244
+ dependencies[:mailer].send_mail(profile: context[:profile])
245
+ end
246
+
247
+ def output
248
+ result.output = { model: context[:profile] }
249
+ end
250
+ end
251
+ ```
252
+
253
+ ```ruby
254
+ Profile::Create.call(params: {
255
+ first_name: :foo,
256
+ last_name: :bar
257
+ }, dependencies: {
258
+ mailer: MyMailer,
259
+ current_account: Account.find(1)
260
+ })
261
+
262
+ # NOTE: Last name is missing in output model
263
+ #<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: "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: []>}>
264
+ ```
265
+
266
+ ### Example operation with old validations
267
+
268
+ ```ruby
269
+ class Profile::Create < Opera::Operation::Base
270
+ validate :profile_schema
271
+
272
+ step :build_record
273
+ step :old_validation
274
+ step :create
275
+ step :send_email
276
+ step :output
277
+
278
+ def profile_schema
279
+ Dry::Validation.Schema do
280
+ required(:first_name).filled
281
+ end.call(params)
282
+ end
283
+
284
+ def build_record
285
+ context[:profile] = dependencies[:current_account].profiles.build(params)
286
+ context[:profile].force_name_validation = true
287
+ end
288
+
289
+ def old_validation
290
+ return true if context[:profile].valid?
291
+
292
+ result.add_information(missing_validations: "Please check dry validations")
293
+ result.add_errors(context[:profile].errors.messages)
294
+
295
+ false
296
+ end
297
+
298
+ def create
299
+ context[:profile].save
300
+ end
301
+
302
+ def send_email
303
+ dependencies[:mailer].send_mail(profile: context[:profile])
304
+ end
305
+
306
+ def output
307
+ result.output = { model: context[:profile] }
308
+ end
309
+ end
310
+ ```
311
+
312
+ #### Call with valid parameters
313
+
314
+ ```ruby
315
+ Profile::Create.call(params: {
316
+ first_name: :foo,
317
+ last_name: :bar
318
+ }, dependencies: {
319
+ mailer: MyMailer,
320
+ current_account: Account.find(1)
321
+ })
322
+
323
+ #<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: "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: []>}>
324
+ ```
325
+
326
+ #### Call with INVALID parameters
327
+
328
+ ```ruby
329
+ Profile::Create.call(params: {
330
+ first_name: :foo
331
+ }, dependencies: {
332
+ mailer: MyMailer,
333
+ current_account: Account.find(1)
334
+ })
335
+
336
+ #<Opera::Operation::Result:0x0000560ef76ba588 @errors={:last_name=>["can't be blank"]}, @exceptions={}, @information={:missing_validations=>"Please check dry validations"}, @executions=[:build_record, :old_validation]>
337
+ ```
338
+
339
+ ### Example with step that raises exception
340
+
341
+ ```ruby
342
+ class Profile::Create < Opera::Operation::Base
343
+ validate :profile_schema
344
+
345
+ step :build_record
346
+ step :exception
347
+ step :create
348
+ step :send_email
349
+ step :output
350
+
351
+ def profile_schema
352
+ Dry::Validation.Schema do
353
+ required(:first_name).filled
354
+ end.call(params)
355
+ end
356
+
357
+ def build_record
358
+ context[:profile] = dependencies[:current_account].profiles.build(params)
359
+ context[:profile].force_name_validation = true
360
+ end
361
+
362
+ def exception
363
+ raise StandardError, 'Example'
364
+ end
365
+
366
+ def create
367
+ context[:profile] = context[:profile].save
368
+ end
369
+
370
+ def send_email
371
+ return true unless dependencies[:mailer]
372
+
373
+ dependencies[:mailer].send_mail(profile: context[:profile])
374
+ end
375
+
376
+ def output
377
+ result.output(model: context[:profile])
378
+ end
379
+ end
380
+ ```
381
+ ##### Call with step throwing exception
382
+ ```ruby
383
+ result = Profile::Create.call(params: {
384
+ first_name: :foo,
385
+ last_name: :bar
386
+ }, dependencies: {
387
+ current_account: Account.find(1)
388
+ })
389
+
390
+ #<Opera::Operation::Result:0x0000562ad0f897c8 @errors={}, @exceptions={"Profile::Create#exception"=>["Example"]}, @information={}, @executions=[:profile_schema, :build_record, :exception]>
391
+ ```
392
+
393
+ ### Example with step that finishes execution
394
+
395
+ ```ruby
396
+ class Profile::Create < Opera::Operation::Base
397
+ validate :profile_schema
398
+
399
+ step :build_record
400
+ step :create
401
+ step :send_email
402
+ step :output
403
+
404
+ def profile_schema
405
+ Dry::Validation.Schema do
406
+ required(:first_name).filled
407
+ end.call(params)
408
+ end
409
+
410
+ def build_record
411
+ context[:profile] = dependencies[:current_account].profiles.build(params)
412
+ context[:profile].force_name_validation = true
413
+ end
414
+
415
+ def create
416
+ context[:profile] = context[:profile].save
417
+ finish
418
+ end
419
+
420
+ def send_email
421
+ return true unless dependencies[:mailer]
422
+
423
+ dependencies[:mailer].send_mail(profile: context[:profile])
424
+ end
425
+
426
+ def output
427
+ result.output(model: context[:profile])
428
+ end
429
+ end
430
+ ```
431
+ ##### Call
432
+ ```ruby
433
+ result = Profile::Create.call(params: {
434
+ first_name: :foo,
435
+ last_name: :bar
436
+ }, dependencies: {
437
+ current_account: Account.find(1)
438
+ })
439
+
440
+ #<Opera::Operation::Result:0x007fc2c59a8460 @errors={}, @exceptions={}, @information={}, @executions=[:profile_schema, :build_record, :create]>
441
+ ```
442
+
443
+ ### Failing transaction
444
+
445
+ ```ruby
446
+ class Profile::Create < Opera::Operation::Base
447
+ configure do |config|
448
+ config.transaction_class = Profile
449
+ end
450
+
451
+ validate :profile_schema
452
+
453
+ transaction do
454
+ step :create
455
+ step :update
456
+ end
457
+
458
+ step :send_email
459
+ step :output
460
+
461
+ def profile_schema
462
+ Dry::Validation.Schema do
463
+ required(:first_name).filled
464
+ end.call(params)
465
+ end
466
+
467
+ def create
468
+ context[:profile] = dependencies[:current_account].profiles.create(params)
469
+ end
470
+
471
+ def update
472
+ context[:profile].update(example_attr: :Example)
473
+ end
474
+
475
+ def send_email
476
+ return true unless dependencies[:mailer]
477
+
478
+ dependencies[:mailer].send_mail(profile: context[:profile])
479
+ end
480
+
481
+ def output
482
+ result.output = { model: context[:profile] }
483
+ end
484
+ end
485
+ ```
486
+
487
+ #### Example with non-existing attribute
488
+
489
+ ```ruby
490
+ Profile::Create.call(params: {
491
+ first_name: :foo,
492
+ last_name: :bar
493
+ }, dependencies: {
494
+ mailer: MyMailer,
495
+ current_account: Account.find(1)
496
+ })
497
+
498
+ D, [2020-08-14T16:13:30.946466 #2504] DEBUG -- : Account Load (0.5ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
499
+ D, [2020-08-14T16:13:30.960254 #2504] DEBUG -- : (0.2ms) BEGIN
500
+ 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]]
501
+ D, [2020-08-14T16:13:30.986233 #2504] DEBUG -- : (0.2ms) ROLLBACK
502
+ #<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]>
503
+ ```
504
+
505
+ ### Passing transaction
506
+
507
+ ```ruby
508
+ class Profile::Create < Opera::Operation::Base
509
+ configure do |config|
510
+ config.transaction_class = Profile
511
+ end
512
+
513
+ validate :profile_schema
514
+
515
+ transaction do
516
+ step :create
517
+ step :update
518
+ end
519
+
520
+ step :send_email
521
+ step :output
522
+
523
+ def profile_schema
524
+ Dry::Validation.Schema do
525
+ required(:first_name).filled
526
+ end.call(params)
527
+ end
528
+
529
+ def create
530
+ context[:profile] = dependencies[:current_account].profiles.create(params)
531
+ end
532
+
533
+ def update
534
+ context[:profile].update(updated_at: 1.day.ago)
535
+ end
536
+
537
+ def send_email
538
+ return true unless dependencies[:mailer]
539
+
540
+ dependencies[:mailer].send_mail(profile: context[:profile])
541
+ end
542
+
543
+ def output
544
+ result.output = { model: context[:profile] }
545
+ end
546
+ end
547
+ ```
548
+
549
+ #### Example with updating timestamp
550
+
551
+ ```ruby
552
+ Profile::Create.call(params: {
553
+ first_name: :foo,
554
+ last_name: :bar
555
+ }, dependencies: {
556
+ mailer: MyMailer,
557
+ current_account: Account.find(1)
558
+ })
559
+ 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]]
560
+ D, [2020-08-17T12:10:44.856964 #2741] DEBUG -- : (0.2ms) BEGIN
561
+ 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]]
562
+ 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]]
563
+ D, [2020-08-17T12:10:44.898132 #2741] DEBUG -- : (10.3ms) COMMIT
564
+ #<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: "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: []>}>
565
+ ```
566
+
567
+ ### Benchmark
568
+
569
+ ```ruby
570
+ class Profile::Create < Opera::Operation::Base
571
+ validate :profile_schema
572
+
573
+ step :create
574
+ step :update
575
+
576
+ benchmark do
577
+ step :send_email
578
+ step :output
579
+ end
580
+
581
+ def profile_schema
582
+ Dry::Validation.Schema do
583
+ required(:first_name).filled
584
+ end.call(params)
585
+ end
586
+
587
+ def create
588
+ context[:profile] = dependencies[:current_account].profiles.create(params)
589
+ end
590
+
591
+ def update
592
+ context[:profile].update(updated_at: 1.day.ago)
593
+ end
594
+
595
+ def send_email
596
+ return true unless dependencies[:mailer]
597
+
598
+ dependencies[:mailer].send_mail(profile: context[:profile])
599
+ end
600
+
601
+ def output
602
+ result.output = { model: context[:profile] }
603
+ end
604
+ end
605
+ ```
606
+
607
+ #### Example with information (real and total) from benchmark
608
+
609
+ ```ruby
610
+ Profile::Create.call(params: {
611
+ first_name: :foo,
612
+ last_name: :bar
613
+ }, dependencies: {
614
+ current_account: Account.find(1)
615
+ })
616
+ #<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: "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: []>}>
617
+ ```
618
+
619
+ ### Success
620
+
621
+ ```ruby
622
+ class Profile::Create < Opera::Operation::Base
623
+ validate :profile_schema
624
+
625
+ success :populate
626
+
627
+ step :create
628
+ step :update
629
+
630
+ success do
631
+ step :send_email
632
+ step :output
633
+ end
634
+
635
+ def profile_schema
636
+ Dry::Validation.Schema do
637
+ required(:first_name).filled
638
+ end.call(params)
639
+ end
640
+
641
+ def populate
642
+ context[:attributes] = {}
643
+ context[:valid] = false
644
+ end
645
+
646
+ def create
647
+ context[:profile] = dependencies[:current_account].profiles.create(params)
648
+ end
649
+
650
+ def update
651
+ context[:profile].update(updated_at: 1.day.ago)
652
+ end
653
+
654
+ # NOTE: We can add an error in this step and it won't break the execution
655
+ def send_email
656
+ result.add_error('mailer', 'Missing dependency')
657
+ dependencies[:mailer]&.send_mail(profile: context[:profile])
658
+ end
659
+
660
+ def output
661
+ result.output = { model: context[:profile] }
662
+ end
663
+ end
664
+ ```
665
+
666
+ #### Example with information (real and total) from benchmark
667
+
668
+ ```ruby
669
+ Profile::Create.call(params: {
670
+ first_name: :foo,
671
+ last_name: :bar
672
+ }, dependencies: {
673
+ current_account: Account.find(1)
674
+ })
675
+ #<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: []>}>
676
+ ```
677
+
678
+ ### Inner Operation
679
+
680
+ ```ruby
681
+ class Profile::Find < Opera::Operation::Base
682
+ step :find
683
+
684
+ def find
685
+ result.output = Profile.find(params[:id])
686
+ end
687
+ end
688
+
689
+ class Profile::Create < Opera::Operation::Base
690
+ validate :profile_schema
691
+
692
+ operation :find
693
+
694
+ step :create
695
+
696
+ step :output
697
+
698
+ def profile_schema
699
+ Dry::Validation.Schema do
700
+ optional(:id).filled
701
+ end.call(params)
702
+ end
703
+
704
+ def find
705
+ Profile::Find.call(params: params, dependencies: dependencies)
706
+ end
707
+
708
+ def create
709
+ return if context[:find_output]
710
+ puts 'not found'
711
+ end
712
+
713
+ def output
714
+ result.output = { model: context[:find_output] }
715
+ end
716
+ end
717
+ ```
718
+
719
+ #### Example with inner operation doing the find
720
+
721
+ ```ruby
722
+ Profile::Create.call(params: {
723
+ id: 1
724
+ }, dependencies: {
725
+ current_account: Account.find(1)
726
+ })
727
+ #<Opera::Operation::Result:0x007f99b25f0f20 @errors={}, @exceptions={}, @information={}, @executions=[:profile_schema, :find, :create, :output], @output={:model=>{:id=>1, :user_id=>1, :linkedin_uid=>nil, ...}}>
728
+ ```
729
+
730
+ ### Inner Operations
731
+ Expects that method returns array of `Opera::Operation::Result`
732
+
733
+ ```ruby
734
+ class Profile::Create < Opera::Operation::Base
735
+ step :validate
736
+ step :create
737
+
738
+ def validate; end
739
+
740
+ def create
741
+ result.output = { model: "Profile #{Kernel.rand(100)}" }
742
+ end
743
+ end
744
+
745
+ class Profile::CreateMultiple < Opera::Operation::Base
746
+ operations :create_multiple
747
+
748
+ step :output
749
+
750
+ def create_multiple
751
+ (0..params[:number]).map do
752
+ Profile::Create.call
753
+ end
754
+ end
755
+
756
+ def output
757
+ result.output = context[:create_multiple_output]
758
+ end
759
+ end
760
+ ```
761
+
762
+ ```ruby
763
+ Profile::CreateMultiple.call(params: { number: 3 })
764
+
765
+ #<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"}]>
766
+ ```
767
+
768
+ ## Opera::Operation::Result - Instance Methods
769
+
770
+ Sometimes it may be useful to be able to create an instance of the `Result` with preset `output`.
771
+ It can be handy especially in specs. Then just include it in the initializer:
772
+
773
+ ```
774
+ Opera::Operation::Result.new(output: 'success')
775
+ ```
776
+
777
+ >
778
+ - success? - [true, false] - Return true if no errors and no exceptions
779
+ - failure? - [true, false] - Return true if any error or exception
780
+ - output - [Anything] - Return Anything
781
+ - output=(Anything) - Sets content of operation output
782
+ - add_error(key, value) - Adds new error message
783
+ - add_errors(Hash) - Adds multiple error messages
784
+ - add_exception(method, message, classname: nil) - Adds new exception
785
+ - add_exceptions(Hash) - Adds multiple exceptions
786
+ - add_information(Hash) - Adss new information - Useful informations for developers
787
+
788
+ ## Opera::Operation::Base - Class Methods
789
+ >
790
+ - step(Symbol) - single instruction
791
+ - return [Truthly] - continue operation execution
792
+ - return [False] - stops operation execution
793
+ - raise Exception - exception gets captured and stops operation execution
794
+ - operation(Symbol) - single instruction - requires to return Opera::Operation::Result object
795
+ - return [Opera::Operation::Result] - stops operation STEPS execution if any error, exception
796
+ - validate(Symbol) - single dry-validations - requires to return Dry::Validation::Result object
797
+ - return [Dry::Validation::Result] - stops operation STEPS execution if any error but continue with other validations
798
+ - transaction(*Symbols) - list of instructions to be wrapped in transaction
799
+ - return [Truthly] - continue operation execution
800
+ - return [False|Exception] - stops operation execution and breaks transaction/do rollback
801
+ - call(params: Hash, dependencies: Hash?)
802
+ - return [Opera::Operation::Result] - never raises an exception
803
+
804
+ ## Opera::Operation::Base - Instance Methods
805
+ >
806
+ - context [Hash] - used to pass information between steps - only for internal usage
807
+ - params [Hash] - immutable and received in call method
808
+ - dependencies [Hash] - immutable and received in call method
809
+ - finish - this method interrupts the execution of steps after is invoked
810
+
811
+ ## Development
812
+
813
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
814
+
815
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
816
+
817
+ ## Contributing
818
+
819
+ 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/[USERNAME]/opera/blob/master/CODE_OF_CONDUCT.md).
820
+
821
+
822
+ ## License
823
+
824
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
825
+
826
+ ## Code of Conduct
827
+
828
+ 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).