axn 0.1.0.pre.alpha.1.1 → 0.1.0.pre.alpha.2
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 +1 -1
- data/.tool-versions +1 -0
- data/CHANGELOG.md +10 -2
- data/CONTRIBUTING.md +1 -1
- data/README.md +1 -1
- data/docs/.vitepress/config.mjs +18 -10
- data/docs/advanced/rough.md +2 -0
- data/docs/index.md +11 -3
- data/docs/{guide/index.md → intro/overview.md} +11 -32
- data/docs/recipes/memoization.md +46 -0
- data/docs/{usage → recipes}/testing.md +4 -2
- data/docs/reference/action-result.md +32 -9
- data/docs/reference/class.md +69 -12
- data/docs/reference/configuration.md +28 -15
- data/docs/reference/instance.md +96 -13
- data/docs/usage/setup.md +0 -2
- data/docs/usage/using.md +7 -15
- data/docs/usage/writing.md +45 -6
- data/lib/action/attachable/base.rb +43 -0
- data/lib/action/attachable/steps.rb +47 -0
- data/lib/action/attachable/subactions.rb +43 -0
- data/lib/action/attachable.rb +17 -0
- data/lib/action/{configuration.rb → core/configuration.rb} +1 -1
- data/lib/action/{context_facade.rb → core/context_facade.rb} +18 -28
- data/lib/action/{contract.rb → core/contract.rb} +10 -2
- data/lib/action/{exceptions.rb → core/exceptions.rb} +11 -0
- data/lib/action/{hoist_errors.rb → core/hoist_errors.rb} +3 -2
- data/lib/action/{swallow_exceptions.rb → core/swallow_exceptions.rb} +52 -7
- data/lib/axn/factory.rb +102 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +20 -10
- metadata +28 -22
- data/lib/action/organizer.rb +0 -41
- /data/docs/{usage → advanced}/conventions.md +0 -0
- /data/docs/{about/index.md → intro/about.md} +0 -0
- /data/docs/{advanced → recipes}/validating-user-input.md +0 -0
- /data/lib/action/{contract_validator.rb → core/contract_validator.rb} +0 -0
- /data/lib/action/{enqueueable.rb → core/enqueueable.rb} +0 -0
- /data/lib/action/{logging.rb → core/logging.rb} +0 -0
- /data/lib/action/{top_level_around_hook.rb → core/top_level_around_hook.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6ba4df4f3ce11e1a31eb8dfadde856141ed9dc6d10002efe1b544abcb580431
|
4
|
+
data.tar.gz: 0a27844c8b7c341315c8a1d9212fa0745866086cd3fbb65140b813a125ac8496
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 250b751b9a583d7206c9ada4e162ce8d2ee96816466515812e187e3dcdeb50f7024a595968c4e42f3f91a63909a38074e59b738a4f0216a0aaf1f15154e99452
|
7
|
+
data.tar.gz: cba3bee954fb153a99a65ff30478ae1d048fd49fe7834d4ba2446e4ff5e6ff70b3cfe588bc20a5d042665aa5bd9797cc4df10a4483cd59ce5b5d99b85bc4f645
|
data/.rubocop.yml
CHANGED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.3.6
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
3
|
## UNRELEASED
|
4
|
-
|
5
4
|
* N/A
|
6
5
|
|
6
|
+
|
7
|
+
## 0.1.0-alpha.2
|
8
|
+
* Renamed `.rescues` to `.error_from`
|
9
|
+
* Implemented new `.rescues` (like `.error_from`, but avoids triggering on_exception handler)
|
10
|
+
* Prevented `expect`ing and `expose`ing fields that conflict with internal method names
|
11
|
+
* [Action::Result] Removed `failure?` from public interface (prefer negating `ok?`)
|
12
|
+
* Reorganized internals into Core vs (newly added) Attachable
|
13
|
+
* Add some meta-functionality: Axn::Factory + Attachable (subactions + steps support)
|
14
|
+
|
7
15
|
## 0.1.0-alpha.1.1
|
8
|
-
*
|
16
|
+
* Reverted `gets` / `sets` back to `expects` / `exposes`
|
9
17
|
|
10
18
|
## 0.1.0-alpha.1
|
11
19
|
* Initial implementation
|
data/CONTRIBUTING.md
CHANGED
@@ -23,7 +23,7 @@ For the best chance of having your changes merged, please:
|
|
23
23
|
|
24
24
|
## Bug Reports
|
25
25
|
|
26
|
-
If you are experiencing unexpected behavior and, after having read [our documentation](https://teamshares.github.io/axn/
|
26
|
+
If you are experiencing unexpected behavior and, after having read [our documentation](https://teamshares.github.io/axn/), are convinced this behavior is a bug, please:
|
27
27
|
|
28
28
|
1. [Search](https://github.com/teamshares/axn/issues) existing issues.
|
29
29
|
2. Collect enough information to reproduce the issue:
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Just spinning this up -- not yet released (i.e. doc updates in flight).
|
|
4
4
|
|
5
5
|
## Installation & Usage
|
6
6
|
|
7
|
-
See our [User Guide](https://teamshares.github.io/axn/
|
7
|
+
See our [User Guide](https://teamshares.github.io/axn/) for details.
|
8
8
|
|
9
9
|
## [!!] Inheritance Support
|
10
10
|
|
data/docs/.vitepress/config.mjs
CHANGED
@@ -9,29 +9,29 @@ export default defineConfig({
|
|
9
9
|
// https://vitepress.dev/reference/default-theme-config
|
10
10
|
nav: [
|
11
11
|
{ text: 'Home', link: '/' },
|
12
|
-
{ text: '
|
12
|
+
{ text: 'Overview', link: '/intro/overview' },
|
13
|
+
{ text: 'Guide', link: '/usage/setup' },
|
14
|
+
{ text: 'Reference', link: '/reference/configuration' }
|
13
15
|
],
|
14
16
|
|
15
17
|
sidebar: [
|
16
18
|
{
|
17
19
|
text: 'Introduction',
|
18
20
|
items: [
|
19
|
-
{ text: 'About', link: '/about
|
20
|
-
{ text: '
|
21
|
+
{ text: 'About', link: '/intro/about' },
|
22
|
+
{ text: 'Overview', link: '/intro/overview' },
|
21
23
|
]
|
22
24
|
},
|
23
25
|
{
|
24
|
-
text: '
|
26
|
+
text: 'Usage Guide',
|
25
27
|
items: [
|
26
|
-
{ text: '
|
28
|
+
{ text: 'Getting Started', link: '/usage/setup' },
|
27
29
|
{ text: 'Writing Actions', link: '/usage/writing' },
|
28
30
|
{ text: 'Using Actions', link: '/usage/using' },
|
29
|
-
{ text: 'Testing Actions', link: '/usage/testing' },
|
30
|
-
{ text: 'Conventions', link: '/usage/conventions' },
|
31
31
|
]
|
32
32
|
},
|
33
33
|
{
|
34
|
-
text: 'Reference',
|
34
|
+
text: 'DSL Reference',
|
35
35
|
items: [
|
36
36
|
{ text: 'Configuration', link: '/reference/configuration' },
|
37
37
|
{ text: 'Class Interface', link: '/reference/class' },
|
@@ -40,10 +40,18 @@ export default defineConfig({
|
|
40
40
|
]
|
41
41
|
},
|
42
42
|
{
|
43
|
-
text: '
|
43
|
+
text: 'Recipes',
|
44
|
+
items: [
|
45
|
+
{ text: 'Memoization', link: '/recipes/memoization' },
|
46
|
+
{ text: 'Validating User Input', link: '/recipes/validating-user-input' },
|
47
|
+
{ text: 'Testing Actions', link: '/recipes/testing' },
|
48
|
+
]
|
49
|
+
},
|
50
|
+
{
|
51
|
+
text: 'Additional Notes',
|
44
52
|
items: [
|
45
53
|
{ text: 'ROUGH NOTES', link: '/advanced/rough' },
|
46
|
-
{ text: '
|
54
|
+
{ text: 'Conventions', link: '/advanced/conventions' },
|
47
55
|
]
|
48
56
|
},
|
49
57
|
],
|
data/docs/advanced/rough.md
CHANGED
@@ -6,6 +6,8 @@
|
|
6
6
|
|
7
7
|
* General note: the inbound/outbound contexts are views into an underlying shared object (passed down through organize calls) -- modifications of one will affect the other (e.g. preprocessing inbound args implicitly transforms them on the underlying context, which is echoed if you also expose it on outbound).
|
8
8
|
|
9
|
+
* `context_for_logging` (and decent #inspect support)
|
10
|
+
|
9
11
|
* Configuring logging (will default to Rails.logger if available, else fall back to basic Logger (but can explicitly set via e.g. `Action.config.logger = Logger.new($stdout`))
|
10
12
|
|
11
13
|
* Note `context_for_logging` is available (filtered to accessible attrs, filtering out sensitive values). Automatically passed into `on_exception` hook.
|
data/docs/index.md
CHANGED
@@ -8,8 +8,14 @@ hero:
|
|
8
8
|
tagline: "**ALPHA release -- everything subject to change**"
|
9
9
|
actions:
|
10
10
|
- theme: brand
|
11
|
-
text:
|
12
|
-
link: /
|
11
|
+
text: Overview
|
12
|
+
link: /intro/overview
|
13
|
+
- theme: alt
|
14
|
+
text: Usage
|
15
|
+
link: /usage/setup
|
16
|
+
- theme: alt
|
17
|
+
text: DSL Reference
|
18
|
+
link: /reference/class
|
13
19
|
# - theme: alt
|
14
20
|
# text: API Examples
|
15
21
|
# link: /api-examples
|
@@ -23,4 +29,6 @@ features:
|
|
23
29
|
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
24
30
|
---
|
25
31
|
|
26
|
-
|
32
|
+
::: danger ALPHA RELEASE
|
33
|
+
Axn is used in production at [Teamshares](https://teamshares.com/), but is still in alpha and is undergoing active development.
|
34
|
+
:::
|
@@ -8,7 +8,7 @@ This library provides a set of conventions for writing business logic in Rails (
|
|
8
8
|
|
9
9
|
* Clear calling semantics: `Foo.call`
|
10
10
|
* A declarative interface
|
11
|
-
* A [consistent return interface](
|
11
|
+
* A [consistent return interface](/intro/overview#return-interface)
|
12
12
|
* Exception swallowing + clear distinction between internal and user-facing errors
|
13
13
|
|
14
14
|
### Minimal example
|
@@ -27,8 +27,6 @@ end
|
|
27
27
|
|
28
28
|
## Inputs and Outflows
|
29
29
|
|
30
|
-
### Overview
|
31
|
-
|
32
30
|
Most actions require inputs, and many return values to the caller; no need for any `def initialize` boilerplate, just add:
|
33
31
|
|
34
32
|
* `expects :foo` to declare inputs the class expects to receive.
|
@@ -43,25 +41,14 @@ Most actions require inputs, and many return values to the caller; no need for a
|
|
43
41
|
By design you _cannot access anything you do not explicitly `expose` from outside the action itself_. Making the external interface explicit helps maintainability by ensuring you can refactor internals without breaking existing callsites.
|
44
42
|
:::
|
45
43
|
|
46
|
-
### Details
|
47
|
-
Both `expects` and `exposes` support a variety of options:
|
48
|
-
|
49
|
-
| Option | Example (same for `exposes`) | Meaning |
|
50
|
-
| -- | -- | -- |
|
51
|
-
| `sensitive` | `expects :password, sensitive: true` | Filters the fields value when logging, reporting errors, or calling `inspect`
|
52
|
-
| `default` | `expects :foo, default: 123` | If `foo` isn't provided, it'll default to this value
|
53
|
-
| `allow_blank` | `expects :foo, allow_blank: true` | Don't fail if the value is blank
|
54
|
-
| `type` | `expects :foo, type: String` | Custom type validation -- fail unless `foo.is_a?(String)`
|
55
|
-
| anything else | `expects :foo, inclusion: { in: [:apple, :peach] }` | Any other arguments will be processed [as ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html) (i.e. as if passed to `validate :foo, <...>` on an ActiveRecord model)
|
56
|
-
|
57
44
|
::: warning
|
58
|
-
The declarative interface (`expects` and `exposes`) constitutes a contract you are making _with yourself_ (and your fellow developers). **This is _not_ for validating user input** -- [there's a Form Object pattern for that](/
|
45
|
+
The declarative interface (`expects` and `exposes`) constitutes a contract you are making _with yourself_ (and your fellow developers). **This is _not_ for validating user input** -- [there's a Form Object pattern for that](/recipes/validating-user-input).
|
59
46
|
:::
|
60
47
|
|
61
|
-
If any expectations
|
48
|
+
If any declared expectations or exposures are _not_ met the action will fail, setting `error` to a generic error message (because a failed validation means _you_ called _your own_ service wrong; there's nothing the end user can do about that).
|
62
49
|
|
63
50
|
|
64
|
-
###
|
51
|
+
### Example
|
65
52
|
|
66
53
|
```ruby
|
67
54
|
class Actions::Slack::Post
|
@@ -88,23 +75,16 @@ end
|
|
88
75
|
|
89
76
|
## Return interface {#return-interface}
|
90
77
|
|
91
|
-
### Overview
|
92
78
|
|
93
79
|
The return value of an Action call is always an `Action::Result`, which provides a consistent interface:
|
94
80
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
| 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)
|
81
|
+
* `ok?` will return a boolean (false if any errors or exceptions occurred, otherwise true)
|
82
|
+
* if OK, `success` will return a string that is _safe to show end users_
|
83
|
+
* if _not_ OK, `error` will return an error string that is _safe to show end users_
|
84
|
+
* `message` is a helper to return the relevant message in either case (defined as `ok? ? success : error`)
|
100
85
|
|
101
|
-
### Details
|
102
|
-
|
103
|
-
::: danger ALPHA
|
104
|
-
* TODO: link to a reference page for the full interface.
|
105
|
-
:::
|
106
86
|
|
107
|
-
###
|
87
|
+
### Example
|
108
88
|
|
109
89
|
This interface yields a common usage pattern:
|
110
90
|
|
@@ -139,7 +119,7 @@ Note this simple pattern handles multiple levels of "failure" ([details below](#
|
|
139
119
|
::: tip BIG IDEA
|
140
120
|
By design, `result.error` is always safe to show to the user.
|
141
121
|
|
142
|
-
|
122
|
+
Calling code _usually_ only cares about `ok?` and `error` -- no complex error handling needed. :star_struck:
|
143
123
|
:::
|
144
124
|
|
145
125
|
|
@@ -151,7 +131,6 @@ For _known_ failure modes, you can call `fail!("Some user-facing explanation")`
|
|
151
131
|
|
152
132
|
### Internal errors (uncaught `raise`)
|
153
133
|
|
154
|
-
Any exceptions will be swallowed and the action failed (i.e. _not_ `ok?`). `result.error` will be set to a generic error message ("Something went wrong" by default, but highly configurable).
|
155
|
-
<!-- TODO: link to messaging configs -->
|
134
|
+
Any exceptions will be swallowed and the action failed (i.e. _not_ `ok?`). `result.error` will be set to a generic error message ("Something went wrong" by default, but [highly configurable](/reference/class#messages)).
|
156
135
|
|
157
136
|
The swallowed exception will be available on `result.exception` for your introspection, but it'll also be passed to your `on_exception` handler so, [with a bit of configuration](/usage/setup), you can trust that any exceptions have been logged to your error tracking service automatically (one more thing the dev doesn't need to think about).
|
@@ -0,0 +1,46 @@
|
|
1
|
+
### Adding memoization
|
2
|
+
|
3
|
+
For a practical example of [the `additional_includes` configuration](/reference/configuration#additional-includes) in practice, consider adding new functionality to all Actions.
|
4
|
+
|
5
|
+
For instance, at Teamshares we automatically add memoization support (via [memo_wise](https://github.com/panorama-ed/memo_wise)) to all Actions. But we didn't want to add another dependency to the core library, so we've implemented this by:
|
6
|
+
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
Action.configure do |c|
|
10
|
+
c.additional_includes = [TS::Memoization]
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
module TS::Memoization
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
included do
|
19
|
+
prepend MemoWise
|
20
|
+
end
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
def memo(...) = memo_wise(...)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
And with those pieces in place `memo` is available in all Actions:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class ContrivedExample
|
32
|
+
include Action
|
33
|
+
|
34
|
+
exposes :nums
|
35
|
+
|
36
|
+
def call
|
37
|
+
expose nums: Array.new(10) { random_number }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
memo def random_number = rand(1..100) # [!code focus]
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Because of the `memo` usage, `ContrivedExample.call.nums` will be a ten-element array of _the same number_, rather than re-calling `rand` for each element.
|
@@ -7,7 +7,9 @@
|
|
7
7
|
Configuring rspec to treat files in spec/actions as service specs:
|
8
8
|
|
9
9
|
```ruby
|
10
|
-
|
11
|
-
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.define_derived_metadata(file_path: "spec/actions") do |metadata|
|
12
|
+
metadata[:type] = :service
|
13
|
+
end
|
12
14
|
end
|
13
15
|
```
|
@@ -1,12 +1,35 @@
|
|
1
|
-
|
2
|
-
* TODO: convert this rough outline into actual documentation
|
3
|
-
:::
|
1
|
+
# `Action::Result`
|
4
2
|
|
3
|
+
Every `call` invocation on an Action will return an `Action::Result` instance, which provides a consistent interface:
|
5
4
|
|
6
|
-
|
5
|
+
| Method | Description |
|
6
|
+
| -- | -- |
|
7
|
+
| `ok?` | `true` if the call succeeded, `false` if not.
|
8
|
+
| `error` | User-facing error message (string), if not `ok?` (else nil)
|
9
|
+
| `success` | User-facing success message (string), if `ok?` (else nil)
|
10
|
+
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
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
|
+
| 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)
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `messages` declaration](/reference/class#messages).
|
15
|
+
|
16
|
+
### Clarification of exposed values
|
17
|
+
|
18
|
+
In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
|
19
|
+
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class Foo
|
23
|
+
include Action
|
24
|
+
|
25
|
+
exposes :bar, :baz # [!code focus]
|
26
|
+
|
27
|
+
def call
|
28
|
+
expose bar: 1, baz: 2
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
result = Foo.call # [!code focus]
|
33
|
+
```
|
34
|
+
|
35
|
+
`result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
|
data/docs/reference/class.md
CHANGED
@@ -1,18 +1,75 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Class Methods
|
2
|
+
|
3
|
+
## `.expects` and `.exposes`
|
4
|
+
|
5
|
+
Actions have a _declarative interface_, whereby you explicitly declare both inbound and outbound arguments. Specifically, variables you expect to receive are specified via `expects`, and variables you intend to expose are specified via `exposes`.
|
6
|
+
|
7
|
+
Both `expects` and `exposes` support the same core options:
|
8
|
+
|
9
|
+
| Option | Example (same for `exposes`) | Meaning |
|
10
|
+
| -- | -- | -- |
|
11
|
+
| `sensitive` | `expects :password, sensitive: true` | Filters the field's value when logging, reporting errors, or calling `inspect`
|
12
|
+
| `default` | `expects :foo, default: 123` | If `foo` isn't explicitly set, it'll default to this value
|
13
|
+
| `allow_blank` | `expects :foo, allow_blank: true` | Don't fail if the value is blank
|
14
|
+
| `type` | `expects :foo, type: String` | Custom type validation -- fail unless `name.is_a?(String)`
|
15
|
+
| anything else | `expects :foo, inclusion: { in: [:apple, :peach] }` | Any other arguments will be processed [as ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html) (i.e. as if passed to `validates :foo, <...>` on an ActiveRecord model)
|
16
|
+
|
17
|
+
|
18
|
+
### Validation details
|
19
|
+
|
20
|
+
::: warning
|
21
|
+
While we _support_ complex interface validations, in practice you usually just want a `type`, if anything. Remember this is your validation about how the action is called, _not_ pretty user-facing errors (there's [a different pattern for that](/recipes/validating-user-input)).
|
3
22
|
:::
|
4
23
|
|
5
|
-
|
24
|
+
In addition to the [standard ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html), we also support three additional custom validators:
|
25
|
+
* `type: Foo` - fails unless the provided value `.is_a?(Foo)`
|
26
|
+
* `boolean: true` - wrapper to handle a boolean field (since ruby doesn't have a Boolean class, so we can't use `type:` directly)
|
27
|
+
* `validate: [callable]` - Support custom validations (fails if any string is returned OR if it raises an exception)
|
28
|
+
* Example:
|
29
|
+
```ruby
|
30
|
+
expects :foo, validate: ->(value) { "must be pretty big" unless value > 10 }
|
31
|
+
```
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
### Details specific to `.expects`
|
36
|
+
|
37
|
+
`expects` also supports a `preprocess` option that, if set to a callable, will be executed _before_ applying any validations. This can be useful for type coercion, e.g.:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
expects :date, type: Date, preprocess: ->(d) { d.is_a?(Date) ? d : Date.parse(d) }
|
41
|
+
```
|
42
|
+
|
43
|
+
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.
|
44
|
+
|
45
|
+
### Details specific to `.exposes`
|
46
|
+
|
47
|
+
Remember that you'll need [a corresponding `expose` call](/reference/instance#expose) for every variable you declare via `exposes`.
|
48
|
+
|
49
|
+
|
50
|
+
## `.messages`
|
51
|
+
|
52
|
+
The `messages` declaration allows you to customize the `error` and `success` messages on the returned result.
|
53
|
+
|
54
|
+
Accepts `error` and/or `success` keys. Values can be a string (returned directly) or a callable (evaluated in the action's context, so can access instance methods). If `error` is provided with a callable that expects a positional argument, the exception that was raised will be passed in as that value.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
messages success: "All good!", error: ->(e) { "Bad news: #{e.message}" }
|
58
|
+
```
|
59
|
+
|
60
|
+
## `error_for` and `rescues`
|
61
|
+
|
62
|
+
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.
|
6
63
|
|
7
|
-
|
8
|
-
* `exposes`
|
9
|
-
* `messages`
|
64
|
+
`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 global error handler.
|
10
65
|
|
11
|
-
|
12
|
-
|
13
|
-
* Note we have two custom validations: boolean: true and the implicit type: foo. (maybe with array of types?)
|
14
|
-
Note a third allows custom validations: `expects :foo, validate: ->(value) { "must be pretty big" unless value > 10 }` (error raised if any string returned OR if it raises an exception)
|
66
|
+
```ruby
|
67
|
+
messages error: "bad"
|
15
68
|
|
16
|
-
|
69
|
+
# Note this will NOT trigger Action.config.on_exception
|
70
|
+
rescues ActiveRecord::InvalidRecord => "Invalid params provided"
|
17
71
|
|
18
|
-
|
72
|
+
# These WILL trigger error handler (second demonstrates callable matcher AND message)
|
73
|
+
error_for ArgumentError, ->(e) { "Argument error: #{e.message}"
|
74
|
+
error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }
|
75
|
+
```
|
@@ -5,27 +5,15 @@ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call
|
|
5
5
|
|
6
6
|
```ruby
|
7
7
|
Action.configure do |c|
|
8
|
-
c.global_debug_logging = false
|
9
|
-
|
10
8
|
c.on_exception = ...
|
11
9
|
|
12
10
|
c.top_level_around_hook = ...
|
13
11
|
|
14
12
|
c.additional_includes = []
|
15
|
-
end
|
16
|
-
```
|
17
|
-
|
18
|
-
## `global_debug_logging`
|
19
|
-
|
20
|
-
By default, every `action.call` will emit _debug_ log lines when it is called (including the action class and any arguments it was provided) and after it completes (including the execution time and the outcome).
|
21
|
-
|
22
|
-
You can bump the log level from `debug` to `info` for specific actions by including their class name (comma separated, if multiple) in a `SA_DEBUG_TARGETS` ENV variable.
|
23
13
|
|
24
|
-
|
14
|
+
c.global_debug_logging = false
|
25
15
|
|
26
|
-
|
27
|
-
Action.configure do |c|
|
28
|
-
c.global_debug_logging = true
|
16
|
+
c.logger = ...
|
29
17
|
end
|
30
18
|
```
|
31
19
|
|
@@ -42,7 +30,7 @@ For example, if you're using Honeybadger this could look something like:
|
|
42
30
|
message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
|
43
31
|
|
44
32
|
Rails.logger.warn(message)
|
45
|
-
Honeybadger.notify(message, context:)
|
33
|
+
Honeybadger.notify(message, context: { axn_context: context })
|
46
34
|
end
|
47
35
|
end
|
48
36
|
```
|
@@ -77,6 +65,10 @@ A couple notes:
|
|
77
65
|
* `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.
|
78
66
|
|
79
67
|
|
68
|
+
## `logger`
|
69
|
+
|
70
|
+
Defaults to `Rails.logger`, if present, otherwise falls back to `Logger.new($stdout)`. But can be set to a custom logger as necessary.
|
71
|
+
|
80
72
|
## `additional_includes`
|
81
73
|
|
82
74
|
This is much less critical than the preceding options, but on the off chance you want to add additional customization to _all_ your actions you can set additional modules to be included alongside `include Action`.
|
@@ -88,3 +80,24 @@ For example:
|
|
88
80
|
c.additional_includes = [SomeFancyCustomModule]
|
89
81
|
end
|
90
82
|
```
|
83
|
+
|
84
|
+
For a practical example of this in practice, see [our 'memoization' recipe](/recipes/memoization).
|
85
|
+
|
86
|
+
## `global_debug_logging`
|
87
|
+
|
88
|
+
By default, every `action.call` will emit _debug_ log lines when it is called and after it completes:
|
89
|
+
|
90
|
+
```
|
91
|
+
[YourCustomAction] About to execute with: {:foo=>"bar"}
|
92
|
+
[YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
|
93
|
+
```
|
94
|
+
|
95
|
+
You can bump the log level from `debug` to `info` for specific actions by including their class name (comma separated, if multiple) in a `SA_DEBUG_TARGETS` ENV variable.
|
96
|
+
|
97
|
+
You can also turn this on _globally_ by setting `global_debug_logging = true`.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Action.configure do |c|
|
101
|
+
c.global_debug_logging = true
|
102
|
+
end
|
103
|
+
```
|
data/docs/reference/instance.md
CHANGED
@@ -1,17 +1,100 @@
|
|
1
|
-
|
2
|
-
* TODO: convert this rough outline into actual documentation
|
3
|
-
:::
|
1
|
+
# Instance Methods
|
4
2
|
|
3
|
+
## `#expose`
|
5
4
|
|
6
|
-
|
7
|
-
* `expose`
|
8
|
-
* `fail!`
|
9
|
-
* `log`
|
10
|
-
* `try` - any exceptions raised by the block will trigger the on_exception handler, but then will be swallowed (the action is _not_ failed)
|
11
|
-
* Edge case: explicit `fail!` calls _will_ still fail the action
|
12
|
-
* `hoist_errors`
|
13
|
-
* Edge case: intent is a single action call in the block -- if there are multiple calls, only the last one will be checked (anything explicitly _raised_ will still be handled).
|
14
|
-
<!-- TODO: is there difference between `SubAction.call!` and `hoist_errors { SubAction.call }`?? -->
|
15
|
-
* `context_for_logging` (and decent #inspect support)
|
5
|
+
Used to set a value on the Action::Result. Remember you can only `expose` keys that you have declared in [the class-level interface](/reference/class).
|
16
6
|
|
7
|
+
* Accepts two positional arguments (the key and value to set, respectively): `expose :some_key, 123`
|
8
|
+
* Accepts a hash with one or more key/value pairs: `expose some_key: 123, another: 456`
|
17
9
|
|
10
|
+
Primarily used for its side effects, but it does return a Hash with the key/value pair(s) you exposed.
|
11
|
+
|
12
|
+
|
13
|
+
## `#fail!`
|
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.
|
16
|
+
|
17
|
+
## `#log`
|
18
|
+
|
19
|
+
Helper method to log (via the [configurable](/reference/configuration#logger) `Action.config.logger`) the string you provide (prefixed with the Action's class name).
|
20
|
+
|
21
|
+
* First argument (required) is a string message to log
|
22
|
+
* Also accepts a `level:` keyword argument to change the log level (defaults to `info`)
|
23
|
+
|
24
|
+
Primarily used for its side effects; returns whatever the underlying `Action.config.logger` instance returns but it does return a Hash with the key/value pair(s) you exposed.
|
25
|
+
|
26
|
+
## `#try`
|
27
|
+
|
28
|
+
Accepts a block. Any exceptions raised within that block will be swallowed, but _they will NOT fail the action_!
|
29
|
+
|
30
|
+
A few details:
|
31
|
+
* An explicit `fail!` call _will_ still fail the action
|
32
|
+
* Any exceptions swallowed _will_ still be reported via the `on_exception` handler
|
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.
|
35
|
+
|
36
|
+
Example:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Foo
|
40
|
+
include Action
|
41
|
+
|
42
|
+
after do
|
43
|
+
try { send_slack_notifications } # [!code focus]
|
44
|
+
end
|
45
|
+
|
46
|
+
def call = ...
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def send_slack_notifications = ...
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
## `#hoist_errors`
|
55
|
+
|
56
|
+
Useful when calling one Action from within another. By default the nested action call will return an Action::Result, but it's up to you to check if the result is `ok?` and to handle potential failure modes... and in practice this is easy to miss.
|
57
|
+
|
58
|
+
By wrapping your nested call in `hoist_errors`, it will _automatically_ fail the parent action if the nested call fails.
|
59
|
+
|
60
|
+
Accepts a `prefix` keyword argument -- when set, prefixes the `error` message from any failures in the block (useful to return different error messages for each if you're calling multiple sub-actions in a single service).
|
61
|
+
|
62
|
+
NOTE: expects a single action call in the block -- if there are multiple calls, only the last one will be checked for `ok?` (although anything _raised_ in the block will still be handled).
|
63
|
+
|
64
|
+
### Example
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class SubAction
|
68
|
+
include Action
|
69
|
+
|
70
|
+
def call
|
71
|
+
fail! "bad news"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class MainAction
|
76
|
+
include Action
|
77
|
+
|
78
|
+
def call
|
79
|
+
SubAction.call
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
_Without_ `hoist_errors`, `MainAction.call` returns an `ok?` result, even though `SubAction.call` always fails, because we haven't explicitly handled the nested call.
|
85
|
+
|
86
|
+
By adding `hoist_errors`, though:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class MainAction
|
90
|
+
include Action
|
91
|
+
|
92
|
+
def call
|
93
|
+
hoist_errors(prefix: "From subaction:") do
|
94
|
+
SubAction.call
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
`MainAction.call` now returns a _failed_ result, and `result.error` is "From subaction: bad news".
|