axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4
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/.cursor/commands/pr.md +36 -0
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
data/docs/recipes/memoization.md
CHANGED
|
@@ -1,46 +1,131 @@
|
|
|
1
|
-
|
|
1
|
+
# Memoization
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Axn has built-in memoization support via the `memo` helper. This caches the result of method calls, ensuring they're only computed once per action execution.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Basic Usage
|
|
6
6
|
|
|
7
|
+
The `memo` helper works out of the box for methods without arguments:
|
|
7
8
|
|
|
8
9
|
```ruby
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
class GenerateReport
|
|
11
|
+
include Axn
|
|
12
|
+
|
|
13
|
+
expects :company, model: Company
|
|
14
|
+
exposes :report
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
expose report: {
|
|
18
|
+
total_revenue: total_revenue,
|
|
19
|
+
top_products: top_products.map(&:name),
|
|
20
|
+
# top_products is only queried once, even though it's called twice
|
|
21
|
+
product_count: top_products.count
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
memo def top_products
|
|
28
|
+
company.products.order(sales_count: :desc).limit(10)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
memo def total_revenue
|
|
32
|
+
company.orders.sum(:total)
|
|
11
33
|
end
|
|
34
|
+
end
|
|
12
35
|
```
|
|
13
36
|
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
- `memo` wraps the method and caches its return value on first call
|
|
40
|
+
- Subsequent calls return the cached value without re-executing the method
|
|
41
|
+
- Memoization is scoped to the action instance, so each `call` starts fresh
|
|
42
|
+
|
|
43
|
+
## Methods With Arguments
|
|
44
|
+
|
|
45
|
+
For methods that accept arguments, Axn supports the `memo_wise` gem:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# Gemfile
|
|
49
|
+
gem "memo_wise"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With `memo_wise` available, you can automatically memoize methods with arguments:
|
|
53
|
+
|
|
14
54
|
```ruby
|
|
15
|
-
|
|
16
|
-
|
|
55
|
+
class CalculatePricing
|
|
56
|
+
include Axn
|
|
17
57
|
|
|
18
|
-
|
|
19
|
-
|
|
58
|
+
expects :product
|
|
59
|
+
exposes :pricing
|
|
60
|
+
|
|
61
|
+
def call
|
|
62
|
+
expose pricing: {
|
|
63
|
+
retail: price_for(:retail),
|
|
64
|
+
wholesale: price_for(:wholesale),
|
|
65
|
+
# Each unique argument is cached separately
|
|
66
|
+
bulk: price_for(:bulk)
|
|
67
|
+
}
|
|
20
68
|
end
|
|
21
69
|
|
|
22
|
-
|
|
23
|
-
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
memo def price_for(tier)
|
|
73
|
+
# Complex pricing calculation...
|
|
74
|
+
PricingEngine.calculate(product, tier:)
|
|
24
75
|
end
|
|
25
76
|
end
|
|
26
77
|
```
|
|
27
78
|
|
|
28
|
-
|
|
79
|
+
If you try to use `memo` on a method with arguments without `memo_wise` installed, you'll get a helpful error:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
ArgumentError: Memoization of methods with arguments requires the 'memo_wise' gem.
|
|
83
|
+
Please add 'memo_wise' to your Gemfile or use a method without arguments.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## When to Use Memoization
|
|
87
|
+
|
|
88
|
+
Memoization is particularly useful for:
|
|
89
|
+
|
|
90
|
+
- **Database queries** called multiple times within an action
|
|
91
|
+
- **API calls** or external service lookups
|
|
92
|
+
- **Complex computations** that are expensive to repeat
|
|
29
93
|
|
|
30
94
|
```ruby
|
|
31
|
-
class
|
|
32
|
-
include
|
|
95
|
+
class SyncUserData
|
|
96
|
+
include Axn
|
|
33
97
|
|
|
34
|
-
|
|
98
|
+
expects :user, model: User
|
|
35
99
|
|
|
36
100
|
def call
|
|
37
|
-
|
|
101
|
+
update_profile if needs_profile_update?
|
|
102
|
+
update_preferences if needs_preferences_update?
|
|
103
|
+
notify_if_changed
|
|
38
104
|
end
|
|
39
105
|
|
|
40
106
|
private
|
|
41
107
|
|
|
42
|
-
|
|
108
|
+
# Called multiple times - only fetches once
|
|
109
|
+
memo def external_data
|
|
110
|
+
ExternalApi.fetch_user_data(user.external_id)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def needs_profile_update?
|
|
114
|
+
external_data[:profile_version] > user.profile_version
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def needs_preferences_update?
|
|
118
|
+
external_data[:preferences_hash] != user.preferences_hash
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def notify_if_changed
|
|
122
|
+
# ...
|
|
123
|
+
end
|
|
43
124
|
end
|
|
44
125
|
```
|
|
45
126
|
|
|
46
|
-
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
- Memoization persists only for the duration of a single action execution
|
|
130
|
+
- When `memo_wise` is available, Axn automatically uses it (no configuration needed)
|
|
131
|
+
- See the [memo_wise documentation](https://github.com/panorama-ed/memo_wise) for advanced features like cache resetting
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# RuboCop Integration
|
|
2
2
|
|
|
3
|
-
Axn provides custom RuboCop
|
|
3
|
+
Axn provides a custom RuboCop cop to help enforce proper result handling when calling Actions.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What It Does
|
|
6
6
|
|
|
7
|
-
The `Axn/UncheckedResult` cop
|
|
7
|
+
The `Axn/UncheckedResult` cop detects when you call another Action from within an Action but don't properly handle the result. This helps prevent silent failures and ensures consistent error handling patterns.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
> **⚠️ Warning**: This cop uses static analysis and cannot distinguish between actual Axn classes and other classes that happen to have a `call` method. If you're using legacy services or other service patterns alongside Axn, you may encounter false positives. Use RuboCop disable comments for intentional violations.
|
|
10
|
+
>
|
|
11
|
+
> **💡 Tip**: If you're using the Actions namespace (see [Rails Integration](/usage/setup#rails-integration-optional)), you can configure the cop to only check `Actions::*` classes, eliminating false positives from other service objects.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
10
14
|
|
|
11
15
|
### 1. Add to Your .rubocop.yml
|
|
12
16
|
|
|
@@ -14,137 +18,37 @@ The `Axn/UncheckedResult` cop enforces proper result handling when calling Actio
|
|
|
14
18
|
require:
|
|
15
19
|
- axn/rubocop
|
|
16
20
|
|
|
17
|
-
# Enable Axn's custom cop
|
|
18
21
|
Axn/UncheckedResult:
|
|
19
22
|
Enabled: true
|
|
20
|
-
|
|
21
|
-
CheckNonNested: true # Check non-nested Action calls
|
|
22
|
-
Severity: warning # or error
|
|
23
|
+
Severity: warning
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
### 2. Verify Installation
|
|
26
27
|
|
|
27
|
-
Run RuboCop to ensure the cop is loaded:
|
|
28
|
-
|
|
29
28
|
```bash
|
|
30
29
|
bundle exec rubocop --show-cops | grep Axn
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
You should see
|
|
34
|
-
```
|
|
35
|
-
Axn/UncheckedResult
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Configuration Options
|
|
39
|
-
|
|
40
|
-
### CheckNested
|
|
41
|
-
|
|
42
|
-
Controls whether the cop checks Action calls that are inside other Action classes.
|
|
43
|
-
|
|
44
|
-
```yaml
|
|
45
|
-
Axn/UncheckedResult:
|
|
46
|
-
CheckNested: true # Check nested calls (default)
|
|
47
|
-
CheckNested: false # Skip nested calls
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**When to use `CheckNested: false`:**
|
|
51
|
-
- You're gradually adopting the rule and want to focus on top-level calls first
|
|
52
|
-
- Your team has different standards for nested vs. non-nested calls
|
|
53
|
-
- You're using a different pattern for nested Action handling
|
|
54
|
-
|
|
55
|
-
### CheckNonNested
|
|
56
|
-
|
|
57
|
-
Controls whether the cop checks Action calls that are outside Action classes.
|
|
58
|
-
|
|
59
|
-
```yaml
|
|
60
|
-
Axn/UncheckedResult:
|
|
61
|
-
CheckNonNested: true # Check non-nested calls (default)
|
|
62
|
-
CheckNonNested: false # Skip non-nested calls
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**When to use `CheckNonNested: false`:**
|
|
66
|
-
- You're only concerned about nested Action calls
|
|
67
|
-
- Top-level Action calls are handled by other tools or processes
|
|
68
|
-
- You want to focus on the most critical use case first
|
|
69
|
-
|
|
70
|
-
### Severity
|
|
71
|
-
|
|
72
|
-
Controls how violations are reported.
|
|
73
|
-
|
|
74
|
-
```yaml
|
|
75
|
-
Axn/UncheckedResult:
|
|
76
|
-
Severity: warning # Show as warnings (default)
|
|
77
|
-
Severity: error # Show as errors (fails CI)
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Common Configuration Patterns
|
|
32
|
+
You should see `Axn/UncheckedResult` in the output.
|
|
81
33
|
|
|
82
|
-
|
|
34
|
+
## Basic Usage
|
|
83
35
|
|
|
84
|
-
|
|
85
|
-
Axn/UncheckedResult:
|
|
86
|
-
Enabled: true
|
|
87
|
-
CheckNested: true
|
|
88
|
-
CheckNonNested: true
|
|
89
|
-
Severity: error
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Gradual Adoption (Recommended for Existing Projects)
|
|
93
|
-
|
|
94
|
-
```yaml
|
|
95
|
-
Axn/UncheckedResult:
|
|
96
|
-
Enabled: true
|
|
97
|
-
CheckNested: true # Start with nested calls
|
|
98
|
-
CheckNonNested: false # Add this later
|
|
99
|
-
Severity: warning # Start with warnings
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Nested-Only Focus
|
|
103
|
-
|
|
104
|
-
```yaml
|
|
105
|
-
Axn/UncheckedResult:
|
|
106
|
-
Enabled: true
|
|
107
|
-
CheckNested: true
|
|
108
|
-
CheckNonNested: false
|
|
109
|
-
Severity: warning
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## What the Cop Checks
|
|
113
|
-
|
|
114
|
-
The cop analyzes your code to determine if you're:
|
|
115
|
-
|
|
116
|
-
1. **Inside an Action class** - Classes that `include Action`
|
|
117
|
-
2. **Inside the `call` method** - Only the main execution method
|
|
118
|
-
3. **Calling another Action** - Using `.call` on Action classes
|
|
119
|
-
4. **Properly handling the result** - One of the acceptable patterns
|
|
120
|
-
|
|
121
|
-
## What the Cop Ignores
|
|
122
|
-
|
|
123
|
-
The cop will NOT report offenses for:
|
|
124
|
-
|
|
125
|
-
- Action calls outside of Action classes (if `CheckNonNested: false`)
|
|
126
|
-
- Action calls in methods other than `call`
|
|
127
|
-
- Action calls that use `call!` (bang method)
|
|
128
|
-
- Action calls where the result is properly handled
|
|
129
|
-
|
|
130
|
-
## Proper Result Handling Patterns
|
|
131
|
-
|
|
132
|
-
### ✅ Using call!
|
|
36
|
+
### ✅ Good - Using call!
|
|
133
37
|
|
|
134
38
|
```ruby
|
|
135
39
|
class OuterAction
|
|
136
|
-
include
|
|
40
|
+
include Axn
|
|
137
41
|
def call
|
|
138
42
|
InnerAction.call!(param: "value") # Exceptions bubble up
|
|
139
43
|
end
|
|
140
44
|
end
|
|
141
45
|
```
|
|
142
46
|
|
|
143
|
-
### ✅ Checking result
|
|
47
|
+
### ✅ Good - Checking the result
|
|
144
48
|
|
|
145
49
|
```ruby
|
|
146
50
|
class OuterAction
|
|
147
|
-
include
|
|
51
|
+
include Axn
|
|
148
52
|
def call
|
|
149
53
|
result = InnerAction.call(param: "value")
|
|
150
54
|
return result unless result.ok?
|
|
@@ -153,200 +57,50 @@ class OuterAction
|
|
|
153
57
|
end
|
|
154
58
|
```
|
|
155
59
|
|
|
156
|
-
###
|
|
157
|
-
|
|
158
|
-
```ruby
|
|
159
|
-
class OuterAction
|
|
160
|
-
include Action
|
|
161
|
-
def call
|
|
162
|
-
result = InnerAction.call(param: "value")
|
|
163
|
-
if result.failed?
|
|
164
|
-
return result
|
|
165
|
-
end
|
|
166
|
-
# Process successful result...
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### ✅ Accessing result.error
|
|
172
|
-
|
|
173
|
-
```ruby
|
|
174
|
-
class OuterAction
|
|
175
|
-
include Action
|
|
176
|
-
def call
|
|
177
|
-
result = InnerAction.call(param: "value")
|
|
178
|
-
if result.error
|
|
179
|
-
return result
|
|
180
|
-
end
|
|
181
|
-
# Process successful result...
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### ✅ Returning the result
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
class OuterAction
|
|
190
|
-
include Action
|
|
191
|
-
def call
|
|
192
|
-
result = InnerAction.call(param: "value")
|
|
193
|
-
result # Result is returned, so it's properly handled
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### ✅ Using result in expose
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
class OuterAction
|
|
202
|
-
include Action
|
|
203
|
-
exposes :nested_result
|
|
204
|
-
def call
|
|
205
|
-
result = InnerAction.call(param: "value")
|
|
206
|
-
expose nested_result: result # Result is used, so it's properly handled
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### ✅ Passing result to another method
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
class OuterAction
|
|
215
|
-
include Action
|
|
216
|
-
def call
|
|
217
|
-
result = InnerAction.call(param: "value")
|
|
218
|
-
process_result(result) # Result is used, so it's properly handled
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Common Anti-Patterns
|
|
224
|
-
|
|
225
|
-
### ❌ Ignoring the result
|
|
60
|
+
### ❌ Bad - Ignoring the result
|
|
226
61
|
|
|
227
62
|
```ruby
|
|
228
63
|
class OuterAction
|
|
229
|
-
include
|
|
64
|
+
include Axn
|
|
230
65
|
def call
|
|
231
|
-
InnerAction.call(param: "value") #
|
|
66
|
+
InnerAction.call(param: "value") # Will trigger offense
|
|
232
67
|
# This continues even if InnerAction fails
|
|
233
68
|
end
|
|
234
69
|
end
|
|
235
70
|
```
|
|
236
71
|
|
|
237
|
-
|
|
72
|
+
## Configuration
|
|
238
73
|
|
|
239
|
-
|
|
240
|
-
class OuterAction
|
|
241
|
-
include Action
|
|
242
|
-
def call
|
|
243
|
-
result = InnerAction.call(param: "value") # Assigned but never used
|
|
244
|
-
# Will trigger offense unless result is properly handled
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
```
|
|
74
|
+
The cop supports flexible configuration:
|
|
248
75
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
some_other_method(result.some_other_attribute) # Not checking success/failure
|
|
257
|
-
# Will trigger offense - need to check result.ok? first
|
|
258
|
-
end
|
|
259
|
-
end
|
|
76
|
+
```yaml
|
|
77
|
+
Axn/UncheckedResult:
|
|
78
|
+
Enabled: true
|
|
79
|
+
CheckNested: true # Check nested Axn calls (default: true)
|
|
80
|
+
CheckNonNested: true # Check non-nested Axn calls (default: true)
|
|
81
|
+
ActionsNamespace: "Actions" # Only check Actions::* classes (optional)
|
|
82
|
+
Severity: warning # or error
|
|
260
83
|
```
|
|
261
84
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
### For New Projects
|
|
265
|
-
|
|
266
|
-
1. Enable the cop with full enforcement from the start
|
|
267
|
-
2. Use `Severity: error` to catch violations early
|
|
268
|
-
3. Train your team on the proper patterns
|
|
269
|
-
|
|
270
|
-
### For Existing Projects
|
|
85
|
+
### ActionsNamespace Configuration
|
|
271
86
|
|
|
272
|
-
|
|
273
|
-
2. **Phase 2**: Fix all nested Action violations
|
|
274
|
-
3. **Phase 3**: Enable `CheckNonNested: true`
|
|
275
|
-
4. **Phase 4**: Fix all non-nested Action violations
|
|
276
|
-
5. **Phase 5**: Set `Severity: error`
|
|
87
|
+
When using the Actions namespace, you can configure the cop to only check calls on `Actions::*` classes:
|
|
277
88
|
|
|
278
|
-
|
|
89
|
+
```yaml
|
|
90
|
+
Axn/UncheckedResult:
|
|
91
|
+
ActionsNamespace: "Actions"
|
|
92
|
+
```
|
|
279
93
|
|
|
280
|
-
|
|
94
|
+
This eliminates false positives from other service objects while still catching unchecked Axn action calls:
|
|
281
95
|
|
|
282
96
|
```ruby
|
|
283
97
|
class OuterAction
|
|
284
|
-
include
|
|
98
|
+
include Axn
|
|
285
99
|
def call
|
|
286
|
-
#
|
|
287
|
-
InnerAction.call(param: "value") #
|
|
288
|
-
# rubocop:enable Axn/UncheckedResult
|
|
100
|
+
SomeService.call(param: "value") # Won't trigger cop
|
|
101
|
+
Actions::InnerAction.call(param: "value") # Will trigger cop
|
|
289
102
|
end
|
|
290
103
|
end
|
|
291
104
|
```
|
|
292
105
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
### Cop Not Loading
|
|
296
|
-
|
|
297
|
-
If you see "uninitialized constant" errors:
|
|
298
|
-
|
|
299
|
-
1. Ensure the gem is properly installed: `bundle list | grep axn`
|
|
300
|
-
2. Check your `.rubocop.yml` syntax
|
|
301
|
-
3. Verify the require path: `require: - axn/rubocop`
|
|
302
|
-
|
|
303
|
-
### False Positives
|
|
304
|
-
|
|
305
|
-
If the cop reports violations for properly handled results:
|
|
306
|
-
|
|
307
|
-
1. Check that you're using the exact patterns shown above
|
|
308
|
-
2. Ensure the result variable name matches exactly
|
|
309
|
-
3. Verify the result is being used in an acceptable way
|
|
310
|
-
|
|
311
|
-
### Performance Issues
|
|
312
|
-
|
|
313
|
-
The cop analyzes AST nodes, so it's generally fast. If you experience slowdowns:
|
|
314
|
-
|
|
315
|
-
1. Ensure you're not running RuboCop on very large files
|
|
316
|
-
2. Consider using RuboCop's `--parallel` option
|
|
317
|
-
3. Use `.rubocop_todo.yml` for gradual adoption
|
|
318
|
-
|
|
319
|
-
## Best Practices
|
|
320
|
-
|
|
321
|
-
1. **Start Small**: Begin with warnings and nested calls only
|
|
322
|
-
2. **Be Consistent**: Choose one pattern and stick with it
|
|
323
|
-
3. **Train Your Team**: Make sure everyone understands the rules
|
|
324
|
-
4. **Review Regularly**: Use the cop in your CI/CD pipeline
|
|
325
|
-
5. **Document Exceptions**: Use disable comments sparingly and document why
|
|
326
|
-
|
|
327
|
-
## Integration with CI/CD
|
|
328
|
-
|
|
329
|
-
Add RuboCop to your CI pipeline to catch violations early:
|
|
330
|
-
|
|
331
|
-
```yaml
|
|
332
|
-
# .github/workflows/rubocop.yml
|
|
333
|
-
name: RuboCop
|
|
334
|
-
on: [push, pull_request]
|
|
335
|
-
jobs:
|
|
336
|
-
rubocop:
|
|
337
|
-
runs-on: ubuntu-latest
|
|
338
|
-
steps:
|
|
339
|
-
- uses: actions/checkout@v3
|
|
340
|
-
- uses: ruby/setup-ruby@v1
|
|
341
|
-
with:
|
|
342
|
-
ruby-version: 3.2
|
|
343
|
-
- run: bundle install
|
|
344
|
-
- run: bundle exec rubocop
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Related Resources
|
|
348
|
-
|
|
349
|
-
- [Action Result Reference](/reference/action-result)
|
|
350
|
-
- [Configuration Guide](/reference/configuration)
|
|
351
|
-
- [Testing Recipes](/recipes/testing)
|
|
352
|
-
- [Best Practices Guide](/advanced/conventions)
|
|
106
|
+
For detailed configuration options, usage patterns, and troubleshooting, see the [technical documentation](https://github.com/teamshares/axn/blob/main/lib/rubocop/cop/axn/README.md).
|
data/docs/recipes/testing.md
CHANGED
|
@@ -8,23 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
Say you're writing unit specs for PrimaryAction that calls Subaction, and you want to mock out the Subaction call.
|
|
10
10
|
|
|
11
|
-
To generate a successful
|
|
11
|
+
To generate a successful Axn::Result:
|
|
12
12
|
|
|
13
|
-
* Base case: `
|
|
14
|
-
* [Optional] Custom message: `
|
|
15
|
-
* [Optional] Custom exposures: `
|
|
13
|
+
* Base case: `Axn::Result.ok`
|
|
14
|
+
* [Optional] Custom message: `Axn::Result.ok("It went awesome")`
|
|
15
|
+
* [Optional] Custom exposures: `Axn::Result.ok("It went awesome", some_var: 123)`
|
|
16
16
|
|
|
17
|
-
To generate a failed
|
|
17
|
+
To generate a failed Axn::Result:
|
|
18
18
|
|
|
19
|
-
* Base case: `
|
|
20
|
-
* [Optional] Custom message: `
|
|
21
|
-
* [Optional] Custom exposures: `
|
|
22
|
-
* [Optional] Custom exception: `
|
|
19
|
+
* Base case: `Axn::Result.error`
|
|
20
|
+
* [Optional] Custom message: `Axn::Result.error("It went poorly")`
|
|
21
|
+
* [Optional] Custom exposures: `Axn::Result.error("It went poorly", some_var: 123)`
|
|
22
|
+
* [Optional] Custom exception: `Axn::Result.error(some_var: 123) { raise FooBarException.new("bad thing") }`
|
|
23
23
|
|
|
24
24
|
Either way, using those to mock an actual call would look something like this in your rspec:
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
|
-
let(:subaction_response) {
|
|
27
|
+
let(:subaction_response) { Axn::Result.ok("custom message", foo: 1) }
|
|
28
28
|
|
|
29
29
|
before do
|
|
30
30
|
expect(Subaction).to receive(:call).and_return(subaction_response)
|
|
@@ -38,7 +38,7 @@ The semantics of call-bang are a little different -- if Subaction is called via
|
|
|
38
38
|
### Success
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
|
-
let(:subaction_response) {
|
|
41
|
+
let(:subaction_response) { Axn::Result.ok("custom message", foo: 1) }
|
|
42
42
|
|
|
43
43
|
before do
|
|
44
44
|
expect(Subaction).to receive(:call!).and_return(subaction_response)
|
|
@@ -57,18 +57,18 @@ before do
|
|
|
57
57
|
end
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
NOTE: to mock subaction failing via explicit `fail!` call, you'd use an `
|
|
60
|
+
NOTE: to mock subaction failing via explicit `fail!` call, you'd use an `Axn::Failure` exception class.
|
|
61
61
|
|
|
62
62
|
## Mocking Axn arguments
|
|
63
63
|
|
|
64
|
-
Be aware that in order to improve testing ergonomics, the `type` validation will return `true` for _any_ `RSpec::Mocks::` subclass _as long as `
|
|
64
|
+
Be aware that in order to improve testing ergonomics, the `type` validation will return `true` for _any_ `RSpec::Mocks::` subclass _as long as `Axn.config.env.test?` is `true`_.
|
|
65
65
|
|
|
66
66
|
This makes it much easier to test Axns, as you can pass in mocks without immediately failing the inbound validation.
|
|
67
67
|
|
|
68
68
|
```ruby
|
|
69
69
|
subject(:result) { action.call!(sym:) }
|
|
70
70
|
|
|
71
|
-
let(:action) {
|
|
71
|
+
let(:action) { build_axn { expects :sym, type: Symbol } }
|
|
72
72
|
|
|
73
73
|
context "with a symbol" do
|
|
74
74
|
let(:sym) { :hello }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Validating _user_ input
|
|
2
2
|
|
|
3
3
|
::: danger ALPHA
|
|
4
|
-
This has not yet been fully fleshed out. For now, the general idea is that user-facing validation is a _separate layer_ from the declarative expectations about what inputs your
|
|
4
|
+
This has not yet been fully fleshed out. For now, the general idea is that user-facing validation is a _separate layer_ from the declarative expectations about what inputs your Axn takes (e.g. `expects :params` and pass to a form object, rather than accepting field-level params directly).
|
|
5
5
|
:::
|
|
6
6
|
|
|
7
7
|
|