axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.3

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +43 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +41 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. data/yarn.lock +0 -1166
@@ -0,0 +1,355 @@
1
+ # Profiling
2
+
3
+ Axn supports performance profiling using [Vernier](https://github.com/Shopify/vernier), a Ruby sampling profiler that provides detailed insights into your action's performance characteristics.
4
+
5
+ ## Overview
6
+
7
+ Profiling helps you identify performance bottlenecks in your actions by capturing detailed execution traces. Vernier is particularly useful for:
8
+
9
+ - Identifying slow methods and code paths
10
+ - Understanding memory allocation patterns
11
+ - Analyzing call stacks and execution flow
12
+ - Optimizing performance-critical actions
13
+
14
+ ## Setup
15
+
16
+ ### 1. Install Vernier
17
+
18
+ Add the Vernier gem to your Gemfile:
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem 'vernier', '~> 0.1'
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ **Note:** Vernier is not included as a dependency of Axn, so you must explicitly add it to your Gemfile if you want to use profiling features.
32
+
33
+ ### 2. Enable Profiling
34
+
35
+ No global configuration is needed! Simply call `profile` on the actions you want to profile.
36
+
37
+ ## Basic Usage
38
+
39
+ Profiling is enabled per-action by calling the `profile` method. You can only call `profile` **once per action** - subsequent calls will override the previous one. This prevents accidental profiling of all actions and ensures you only profile what you intend to analyze.
40
+
41
+ ### Simple Profiling
42
+
43
+ Enable profiling on any action:
44
+
45
+ ```ruby
46
+ class UserCreation
47
+ include Axn
48
+
49
+ # Always profile this action
50
+ profile
51
+
52
+ expects :user_params
53
+
54
+ def call
55
+ user = User.create!(user_params)
56
+ send_welcome_email(user)
57
+ end
58
+
59
+ private
60
+
61
+ def send_welcome_email(user)
62
+ UserMailer.welcome(user).deliver_now
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Conditional Profiling
68
+
69
+ Profile only under specific conditions:
70
+
71
+ ```ruby
72
+ class DataProcessing
73
+ include Axn
74
+
75
+ # Profile only when processing large datasets (only one profile call per action)
76
+ profile if: -> { record_count > 1000 }
77
+
78
+ expects :records, :record_count
79
+
80
+ def call
81
+ records.each { |record| process_record(record) }
82
+ end
83
+ end
84
+ ```
85
+
86
+ **Alternative using a method:**
87
+
88
+ ```ruby
89
+ class DataProcessing
90
+ include Axn
91
+
92
+ # Profile using a method (only one profile call per action)
93
+ profile if: :should_profile?
94
+
95
+ expects :records, :record_count, :debug_mode, type: :boolean, default: false
96
+
97
+ def should_profile?
98
+ record_count > 1000 || debug_mode
99
+ end
100
+
101
+ def call
102
+ records.each { |record| process_record(record) }
103
+ end
104
+ end
105
+ ```
106
+
107
+
108
+ ## Advanced Usage
109
+
110
+ ### Sampling Rate Control
111
+
112
+ Adjust the sampling rate per action:
113
+
114
+ ```ruby
115
+ class DevelopmentAction
116
+ include Axn
117
+
118
+ # High sampling rate for development (more detailed data)
119
+ profile(sample_rate: 0.5) if Rails.env.development?
120
+
121
+ def call
122
+ # Action logic
123
+ end
124
+ end
125
+
126
+ class ProductionAction
127
+ include Axn
128
+
129
+ # Low sampling rate for production (minimal overhead)
130
+ profile(sample_rate: 0.01) if Rails.env.production?
131
+
132
+ def call
133
+ # Action logic
134
+ end
135
+ end
136
+ ```
137
+
138
+ ### Custom Output Directory
139
+
140
+ Organize profiles by environment or feature:
141
+
142
+ ```ruby
143
+ class MyAction
144
+ include Axn
145
+
146
+ # Custom output directory
147
+ profile(output_dir: Rails.root.join("tmp", "profiles", Rails.env))
148
+
149
+ def call
150
+ # Action logic
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### Multiple Conditions
156
+
157
+ Combine multiple profiling conditions:
158
+
159
+ ```ruby
160
+ class ComplexAction
161
+ include Axn
162
+
163
+ # Profile when debug mode is enabled OR when processing admin users
164
+ profile if: -> { debug_mode || user.admin? }
165
+
166
+ expects :user, :debug_mode, type: :boolean, default: false
167
+
168
+ def call
169
+ # Complex logic
170
+ end
171
+ end
172
+ ```
173
+
174
+ ## Viewing and Analyzing Profiles
175
+
176
+ ### 1. Generate Profile Data
177
+
178
+ Run your action with profiling enabled:
179
+
180
+ ```ruby
181
+ # This will generate a profile file if conditions are met
182
+ result = UserCreation.call(user_params: { name: "John", email: "john@example.com" })
183
+ ```
184
+
185
+ ### 2. Locate Profile Files
186
+
187
+ Profile files are saved as JSON in your configured output directory:
188
+
189
+ ```bash
190
+ # Default location
191
+ ls tmp/profiles/
192
+
193
+ # Example output
194
+ axn_UserCreation_1703123456.json
195
+ axn_DataProcessing_1703123457.json
196
+ ```
197
+
198
+ ### 3. View in Firefox Profiler
199
+
200
+ 1. Open [profiler.firefox.com](https://profiler.firefox.com/)
201
+ 2. Click "Load a profile from file"
202
+ 3. Select your generated JSON file
203
+ 4. Analyze the performance data
204
+
205
+ ### 4. Understanding the Profile
206
+
207
+ The Firefox Profiler provides several views:
208
+
209
+ - **Call Tree**: Shows the complete call stack with timing
210
+ - **Flame Graph**: Visual representation of call stacks
211
+ - **Stack Chart**: Timeline view of function calls
212
+ - **Markers**: Custom markers and events
213
+
214
+ ## Best Practices
215
+
216
+ ### 1. Use Conditional Profiling
217
+
218
+ Avoid profiling all actions in production:
219
+
220
+ ```ruby
221
+ # Good: Conditional profiling
222
+ profile if: -> { Rails.env.development? || debug_mode }
223
+
224
+ # Avoid: Always profiling in production
225
+ profile # This can impact performance
226
+ ```
227
+
228
+ ### 2. Appropriate Sampling Rates
229
+
230
+ Choose sampling rates based on your environment:
231
+
232
+ ```ruby
233
+ class MyAction
234
+ include Axn
235
+
236
+ # High detail for debugging
237
+ profile(sample_rate: 0.5) if Rails.env.development?
238
+
239
+ # Moderate sampling for staging
240
+ profile(sample_rate: 0.1) if Rails.env.staging?
241
+
242
+ # Minimal overhead for production
243
+ profile(sample_rate: 0.01) if Rails.env.production?
244
+
245
+ def call
246
+ # Action logic
247
+ end
248
+ end
249
+ ```
250
+
251
+ ### 3. Profile Specific Scenarios
252
+
253
+ Focus on performance-critical paths:
254
+
255
+ ```ruby
256
+ class OrderProcessing
257
+ include Axn
258
+
259
+ # Profile only expensive operations
260
+ profile if: -> { order.total > 1000 }
261
+
262
+ expects :order
263
+
264
+ def call
265
+ process_payment
266
+ send_confirmation
267
+ update_inventory
268
+ end
269
+ end
270
+ ```
271
+
272
+ ### 4. Clean Up Old Profiles
273
+
274
+ Implement profile cleanup to avoid disk space issues:
275
+
276
+ ```ruby
277
+ # Add to a rake task or cron job
278
+ namespace :profiles do
279
+ desc "Clean up old profile files"
280
+ task cleanup: :environment do
281
+ profile_dir = Rails.root.join("tmp", "profiles")
282
+ Dir.glob(File.join(profile_dir, "*.json")).each do |file|
283
+ File.delete(file) if File.mtime(file) < 7.days.ago
284
+ end
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## Troubleshooting
290
+
291
+ ### Vernier Not Available
292
+
293
+ If you see this error:
294
+
295
+ ```
296
+ LoadError: Vernier gem is not loaded. Add `gem 'vernier', '~> 0.1'` to your Gemfile to enable profiling.
297
+ ```
298
+
299
+ Make sure to:
300
+ 1. Add `vernier` to your Gemfile
301
+ 2. Run `bundle install`
302
+ 3. Restart your application
303
+
304
+ ### No Profile Files Generated
305
+
306
+ If profile files aren't being generated:
307
+
308
+ 1. Verify your action has `profile` enabled
309
+ 2. Ensure profiling conditions are met
310
+ 3. Check the output directory exists and is writable
311
+
312
+ ### Performance Impact
313
+
314
+ Profiling adds overhead to your application:
315
+
316
+ - **Sampling overhead**: ~1-5% depending on sample rate
317
+ - **File I/O**: Profile files are written to disk
318
+ - **Memory usage**: Slight increase due to sampling
319
+
320
+ Use appropriate sampling rates and conditional profiling to minimize impact.
321
+
322
+ ## Integration with Other Tools
323
+
324
+ ### Datadog Integration
325
+
326
+ Combine profiling with Datadog tracing:
327
+
328
+ ```ruby
329
+ Axn.configure do |c|
330
+ # Datadog tracing
331
+ c.wrap_with_trace = proc do |resource, &action|
332
+ Datadog::Tracing.trace("Action", resource:) do
333
+ action.call
334
+ end
335
+ end
336
+ end
337
+
338
+ class MyAction
339
+ include Axn
340
+
341
+ # Profiling with custom options
342
+ profile(sample_rate: 0.1)
343
+
344
+ def call
345
+ # Action logic
346
+ end
347
+ end
348
+ ```
349
+
350
+ ## Resources
351
+
352
+ - [Vernier GitHub Repository](https://github.com/Shopify/vernier)
353
+ - [Firefox Profiler](https://profiler.firefox.com/)
354
+ - [Ruby Performance Optimization Guide](https://ruby-doc.org/core-3.2.1/doc/performance_rdoc.html)
355
+ - [Axn Configuration Reference](/reference/configuration)
@@ -8,7 +8,7 @@
8
8
 
9
9
  * `context_for_logging` (and decent #inspect support)
10
10
 
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`))
11
+ * Configuring logging (will default to Rails.logger if available, else fall back to basic Logger (but can explicitly set via e.g. `Axn.config.logger = Logger.new($stdout`))
12
12
 
