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 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