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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d62aa4b98c8f159761234817e6b2ded26e0fcafa46d07685049d827281e92235
4
- data.tar.gz: d3ffb46353e17db427393049767a6a03fbf3d5b30e94e4011a47397644364c32
3
+ metadata.gz: 49a6ab763d534efa878ff7f0d833beefeaae604bddb1c2e89090788a1e7df1a0
4
+ data.tar.gz: 828cfbd5ad2b9f753bf938bdb84201edbb406b367c159d4b1f437054a3bc213f
5
5
  SHA512:
6
- metadata.gz: 54a4ea06f021850cc1d4e1e9bf18a6ec20f495871f23efab20380cd2a8135c52df46099f03281944c4dbe4419c836a72309cfd719e769ae3fd7e48a2ad7683db
7
- data.tar.gz: 474eea3af1b53ad4ae85096a2e39d4b141b01c8c4f239f710cd13de933046d92b7efc67333902c78ed29c882bda5ada0a44e9e2efeab05721681f5ded1633b41
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
- # [FEAT] Added ability to use custom Strategies (via e.g. `use :transaction`)
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 (i.e. doc updates in flight).
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 still forms the basis of this library today.
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.
@@ -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
- ## `error_for` and `rescues`
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
- `error_for` 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).
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
- error_for ArgumentError, ->(e) { "Argument error: #{e.message}" }
112
- error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }
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 [`error_for` and `rescues`](#error-for-and-rescues):
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.top_level_around_hook = proc do |resource, &action|
77
+ c.wrap_with_trace = proc do |resource, &action|
71
78
  Datadog::Tracing.trace("Action", resource:) do
72
- (outcome, _exception) = action.call
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`
@@ -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 (including triggering any [rollback handler](/reference/class#rollback) you have defined) and sets `result.error` to the provided string.
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 and roll back.
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
 
@@ -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 a `top_level_around_hook`](/reference/configuration#top-level-around-hook).
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
 
@@ -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 `after` hooks are supported. They can receive a block directly, or the symbol name of a local method.
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.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "attachable/base"
4
- require_relative "attachable/steps"
5
- require_relative "attachable/subactions"
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 :top_level_around_hook
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 :called!, :rollback!, :each_pair, to: :context
123
+ delegate :each_pair, to: :context
124
124
 
125
125
  # External interface
126
126
  delegate :success?, :exception, to: :context