13
13
  * Note `context_for_logging` is available (filtered to accessible attrs, filtering out sensitive values). Automatically passed into `on_exception` hook.
14
14
 
data/docs/index.md CHANGED
@@ -22,11 +22,13 @@ hero:
22
22
 
23
23
  features:
24
24
  - title: Declarative interface
25
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
25
+ details: Clear, explicit contracts for inputs and outputs with `expects` and `exposes`
26
26
  - title: Exception swallowing
27
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
27
+ details: Automatic error handling with user-safe error messages and internal logging
28
+ - title: Advanced Patterns
29
+ details: Mountable actions, workflow composition, and background processing capabilities
28
30
  - title: Default Observability
29
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
31
+ details: Built-in logging, timing, and error tracking out of the box
30
32
  ---
31
33
 
32
34
  ::: danger ALPHA RELEASE
data/docs/intro/about.md CHANGED
@@ -17,7 +17,7 @@ A simple, declarative core API. Concise enough to pick up quickly, but sufficien
17
17
  - Consistent return interface (including exception swallowing)
18
18
  - Clear distinction between user-facing and internal errors
19
19
  - Minimal boilerplate
20
- - Easy backgrounding (no need for a separate Worker class just to wrap a service call)
20
+ - Easy async execution (no need for a separate Worker class just to wrap a service call)
21
21
 
