next_station 0.1.0
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 +7 -0
- data/.aiignore +36 -0
- data/.idea/.gitignore +10 -0
- data/.idea/inspectionProfiles/Project_Default.xml +8 -0
- data/.idea/junie.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/next_station.iml +54 -0
- data/.idea/vcs.xml +6 -0
- data/AGENTS.md +157 -0
- data/Gemfile +11 -0
- data/PLUGIN_SYSTEM_GUIDE.md +521 -0
- data/README.md +790 -0
- data/TODO.txt +6 -0
- data/examples/plugin_http_example.rb +102 -0
- data/lib/next_station/config/errors.yml +149 -0
- data/lib/next_station/config.rb +49 -0
- data/lib/next_station/environment.rb +42 -0
- data/lib/next_station/errors.rb +21 -0
- data/lib/next_station/logging/formatters/console.rb +38 -0
- data/lib/next_station/logging/formatters/json.rb +80 -0
- data/lib/next_station/logging/subscribers/base.rb +70 -0
- data/lib/next_station/logging/subscribers/custom.rb +25 -0
- data/lib/next_station/logging/subscribers/operation.rb +41 -0
- data/lib/next_station/logging/subscribers/step.rb +54 -0
- data/lib/next_station/logging.rb +35 -0
- data/lib/next_station/operation/class_methods.rb +299 -0
- data/lib/next_station/operation/errors.rb +97 -0
- data/lib/next_station/operation/node.rb +49 -0
- data/lib/next_station/operation.rb +393 -0
- data/lib/next_station/plugins.rb +23 -0
- data/lib/next_station/result.rb +124 -0
- data/lib/next_station/state.rb +64 -0
- data/lib/next_station/types.rb +11 -0
- data/lib/next_station/version.rb +5 -0
- data/lib/next_station.rb +36 -0
- metadata +203 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# NextStation Plugin System Guide
|
|
2
|
+
|
|
3
|
+
NextStation's Plugin System allows you to extend the core functionality of operations without modifying the gem itself. This guide explains how the system works and how to design and implement your own plugins.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
A plugin in NextStation is a standard Ruby module. When you call `plugin :my_plugin` in an `Operation`, NextStation:
|
|
8
|
+
1. Loads the module registered as `:my_plugin`.
|
|
9
|
+
2. Extends the `Operation` class with methods from `MyPlugin::ClassMethods`.
|
|
10
|
+
3. Includes `MyPlugin::InstanceMethods` into the `Operation` instance.
|
|
11
|
+
4. Includes `MyPlugin::DSL` into `NextStation::Operation::Node`, making new methods available in the `process` block.
|
|
12
|
+
5. Extends the `NextStation::State` instance with `MyPlugin::State`.
|
|
13
|
+
6. Automatically registers errors defined in `MyPlugin::Errors`.
|
|
14
|
+
7. Calls `MyPlugin.configure(operation_class)` for any additional setup.
|
|
15
|
+
8. Registers lifecycle hooks if the plugin module responds to them.
|
|
16
|
+
|
|
17
|
+
## Designing a Plugin
|
|
18
|
+
|
|
19
|
+
### 1. Structure
|
|
20
|
+
|
|
21
|
+
Your plugin should follow this structure:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
module MyPlugin
|
|
25
|
+
# Extends the Operation class
|
|
26
|
+
module ClassMethods
|
|
27
|
+
# We use the extended hook to add configuration via Dry::Configurable
|
|
28
|
+
def self.extended(base)
|
|
29
|
+
base.extend Dry::Configurable
|
|
30
|
+
|
|
31
|
+
base.instance_eval do
|
|
32
|
+
setting :my_plugin do
|
|
33
|
+
setting :api_key
|
|
34
|
+
setting :timeout, default: 30
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Included in the Operation instance
|
|
41
|
+
module InstanceMethods
|
|
42
|
+
def my_instance_helper
|
|
43
|
+
"Hello from instance"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Example of an InstanceMethod that enriches data
|
|
47
|
+
def enrich_user_data(state)
|
|
48
|
+
user_id = state[:user_id]
|
|
49
|
+
# Logic to fetch additional user data
|
|
50
|
+
state[:user_full_name] = "John Doe" # Fetch from DB or external service
|
|
51
|
+
state
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Example of an InstanceMethod that interacts with an external service
|
|
55
|
+
def notify_external_system(state, payload)
|
|
56
|
+
# Logic to send a notification to an external system
|
|
57
|
+
# MyExternalService.send(payload)
|
|
58
|
+
state
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handler for DSL wrappers
|
|
62
|
+
def run_my_wrapper(node, state)
|
|
63
|
+
# Custom logic before
|
|
64
|
+
result = execute_nodes(node.children, state)
|
|
65
|
+
# Custom logic after
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extended into the Node class (DSL in process block)
|
|
71
|
+
module DSL
|
|
72
|
+
def my_wrapper(&block)
|
|
73
|
+
add_child(NextStation::Operation::Node.new(:wrapper, nil, { handler: :run_wrapper }, &block))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Mixed into the State instance during initialization
|
|
78
|
+
module State
|
|
79
|
+
def plugin_state_helper
|
|
80
|
+
"useful data"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Automatically registers errors
|
|
85
|
+
module Errors
|
|
86
|
+
def self.definitions
|
|
87
|
+
{
|
|
88
|
+
my_plugin_error: {
|
|
89
|
+
message: { en: "Something went wrong in the plugin" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Lifecycle Hooks
|
|
96
|
+
def self.on_operation_start(operation, state)
|
|
97
|
+
# logic
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.on_operation_stop(operation, result)
|
|
101
|
+
# logic
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.around_step(operation, node, state)
|
|
105
|
+
# logic before
|
|
106
|
+
result = yield
|
|
107
|
+
# logic after
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Configuration Hook
|
|
112
|
+
def self.configure(operation_class)
|
|
113
|
+
# Called when the plugin is loaded into a class
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Register it
|
|
118
|
+
NextStation::Plugins.register(:my_plugin, MyPlugin)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Lifecycle Hooks
|
|
122
|
+
|
|
123
|
+
NextStation provides several hooks that your plugin can implement to intercept different stages of an operation's execution.
|
|
124
|
+
|
|
125
|
+
#### `on_operation_start(operation, state)`
|
|
126
|
+
Called once at the very beginning of the `call` method, before any steps are executed. Useful for initializing plugin-specific state or logging.
|
|
127
|
+
|
|
128
|
+
**Example: Performance Monitoring**
|
|
129
|
+
```ruby
|
|
130
|
+
def self.on_operation_start(operation, state)
|
|
131
|
+
state[:_start_time] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### `on_operation_stop(operation, result)`
|
|
136
|
+
Called once after the operation completes, whether it succeeded, failed, or was halted.
|
|
137
|
+
|
|
138
|
+
**Example: Log Operation Result**
|
|
139
|
+
```ruby
|
|
140
|
+
def self.on_operation_stop(operation, result)
|
|
141
|
+
status = result.success? ? "SUCCESS" : "FAILURE"
|
|
142
|
+
operation.publish_log(:info, "Operation #{operation.class.name} finished with status: #{status}")
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### `around_step(operation, node, state)`
|
|
147
|
+
Wraps the execution of every single step. **Important:** You must `yield` to allow the step (and other plugins) to execute. It receives the `node`, which contains the step `name` and `options`.
|
|
148
|
+
|
|
149
|
+
**Example: Step Execution Logging**
|
|
150
|
+
```ruby
|
|
151
|
+
def self.around_step(operation, node, state)
|
|
152
|
+
operation.publish_log(:debug, "Starting step: #{node.name}")
|
|
153
|
+
result = yield
|
|
154
|
+
operation.publish_log(:debug, "Finished step: #{node.name}")
|
|
155
|
+
result
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `on_step_success(operation, node, state)`
|
|
160
|
+
Called after a step method returns successfully (and returns a `State` object).
|
|
161
|
+
|
|
162
|
+
**Example: Audit Trail**
|
|
163
|
+
```ruby
|
|
164
|
+
def self.on_step_success(operation, node, state)
|
|
165
|
+
state[:audit_trail] ||= []
|
|
166
|
+
state[:audit_trail] << { step: node.name, timestamp: Time.now }
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### `on_step_failure(operation, node, state, error)`
|
|
171
|
+
Called when a step raises an exception. This is called *before* the exception is re-raised or handled by retry logic.
|
|
172
|
+
|
|
173
|
+
**Example: Error Reporting**
|
|
174
|
+
```ruby
|
|
175
|
+
def self.on_step_failure(operation, node, state, error)
|
|
176
|
+
# Sentry.capture_exception(error, extra: { step: node.name, state: state.to_h })
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Advanced Plugin Design
|
|
183
|
+
|
|
184
|
+
To create truly powerful plugins, you can leverage state extensions, custom errors, and configuration parameters.
|
|
185
|
+
|
|
186
|
+
### 1. Extending State
|
|
187
|
+
The `State` module in your plugin is mixed into every `NextStation::State` instance created for the operation. Use this to provide helper methods that simplify step logic.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
module MyPlugin
|
|
191
|
+
module State
|
|
192
|
+
def authenticated?
|
|
193
|
+
context[:current_user].present?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def admin?
|
|
197
|
+
authenticated? && context[:current_user].admin?
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
*Usage in a step:*
|
|
203
|
+
```ruby
|
|
204
|
+
def check_permission(state)
|
|
205
|
+
error!(:unauthorized) unless state.admin?
|
|
206
|
+
state
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 2. Custom Errors and Parameters
|
|
211
|
+
Plugins can register errors and even allow users to configure them.
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
module MyPlugin
|
|
215
|
+
module Errors
|
|
216
|
+
def self.definitions
|
|
217
|
+
{
|
|
218
|
+
plugin_error: {
|
|
219
|
+
message: { en: "Operation failed in %{plugin_name}: %{reason}" }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
*Usage in a plugin's InstanceMethods:*
|
|
227
|
+
```ruby
|
|
228
|
+
module InstanceMethods
|
|
229
|
+
def fail_with_reason(reason)
|
|
230
|
+
error!(type: :plugin_error, msg_keys: { plugin_name: "MyPlugin", reason: reason })
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 3. Instance Methods
|
|
236
|
+
The `InstanceMethods` module in your plugin is included in every `Operation` instance that uses the plugin. This is the place for shared logic that doesn't belong to a specific step, as well as for implementing **wrapper handlers**.
|
|
237
|
+
|
|
238
|
+
#### Helper Methods
|
|
239
|
+
You can define methods that simplify common tasks across different operations or steps. These methods have access to all the internals of the `Operation` class.
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
module MyPlugin
|
|
243
|
+
module InstanceMethods
|
|
244
|
+
# An instance helper that can be called from any step
|
|
245
|
+
def build_payload(state, user_id)
|
|
246
|
+
{
|
|
247
|
+
user_id: user_id,
|
|
248
|
+
timestamp: Time.now.iso8601,
|
|
249
|
+
source: self.class.name
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# A helper to handle common error conditions
|
|
254
|
+
def fail_gracefully!(reason)
|
|
255
|
+
error!(type: :plugin_error, msg_keys: { reason: reason })
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
*Usage in a step:*
|
|
261
|
+
```ruby
|
|
262
|
+
def some_step(state)
|
|
263
|
+
payload = build_payload(state, state[:user_id])
|
|
264
|
+
# ExternalService.send(payload)
|
|
265
|
+
state
|
|
266
|
+
rescue => e
|
|
267
|
+
fail_gracefully!(e.message)
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Wrapper Handlers
|
|
272
|
+
If your plugin provides a custom DSL (like a `transaction` block), the `InstanceMethods` module is where you implement the logic to handle that wrapper.
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
module MyPlugin
|
|
276
|
+
module InstanceMethods
|
|
277
|
+
def run_my_wrapper(node, state)
|
|
278
|
+
# 1. Access node options provided in the DSL
|
|
279
|
+
timeout = node.options[:timeout] || 10
|
|
280
|
+
|
|
281
|
+
# 2. Perform custom logic before executing inner steps
|
|
282
|
+
# MyService.start_timer(timeout)
|
|
283
|
+
|
|
284
|
+
# 3. Execute the children steps within this block
|
|
285
|
+
result_state = execute_nodes(node.children, state)
|
|
286
|
+
|
|
287
|
+
# 4. Perform custom logic after execution
|
|
288
|
+
# MyService.stop_timer
|
|
289
|
+
|
|
290
|
+
result_state
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 4. Plugin Configuration
|
|
297
|
+
The recommended way to handle plugin configuration is using `Dry::Configurable`. NextStation ensures that the `config` object is available for your plugin to extend.
|
|
298
|
+
|
|
299
|
+
Using `Dry::Configurable` provides a standardized, thread-safe way to define settings with defaults and nested namespaces.
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
module MyPlugin
|
|
303
|
+
module ClassMethods
|
|
304
|
+
def self.extended(base)
|
|
305
|
+
# Extend the operation class with configuration
|
|
306
|
+
base.extend Dry::Configurable
|
|
307
|
+
|
|
308
|
+
base.instance_eval do
|
|
309
|
+
# It is highly recommended to namespace your plugin settings
|
|
310
|
+
setting :my_plugin do
|
|
311
|
+
setting :timeout, default: 30
|
|
312
|
+
setting :retries, default: 3
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
*Usage in an Operation:*
|
|
320
|
+
```ruby
|
|
321
|
+
class MyOperation < NextStation::Operation
|
|
322
|
+
plugin :my_plugin
|
|
323
|
+
|
|
324
|
+
# Configure your plugin using the standardized DSL
|
|
325
|
+
config.my_plugin.timeout = 60
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
While you can still use plain Ruby class methods and instance variables for configuration, `Dry::Configurable` is the preferred approach for consistency and robustness.
|
|
330
|
+
|
|
331
|
+
### 4. Designing instructions best practices:
|
|
332
|
+
When designing a plugin:
|
|
333
|
+
1. **Be Explicit**: Use clear naming conventions for hooks and methods.
|
|
334
|
+
2. **Document Options**: If your DSL methods take options, document them clearly in the `DSL` module.
|
|
335
|
+
3. **Use Namespace**: Prefix plugin-specific state keys (e.g., `state[:_my_plugin_data]`) to avoid collisions.
|
|
336
|
+
4. **Leverage Context**: Use `state.context` for read-only global data and `state` for mutable operation-specific data.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Using Plugin Features (End-User Perspective)
|
|
341
|
+
|
|
342
|
+
When a plugin is loaded into an operation, its features are seamlessly integrated. Here is how a developer using your plugin will interact with its components.
|
|
343
|
+
|
|
344
|
+
### 1. Calling Instance Helpers from Steps
|
|
345
|
+
|
|
346
|
+
The methods defined in the plugin's `InstanceMethods` are directly available within your operation instance. You can call them from any step method.
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
class RegisterUser < NextStation::Operation
|
|
350
|
+
plugin :my_plugin # This plugin provides `build_payload` and `fail_gracefully!`
|
|
351
|
+
|
|
352
|
+
process do
|
|
353
|
+
step :process_user
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def process_user(state)
|
|
357
|
+
# 1. Using a helper method from the plugin
|
|
358
|
+
payload = build_payload(state, state[:user_id])
|
|
359
|
+
|
|
360
|
+
# ... logic to use the payload ...
|
|
361
|
+
|
|
362
|
+
state
|
|
363
|
+
rescue => e
|
|
364
|
+
# 2. Using another helper to handle errors consistently
|
|
365
|
+
fail_gracefully!(e.message)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 2. Using State Extensions
|
|
371
|
+
|
|
372
|
+
Methods from the plugin's `State` module are mixed into the `state` object. This makes your steps much more readable by moving complex logic into the state itself.
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
class DeleteArticle < NextStation::Operation
|
|
376
|
+
plugin :auth_plugin # This plugin provides `admin?` on the state object
|
|
377
|
+
|
|
378
|
+
process do
|
|
379
|
+
step :authorize
|
|
380
|
+
step :delete
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def authorize(state)
|
|
384
|
+
# The `admin?` method is provided by the auth_plugin's State module
|
|
385
|
+
unless state.admin?
|
|
386
|
+
error!(:unauthorized)
|
|
387
|
+
end
|
|
388
|
+
state
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def delete(state)
|
|
392
|
+
# ... delete logic ...
|
|
393
|
+
state
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 3. Using DSL Wrappers in the Process Block
|
|
399
|
+
|
|
400
|
+
DSL methods provided by the plugin's `DSL` module allow you to wrap steps or groups of steps, providing powerful flow control or automatic behavior (like transactions or logging).
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
class UpdateOrder < NextStation::Operation
|
|
404
|
+
plugin :transactional # This plugin provides the `transaction` block
|
|
405
|
+
|
|
406
|
+
process do
|
|
407
|
+
step :validate
|
|
408
|
+
|
|
409
|
+
# Everything inside this block is wrapped in a DB transaction
|
|
410
|
+
transaction do
|
|
411
|
+
step :update_stock
|
|
412
|
+
step :capture_payment
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
step :notify_user
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 4. Configuring the Plugin
|
|
421
|
+
|
|
422
|
+
As an end-user, you configure the plugin at the class level using the `config` object. These settings are then used by the plugin's internal logic.
|
|
423
|
+
|
|
424
|
+
```ruby
|
|
425
|
+
class ImportData < NextStation::Operation
|
|
426
|
+
plugin :api_client_plugin
|
|
427
|
+
|
|
428
|
+
# Configure plugin-specific settings
|
|
429
|
+
config.api_client.timeout = 60
|
|
430
|
+
config.api_client.retries = 5
|
|
431
|
+
|
|
432
|
+
process do
|
|
433
|
+
step :fetch_remote_data
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Example: ActiveRecord Transaction Plugin
|
|
441
|
+
|
|
442
|
+
This plugin wraps a group of steps in a database transaction.
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
module NextStation
|
|
446
|
+
module Plugins
|
|
447
|
+
module Transactional
|
|
448
|
+
module DSL
|
|
449
|
+
def transaction(&block)
|
|
450
|
+
add_child(
|
|
451
|
+
NextStation::Operation::Node.new(
|
|
452
|
+
:wrapper,
|
|
453
|
+
nil,
|
|
454
|
+
{ handler: :run_transaction },
|
|
455
|
+
&block
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
module InstanceMethods
|
|
462
|
+
def run_transaction(node, state)
|
|
463
|
+
ActiveRecord::Base.transaction do
|
|
464
|
+
execute_nodes(node.children, state)
|
|
465
|
+
end
|
|
466
|
+
rescue ActiveRecord::Rollback
|
|
467
|
+
# The operation flow will stop because steps inside
|
|
468
|
+
# are executed within execute_nodes.
|
|
469
|
+
state
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
module State
|
|
474
|
+
def in_transaction?
|
|
475
|
+
# You could implement logic to check transaction depth
|
|
476
|
+
true
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
module Errors
|
|
481
|
+
def self.definitions
|
|
482
|
+
{
|
|
483
|
+
transaction_error: {
|
|
484
|
+
message: { en: "Transaction failed: %{message}" }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
register(:transactional, Transactional)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Usage:
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
class CreateUser < NextStation::Operation
|
|
500
|
+
plugin :transactional
|
|
501
|
+
|
|
502
|
+
process do
|
|
503
|
+
step :validate
|
|
504
|
+
transaction do
|
|
505
|
+
step :create_user
|
|
506
|
+
step :create_profile
|
|
507
|
+
end
|
|
508
|
+
step :send_welcome_email
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# ... step definitions ...
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Tips for Plugin Developers
|
|
516
|
+
|
|
517
|
+
1. **Thread Safety**: Ensure your plugin does not use mutable global state.
|
|
518
|
+
2. **Inheritance**: NextStation handles plugin inheritance. If a parent class uses a plugin, the subclass will also have it.
|
|
519
|
+
3. **Namespace**: Use a unique namespace for your plugin methods to avoid collisions with other plugins or core NextStation methods.
|
|
520
|
+
4. **Error Handling**: Use the `Errors` module to register custom errors. This allows users to customize the messages in their operations.
|
|
521
|
+
5. **State Extension**: Be careful when extending `State`. Only add methods that are genuinely useful for steps across many operations.
|