action_operation 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|