opera 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +109 -1060
- data/benchmarks/operation_benchmark.rb +385 -0
- data/docs/examples/always.md +267 -0
- data/docs/examples/basic-operation.md +79 -0
- data/docs/examples/context-params-dependencies.md +122 -0
- data/docs/examples/finish-if.md +67 -0
- data/docs/examples/inner-operations.md +94 -0
- data/docs/examples/success-blocks.md +68 -0
- data/docs/examples/transactions.md +227 -0
- data/docs/examples/validations.md +139 -0
- data/docs/examples/within.md +166 -0
- data/lib/opera/operation/builder.rb +21 -3
- data/lib/opera/operation/config.rb +2 -6
- data/lib/opera/operation/executor.rb +16 -4
- data/lib/opera/operation/instructions/executors/always.rb +15 -0
- data/lib/opera/operation/instructions/executors/finish_if.rb +1 -2
- data/lib/opera/operation/instructions/executors/operation.rb +1 -2
- data/lib/opera/operation/instructions/executors/operations.rb +1 -2
- data/lib/opera/operation/instructions/executors/step.rb +1 -6
- data/lib/opera/operation/instructions/executors/success.rb +5 -2
- data/lib/opera/operation/instructions/executors/validate.rb +1 -2
- data/lib/opera/operation/instructions/executors/within.rb +1 -1
- data/lib/opera/operation.rb +1 -0
- data/lib/opera/version.rb +1 -1
- metadata +13 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Context, Params & Dependencies
|
|
2
|
+
|
|
3
|
+
Opera provides typed accessor blocks for managing state within an operation.
|
|
4
|
+
|
|
5
|
+
## context
|
|
6
|
+
|
|
7
|
+
Mutable hash for passing data between steps. Supports `attr_reader`, `attr_writer`, and `attr_accessor`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
context do
|
|
11
|
+
attr_accessor :profile
|
|
12
|
+
attr_accessor :account, default: -> { Account.new }
|
|
13
|
+
attr_reader :schema_output
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `attr_accessor` defines getter and setter methods that read/write to the `context` hash
|
|
18
|
+
- `attr_reader` defines only a getter
|
|
19
|
+
- `default` accepts a lambda, evaluated lazily on first access when the key is missing
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
context do
|
|
23
|
+
attr_accessor :profile, :account
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
step :fetch_profile
|
|
27
|
+
step :update_profile
|
|
28
|
+
|
|
29
|
+
def fetch_profile
|
|
30
|
+
self.profile = ProfileFetcher.call # sets context[:profile]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_profile
|
|
34
|
+
profile.update!(name: 'John') # reads profile from context[:profile]
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## params
|
|
39
|
+
|
|
40
|
+
Immutable hash received in the `call` method. Only supports `attr_reader`.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
params do
|
|
44
|
+
attr_reader :activity, :requester
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## dependencies
|
|
49
|
+
|
|
50
|
+
Immutable hash received in the `call` method. Only supports `attr_reader`.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
dependencies do
|
|
54
|
+
attr_reader :current_account, :mailer
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## context_reader with defaults
|
|
59
|
+
|
|
60
|
+
Use `context_reader` to read step outputs from the context hash:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
context_reader :schema_output
|
|
64
|
+
|
|
65
|
+
validate :schema # context = { schema_output: { id: 1 } }
|
|
66
|
+
step :do_something
|
|
67
|
+
|
|
68
|
+
def do_something
|
|
69
|
+
puts schema_output # outputs: { id: 1 }
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `default` to provide a fallback value when the key is missing:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
context_reader :profile, default: -> { Profile.new }
|
|
77
|
+
|
|
78
|
+
step :fetch_profile
|
|
79
|
+
step :do_something
|
|
80
|
+
|
|
81
|
+
def fetch_profile
|
|
82
|
+
return if App.http_disabled?
|
|
83
|
+
|
|
84
|
+
context[:profile] = ProfileFetcher.call
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_profile
|
|
88
|
+
profile.name = 'John'
|
|
89
|
+
profile.save!
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Best practices
|
|
94
|
+
|
|
95
|
+
**Good** -- Use `context_reader` for step outputs and shared state:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
context_reader :schema_output
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Bad** -- Don't use `context_reader` with `default` for transient objects that aren't stored in context:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# BAD: suggests serializer is part of persistent state
|
|
105
|
+
context_reader :serializer, default: -> { ProfileSerializer.new }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Better** -- Use private methods for transient dependencies:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
step :output
|
|
112
|
+
|
|
113
|
+
def output
|
|
114
|
+
self.result = serializer.to_json({...})
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def serializer
|
|
120
|
+
ProfileSerializer.new
|
|
121
|
+
end
|
|
122
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Finish If
|
|
2
|
+
|
|
3
|
+
`finish_if` evaluates a method and stops execution (successfully) if the method returns a truthy value. Subsequent steps are skipped, but the operation is considered successful.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Profile::Create < Opera::Operation::Base
|
|
7
|
+
context do
|
|
8
|
+
attr_accessor :profile
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
dependencies do
|
|
12
|
+
attr_reader :current_account, :mailer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
validate :profile_schema
|
|
16
|
+
|
|
17
|
+
step :create
|
|
18
|
+
finish_if :profile_create_only
|
|
19
|
+
step :update
|
|
20
|
+
|
|
21
|
+
success do
|
|
22
|
+
step :send_email
|
|
23
|
+
step :output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def profile_schema
|
|
27
|
+
Dry::Validation.Schema do
|
|
28
|
+
required(:first_name).filled
|
|
29
|
+
end.call(params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create
|
|
33
|
+
self.profile = current_account.profiles.create(params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def profile_create_only
|
|
37
|
+
dependencies[:create_only].present?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update
|
|
41
|
+
profile.update(updated_at: 1.day.ago)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# NOTE: We can add an error in this step and it won't break the execution
|
|
45
|
+
def send_email
|
|
46
|
+
result.add_error('mailer', 'Missing dependency')
|
|
47
|
+
mailer&.send_mail(profile: profile)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output
|
|
51
|
+
result.output = { model: context[:profile] }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Profile::Create.call(params: {
|
|
60
|
+
first_name: :foo,
|
|
61
|
+
last_name: :bar
|
|
62
|
+
}, dependencies: {
|
|
63
|
+
create_only: true,
|
|
64
|
+
current_account: Account.find(1)
|
|
65
|
+
})
|
|
66
|
+
#<Opera::Operation::Result:0x007fd0248e5638 @errors={}, @information={}, @executions=[:profile_schema, :create, :profile_create_only], @output={}>
|
|
67
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Inner Operations
|
|
2
|
+
|
|
3
|
+
## Single operation
|
|
4
|
+
|
|
5
|
+
Use `operation` to call another Opera operation from within a step. The method must return an `Opera::Operation::Result`. If the inner operation fails, errors are propagated and execution stops. If it succeeds, its output is stored in context as `:<method_name>_output`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Profile::Find < Opera::Operation::Base
|
|
9
|
+
step :find
|
|
10
|
+
|
|
11
|
+
def find
|
|
12
|
+
result.output = Profile.find(params[:id])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Profile::Create < Opera::Operation::Base
|
|
17
|
+
validate :profile_schema
|
|
18
|
+
|
|
19
|
+
operation :find
|
|
20
|
+
|
|
21
|
+
step :create
|
|
22
|
+
|
|
23
|
+
step :output
|
|
24
|
+
|
|
25
|
+
def profile_schema
|
|
26
|
+
Dry::Validation.Schema do
|
|
27
|
+
optional(:id).filled
|
|
28
|
+
end.call(params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find
|
|
32
|
+
Profile::Find.call(params: params, dependencies: dependencies)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
return if context[:find_output]
|
|
37
|
+
puts 'not found'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output
|
|
41
|
+
result.output = { model: context[:find_output] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Example with inner operation doing the find
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Profile::Create.call(params: {
|
|
50
|
+
id: 1
|
|
51
|
+
}, dependencies: {
|
|
52
|
+
current_account: Account.find(1)
|
|
53
|
+
})
|
|
54
|
+
#<Opera::Operation::Result:0x007f99b25f0f20 @errors={}, @information={}, @executions=[:profile_schema, :find, :create, :output], @output={:model=>{:id=>1, :user_id=>1, :linkedin_uid=>nil, ...}}>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Multiple operations
|
|
58
|
+
|
|
59
|
+
Use `operations` when a method returns an array of `Opera::Operation::Result` objects. If any of the inner operations fail, all their errors are collected and execution stops.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class Profile::Create < Opera::Operation::Base
|
|
63
|
+
step :validate
|
|
64
|
+
step :create
|
|
65
|
+
|
|
66
|
+
def validate; end
|
|
67
|
+
|
|
68
|
+
def create
|
|
69
|
+
result.output = { model: "Profile #{Kernel.rand(100)}" }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Profile::CreateMultiple < Opera::Operation::Base
|
|
74
|
+
operations :create_multiple
|
|
75
|
+
|
|
76
|
+
step :output
|
|
77
|
+
|
|
78
|
+
def create_multiple
|
|
79
|
+
(0..params[:number]).map do
|
|
80
|
+
Profile::Create.call
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def output
|
|
85
|
+
result.output = context[:create_multiple_output]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Profile::CreateMultiple.call(params: { number: 3 })
|
|
92
|
+
|
|
93
|
+
#<Opera::Operation::Result:0x0000564189f38c90 @errors={}, @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"}]>
|
|
94
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Success Blocks
|
|
2
|
+
|
|
3
|
+
Steps inside a `success` block continue executing even if they return `false`. Errors added inside a success block **do** stop execution of subsequent non-success steps. Use success blocks for side effects that shouldn't halt the pipeline on falsy returns (e.g., sending emails, logging).
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Profile::Create < Opera::Operation::Base
|
|
7
|
+
context do
|
|
8
|
+
attr_accessor :profile
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
dependencies do
|
|
12
|
+
attr_reader :current_account, :mailer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
validate :profile_schema
|
|
16
|
+
|
|
17
|
+
success :populate
|
|
18
|
+
|
|
19
|
+
step :create
|
|
20
|
+
step :update
|
|
21
|
+
|
|
22
|
+
success do
|
|
23
|
+
step :send_email
|
|
24
|
+
step :output
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def profile_schema
|
|
28
|
+
Dry::Validation.Schema do
|
|
29
|
+
required(:first_name).filled
|
|
30
|
+
end.call(params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def populate
|
|
34
|
+
context[:attributes] = {}
|
|
35
|
+
context[:valid] = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create
|
|
39
|
+
self.profile = current_account.profiles.create(params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update
|
|
43
|
+
profile.update(updated_at: 1.day.ago)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# NOTE: We can add an error in this step and it won't break the execution
|
|
47
|
+
def send_email
|
|
48
|
+
result.add_error('mailer', 'Missing dependency')
|
|
49
|
+
mailer&.send_mail(profile: profile)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def output
|
|
53
|
+
result.output = { model: context[:profile] }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example output
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
Profile::Create.call(params: {
|
|
62
|
+
first_name: :foo,
|
|
63
|
+
last_name: :bar
|
|
64
|
+
}, dependencies: {
|
|
65
|
+
current_account: Account.find(1)
|
|
66
|
+
})
|
|
67
|
+
#<Opera::Operation::Result:0x007fd0248e5638 @errors={"mailer"=>["Missing dependency"]}, @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: []>}>
|
|
68
|
+
```
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Transactions
|
|
2
|
+
|
|
3
|
+
Wrap multiple steps in a database transaction. If any step adds an error or raises an exception, the transaction is rolled back.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Set the transaction class either globally or per-operation:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Global
|
|
11
|
+
Opera::Operation::Config.configure do |config|
|
|
12
|
+
config.transaction_class = ActiveRecord::Base
|
|
13
|
+
config.transaction_method = :transaction # default
|
|
14
|
+
config.transaction_options = { requires_new: true } # optional
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Per-operation
|
|
18
|
+
class MyOperation < Opera::Operation::Base
|
|
19
|
+
configure do |config|
|
|
20
|
+
config.transaction_class = Profile
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Failing transaction
|
|
26
|
+
|
|
27
|
+
When a step inside a transaction fails, the entire transaction is rolled back:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class Profile::Create < Opera::Operation::Base
|
|
31
|
+
configure do |config|
|
|
32
|
+
config.transaction_class = Profile
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context do
|
|
36
|
+
attr_accessor :profile
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
dependencies do
|
|
40
|
+
attr_reader :current_account, :mailer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
validate :profile_schema
|
|
44
|
+
|
|
45
|
+
transaction do
|
|
46
|
+
step :create
|
|
47
|
+
step :update
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
step :send_email
|
|
51
|
+
step :output
|
|
52
|
+
|
|
53
|
+
def profile_schema
|
|
54
|
+
Dry::Validation.Schema do
|
|
55
|
+
required(:first_name).filled
|
|
56
|
+
end.call(params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create
|
|
60
|
+
self.profile = current_account.profiles.create(params)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def update
|
|
64
|
+
profile.update(example_attr: :Example)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def send_email
|
|
68
|
+
return true unless mailer
|
|
69
|
+
|
|
70
|
+
mailer.send_mail(profile: profile)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def output
|
|
74
|
+
result.output = { model: profile }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Example with non-existing attribute
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Profile::Create.call(params: {
|
|
83
|
+
first_name: :foo,
|
|
84
|
+
last_name: :bar
|
|
85
|
+
}, dependencies: {
|
|
86
|
+
mailer: MyMailer,
|
|
87
|
+
current_account: Account.find(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
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]]
|
|
91
|
+
D, [2020-08-14T16:13:30.960254 #2504] DEBUG -- : (0.2ms) BEGIN
|
|
92
|
+
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]]
|
|
93
|
+
D, [2020-08-14T16:13:30.986233 #2504] DEBUG -- : (0.2ms) ROLLBACK
|
|
94
|
+
D, [2020-08-14T16:13:30.988231 #2504] DEBUG -- : unknown attribute 'example_attr' for Profile. (ActiveModel::UnknownAttributeError)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Passing transaction
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class Profile::Create < Opera::Operation::Base
|
|
101
|
+
configure do |config|
|
|
102
|
+
config.transaction_class = Profile
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context do
|
|
106
|
+
attr_accessor :profile
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
dependencies do
|
|
110
|
+
attr_reader :current_account, :mailer
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
validate :profile_schema
|
|
114
|
+
|
|
115
|
+
transaction do
|
|
116
|
+
step :create
|
|
117
|
+
step :update
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
step :send_email
|
|
121
|
+
step :output
|
|
122
|
+
|
|
123
|
+
def profile_schema
|
|
124
|
+
Dry::Validation.Schema do
|
|
125
|
+
required(:first_name).filled
|
|
126
|
+
end.call(params)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def create
|
|
130
|
+
self.profile = current_account.profiles.create(params)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def update
|
|
134
|
+
profile.update(updated_at: 1.day.ago)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def send_email
|
|
138
|
+
return true unless mailer
|
|
139
|
+
|
|
140
|
+
mailer.send_mail(profile: profile)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def output
|
|
144
|
+
result.output = { model: profile }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Example with updating timestamp
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Profile::Create.call(params: {
|
|
153
|
+
first_name: :foo,
|
|
154
|
+
last_name: :bar
|
|
155
|
+
}, dependencies: {
|
|
156
|
+
mailer: MyMailer,
|
|
157
|
+
current_account: Account.find(1)
|
|
158
|
+
})
|
|
159
|
+
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]]
|
|
160
|
+
D, [2020-08-17T12:10:44.856964 #2741] DEBUG -- : (0.2ms) BEGIN
|
|
161
|
+
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]]
|
|
162
|
+
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]]
|
|
163
|
+
D, [2020-08-17T12:10:44.898132 #2741] DEBUG -- : (10.3ms) COMMIT
|
|
164
|
+
#<Opera::Operation::Result:0x0000556528f29058 @errors={}, @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: []>}>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Using finish! inside a transaction
|
|
168
|
+
|
|
169
|
+
Calling `finish!` inside a transaction stops execution without rolling back -- the transaction commits successfully:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class Profile::Create < Opera::Operation::Base
|
|
173
|
+
context do
|
|
174
|
+
attr_accessor :profile
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
dependencies do
|
|
178
|
+
attr_reader :current_account, :mailer
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
validate :profile_schema
|
|
182
|
+
|
|
183
|
+
step :build_record
|
|
184
|
+
step :create
|
|
185
|
+
step :send_email
|
|
186
|
+
step :output
|
|
187
|
+
|
|
188
|
+
def profile_schema
|
|
189
|
+
Dry::Validation.Schema do
|
|
190
|
+
required(:first_name).filled
|
|
191
|
+
end.call(params)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_record
|
|
195
|
+
self.profile = current_account.profiles.build(params)
|
|
196
|
+
self.profile.force_name_validation = true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def create
|
|
200
|
+
self.profile = profile.save
|
|
201
|
+
finish!
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def send_email
|
|
205
|
+
return true unless mailer
|
|
206
|
+
|
|
207
|
+
mailer.send_mail(profile: profile)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def output
|
|
211
|
+
result.output(model: profile)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Call
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
result = Profile::Create.call(params: {
|
|
220
|
+
first_name: :foo,
|
|
221
|
+
last_name: :bar
|
|
222
|
+
}, dependencies: {
|
|
223
|
+
current_account: Account.find(1)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
#<Opera::Operation::Result:0x007fc2c59a8460 @errors={}, @information={}, @executions=[:profile_schema, :build_record, :create]>
|
|
227
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Validations
|
|
2
|
+
|
|
3
|
+
Opera supports `Dry::Validation::Result` and `Opera::Operation::Result` as return types from validate steps. Validations accumulate errors -- all validations run even if earlier ones fail, so the caller gets all errors at once.
|
|
4
|
+
|
|
5
|
+
## Example with sanitizing parameters
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Profile::Create < Opera::Operation::Base
|
|
9
|
+
context do
|
|
10
|
+
attr_accessor :profile
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
dependencies do
|
|
14
|
+
attr_reader :current_account, :mailer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
validate :profile_schema
|
|
18
|
+
|
|
19
|
+
step :create
|
|
20
|
+
step :send_email
|
|
21
|
+
step :output
|
|
22
|
+
|
|
23
|
+
def profile_schema
|
|
24
|
+
Dry::Validation.Schema do
|
|
25
|
+
configure { config.input_processor = :sanitizer }
|
|
26
|
+
|
|
27
|
+
required(:first_name).filled
|
|
28
|
+
end.call(params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create
|
|
32
|
+
self.profile = current_account.profiles.create(context[:profile_schema_output])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def send_email
|
|
36
|
+
return true unless mailer
|
|
37
|
+
|
|
38
|
+
mailer.send_mail(profile: profile)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def output
|
|
42
|
+
result.output = { model: profile }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
Profile::Create.call(params: {
|
|
49
|
+
first_name: :foo,
|
|
50
|
+
last_name: :bar
|
|
51
|
+
}, dependencies: {
|
|
52
|
+
mailer: MyMailer,
|
|
53
|
+
current_account: Account.find(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# NOTE: Last name is missing in output model
|
|
57
|
+
#<Opera::Operation::Result:0x000055e36a1fab78 @errors={}, @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: []>}>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Example with old (ActiveModel) validations
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class Profile::Create < Opera::Operation::Base
|
|
64
|
+
context do
|
|
65
|
+
attr_accessor :profile
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
dependencies do
|
|
69
|
+
attr_reader :current_account, :mailer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
validate :profile_schema
|
|
73
|
+
|
|
74
|
+
step :build_record
|
|
75
|
+
step :old_validation
|
|
76
|
+
step :create
|
|
77
|
+
step :send_email
|
|
78
|
+
step :output
|
|
79
|
+
|
|
80
|
+
def profile_schema
|
|
81
|
+
Dry::Validation.Schema do
|
|
82
|
+
required(:first_name).filled
|
|
83
|
+
end.call(params)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_record
|
|
87
|
+
self.profile = current_account.profiles.build(params)
|
|
88
|
+
self.profile.force_name_validation = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def old_validation
|
|
92
|
+
return true if profile.valid?
|
|
93
|
+
|
|
94
|
+
result.add_information(missing_validations: "Please check dry validations")
|
|
95
|
+
result.add_errors(profile.errors.messages)
|
|
96
|
+
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create
|
|
101
|
+
profile.save
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def send_email
|
|
105
|
+
mailer.send_mail(profile: profile)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def output
|
|
109
|
+
result.output = { model: profile }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Call with valid parameters
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Profile::Create.call(params: {
|
|
118
|
+
first_name: :foo,
|
|
119
|
+
last_name: :bar
|
|
120
|
+
}, dependencies: {
|
|
121
|
+
mailer: MyMailer,
|
|
122
|
+
current_account: Account.find(1)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
#<Opera::Operation::Result:0x0000560ebc9e7a98 @errors={}, @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: []>}>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Call with INVALID parameters
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Profile::Create.call(params: {
|
|
132
|
+
first_name: :foo
|
|
133
|
+
}, dependencies: {
|
|
134
|
+
mailer: MyMailer,
|
|
135
|
+
current_account: Account.find(1)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
#<Opera::Operation::Result:0x0000560ef76ba588 @errors={:last_name=>["can't be blank"]}, @information={:missing_validations=>"Please check dry validations"}, @executions=[:build_record, :old_validation]>
|
|
139
|
+
```
|