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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f97e8fc036a03a7ff4b0fdab9e4316d2312ad17ae5cf1a1844dc1ceb7cf84ad1
4
- data.tar.gz: e2e2b98a9c87a50b42acbe2b2d1cfa507a4972d2871f6e26ad5a938d5a28c464
3
+ metadata.gz: df39296baae3026a586088215fec2ab3b0bd6630141beb4c67167d38f61ae663
4
+ data.tar.gz: c767f034411fc574752a5a8d43d94a7fab54ad9ff4c6f57bcb2da96722709bf9
5
5
  SHA512:
6
- metadata.gz: b0d55912ff981d7298d3c9ca0c24babeea983427af130714e808d8767913cfde985b9a5fd5cb611d2f87c9c0986f133daa6357dd5645d460d740ea4f29e44e42
7
- data.tar.gz: 90dca03f785f51506abb76bdcda2a2448e1236de32957ab5e9592e2252c3f63f125eb83c7e4e17477619d534a06fcbeb07788e266182b1e2abadf32bd89a1eb1
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.0.0"
299
+ gem "action_operation", "2.1.0"
242
300
 
243
301
  And then execute:
244
302
 
@@ -1,7 +1,7 @@
1
1
  module ActionOperation
2
2
  class Error
3
3
  class MissingError < Error
4
- def initialize(step)
4
+ def initialize(step:)
5
5
  @step = step
6
6
  end
7
7
 
@@ -1,7 +1,7 @@
1
1
  module ActionOperation
2
2
  class Error
3
3
  class MissingSchema < Error
4
- def initialize(step)
4
+ def initialize(step:)
5
5
  @step = step
6
6
  end
7
7
 
@@ -1,7 +1,7 @@
1
1
  module ActionOperation
2
2
  class Error
3
3
  class MissingTask < Error
4
- def initialize(step)
4
+ def initialize(step:)
5
5
  @step = step
6
6
  end
7
7
 
@@ -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
@@ -3,5 +3,6 @@ module ActionOperation
3
3
  require_relative "error/missing_error"
4
4
  require_relative "error/missing_schema"
5
5
  require_relative "error/missing_task"
6
+ require_relative "error/step_schema_mismatch"
6
7
  end
7
8
  end
@@ -1,3 +1,3 @@
1
1
  module ActionOperation
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -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 refernece it in the rescue
50
+ # NOTE: We only care about this so we can reference it in the rescue
31
51
  @latest_step = step
32
52
 
33
- value = public_send(step.name, state: self.class.schemas.fetch(step.name).new(state))
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
- when State then value.raw
37
- when Drift then break call(start: right.find_index { |step| step.name == value.to }, raw: raw)
38
- else state
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
- rescue *left.select(&:exception).map(&:exception).uniq => handled_exception
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
- value = public_send(step.name, exception: exception, state: self.class.schemas.fetch(@latest_step.name).new(raw), step: @latest_step)
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(arguments) }
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.0.0
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-05 00:00:00.000000000 Z
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