axn 0.1.0.pre.alpha.2.6 → 0.1.0.pre.alpha.2.7
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/.rubocop.yml +4 -1
- data/CHANGELOG.md +23 -2
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +140 -19
- data/docs/reference/configuration.md +42 -20
- data/docs/usage/writing.md +1 -1
- data/lib/action/attachable/steps.rb +16 -1
- data/lib/action/configuration.rb +2 -3
- data/lib/action/context.rb +28 -18
- data/lib/action/core/automatic_logging.rb +24 -8
- data/lib/action/core/context/facade.rb +39 -0
- data/lib/action/core/context/facade_inspector.rb +63 -0
- data/lib/action/core/context/internal.rb +38 -0
- data/lib/action/core/contract.rb +25 -8
- data/lib/action/core/contract_for_subfields.rb +1 -1
- data/lib/action/core/contract_validation.rb +15 -4
- data/lib/action/core/flow/callbacks.rb +54 -0
- data/lib/action/core/flow/exception_execution.rb +65 -0
- data/lib/action/core/flow/handlers/base_handler.rb +32 -0
- data/lib/action/core/flow/handlers/callback_handler.rb +21 -0
- data/lib/action/core/flow/handlers/invoker.rb +73 -0
- data/lib/action/core/flow/handlers/matcher.rb +85 -0
- data/lib/action/core/flow/handlers/message_handler.rb +27 -0
- data/lib/action/core/flow/handlers/registry.rb +40 -0
- data/lib/action/core/flow/handlers.rb +17 -0
- data/lib/action/core/flow/messages.rb +75 -0
- data/lib/action/core/flow.rb +19 -0
- data/lib/action/core/hoist_errors.rb +2 -2
- data/lib/action/core/hooks.rb +15 -15
- data/lib/action/core/logging.rb +2 -2
- data/lib/action/core/timing.rb +18 -0
- data/lib/action/core/tracing.rb +17 -0
- data/lib/action/core/validation/fields.rb +2 -0
- data/lib/action/core.rb +25 -78
- data/lib/action/enqueueable/via_sidekiq.rb +2 -2
- data/lib/action/result.rb +114 -0
- data/lib/axn/factory.rb +45 -7
- data/lib/axn/version.rb +1 -1
- metadata +18 -5
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/event_handlers.rb +0 -62
- data/lib/action/core/handle_exceptions.rb +0 -143
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b128603df1eec45048f8656f01508a2e9a706e9acd92ce74813cac0bb038f550
|
4
|
+
data.tar.gz: cfc86ffaadab4cc758546eab39992de2d7f3c89547007a2d88643b303d0edd4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2767343c37a0612a446482b52320d2ab85009af9e4094fffa0be31577bdf4a67e228f08307e31c0d182dbd3b944881f10af3c7e80d19a7d6c6f6f08a33294417
|
7
|
+
data.tar.gz: fb5d3942ecbeb2e52f4f4376b5d1b90db696470704d39d1b2db55e6880956073f12f0213a13ad8a564ee2048ad76350bebb811ff90db125bf9724d2d31d3167a
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,28 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
4
|
-
*
|
3
|
+
## 0.1.0-alpha.2.7
|
4
|
+
* [BREAKING] Replaced `messages` declaration with separate `success` and `error` calls
|
5
|
+
* [BREAKING] Removed `rescues` method (use `error_from` for custom error messages; all exceptions now report to `on_exception` handlers)
|
6
|
+
* [BREAKING] Replaced `error_from` with an optional `if:` argument on `error`
|
7
|
+
* [FEAT] Implemented conditional success message filtering as well
|
8
|
+
* [FEAT] Added block support for `error` and `success`
|
9
|
+
* [FEAT] `if:` now supports symbol predicates referencing instance methods (arity 0, 1, or keyword `exception:`). If the method accepts `exception:` it is passed as a keyword; else if it accepts one positional arg, it is passed positionally; otherwise it is called with no args. If the method is missing, the symbol falls back to constant lookup (e.g., `:ArgumentError`).
|
10
|
+
* [FEAT] `success`/`error` and callbacks now accept symbol method names (e.g., `success :local_method`). Handlers can receive the exception via keyword (`exception:`) or single positional argument; otherwise they are called with no args.
|
11
|
+
* [BREAKING] Updated callback methods (`on_success`, `on_error`, `on_failure`, `on_exception`) to use consistent `if:` interface (matching messages)
|
12
|
+
* [FEAT] Added `unless:` support to both `success`/`error` messages and callbacks (`on_success`, `on_error`, `on_failure`, `on_exception`)
|
13
|
+
|
14
|
+
## 0.1.0-alpha.2.6.1
|
15
|
+
* [FEAT] Added `elapsed_time` and `outcome` methods to `Action::Result`
|
16
|
+
* `elapsed_time` returns execution time in milliseconds (Float)
|
17
|
+
* `outcome` returns execution outcome as symbol (`:success`, `:failure`, or `:exception`)
|
18
|
+
* [BREAKING] `emit_metrics` hook now receives the full `Action::Result` object instead of just the outcome
|
19
|
+
* Provides access to both outcome and elapsed time for richer metrics
|
20
|
+
* Example: `proc { |resource, result| TS::Metrics.histogram("action.duration", result.elapsed_time) }`
|
21
|
+
* [BREAKING] Replaced `Action.config.default_log_level` and `default_autolog_level` with simpler `log_level`
|
22
|
+
* [BREAKING] `autolog_level` method overrides with e.g. `auto_log :warn` or `auto_log false`
|
23
|
+
* [BREAKING] Direct access to exposed fields in callables no longer works -- `foo` becomes `result.foo`
|
24
|
+
* [BREAKING] Removed `success?` check on Action::Result (use `ok?` instead)
|
25
|
+
* [FEAT] Added callback and strategy support to Axn::Factory.build
|
5
26
|
|
6
27
|
## 0.1.0-alpha.2.6
|
7
28
|
* Inline interactor code (no more dependency on unpublished forked branch to support inheritance)
|
@@ -9,6 +9,8 @@ Every `call` invocation on an Action will return an `Action::Result` instance, w
|
|
9
9
|
| `success` | User-facing success message (string), if `ok?` (else nil)
|
10
10
|
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
11
11
|
| `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
|
12
|
+
| `outcome` | The execution outcome as a symbol (`:success`, `:failure`, or `:exception`)
|
13
|
+
| `elapsed_time` | Execution time in milliseconds (Float)
|
12
14
|
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
13
15
|
|
14
16
|
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `messages` declaration](/reference/class#messages).
|
data/docs/reference/class.md
CHANGED
@@ -85,40 +85,148 @@ expects :date, type: Date, preprocess: ->(d) { d.is_a?(Date) ? d : Date.parse(d)
|
|
85
85
|
|
86
86
|
will succeed if given _either_ an actual Date object _or_ a string that Date.parse can convert into one. If the preprocess callable raises an exception, that'll be swallowed and the action failed.
|
87
87
|
|
88
|
-
## `.
|
88
|
+
## `.success` and `.error`
|
89
89
|
|
90
|
-
The `
|
90
|
+
The `success` and `error` declarations allow you to customize the `error` and `success` messages on the returned result.
|
91
91
|
|
92
|
-
|
92
|
+
Both methods accept a string (returned directly), a symbol (resolved as a local instance method on the action), or a block (evaluated in the action's context, so can access instance methods and variables).
|
93
|
+
|
94
|
+
When an exception is available (e.g., during `error`), handlers can receive it in either of two equivalent ways:
|
95
|
+
- Keyword form: accept `exception:` and it will be passed as a keyword
|
96
|
+
- Positional form: if the handler accepts a single positional argument, it will be passed positionally
|
97
|
+
|
98
|
+
This applies to both blocks and symbol-backed instance methods. Choose the style that best fits your codebase (clarity vs concision).
|
99
|
+
|
100
|
+
In callables and symbol-backed methods, you can access:
|
101
|
+
- **Input data**: Use field names directly (e.g., `name`)
|
102
|
+
- **Output data**: Use `result.field` pattern (e.g., `result.greeting`)
|
103
|
+
- **Instance methods and variables**: Direct access
|
93
104
|
|
94
105
|
```ruby
|
95
|
-
|
106
|
+
success { "Hello #{name}, your greeting: #{result.greeting}" }
|
107
|
+
error { |e| "Bad news: #{e.message}" }
|
108
|
+
error { |exception:| "Bad news: #{exception.message}" }
|
109
|
+
|
110
|
+
# Using symbol method names
|
111
|
+
success :build_success_message
|
112
|
+
error :build_error_message
|
113
|
+
|
114
|
+
def build_success_message
|
115
|
+
"Hello #{name}, your greeting: #{result.greeting}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def build_error_message(e)
|
119
|
+
"Bad news: #{e.message}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_error_message(exception:)
|
123
|
+
"Bad news: #{exception.message}"
|
124
|
+
end
|
96
125
|
```
|
97
126
|
|
98
|
-
##
|
127
|
+
## Conditional messages
|
128
|
+
|
129
|
+
While `.error` and `.success` set the default messages, you can register conditional messages using an optional `if:` or `unless:` matcher. The matcher can be:
|
99
130
|
|
100
|
-
|
131
|
+
- an exception class (e.g., `ArgumentError`)
|
132
|
+
- a class name string (e.g., `"Action::InboundValidationError"`)
|
133
|
+
- a symbol referencing a local instance method predicate (arity 0 or 1, or keyword `exception:`), e.g. `:bad_input?`
|
134
|
+
- a callable (arity 0 or 1, or keyword `exception:`)
|
101
135
|
|
102
|
-
|
136
|
+
Symbols are resolved as methods on the action instance. If the method accepts `exception:` it will be passed as a keyword; otherwise, if it accepts one positional argument, the raised exception is passed positionally; otherwise it is called with no arguments. If the action does not respond to the symbol, we fall back to constant lookup (e.g., `if: :ArgumentError` behaves like `if: ArgumentError`). Symbols are also supported for the message itself (e.g., `success :method_name`), resolved via the same rules.
|
103
137
|
|
104
138
|
```ruby
|
105
|
-
|
139
|
+
error "bad"
|
106
140
|
|
107
|
-
#
|
108
|
-
|
141
|
+
# Custom message with exception class matcher
|
142
|
+
error "Invalid params provided", if: ActiveRecord::InvalidRecord
|
109
143
|
|
110
|
-
#
|
111
|
-
|
112
|
-
|
144
|
+
# Custom message with callable matcher and message
|
145
|
+
error(if: ArgumentError) { |e| "Argument error: #{e.message}" }
|
146
|
+
error(if: -> { name == "bad" }) { "Bad input #{name}, result: #{result.status}" }
|
147
|
+
|
148
|
+
# Custom message with symbol predicate (arity 0)
|
149
|
+
error "Transient error, please retry", if: :transient_error?
|
150
|
+
|
151
|
+
def transient_error?
|
152
|
+
# local decision based on inputs/outputs
|
153
|
+
name == "temporary"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Symbol predicate (arity 1), receives the exception
|
157
|
+
error(if: :argument_error?) { |e| "Bad argument: #{e.message}" }
|
158
|
+
|
159
|
+
def argument_error?(e)
|
160
|
+
e.is_a?(ArgumentError)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Symbol predicate (keyword), receives the exception via keyword
|
164
|
+
error(if: :argument_error_kw?) { |exception:| "Bad argument: #{exception.message}" }
|
165
|
+
|
166
|
+
def argument_error_kw?(exception:)
|
167
|
+
exception.is_a?(ArgumentError)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Lambda predicate with keyword
|
171
|
+
error "AE", if: ->(exception:) { exception.is_a?(ArgumentError) }
|
172
|
+
|
173
|
+
# Using unless: for inverse logic
|
174
|
+
error "Custom error", unless: :should_skip?
|
175
|
+
|
176
|
+
def should_skip?
|
177
|
+
# local decision based on inputs/outputs
|
178
|
+
name == "temporary"
|
179
|
+
end
|
180
|
+
|
181
|
+
::: warning
|
182
|
+
You cannot use both `if:` and `unless:` for the same message - this will raise an `ArgumentError`.
|
183
|
+
:::
|
184
|
+
|
185
|
+
### Message ordering and inheritance
|
186
|
+
|
187
|
+
Messages are evaluated in **last-defined-first** order, meaning the most recently defined message that matches its conditions will be used. This applies to both success and error messages:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
class ParentAction
|
191
|
+
include Action
|
192
|
+
|
193
|
+
success "Parent success message"
|
194
|
+
error "Parent error message"
|
195
|
+
end
|
196
|
+
|
197
|
+
class ChildAction < ParentAction
|
198
|
+
success "Child success message" # This will be used when action succeeds
|
199
|
+
error "Child error message" # This will be used when action fails
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Within a single class, later definitions override earlier ones:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class MyAction
|
207
|
+
include Action
|
208
|
+
|
209
|
+
success "First success message" # Ignored
|
210
|
+
success "Second success message" # Ignored
|
211
|
+
success "Final success message" # This will be used
|
212
|
+
|
213
|
+
error "First error message" # Ignored
|
214
|
+
error "Second error message" # Ignored
|
215
|
+
error "Final error message" # This will be used
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
When using conditional messages, the system evaluates handlers in the order defined above until it finds one that matches and doesn't raise an exception. If a handler raises an exception, it falls back to the next matching handler, then to static messages, and finally to the default message.
|
113
220
|
```
|
114
221
|
|
115
222
|
## Callbacks
|
116
223
|
|
117
224
|
In addition to the [global exception handler](/reference/configuration#on-exception), 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.
|
118
225
|
|
119
|
-
:::
|
120
|
-
*
|
121
|
-
|
226
|
+
::: tip Callback Ordering
|
227
|
+
* Callbacks are executed in **last-defined-first** order, similar to messages
|
228
|
+
* Child class callbacks execute before parent class callbacks
|
229
|
+
* Multiple matching callbacks of the same type will *all* execute
|
122
230
|
:::
|
123
231
|
|
124
232
|
|
@@ -153,17 +261,30 @@ class Foo
|
|
153
261
|
end
|
154
262
|
```
|
155
263
|
|
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
|
264
|
+
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 conditional messages (`if:` or `unless:`):
|
157
265
|
|
158
266
|
```ruby
|
159
267
|
class Foo
|
160
268
|
include Action
|
161
269
|
|
162
|
-
on_exception NoMethodError do |exception| # [!code focus]
|
270
|
+
on_exception(if: NoMethodError) do |exception| # [!code focus]
|
163
271
|
# e.g. trigger a slack error
|
164
272
|
end
|
165
273
|
|
166
|
-
|
274
|
+
on_exception(unless: :transient_error?) do |exception| # [!code focus]
|
275
|
+
# e.g. trigger a slack error for non-transient errors
|
276
|
+
end
|
277
|
+
|
278
|
+
def transient_error?
|
279
|
+
# local decision based on inputs/outputs
|
280
|
+
name == "temporary"
|
281
|
+
end
|
282
|
+
|
283
|
+
::: warning
|
284
|
+
You cannot use both `if:` and `unless:` for the same callback - this will raise an `ArgumentError`.
|
285
|
+
:::
|
286
|
+
|
287
|
+
on_exception(if: ->(e) { e.is_a?(ZeroDivisionError) }) do # [!code focus]
|
167
288
|
# e.g. trigger a slack error
|
168
289
|
end
|
169
290
|
end
|
@@ -4,18 +4,16 @@ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call
|
|
4
4
|
|
5
5
|
|
6
6
|
```ruby
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
c.default_autolog_level = :debug
|
16
|
-
|
17
|
-
c.logger = ...
|
7
|
+
Action.configure do |c|
|
8
|
+
c.log_level = :info
|
9
|
+
c.logger = ...
|
10
|
+
c.on_exception = proc do |e, action:, context:|
|
11
|
+
message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
|
12
|
+
|
13
|
+
Rails.logger.warn(message)
|
14
|
+
Honeybadger.notify(message, context: { axn_context: context })
|
18
15
|
end
|
16
|
+
end
|
19
17
|
```
|
20
18
|
|
21
19
|
## `on_exception`
|
@@ -68,7 +66,7 @@ If you're using an APM provider, observability can be greatly enhanced by adding
|
|
68
66
|
The framework provides two distinct hooks for observability:
|
69
67
|
|
70
68
|
- **`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
|
69
|
+
- **`emit_metrics`**: A post-execution hook that receives the action result. Do NOT call any blocks.
|
72
70
|
|
73
71
|
For example, to wire up Datadog:
|
74
72
|
|
@@ -80,8 +78,9 @@ For example, to wire up Datadog:
|
|
80
78
|
end
|
81
79
|
end
|
82
80
|
|
83
|
-
c.emit_metrics = proc do |resource,
|
84
|
-
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome
|
81
|
+
c.emit_metrics = proc do |resource, result|
|
82
|
+
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome, resource: })
|
83
|
+
TS::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
85
84
|
end
|
86
85
|
end
|
87
86
|
```
|
@@ -89,9 +88,9 @@ For example, to wire up Datadog:
|
|
89
88
|
A couple notes:
|
90
89
|
|
91
90
|
* `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
|
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`)
|
91
|
+
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`success`, `failure`, `exception`) and elapsed time of the action.
|
93
92
|
* 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
|
93
|
+
* The `emit_metrics` hook is called after execution with the result - do not call any blocks
|
95
94
|
|
96
95
|
|
97
96
|
## `logger`
|
@@ -112,11 +111,11 @@ For example:
|
|
112
111
|
|
113
112
|
For a practical example of this in practice, see [our 'memoization' recipe](/recipes/memoization).
|
114
113
|
|
115
|
-
## `
|
114
|
+
## `log_level`
|
116
115
|
|
117
|
-
Sets the log level used when you call `log "Some message"` in your Action. Note this is read via a `
|
116
|
+
Sets the log level used when you call `log "Some message"` in your Action. Note this is read via a `log_level` class method, so you can easily use inheritance to support different log levels for different sets of actions.
|
118
117
|
|
119
|
-
##
|
118
|
+
## Automatic Logging
|
120
119
|
|
121
120
|
By default, every `action.call` will emit log lines when it is called and after it completes:
|
122
121
|
|
@@ -125,4 +124,27 @@ By default, every `action.call` will emit log lines when it is called and after
|
|
125
124
|
[YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
|
126
125
|
```
|
127
126
|
|
128
|
-
|
127
|
+
Automatic logging will log at `Action.config.log_level` by default, but can be overridden or disabled using the declarative `auto_log` method:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# Set default for all actions (affects both explicit logging and automatic logging)
|
131
|
+
Action.configure do |c|
|
132
|
+
c.log_level = :debug
|
133
|
+
end
|
134
|
+
|
135
|
+
# Override for specific actions
|
136
|
+
class MyAction
|
137
|
+
auto_log :warn # Use warn level for this action
|
138
|
+
end
|
139
|
+
|
140
|
+
class SilentAction
|
141
|
+
auto_log false # Disable automatic logging for this action
|
142
|
+
end
|
143
|
+
|
144
|
+
# Use default level (no auto_log call needed)
|
145
|
+
class DefaultAction
|
146
|
+
# Uses Action.config.log_level
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
|
data/docs/usage/writing.md
CHANGED
@@ -79,7 +79,7 @@ class Foo
|
|
79
79
|
expects :name, type: String
|
80
80
|
exposes :meaning_of_life
|
81
81
|
|
82
|
-
messages success: -> { "Revealed
|
82
|
+
messages success: -> { "Revealed to #{name}: #{result.meaning_of_life}" }, # [!code focus:2]
|
83
83
|
error: ->(e) { "No secret of life for you: #{e.message}" }
|
84
84
|
|
85
85
|
def call
|
@@ -38,10 +38,25 @@ module Action
|
|
38
38
|
step = Entry.new(label: "Step #{idx + 1}", axn: step) if step.is_a?(Class)
|
39
39
|
|
40
40
|
hoist_errors(prefix: "#{step.label} step") do
|
41
|
-
step.axn.call(
|
41
|
+
step.axn.call(**merged_context_data).tap do |step_result|
|
42
|
+
merge_step_exposures!(step_result)
|
43
|
+
end
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def merged_context_data
|
51
|
+
@__context.__combined_data
|
52
|
+
end
|
53
|
+
|
54
|
+
# Each step can expect the data exposed from the previous steps
|
55
|
+
def merge_step_exposures!(step_result)
|
56
|
+
step_result.declared_fields.each do |field|
|
57
|
+
@__context.exposed_data[field] = step_result.public_send(field)
|
58
|
+
end
|
59
|
+
end
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end
|
data/lib/action/configuration.rb
CHANGED
@@ -3,10 +3,9 @@
|
|
3
3
|
module Action
|
4
4
|
class Configuration
|
5
5
|
attr_accessor :wrap_with_trace, :emit_metrics
|
6
|
-
attr_writer :logger, :env, :on_exception, :additional_includes, :
|
6
|
+
attr_writer :logger, :env, :on_exception, :additional_includes, :log_level
|
7
7
|
|
8
|
-
def
|
9
|
-
def default_autolog_level = @default_autolog_level ||= :info
|
8
|
+
def log_level = @log_level ||= :info
|
10
9
|
|
11
10
|
def additional_includes = @additional_includes ||= []
|
12
11
|
|
data/lib/action/context.rb
CHANGED
@@ -1,28 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
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
3
|
module Action
|
8
|
-
class Context
|
9
|
-
|
10
|
-
self === context ? context : new(context)
|
11
|
-
end
|
4
|
+
class Context
|
5
|
+
attr_accessor :provided_data, :exposed_data
|
12
6
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
7
|
+
def initialize(**provided_data)
|
8
|
+
@provided_data = provided_data
|
9
|
+
@exposed_data = {}
|
16
10
|
|
17
|
-
|
18
|
-
@failure
|
11
|
+
# Framework-managed fields
|
12
|
+
@failure = false
|
13
|
+
@exception = nil
|
14
|
+
@error_from_user = nil
|
15
|
+
@error_prefix = nil
|
16
|
+
@elapsed_time = nil
|
19
17
|
end
|
20
18
|
|
21
|
-
def fail!(
|
22
|
-
|
23
|
-
|
24
|
-
raise Action::Failure, self
|
19
|
+
def fail!(message = nil)
|
20
|
+
@error_from_user = message if message.present?
|
21
|
+
raise Action::Failure, message
|
25
22
|
end
|
23
|
+
|
24
|
+
# INTERNAL: base for further filtering (for logging) or providing user with usage hints
|
25
|
+
def __combined_data = @provided_data.merge(@exposed_data)
|
26
|
+
|
27
|
+
# Framework state methods
|
28
|
+
def ok? = !@failure
|
29
|
+
def failed? = @failure || false
|
30
|
+
|
31
|
+
# Framework field accessors
|
32
|
+
attr_accessor :exception, :error_from_user, :error_prefix, :elapsed_time
|
33
|
+
|
34
|
+
# Internal failure state setter (for framework use)
|
35
|
+
attr_writer :failure
|
36
|
+
private :failure=
|
26
37
|
end
|
27
38
|
end
|
28
|
-
# rubocop:enable Style/OpenStructUse, Style/CaseEquality
|
@@ -7,19 +7,34 @@ module Action
|
|
7
7
|
base.class_eval do
|
8
8
|
extend ClassMethods
|
9
9
|
include InstanceMethods
|
10
|
+
|
11
|
+
# Single class_attribute - nil means disabled, any level means enabled
|
12
|
+
class_attribute :auto_log_level, default: Action.config.log_level
|
10
13
|
end
|
11
14
|
end
|
12
15
|
|
13
16
|
module ClassMethods
|
14
|
-
def
|
17
|
+
def auto_log(level)
|
18
|
+
self.auto_log_level = level.presence
|
19
|
+
end
|
15
20
|
end
|
16
21
|
|
17
22
|
module InstanceMethods
|
18
23
|
private
|
19
24
|
|
25
|
+
def _with_logging
|
26
|
+
_log_before if self.class.auto_log_level
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
_log_after if self.class.auto_log_level
|
30
|
+
end
|
31
|
+
|
20
32
|
def _log_before
|
21
|
-
|
22
|
-
|
33
|
+
level = self.class.auto_log_level
|
34
|
+
return unless level
|
35
|
+
|
36
|
+
self.class.public_send(
|
37
|
+
level,
|
23
38
|
[
|
24
39
|
"About to execute",
|
25
40
|
_log_context(:inbound),
|
@@ -30,13 +45,14 @@ module Action
|
|
30
45
|
Axn::Util.piping_error("logging before hook", action: self, exception: e)
|
31
46
|
end
|
32
47
|
|
33
|
-
def _log_after
|
34
|
-
|
48
|
+
def _log_after
|
49
|
+
level = self.class.auto_log_level
|
50
|
+
return unless level
|
35
51
|
|
36
|
-
public_send(
|
37
|
-
|
52
|
+
self.class.public_send(
|
53
|
+
level,
|
38
54
|
[
|
39
|
-
"Execution completed (with outcome: #{outcome}) in #{
|
55
|
+
"Execution completed (with outcome: #{result.outcome}) in #{result.elapsed_time} milliseconds",
|
40
56
|
_log_context(:outbound),
|
41
57
|
].compact.join(". Set: "),
|
42
58
|
after: Action.config.env.production? ? nil : "\n------\n",
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/parameter_filter"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
class ContextFacade
|
7
|
+
def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
|
8
|
+
if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
|
9
|
+
raise "Action::ContextFacade is an abstract class and should not be instantiated directly"
|
10
|
+
end
|
11
|
+
|
12
|
+
@context = context
|
13
|
+
@action = action
|
14
|
+
@declared_fields = declared_fields
|
15
|
+
|
16
|
+
(@declared_fields + Array(implicitly_allowed_fields)).each do |field|
|
17
|
+
singleton_class.define_method(field) do
|
18
|
+
context_data_source[field]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :declared_fields
|
24
|
+
|
25
|
+
def inspect = ContextFacadeInspector.new(facade: self, action:, context:).call
|
26
|
+
|
27
|
+
def fail!(...)
|
28
|
+
raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :action, :context
|
34
|
+
|
35
|
+
def action_name = @action.class.name.presence || "The action"
|
36
|
+
|
37
|
+
def context_data_source = raise NotImplementedError
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class ContextFacadeInspector
|
5
|
+
def initialize(action:, facade:, context:)
|
6
|
+
@action = action
|
7
|
+
@facade = facade
|
8
|
+
@context = context
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
str = [status, visible_fields].compact_blank.join(" ")
|
13
|
+
|
14
|
+
"#<#{class_name} #{str}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :action, :facade, :context
|
20
|
+
|
21
|
+
def status
|
22
|
+
return unless facade.is_a?(Action::Result)
|
23
|
+
|
24
|
+
return "[OK]" if context.ok?
|
25
|
+
unless context.exception
|
26
|
+
return context.error_from_user.present? ? "[failed with '#{context.error_from_user}']" : "[failed]"
|
27
|
+
end
|
28
|
+
|
29
|
+
%([failed with #{context.exception.class.name}: '#{context.exception.message}'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def visible_fields
|
33
|
+
declared_fields.map do |field|
|
34
|
+
value = facade.public_send(field)
|
35
|
+
|
36
|
+
"#{field}: #{format_for_inspect(field, value)}"
|
37
|
+
end.join(", ")
|
38
|
+
end
|
39
|
+
|
40
|
+
def class_name = facade.class.name
|
41
|
+
def declared_fields = facade.send(:declared_fields)
|
42
|
+
|
43
|
+
def format_for_inspect(field, value)
|
44
|
+
return value.inspect if value.nil?
|
45
|
+
|
46
|
+
# Initially based on https://github.com/rails/rails/blob/800976975253be2912d09a80757ee70a2bb1e984/activerecord/lib/active_record/attribute_methods.rb#L527
|
47
|
+
inspected_value = if value.is_a?(String) && value.length > 50
|
48
|
+
"#{value[0, 50]}...".inspect
|
49
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
50
|
+
%("#{value.to_fs(:inspect)}")
|
51
|
+
elsif defined?(::ActiveRecord::Relation) && value.instance_of?(::ActiveRecord::Relation)
|
52
|
+
# Avoid hydrating full AR relation (i.e. avoid loading records just to report an error)
|
53
|
+
"#{value.name}::ActiveRecord_Relation"
|
54
|
+
else
|
55
|
+
value.inspect
|
56
|
+
end
|
57
|
+
|
58
|
+
inspection_filter.filter_param(field, inspected_value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspection_filter = action.send(:inspection_filter)
|
62
|
+
end
|
63
|
+
end
|