axn 0.1.0.pre.alpha.2.5.3 → 0.1.0.pre.alpha.2.6
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 +15 -1
- data/README.md +2 -11
- data/docs/reference/class.md +5 -5
- data/docs/reference/configuration.md +15 -4
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +8 -8
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +1 -1
- data/lib/action/context.rb +28 -0
- data/lib/action/core/automatic_logging.rb +77 -0
- data/lib/action/core/context_facade.rb +1 -1
- data/lib/action/core/contract.rb +153 -214
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +51 -0
- data/lib/action/core/handle_exceptions.rb +102 -122
- data/lib/action/core/hoist_errors.rb +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +22 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core.rb +153 -0
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/axn/factory.rb +0 -12
- data/lib/axn/testing/spec_helpers.rb +0 -2
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +10 -19
- data/lib/action/core/top_level_around_hook.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49a6ab763d534efa878ff7f0d833beefeaae604bddb1c2e89090788a1e7df1a0
|
4
|
+
data.tar.gz: 828cfbd5ad2b9f753bf938bdb84201edbb406b367c159d4b1f437054a3bc213f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8be59eb695a3df836513fb477b2338bb539da209821d17ab6502314ea57e62204e9a0e5ae49ec579db5d5c8e99ca6de57596acf577ee2027d8410e23a5013f15
|
7
|
+
data.tar.gz: 1a559aff5de117890928f67a5e0adefe1f2615c24f15ac6357dff0c74bd3f9a9470110618228a30aaba35ae489844cca4efd1f701aba0f1805c18b5c71edd7a9
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,20 @@
|
|
3
3
|
## Unreleased
|
4
4
|
* N/A
|
5
5
|
|
6
|
+
## 0.1.0-alpha.2.6
|
7
|
+
* Inline interactor code (no more dependency on unpublished forked branch to support inheritance)
|
8
|
+
* Refactor internals to clean implementation now that we have direct control
|
9
|
+
* [BREAKING] Replaced `Action.config.top_level_around_hook` with `.wrap_with_trace` and `.emit_metrics`
|
10
|
+
* [BREAKING] the order of hooks with inheritance has changed to more intuitively follow the natural pattern of setup (general → specific) and teardown (specific → general):
|
11
|
+
* **Before hooks**: Parent → Child (general setup first, then specific)
|
12
|
+
* **After hooks**: Child → Parent (specific cleanup first, then general)
|
13
|
+
* **Around hooks**: Parent wraps child (parent outside, child inside)
|
14
|
+
* Removed non-functional #rollback traces (use on_exception hook instead)
|
15
|
+
* Clean requires structure
|
16
|
+
|
17
|
+
## 0.1.0-alpha.2.5.3.1
|
18
|
+
* Remove explicit 'require rspec' from `axn/testing/spec_helpers` (must already be loaded)
|
19
|
+
|
6
20
|
## 0.1.0-alpha.2.5.3
|
7
21
|
* More aggressive logging of swallowed exceptions when not in production mode
|
8
22
|
* Make automatic pre/post logging more digestible
|
@@ -10,7 +24,7 @@
|
|
10
24
|
## 0.1.0-alpha.2.5.2
|
11
25
|
* [BREAKING] Removing `EnqueueAllInBackground` + `EnqueueAllWorker` - better + simply solved at application level
|
12
26
|
* [TEST] Expose spec helpers to consumers (add `require "axn/testing/spec_helpers"` to your `spec_helper.rb`)
|
13
|
-
|
27
|
+
* [FEAT] Added ability to use custom Strategies (via e.g. `use :transaction`)
|
14
28
|
|
15
29
|
## 0.1.0-alpha.2.5.1.2
|
16
30
|
* [BUGFIX] Subfield expectations: now support hashes with string keys (using with_indifferent_access)
|
data/README.md
CHANGED
@@ -1,20 +1,11 @@
|
|
1
1
|
# Axn -- [AHK-sin] (a.k.a. "Action")
|
2
2
|
|
3
|
-
Just spinning this up -- not yet released
|
3
|
+
Just spinning this up -- not yet publicly released, changes coming frequently.
|
4
4
|
|
5
5
|
## Installation & Usage
|
6
6
|
|
7
7
|
See our [User Guide](https://teamshares.github.io/axn/) for details.
|
8
8
|
|
9
|
-
## [!!] Inheritance Support
|
10
|
-
|
11
|
-
Out of the box Axn only supports a direct style (every action must `include Action`).
|
12
|
-
|
13
|
-
If you want to support inheritance, you'll need to add this line to your `Gemfile` (we're layered over Interactor, and their released version doesn't yet support inheritance):
|
14
|
-
|
15
|
-
gem "interactor", github: "kaspermeyer/interactor", branch: "fix-hook-inheritance"
|
16
|
-
|
17
|
-
|
18
9
|
## Development
|
19
10
|
|
20
11
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -27,4 +18,4 @@ See our [contribution guidelines](CONTRIBUTING.md) for more information.
|
|
27
18
|
|
28
19
|
## Thank You
|
29
20
|
|
30
|
-
A very special thank you to [Collective Idea](https://collectiveidea.com/)'s fantastic [Interactor](https://github.com/collectiveidea/interactor?tab=readme-ov-file#interactor) library, which [we](https://www.teamshares.com/) used successfully for a number of years and which
|
21
|
+
A very special thank you to [Collective Idea](https://collectiveidea.com/)'s fantastic [Interactor](https://github.com/collectiveidea/interactor?tab=readme-ov-file#interactor) library, which [we](https://www.teamshares.com/) used successfully for a number of years and which we used to scaffold early versions of this library.
|
data/docs/reference/class.md
CHANGED
@@ -95,11 +95,11 @@ Accepts `error` and/or `success` keys. Values can be a string (returned directl
|
|
95
95
|
messages success: "All good!", error: ->(e) { "Bad news: #{e.message}" }
|
96
96
|
```
|
97
97
|
|
98
|
-
## `
|
98
|
+
## `error_from` and `rescues`
|
99
99
|
|
100
100
|
While `.messages` sets the _default_ error/success messages and is more commonly used, there are times when you want specific error messages for specific failure cases.
|
101
101
|
|
102
|
-
`
|
102
|
+
`error_from` and `rescues` both register a matcher (exception class, exception class name (string), or callable) and a message to use if the matcher succeeds. They act exactly the same, except if a matcher registered with `rescues` succeeds, the exception _will not_ trigger the configured exception handlers (global or specific to this class).
|
103
103
|
|
104
104
|
```ruby
|
105
105
|
messages error: "bad"
|
@@ -108,8 +108,8 @@ messages error: "bad"
|
|
108
108
|
rescues ActiveRecord::InvalidRecord => "Invalid params provided"
|
109
109
|
|
110
110
|
# These WILL trigger error handler (second demonstrates callable matcher AND message)
|
111
|
-
|
112
|
-
|
111
|
+
error_from ArgumentError, ->(e) { "Argument error: #{e.message}" }
|
112
|
+
error_from -> { name == "bad" }, -> { "was given bad name: #{name}" }
|
113
113
|
```
|
114
114
|
|
115
115
|
## Callbacks
|
@@ -153,7 +153,7 @@ class Foo
|
|
153
153
|
end
|
154
154
|
```
|
155
155
|
|
156
|
-
Note that by default the `on_exception` block will be applied to _any_ `StandardError` that is raised, but you can specify a matcher using the same logic as for [`
|
156
|
+
Note that by default the `on_exception` block will be applied to _any_ `StandardError` that is raised, but you can specify a matcher using the same logic as for [`error_from` and `rescues`](#error-for-and-rescues):
|
157
157
|
|
158
158
|
```ruby
|
159
159
|
class Foo
|
@@ -63,17 +63,26 @@ A couple notes:
|
|
63
63
|
|
64
64
|
If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Action calls and/or emitting count metrics after each call completes.
|
65
65
|
|
66
|
+
### Tracing and Metrics
|
67
|
+
|
68
|
+
The framework provides two distinct hooks for observability:
|
69
|
+
|
70
|
+
- **`wrap_with_trace`**: An around hook that wraps the entire action execution. You MUST call the provided block to execute the action.
|
71
|
+
- **`emit_metrics`**: A post-execution hook that receives the action outcome. Do NOT call any blocks.
|
72
|
+
|
66
73
|
For example, to wire up Datadog:
|
67
74
|
|
68
75
|
```ruby
|
69
76
|
Action.configure do |c|
|
70
|
-
c.
|
77
|
+
c.wrap_with_trace = proc do |resource, &action|
|
71
78
|
Datadog::Tracing.trace("Action", resource:) do
|
72
|
-
|
73
|
-
|
74
|
-
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome:, resource: })
|
79
|
+
action.call
|
75
80
|
end
|
76
81
|
end
|
82
|
+
|
83
|
+
c.emit_metrics = proc do |resource, outcome|
|
84
|
+
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome:, resource: })
|
85
|
+
end
|
77
86
|
end
|
78
87
|
```
|
79
88
|
|
@@ -81,6 +90,8 @@ A couple notes:
|
|
81
90
|
|
82
91
|
* `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
|
83
92
|
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that outcome (`success`, `failure`, `exception`) of the action is reported so you can easily track e.g. success rates per action.
|
93
|
+
* The `wrap_with_trace` hook is an around hook - you must call the provided block to execute the action
|
94
|
+
* The `emit_metrics` hook is called after execution with the outcome - do not call any blocks
|
84
95
|
|
85
96
|
|
86
97
|
## `logger`
|
data/docs/reference/instance.md
CHANGED
@@ -12,7 +12,7 @@ Primarily used for its side effects, but it does return a Hash with the key/valu
|
|
12
12
|
|
13
13
|
## `#fail!`
|
14
14
|
|
15
|
-
Called with a string, it immediately halts execution
|
15
|
+
Called with a string, it immediately halts execution and sets `result.error` to the provided string.
|
16
16
|
|
17
17
|
## `#log`
|
18
18
|
|
@@ -31,7 +31,7 @@ A few details:
|
|
31
31
|
* An explicit `fail!` call _will_ still fail the action
|
32
32
|
* Any exceptions swallowed _will_ still be reported via the `on_exception` handler
|
33
33
|
|
34
|
-
This is primarily useful in an after block, e.g. trigger notifications after an action has been taken. If the notification fails to send you DO want to log the failure somewhere to investigate, but since the core action has already been taken often you do _not_ want to fail
|
34
|
+
This is primarily useful in an after block, e.g. trigger notifications after an action has been taken. If the notification fails to send you DO want to log the failure somewhere to investigate, but since the core action has already been taken often you do _not_ want to fail.
|
35
35
|
|
36
36
|
Example:
|
37
37
|
|
data/docs/strategies/index.md
CHANGED
@@ -155,7 +155,7 @@ Action::Strategies.register(:retry, RetryStrategy)
|
|
155
155
|
|
156
156
|
### Example: Complete Custom Strategy
|
157
157
|
|
158
|
-
Here's a complete example of a custom strategy that adds performance monitoring:
|
158
|
+
Here's a complete example of a custom strategy that adds performance monitoring (note Axn already logs elapsed time, this is just a toy example):
|
159
159
|
|
160
160
|
```ruby
|
161
161
|
module PerformanceMonitoringStrategy
|
data/docs/usage/setup.md
CHANGED
@@ -19,6 +19,6 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
19
19
|
|
20
20
|
### Metrics / Tracing
|
21
21
|
|
22
|
-
If you're using an APM provider, observability can be greatly enhanced by [configuring
|
22
|
+
If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing-and-metrics).
|
23
23
|
|
24
24
|
|
data/docs/usage/writing.md
CHANGED
@@ -104,18 +104,11 @@ Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life i
|
|
104
104
|
|
105
105
|
In addition to `#call`, there are a few additional pieces to be aware of:
|
106
106
|
|
107
|
-
<!-- ### `#rollback`
|
108
|
-
*** TODO: rollback actually only applies to rolling back *completed* steps of a multi-step Axn chain. Do not document for now -- need to decide if adding a trigger-when-axn-itself-fails rollback path. ***
|
109
107
|
|
110
|
-
::: danger ALPHA
|
111
|
-
* ⚠️ `#rollback` is _expected_ to be added shortly, but is not yet functional!
|
112
|
-
:::
|
113
|
-
|
114
|
-
If you define a `#rollback` method, it'll be called (_before_ returning an `Action::Result` to the caller) whenever your action fails. -->
|
115
108
|
|
116
109
|
### Hooks
|
117
110
|
|
118
|
-
`before` and `
|
111
|
+
`before`, `after`, and `around` hooks are supported. They can receive a block directly, or the symbol name of a local method.
|
119
112
|
|
120
113
|
Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully).
|
121
114
|
|
@@ -150,6 +143,13 @@ in call
|
|
150
143
|
after hook
|
151
144
|
```
|
152
145
|
|
146
|
+
**Hook Ordering with Inheritance:**
|
147
|
+
- **Around hooks**: Parent wraps child (parent outside, child inside)
|
148
|
+
- **Before hooks**: Parent → Child (general setup first, then specific)
|
149
|
+
- **After hooks**: Child → Parent (specific cleanup first, then general)
|
150
|
+
|
151
|
+
This follows the natural pattern of setup (general → specific) and teardown (specific → general).
|
152
|
+
|
153
153
|
### Callbacks
|
154
154
|
|
155
155
|
A number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails. See the [Class Interface docs](/reference/class#callbacks) for details.
|
data/lib/action/attachable.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require "action/attachable/base"
|
4
|
+
require "action/attachable/steps"
|
5
|
+
require "action/attachable/subactions"
|
6
6
|
|
7
7
|
module Action
|
8
8
|
module Attachable
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Action
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :wrap_with_trace, :emit_metrics
|
6
6
|
attr_writer :logger, :env, :on_exception, :additional_includes, :default_log_level, :default_autolog_level
|
7
7
|
|
8
8
|
def default_log_level = @default_log_level ||= :info
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# NOTE: This is a temporary file to be removed when we have a better way to handle context.
|
4
|
+
# rubocop:disable Style/OpenStructUse, Style/CaseEquality
|
5
|
+
require "ostruct"
|
6
|
+
|
7
|
+
module Action
|
8
|
+
class Context < OpenStruct
|
9
|
+
def self.build(context = {})
|
10
|
+
self === context ? context : new(context)
|
11
|
+
end
|
12
|
+
|
13
|
+
def success?
|
14
|
+
!failure?
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure?
|
18
|
+
@failure || false
|
19
|
+
end
|
20
|
+
|
21
|
+
def fail!(context = {})
|
22
|
+
context.each { |key, value| self[key.to_sym] = value }
|
23
|
+
@failure = true
|
24
|
+
raise Action::Failure, self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
# rubocop:enable Style/OpenStructUse, Style/CaseEquality
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module AutomaticLogging
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend ClassMethods
|
9
|
+
include InstanceMethods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def autolog_level = Action.config.default_autolog_level
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
private
|
19
|
+
|
20
|
+
def _log_before
|
21
|
+
public_send(
|
22
|
+
self.class.autolog_level,
|
23
|
+
[
|
24
|
+
"About to execute",
|
25
|
+
_log_context(:inbound),
|
26
|
+
].compact.join(" with: "),
|
27
|
+
before: Action.config.env.production? ? nil : "\n------\n",
|
28
|
+
)
|
29
|
+
rescue StandardError => e
|
30
|
+
Axn::Util.piping_error("logging before hook", action: self, exception: e)
|
31
|
+
end
|
32
|
+
|
33
|
+
def _log_after(outcome:, timing_start:)
|
34
|
+
elapsed_mils = Core::Timing.elapsed_ms(timing_start)
|
35
|
+
|
36
|
+
public_send(
|
37
|
+
self.class.autolog_level,
|
38
|
+
[
|
39
|
+
"Execution completed (with outcome: #{outcome}) in #{elapsed_mils} milliseconds",
|
40
|
+
_log_context(:outbound),
|
41
|
+
].compact.join(". Set: "),
|
42
|
+
after: Action.config.env.production? ? nil : "\n------\n",
|
43
|
+
)
|
44
|
+
rescue StandardError => e
|
45
|
+
Axn::Util.piping_error("logging after hook", action: self, exception: e)
|
46
|
+
end
|
47
|
+
|
48
|
+
def _log_context(direction)
|
49
|
+
data = context_for_logging(direction)
|
50
|
+
return unless data.present?
|
51
|
+
|
52
|
+
max_length = 150
|
53
|
+
suffix = "…<truncated>…"
|
54
|
+
|
55
|
+
_log_object(data).tap do |str|
|
56
|
+
return str[0, max_length - suffix.length] + suffix if str.length > max_length
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def _log_object(data)
|
61
|
+
case data
|
62
|
+
when Hash
|
63
|
+
# NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
|
64
|
+
"{#{data.map { |k, v| "#{k}: #{_log_object(v)}" }.join(", ")}}"
|
65
|
+
when Array
|
66
|
+
data.map { |v| _log_object(v) }
|
67
|
+
else
|
68
|
+
return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
|
69
|
+
return "<#{data.class.name}##{data.to_param.presence || "unpersisted"}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
|
70
|
+
|
71
|
+
data.inspect
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -120,7 +120,7 @@ module Action
|
|
120
120
|
end
|
121
121
|
|
122
122
|
# Poke some holes for necessary internal control methods
|
123
|
-
delegate :
|
123
|
+
delegate :each_pair, to: :context
|
124
124
|
|
125
125
|
# External interface
|
126
126
|
delegate :success?, :exception, to: :context
|