22
22
  **Additional benefits devs get for free:**
23
23
 
@@ -17,7 +17,7 @@ Your logic goes in a <abbr title="Plain Old Ruby Object">PORO</abbr>. The only r
17
17
 
18
18
  ```ruby
19
19
  class Foo
20
- include Action
20
+ include Axn
21
21
 
22
22
  def call
23
23
  log "Doesn't do much, but this technically works..."
@@ -31,7 +31,7 @@ Most actions require inputs, and many return values to the caller; no need for a
31
31
 
32
32
  * `expects :foo` to declare inputs the class expects to receive.
33
33
 
34
- You pass the `expect`ed keyword arguments to `call`, then reference their values as local `attr_reader`s.
34
+ You pass the `expect`ed keyword arguments to `call`, then reference their values as local `attr_reader`s. Use `optional: true` for fields that may be missing or blank.
35
35
 
36
36
  * `exposes :bar` to declare any outputs the class will expose.
37
37
 
@@ -52,7 +52,7 @@ If any declared expectations or exposures are _not_ met the action will fail, se
52
52
 
53
53
  ```ruby
54
54
  class Actions::Slack::Post
55
- include Action
55
+ include Axn
56
56
  VALID_CHANNELS = [ ... ]
57
57
 
58
58
  expects :channel, default: VALID_CHANNELS.first, inclusion: { in: VALID_CHANNELS } # [!code focus:4]
@@ -76,7 +76,7 @@ end
76
76
  ## Return interface {#return-interface}
77
77
 
78
78
 
79
- The return value of an Action call is always an `Action::Result`, which provides a consistent interface:
79
+ The return value of an Axn call is always an `Axn::Result`, which provides a consistent interface:
80
80
 
81
81
  * `ok?` will return a boolean (false if any errors or exceptions occurred, otherwise true)
82
82
  * if OK, `success` will return a string that is _safe to show end users_
@@ -140,7 +140,7 @@ When configuring custom error and success messages, remember to define your stat
140
140
 
141
141
  ```ruby
142
142
  class MyAction
143
- include Action
143
+ include Axn
144
144
 
145
145
  # Static fallback messages first
146
146
  success "Default success message"
@@ -6,7 +6,7 @@ For instance, at Teamshares we automatically add memoization support (via [memo_
6
6
 
7
7
 
8
8
  ```ruby
9
- Action.configure do |c|
9
+ Axn.configure do |c|
10
10
  c.additional_includes = [TS::Memoization]
11
11
  end
12
12
  ```
@@ -29,7 +29,7 @@ And with those pieces in place `memo` is available in all Actions:
29
29
 
30
30
  ```ruby
31
31
  class ContrivedExample
32
- include Action
32
+ include Axn
33
33
 
34
34
  exposes :nums
35
35