opera 0.5.0 → 0.5.1
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 +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +106 -1060
- data/benchmarks/operation_benchmark.rb +330 -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/config.rb +2 -6
- data/lib/opera/operation/executor.rb +13 -4
- 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/version.rb +1 -1
- metadata +11 -2
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Within
|
|
2
|
+
|
|
3
|
+
`within` wraps one or more steps with a method you define on the operation. The method must `yield` to execute the nested steps. If it does not yield, the nested steps are skipped. Normal break conditions (errors, `finish!`) still apply inside the block.
|
|
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
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
step :build
|
|
16
|
+
|
|
17
|
+
within :read_from_replica do
|
|
18
|
+
step :check_duplicate
|
|
19
|
+
step :validate_quota
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
step :create
|
|
23
|
+
step :output
|
|
24
|
+
|
|
25
|
+
def build
|
|
26
|
+
self.profile = current_account.profiles.build(params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_duplicate
|
|
30
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_quota
|
|
34
|
+
result.add_error(:base, 'quota exceeded') if current_account.profiles.count >= 100
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create
|
|
38
|
+
profile.save!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def output
|
|
42
|
+
result.output = { model: profile }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def read_from_replica(&block)
|
|
48
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Inline usage
|
|
54
|
+
|
|
55
|
+
The wrapper method can also be used inline inside any step method when you need the wrapper for only part of that method's logic:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
def some_step
|
|
59
|
+
value = read_from_replica { Profile.count }
|
|
60
|
+
result.output = { count: value }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def read_from_replica(&block)
|
|
66
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Mixing step and operation inside within
|
|
71
|
+
|
|
72
|
+
`within` can wrap any combination of `step` and `operation` instructions. All of them execute inside the wrapper, and their outputs are available in context afterwards as usual.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Profile::Create < Opera::Operation::Base
|
|
76
|
+
context do
|
|
77
|
+
attr_accessor :profile
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
dependencies do
|
|
81
|
+
attr_reader :current_account, :quota_checker
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
within :read_from_replica do
|
|
85
|
+
step :check_duplicate
|
|
86
|
+
operation :fetch_quota
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
step :create
|
|
90
|
+
step :output
|
|
91
|
+
|
|
92
|
+
def check_duplicate
|
|
93
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fetch_quota
|
|
97
|
+
quota_checker.call(params: params)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create
|
|
101
|
+
self.profile = current_account.profiles.create(params)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def output
|
|
105
|
+
result.output = { model: profile, quota: context[:fetch_quota_output] }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def read_from_replica(&block)
|
|
111
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Nesting within inside a transaction
|
|
117
|
+
|
|
118
|
+
`within` can be placed inside a `transaction` block alongside other instructions. If any step or operation inside `within` fails, the error propagates up and the transaction is rolled back as normal.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class Profile::Create < Opera::Operation::Base
|
|
122
|
+
configure do |config|
|
|
123
|
+
config.transaction_class = ActiveRecord::Base
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
context do
|
|
127
|
+
attr_accessor :profile
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
dependencies do
|
|
131
|
+
attr_reader :current_account, :quota_checker, :audit_logger
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
transaction do
|
|
135
|
+
within :read_from_replica do
|
|
136
|
+
step :check_duplicate
|
|
137
|
+
operation :fetch_quota
|
|
138
|
+
end
|
|
139
|
+
operation :write_audit_log
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
step :output
|
|
143
|
+
|
|
144
|
+
def check_duplicate
|
|
145
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def fetch_quota
|
|
149
|
+
quota_checker.call(params: params)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def write_audit_log
|
|
153
|
+
audit_logger.call(params: params)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def output
|
|
157
|
+
result.output = { quota: context[:fetch_quota_output] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def read_from_replica(&block)
|
|
163
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
@@ -17,7 +17,7 @@ module Opera
|
|
|
17
17
|
@instrumentation_class = self.class.instrumentation_class
|
|
18
18
|
|
|
19
19
|
@mode = self.class.mode || DEVELOPMENT_MODE
|
|
20
|
-
@reporter =
|
|
20
|
+
@reporter = self.class.reporter
|
|
21
21
|
|
|
22
22
|
validate!
|
|
23
23
|
end
|
|
@@ -26,10 +26,6 @@ module Opera
|
|
|
26
26
|
yield self
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def custom_reporter
|
|
30
|
-
Rails.application.config.x.reporter.presence if defined?(Rails)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
29
|
private
|
|
34
30
|
|
|
35
31
|
def validate!
|
|
@@ -47,7 +43,7 @@ module Opera
|
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
def development_mode?
|
|
50
|
-
mode ==
|
|
46
|
+
mode == DEVELOPMENT_MODE
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def production_mode?
|
|
@@ -20,15 +20,24 @@ module Opera
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def evaluate_instructions(instructions = [])
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
while instruction_copy.any?
|
|
26
|
-
instruction = instruction_copy.shift
|
|
23
|
+
instructions.each do |instruction|
|
|
27
24
|
evaluate_instruction(instruction)
|
|
28
25
|
break if break_condition
|
|
29
26
|
end
|
|
30
27
|
end
|
|
31
28
|
|
|
29
|
+
# Executes the operation method named in the instruction, instruments it,
|
|
30
|
+
# and records the execution. This is the shared primitive that all executors
|
|
31
|
+
# use to invoke a step method without mutating the instruction hash.
|
|
32
|
+
def execute_step(instruction)
|
|
33
|
+
method = instruction[:method]
|
|
34
|
+
|
|
35
|
+
Instrumentation.new(operation).instrument(name: "##{method}", level: :step) do
|
|
36
|
+
result.add_execution(method) unless production_mode?
|
|
37
|
+
operation.send(method)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
33
42
|
def evaluate_instruction(instruction)
|
|
34
43
|
case instruction[:kind]
|
|
@@ -6,12 +6,7 @@ module Opera
|
|
|
6
6
|
module Executors
|
|
7
7
|
class Step < Executor
|
|
8
8
|
def call(instruction)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Instrumentation.new(operation).instrument(name: "##{method}", level: :step) do
|
|
12
|
-
operation.result.add_execution(method) unless production_mode?
|
|
13
|
-
operation.send(method)
|
|
14
|
-
end
|
|
9
|
+
execute_step(instruction)
|
|
15
10
|
end
|
|
16
11
|
end
|
|
17
12
|
end
|