opera 0.1.0

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