action_operation 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +59 -1
- data/lib/action_operation/error/missing_error.rb +1 -1
- data/lib/action_operation/error/missing_schema.rb +1 -1
- data/lib/action_operation/error/missing_task.rb +1 -1
- data/lib/action_operation/error/step_schema_mismatch.rb +15 -0
- data/lib/action_operation/error.rb +1 -0
- data/lib/action_operation/version.rb +1 -1
- data/lib/action_operation.rb +82 -17
- data/lib/action_operation_spec.rb +12 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df39296baae3026a586088215fec2ab3b0bd6630141beb4c67167d38f61ae663
|
4
|
+
data.tar.gz: c767f034411fc574752a5a8d43d94a7fab54ad9ff4c6f57bcb2da96722709bf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ceab9d1a6fd7d8202fd82e07cebcc03a01716ceb4c49a53bc20a4453155475893b2b968e104d840107bd0f81d72d75ec80c1cb4e627dd416e0bc31cae78cbbc
|
7
|
+
data.tar.gz: fdfb512b613d99eeeef8903051547dc60285676cb7d2eb95967f5abe2d601716e01048cafb10cf88e0aaf024773fa52688359c49456238b544593c3aa80ad4cd
|
data/README.md
CHANGED
@@ -234,11 +234,69 @@ So here's how this works:
|
|
234
234
|
However, if it finishes successfully we get to push a notification to the document owner in `publish()`.
|
235
235
|
|
236
236
|
|
237
|
+
### Callbacks
|
238
|
+
|
239
|
+
Sometimes we want to make sure an operation or it's individual parts are wrapped in safety measures, like a transaction or a timeout. You can achieve these with special built in instance methods. I'll show you each one and why you would use it.
|
240
|
+
|
241
|
+
To start, the highest wrapper is `around_steps`, which wraps around both tasks and catches. A good use for this is
|
242
|
+
|
243
|
+
``` ruby
|
244
|
+
class AddProductToCart < ApplicationOperation
|
245
|
+
def around_steps(raw:)
|
246
|
+
Rails.logger.tagged("operation-id=#{SecureRandom.uuid}") do
|
247
|
+
Rails.logger.debug("Started adding cart to product operation with (#{raw.to_json})")
|
248
|
+
|
249
|
+
yield
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
Here we're making sure every log we write will be tagged with a unique identifier for the entire operation, an extremely valuable option for debugging. The `around_steps` hook will be told about the raw data it receives in the call (`AddProductToCart.({cart: current_cart, product: product})`).
|
256
|
+
|
257
|
+
While `around_steps` is on the entire operation, you might want individual wrapping. Let me present: `around_step`!
|
258
|
+
|
259
|
+
``` ruby
|
260
|
+
class AddProductToCart < ApplicationOperation
|
261
|
+
def around_step(step:)
|
262
|
+
Rails.logger.tagged("step-id=#{SecureRandom.uuid}") do
|
263
|
+
yield
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
This `around_step` will give you a per-step unique id tag for all logs in a step, another fantastic tool in debugging. This hook will be told of the `Task|Catch` object which responds to `#name` and `#receiver`. Additionally a `Task` responds to `#required` and a `Catch` responds to `#exception`.
|
270
|
+
|
271
|
+
Finally, there are 4 other type specific hooks: `around_tasks`, `around_task`, `around_catches`, and `around_catch`. Here are example uses:
|
272
|
+
|
273
|
+
|
274
|
+
``` ruby
|
275
|
+
class AddProductToCart < ApplicationOperation
|
276
|
+
def around_tasks
|
277
|
+
Timeout.new(30.seconds) do
|
278
|
+
yield
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def around_task(step:, state:)
|
283
|
+
Rails.logger.debug("Working on #{step.receiver}##{step.name} using (#{state.to_json})")
|
284
|
+
|
285
|
+
Timeout.new(10.seconds) do
|
286
|
+
ApplicationRecord.transaction do
|
287
|
+
yield
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
|
237
295
|
## Installing
|
238
296
|
|
239
297
|
Add this line to your application's Gemfile:
|
240
298
|
|
241
|
-
gem "action_operation", "2.
|
299
|
+
gem "action_operation", "2.1.0"
|
242
300
|
|
243
301
|
And then execute:
|
244
302
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActionOperation
|
2
|
+
class Error
|
3
|
+
class StepSchemaMismatch < Error
|
4
|
+
def initialize(step:, schema:, raw:)
|
5
|
+
@step = step
|
6
|
+
@schema = schema
|
7
|
+
@raw = raw
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"#{@step.receiver}##{@step.name} #{cause.message} and received #{@raw}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/action_operation.rb
CHANGED
@@ -11,8 +11,8 @@ module ActionOperation
|
|
11
11
|
|
12
12
|
State = Struct.new(:raw)
|
13
13
|
Drift = Struct.new(:to)
|
14
|
-
Task = Struct.new(:name, :required)
|
15
|
-
Catch = Struct.new(:name, :exception)
|
14
|
+
Task = Struct.new(:name, :receiver, :required)
|
15
|
+
Catch = Struct.new(:name, :receiver, :exception)
|
16
16
|
|
17
17
|
def initialize(raw:)
|
18
18
|
raise ArgumentError, "needs to be a Hash" unless raw.kind_of?(Hash)
|
@@ -20,31 +20,72 @@ module ActionOperation
|
|
20
20
|
@raw = raw
|
21
21
|
end
|
22
22
|
|
23
|
+
private def callbackings(arounds, &process)
|
24
|
+
arounds.reverse.reduce(process) do |compound, callback|
|
25
|
+
callback.call(&compound).call
|
26
|
+
end.call
|
27
|
+
end
|
28
|
+
|
23
29
|
def call(start: nil, raw: @raw)
|
30
|
+
around_steps do
|
31
|
+
begin
|
32
|
+
around_tasks do
|
33
|
+
tasks(start, raw)
|
34
|
+
end
|
35
|
+
rescue *left.select(&:exception).map(&:exception).uniq => handled_exception
|
36
|
+
around_catches do
|
37
|
+
catches(handled_exception, raw)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private def tasks(start, raw)
|
24
44
|
right.from(start || 0).reduce(raw) do |state, step|
|
25
45
|
next state unless step.required || (start && right.at(start) == step)
|
26
46
|
|
27
|
-
raise Error::MissingTask, step unless respond_to?(step.name)
|
28
|
-
raise Error::MissingSchema, step unless self.class.schemas.key?(step.name)
|
47
|
+
raise Error::MissingTask, step: step unless respond_to?(step.name)
|
48
|
+
raise Error::MissingSchema, step: step unless self.class.schemas.key?(step.name)
|
29
49
|
|
30
|
-
# NOTE: We only care about this so we can
|
50
|
+
# NOTE: We only care about this so we can reference it in the rescue
|
31
51
|
@latest_step = step
|
32
52
|
|
33
|
-
|
53
|
+
# puts "#{step.class}::#{step.receiver}##{step.name}"
|
54
|
+
|
55
|
+
begin
|
56
|
+
value = around_task do
|
57
|
+
public_send(step.name, state: self.class.schemas.fetch(step.name).new(state))
|
58
|
+
end
|
59
|
+
# puts "#{step.class}::#{step.receiver}##{step.name} #{value}"
|
60
|
+
rescue SmartParams::Error::InvalidPropertyType => invalid_property_type_exception
|
61
|
+
raise Error::StepSchemaMismatch, step: step, schema: self.class.schemas.fetch(step.name), raw: raw, cause: invalid_property_type_exception
|
62
|
+
end
|
63
|
+
|
34
64
|
|
35
65
|
case value
|
36
|
-
|
37
|
-
|
38
|
-
|
66
|
+
when State then value.raw
|
67
|
+
when Drift then break call(start: right.find_index { |step| step.name == value.to }, raw: state)
|
68
|
+
else state
|
39
69
|
end
|
40
70
|
end
|
41
|
-
|
42
|
-
left.select do |failure|
|
43
|
-
failure.exception === handled_exception
|
44
|
-
end.reduce(handled_exception) do |exception, step|
|
45
|
-
raise Error::MissingError, step unless respond_to?(step.name)
|
71
|
+
end
|
46
72
|
|
47
|
-
|
73
|
+
private def catches(exception, raw)
|
74
|
+
left.select do |failure|
|
75
|
+
failure.exception === exception
|
76
|
+
end.reduce(exception) do |exception, step|
|
77
|
+
raise Error::MissingError, step: step unless respond_to?(step.name)
|
78
|
+
|
79
|
+
# puts "#{step.class}::#{step.receiver}##{step.name}"
|
80
|
+
|
81
|
+
begin
|
82
|
+
value = around_catch do
|
83
|
+
public_send(step.name, exception: exception, state: raw, step: @latest_step)
|
84
|
+
end
|
85
|
+
# puts "#{step.class}::#{step.receiver}##{step.name} #{value}"
|
86
|
+
rescue SmartParams::Error::InvalidPropertyType => invalid_property_type_exception
|
87
|
+
raise Error::StepSchemaMismatch, step: @latest_step, schema: self.class.schemas.fetch(@latest_step.name), raw: raw, cause: invalid_property_type_exception
|
88
|
+
end
|
48
89
|
|
49
90
|
if value.kind_of?(Drift)
|
50
91
|
break call(start: right.find_index { |step| step.name == value.to }, raw: raw)
|
@@ -66,6 +107,30 @@ module ActionOperation
|
|
66
107
|
Drift.new(to)
|
67
108
|
end
|
68
109
|
|
110
|
+
def around_steps(&callback)
|
111
|
+
callback.call
|
112
|
+
end
|
113
|
+
|
114
|
+
def around_step(&callback)
|
115
|
+
callback.call
|
116
|
+
end
|
117
|
+
|
118
|
+
def around_tasks(&callback)
|
119
|
+
callback.call
|
120
|
+
end
|
121
|
+
|
122
|
+
def around_task(&callback)
|
123
|
+
callback.call
|
124
|
+
end
|
125
|
+
|
126
|
+
def around_catches(&callback)
|
127
|
+
callback.call
|
128
|
+
end
|
129
|
+
|
130
|
+
def around_catch(&callback)
|
131
|
+
callback.call
|
132
|
+
end
|
133
|
+
|
69
134
|
private def left
|
70
135
|
self.class.left
|
71
136
|
end
|
@@ -92,11 +157,11 @@ module ActionOperation
|
|
92
157
|
end
|
93
158
|
|
94
159
|
def task(name, required: true)
|
95
|
-
right << Task.new(name, required)
|
160
|
+
right << Task.new(name, self, required)
|
96
161
|
end
|
97
162
|
|
98
163
|
def catch(name, exception: StandardError)
|
99
|
-
left << Catch.new(name, exception)
|
164
|
+
left << Catch.new(name, self, exception)
|
100
165
|
end
|
101
166
|
|
102
167
|
def right
|
@@ -1,14 +1,24 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
RSpec.describe ActionOperation do
|
4
|
-
let(:operation) { DocumentUploadOperation }
|
4
|
+
let(:operation) { DocumentUploadOperation.new(raw: arguments) }
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(operation).to receive(:logger)
|
8
|
+
end
|
5
9
|
|
6
10
|
describe "#call" do
|
7
|
-
subject { operation.call
|
11
|
+
subject { operation.call }
|
8
12
|
|
9
13
|
context "with the right arguments" do
|
10
14
|
let(:arguments) {{document: Document.new}}
|
11
15
|
|
16
|
+
it "calls the around steps callback" do
|
17
|
+
expect(operation).to receive(:logger).twice
|
18
|
+
|
19
|
+
subject
|
20
|
+
end
|
21
|
+
|
12
22
|
it "works" do
|
13
23
|
expect(subject).to match(hash_including(document: a_kind_of(Document), location: "some.s3"))
|
14
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_operation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kurtis Rainbolt-Greene
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -140,6 +140,7 @@ files:
|
|
140
140
|
- lib/action_operation/error/missing_error.rb
|
141
141
|
- lib/action_operation/error/missing_schema.rb
|
142
142
|
- lib/action_operation/error/missing_task.rb
|
143
|
+
- lib/action_operation/error/step_schema_mismatch.rb
|
143
144
|
- lib/action_operation/types.rb
|
144
145
|
- lib/action_operation/version.rb
|
145
146
|
- lib/action_operation/version_spec.rb
|