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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.tool-versions +1 -0
  4. data/CHANGELOG.md +10 -2
  5. data/CONTRIBUTING.md +1 -1
  6. data/README.md +1 -1
  7. data/docs/.vitepress/config.mjs +18 -10
  8. data/docs/advanced/rough.md +2 -0
  9. data/docs/index.md +11 -3
  10. data/docs/{guide/index.md → intro/overview.md} +11 -32
  11. data/docs/recipes/memoization.md +46 -0
  12. data/docs/{usage → recipes}/testing.md +4 -2
  13. data/docs/reference/action-result.md +32 -9
  14. data/docs/reference/class.md +69 -12
  15. data/docs/reference/configuration.md +28 -15
  16. data/docs/reference/instance.md +96 -13
  17. data/docs/usage/setup.md +0 -2
  18. data/docs/usage/using.md +7 -15
  19. data/docs/usage/writing.md +45 -6
  20. data/lib/action/attachable/base.rb +43 -0
  21. data/lib/action/attachable/steps.rb +47 -0
  22. data/lib/action/attachable/subactions.rb +43 -0
  23. data/lib/action/attachable.rb +17 -0
  24. data/lib/action/{configuration.rb → core/configuration.rb} +1 -1
  25. data/lib/action/{context_facade.rb → core/context_facade.rb} +18 -28
  26. data/lib/action/{contract.rb → core/contract.rb} +10 -2
  27. data/lib/action/{exceptions.rb → core/exceptions.rb} +11 -0
  28. data/lib/action/{hoist_errors.rb → core/hoist_errors.rb} +3 -2
  29. data/lib/action/{swallow_exceptions.rb → core/swallow_exceptions.rb} +52 -7
  30. data/lib/axn/factory.rb +102 -0
  31. data/lib/axn/version.rb +1 -1
  32. data/lib/axn.rb +20 -10
  33. metadata +28 -22
  34. data/lib/action/organizer.rb +0 -41
  35. /data/docs/{usage → advanced}/conventions.md +0 -0
  36. /data/docs/{about/index.md → intro/about.md} +0 -0
  37. /data/docs/{advanced → recipes}/validating-user-input.md +0 -0
  38. /data/lib/action/{contract_validator.rb → core/contract_validator.rb} +0 -0
  39. /data/lib/action/{enqueueable.rb → core/enqueueable.rb} +0 -0
  40. /data/lib/action/{logging.rb → core/logging.rb} +0 -0
  41. /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: 9a4820a9fbbd271a71be8a06320e362372d87590246931da17c48bb2299eb4fc
4
- data.tar.gz: 18d6743feaaa914640bf3eaac95c67e79ad059179191a0987896c6fd49ead8d0
3
+ metadata.gz: d6ba4df4f3ce11e1a31eb8dfadde856141ed9dc6d10002efe1b544abcb580431
4
+ data.tar.gz: 0a27844c8b7c341315c8a1d9212fa0745866086cd3fbb65140b813a125ac8496
5
5
  SHA512:
6
- metadata.gz: 459807c65792ef1d0624638750f7f14b66150525c9e643d5f48536b98424be935a4548969cc337d1d226cd9b062b6404968f31e95e43da8284b100450aac0d54
7
- data.tar.gz: 2fec14a17957558f16bf1f813789b61a54b7b7f84a911acd8d80bffa794f8f1f6e559ee6b66b4668691ff360ed0d916ee28f6af2e5428a55c4428add5b6cd4a4
6
+ metadata.gz: 250b751b9a583d7206c9ada4e162ce8d2ee96816466515812e187e3dcdeb50f7024a595968c4e42f3f91a63909a38074e59b738a4f0216a0aaf1f15154e99452
7
+ data.tar.gz: cba3bee954fb153a99a65ff30478ae1d048fd49fe7834d4ba2446e4ff5e6ff70b3cfe588bc20a5d042665aa5bd9797cc4df10a4483cd59ce5b5d99b85bc4f645
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.1
2
+ TargetRubyVersion: 3.2
3
3
  SuggestExtensions: false
4
4
  NewCops: enable
5
5
 
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
- * revert `gets` / `sets` back to `expects` / `exposes`
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/guide/), are convinced this behavior is a bug, please:
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/guide/) for details.
7
+ See our [User Guide](https://teamshares.github.io/axn/) for details.
8
8
 
9
9
  ## [!!] Inheritance Support
10
10
 
@@ -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: 'User Guide', link: '/guide' }
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: 'Summary Overview', link: '/guide/' },
21
+ { text: 'About', link: '/intro/about' },
22
+ { text: 'Overview', link: '/intro/overview' },
21
23
  ]
22
24
  },
23
25
  {
24
- text: 'Getting Started',
26
+ text: 'Usage Guide',
25
27
  items: [
26
- { text: 'Setup', link: '/usage/setup' },
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: 'Advanced Usage',
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: 'Validating User Input', link: '/advanced/validating-user-input' },
54
+ { text: 'Conventions', link: '/advanced/conventions' },
47
55
  ]
48
56
  },
49
57
  ],
@@ -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: User Guide
12
- link: /guide
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](./#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](/advanced/validating-user-input).
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 fail, the action will fail early and set `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).
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
- ### Putting it together
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
- | Method | Description |
96
- | -- | -- |
97
- | `ok?` | `true` if the call succeeded, `false` if not.
98
- | `error` | Will _always_ be set to a safe-to-show-users string if not `ok?`
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
- ### Putting it together
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
- :star_struck: The calling code usually only cares about `ok?` and `error` -- no complex error handling needed.
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
- config.define_derived_metadata(file_path: %r{spec/actions}) do |metadata|
11
- metadata[:type] = :service
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
- ::: danger ALPHA
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
- `Action::Result`
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
- * ok?
9
- * error
10
- * exception
11
- * success
12
- * message
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).
@@ -1,18 +1,75 @@
1
- ::: danger ALPHA
2
- * TODO: convert this rough outline into actual documentation
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
- ## Class-level interface
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
- * `expects`
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
- ### `expects` and `exposes`
12
- * setting `sensitive: true` on any param will filter that value out when inspecting or passing to on_exception
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
- ### #call and #rollback
69
+ # Note this will NOT trigger Action.config.on_exception
70
+ rescues ActiveRecord::InvalidRecord => "Invalid params provided"
17
71
 
18
- ### hooks
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
- You can also turn this on _globally_ by setting `global_debug_logging = true`.
14
+ c.global_debug_logging = false
25
15
 
26
- ```ruby
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
+ ```
@@ -1,17 +1,100 @@
1
- ::: danger ALPHA
2
- * TODO: convert this rough outline into actual documentation
3
- :::
1
+ # Instance Methods
4
2
 
3
+ ## `#expose`
5
4
 
6
- While coding:
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".
data/docs/usage/setup.md CHANGED
@@ -6,8 +6,6 @@ outline: deep
6
6
  ## Installation
7
7
 
8
8
  Adding `axn` to your Gemfile is enough to start using `include Action`.
9
- <!-- todo bundler -->
10
-
11
9
 
12
10
  ## Global Configuration
13
11