bunny_farm 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/docs.yml +38 -0
- data/.gitignore +11 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +61 -0
- data/COMMITS.md +196 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/Rakefile +9 -0
- data/bunny_farm.gemspec +30 -0
- data/config/bunny.yml.erb +29 -0
- data/config/bunny_test.yml.erb +29 -0
- data/config/hipchat.yml.erb +12 -0
- data/docs/api/configuration.md +9 -0
- data/docs/api/consumer.md +8 -0
- data/docs/api/message-class.md +419 -0
- data/docs/api/publisher.md +9 -0
- data/docs/architecture/integration.md +8 -0
- data/docs/architecture/message-flow.md +11 -0
- data/docs/architecture/overview.md +448 -0
- data/docs/architecture/scaling.md +8 -0
- data/docs/assets/actions_dsl_flow.svg +109 -0
- data/docs/assets/architecture_overview.svg +152 -0
- data/docs/assets/best_practices_patterns.svg +203 -0
- data/docs/assets/bunny_farm_logo.png +0 -0
- data/docs/assets/configuration_api_methods.svg +104 -0
- data/docs/assets/configuration_flow.svg +130 -0
- data/docs/assets/configuration_hierarchy.svg +70 -0
- data/docs/assets/data_processing_pipeline.svg +131 -0
- data/docs/assets/debugging_monitoring.svg +165 -0
- data/docs/assets/ecommerce_example_flow.svg +145 -0
- data/docs/assets/email_campaign_example.svg +127 -0
- data/docs/assets/environment_variables_map.svg +78 -0
- data/docs/assets/error_handling_flow.svg +114 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/fields_dsl_structure.svg +89 -0
- data/docs/assets/instance_methods_lifecycle.svg +137 -0
- data/docs/assets/integration_patterns.svg +207 -0
- data/docs/assets/json_serialization_flow.svg +153 -0
- data/docs/assets/logo.svg +4 -0
- data/docs/assets/message_api_overview.svg +126 -0
- data/docs/assets/message_encapsulation.svg +113 -0
- data/docs/assets/message_lifecycle.svg +110 -0
- data/docs/assets/message_structure.svg +138 -0
- data/docs/assets/publisher_consumer_api.svg +120 -0
- data/docs/assets/scaling_deployment_patterns.svg +195 -0
- data/docs/assets/smart_routing_diagram.svg +131 -0
- data/docs/assets/system_architecture_overview.svg +155 -0
- data/docs/assets/task_scheduling_flow.svg +139 -0
- data/docs/assets/testing_strategies.svg +146 -0
- data/docs/assets/workflow_patterns.svg +183 -0
- data/docs/assets/yaml_config_structure.svg +72 -0
- data/docs/configuration/environment-variables.md +14 -0
- data/docs/configuration/overview.md +373 -0
- data/docs/configuration/programmatic-setup.md +10 -0
- data/docs/configuration/yaml-configuration.md +12 -0
- data/docs/core-features/configuration.md +528 -0
- data/docs/core-features/error-handling.md +82 -0
- data/docs/core-features/json-serialization.md +545 -0
- data/docs/core-features/message-design.md +406 -0
- data/docs/core-features/smart-routing.md +467 -0
- data/docs/core-features/task-scheduling.md +67 -0
- data/docs/core-features/workflow-support.md +112 -0
- data/docs/development/contributing.md +345 -0
- data/docs/development/roadmap.md +9 -0
- data/docs/development/testing.md +14 -0
- data/docs/examples/order-processing.md +10 -0
- data/docs/examples/overview.md +269 -0
- data/docs/examples/real-world.md +8 -0
- data/docs/examples/simple-producer-consumer.md +15 -0
- data/docs/examples/task-scheduler.md +9 -0
- data/docs/getting-started/basic-concepts.md +274 -0
- data/docs/getting-started/installation.md +122 -0
- data/docs/getting-started/quick-start.md +158 -0
- data/docs/index.md +106 -0
- data/docs/message-structure/actions-dsl.md +163 -0
- data/docs/message-structure/fields-dsl.md +146 -0
- data/docs/message-structure/instance-methods.md +115 -0
- data/docs/message-structure/overview.md +211 -0
- data/examples/README.md +212 -0
- data/examples/consumer.rb +41 -0
- data/examples/images/message_flow.svg +87 -0
- data/examples/images/order_workflow.svg +122 -0
- data/examples/images/producer_consumer.svg +96 -0
- data/examples/images/task_scheduler.svg +140 -0
- data/examples/order_processor.rb +238 -0
- data/examples/producer.rb +60 -0
- data/examples/simple_message.rb +43 -0
- data/examples/task_scheduler.rb +263 -0
- data/images/architecture_overview.svg +152 -0
- data/images/bunny_farm_logo.png +0 -0
- data/images/configuration_flow.svg +130 -0
- data/images/message_structure.svg +138 -0
- data/lib/bunny_farm/.irbrc +7 -0
- data/lib/bunny_farm/generic_consumer.rb +12 -0
- data/lib/bunny_farm/hash_ext.rb +37 -0
- data/lib/bunny_farm/init_bunny.rb +137 -0
- data/lib/bunny_farm/init_hipchat.rb +49 -0
- data/lib/bunny_farm/message.rb +218 -0
- data/lib/bunny_farm/message_elements.rb +25 -0
- data/lib/bunny_farm/version.rb +3 -0
- data/lib/bunny_farm.rb +9 -0
- data/mkdocs.yml +148 -0
- metadata +244 -0
data/Rakefile
ADDED
data/bunny_farm.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
|
|
5
|
+
require 'bunny_farm/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = "bunny_farm"
|
|
9
|
+
spec.version = BunnyFarm::VERSION
|
|
10
|
+
spec.authors = ["Dewayne VanHoozer"]
|
|
11
|
+
spec.email = ["dvanhoozer@gmail.com"]
|
|
12
|
+
|
|
13
|
+
spec.summary = %q{ Simple AMQP/JSON background job manager for RabbitMQ }
|
|
14
|
+
spec.description = %q{ A lightweight Ruby gem for managing background jobs using RabbitMQ. Messages are encapsulated as classes with JSON serialization and routing keys in the format MessageClassName.action for simple, organized job processing. }
|
|
15
|
+
spec.homepage = "https://github.com/MadBomber/bunny_farm"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
|
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_dependency "activesupport"
|
|
22
|
+
spec.add_dependency "concurrent-ruby"
|
|
23
|
+
spec.add_dependency "hashie"
|
|
24
|
+
spec.add_dependency "bunny"
|
|
25
|
+
|
|
26
|
+
spec.add_development_dependency "bundler" #, "~> 1.8"
|
|
27
|
+
spec.add_development_dependency "rake" #, "~> 10.0"
|
|
28
|
+
spec.add_development_dependency "minitest"
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
defaults: &defaults
|
|
2
|
+
host: <%= ENV['AMQP_HOST'] || 'localhost' %>
|
|
3
|
+
vhost: <%= ENV['AMQP_VHOST'] || 'sandbox' %>
|
|
4
|
+
port: <%= (ENV['AMQP_PORT'] || 5672).to_i %>
|
|
5
|
+
user: <%= ENV['AMQP_USER'] || 'guest' %>
|
|
6
|
+
pass: <%= ENV['AMQP_PASS'] || 'guest' %>
|
|
7
|
+
exchange_name: <%= ENV['AMQP_EXCHANGE'] || 'sandbox' %>
|
|
8
|
+
queue_name: <%= ENV['AMQP_QUEUE'] || 'my_queue' %>
|
|
9
|
+
routing_key: <%= ENV['AMQP_ROUTING_KEY'] || "'#.new'" %>
|
|
10
|
+
app_name: <%= ENV['AMQP_APP_NAME'] || 'bunny_farm_job' %>
|
|
11
|
+
heartbeat: :server # defualt: will use RabbitMQ setting
|
|
12
|
+
threaded: true # default
|
|
13
|
+
network_recovery_interval: 15.0 # default is in seconds
|
|
14
|
+
automatically_recover: true # default
|
|
15
|
+
frame_max: 131072 # default
|
|
16
|
+
|
|
17
|
+
development:
|
|
18
|
+
<<: *defaults
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
<<: *defaults
|
|
22
|
+
|
|
23
|
+
production:
|
|
24
|
+
<<: *defaults
|
|
25
|
+
host: amqp.example.com
|
|
26
|
+
vhost: bunny_farm
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
defaults: &defaults
|
|
2
|
+
host: <%= ENV['AMQP_HOST'] || 'localhost' %>
|
|
3
|
+
vhost: <%= ENV['AMQP_VHOST'] || '/' %>
|
|
4
|
+
port: <%= (ENV['AMQP_PORT'] || 5672).to_i %>
|
|
5
|
+
user: <%= ENV['AMQP_USER'] || 'guest' %>
|
|
6
|
+
pass: <%= ENV['AMQP_PASS'] || 'guest' %>
|
|
7
|
+
exchange_name: <%= ENV['AMQP_EXCHANGE'] || 'test_exchange' %>
|
|
8
|
+
queue_name: <%= ENV['AMQP_QUEUE'] || 'test_queue' %>
|
|
9
|
+
routing_key: <%= ENV['AMQP_ROUTING_KEY'] || "'#.test'" %>
|
|
10
|
+
app_name: <%= ENV['AMQP_APP_NAME'] || 'bunny_farm_test' %>
|
|
11
|
+
heartbeat: :server # defualt: will use RabbitMQ setting
|
|
12
|
+
threaded: true # default
|
|
13
|
+
network_recovery_interval: 15.0 # default is in seconds
|
|
14
|
+
automatically_recover: true # default
|
|
15
|
+
frame_max: 131072 # default
|
|
16
|
+
|
|
17
|
+
development:
|
|
18
|
+
<<: *defaults
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
<<: *defaults
|
|
22
|
+
vhost: /
|
|
23
|
+
user: guest
|
|
24
|
+
pass: guest
|
|
25
|
+
|
|
26
|
+
production:
|
|
27
|
+
<<: *defaults
|
|
28
|
+
host: amqp.example.com
|
|
29
|
+
vhost: bunny_farm
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Message Class API Reference
|
|
2
|
+
|
|
3
|
+
The `BunnyFarm::Message` class is the foundation of the BunnyFarm library. All your message classes inherit from this base class to gain message processing capabilities.
|
|
4
|
+
|
|
5
|
+
## Class Definition
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class YourMessage < BunnyFarm::Message
|
|
9
|
+
fields :field1, :field2, { nested: [:field3, :field4] }
|
|
10
|
+
actions :action1, :action2
|
|
11
|
+
|
|
12
|
+
def action1
|
|
13
|
+
# Your processing logic
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Class Methods
|
|
19
|
+
|
|
20
|
+
### `fields(*field_definitions)`
|
|
21
|
+
|
|
22
|
+
Defines the expected data structure for the message.
|
|
23
|
+
|
|
24
|
+
**Parameters:**
|
|
25
|
+
- `field_definitions` - Variable number of field definitions
|
|
26
|
+
|
|
27
|
+
**Field Definition Types:**
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Simple fields
|
|
31
|
+
fields :name, :email, :age
|
|
32
|
+
|
|
33
|
+
# Nested objects
|
|
34
|
+
fields { customer: [:name, :email, :phone] }
|
|
35
|
+
|
|
36
|
+
# Mixed definitions
|
|
37
|
+
fields :order_id, :total,
|
|
38
|
+
{ customer: [:name, :email] },
|
|
39
|
+
{ items: [:product_id, :quantity, :price] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Example:**
|
|
43
|
+
```ruby
|
|
44
|
+
class OrderMessage < BunnyFarm::Message
|
|
45
|
+
fields :order_id, :total,
|
|
46
|
+
{ customer: [:name, :email, :phone] },
|
|
47
|
+
{ billing_address: [:street, :city, :state, :zip] },
|
|
48
|
+
{ items: [:product_id, :quantity, :price] }
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `actions(*action_names)`
|
|
53
|
+
|
|
54
|
+
Defines the available actions (operations) for this message type.
|
|
55
|
+
|
|
56
|
+
**Parameters:**
|
|
57
|
+
- `action_names` - Variable number of action symbols
|
|
58
|
+
|
|
59
|
+
**Example:**
|
|
60
|
+
```ruby
|
|
61
|
+
class OrderMessage < BunnyFarm::Message
|
|
62
|
+
actions :validate, :process_payment, :fulfill, :ship, :cancel
|
|
63
|
+
|
|
64
|
+
def validate
|
|
65
|
+
# Validation logic
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process_payment
|
|
69
|
+
# Payment logic
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Routing Keys:**
|
|
75
|
+
Each action creates a routing key in the format: `MessageClassName.action`
|
|
76
|
+
- `OrderMessage.validate`
|
|
77
|
+
- `OrderMessage.process_payment`
|
|
78
|
+
- `OrderMessage.fulfill`
|
|
79
|
+
|
|
80
|
+
## Instance Methods
|
|
81
|
+
|
|
82
|
+
### Data Access
|
|
83
|
+
|
|
84
|
+
#### `[](key)`
|
|
85
|
+
Get a field value using hash-like syntax.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `key` - Field name as symbol or string
|
|
89
|
+
|
|
90
|
+
**Returns:**
|
|
91
|
+
- Field value or `nil` if not set
|
|
92
|
+
|
|
93
|
+
**Example:**
|
|
94
|
+
```ruby
|
|
95
|
+
message = OrderMessage.new
|
|
96
|
+
value = message[:customer_name]
|
|
97
|
+
nested = message[:customer][:email]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### `[]=(key, value)`
|
|
101
|
+
Set a field value using hash-like syntax.
|
|
102
|
+
|
|
103
|
+
**Parameters:**
|
|
104
|
+
- `key` - Field name as symbol or string
|
|
105
|
+
- `value` - Value to set
|
|
106
|
+
|
|
107
|
+
**Example:**
|
|
108
|
+
```ruby
|
|
109
|
+
message[:customer_name] = "John Doe"
|
|
110
|
+
message[:customer] = { name: "John", email: "john@example.com" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### `keys`
|
|
114
|
+
Get all available field keys.
|
|
115
|
+
|
|
116
|
+
**Returns:**
|
|
117
|
+
- Array of field keys
|
|
118
|
+
|
|
119
|
+
**Example:**
|
|
120
|
+
```ruby
|
|
121
|
+
message.keys # => [:order_id, :customer, :items]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### State Management
|
|
125
|
+
|
|
126
|
+
#### `success!`
|
|
127
|
+
Mark the current operation as successful.
|
|
128
|
+
|
|
129
|
+
**Example:**
|
|
130
|
+
```ruby
|
|
131
|
+
def process_order
|
|
132
|
+
# ... processing logic
|
|
133
|
+
success!
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `failure(error_message)`
|
|
138
|
+
Mark the current operation as failed with an error message.
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
- `error_message` - String describing the failure
|
|
142
|
+
|
|
143
|
+
**Example:**
|
|
144
|
+
```ruby
|
|
145
|
+
def process_payment
|
|
146
|
+
if payment_declined?
|
|
147
|
+
failure("Payment was declined by bank")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `successful?`
|
|
153
|
+
Check if the current operation was successful.
|
|
154
|
+
|
|
155
|
+
**Returns:**
|
|
156
|
+
- `true` if successful, `false` otherwise
|
|
157
|
+
|
|
158
|
+
**Example:**
|
|
159
|
+
```ruby
|
|
160
|
+
def process_order
|
|
161
|
+
validate_order
|
|
162
|
+
return unless successful?
|
|
163
|
+
|
|
164
|
+
process_payment
|
|
165
|
+
return unless successful?
|
|
166
|
+
|
|
167
|
+
ship_order
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `failed?`
|
|
172
|
+
Check if the current operation failed.
|
|
173
|
+
|
|
174
|
+
**Returns:**
|
|
175
|
+
- `true` if failed, `false` otherwise
|
|
176
|
+
|
|
177
|
+
**Example:**
|
|
178
|
+
```ruby
|
|
179
|
+
if message.failed?
|
|
180
|
+
puts "Processing failed: #{message.errors.join(', ')}"
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `errors`
|
|
185
|
+
Get array of error messages accumulated during processing.
|
|
186
|
+
|
|
187
|
+
**Returns:**
|
|
188
|
+
- Array of error message strings
|
|
189
|
+
|
|
190
|
+
**Example:**
|
|
191
|
+
```ruby
|
|
192
|
+
def validate
|
|
193
|
+
failure("Name is required") if @items[:name].blank?
|
|
194
|
+
failure("Email is invalid") unless valid_email?(@items[:email])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Later...
|
|
198
|
+
if message.failed?
|
|
199
|
+
message.errors # => ["Name is required", "Email is invalid"]
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Message Operations
|
|
204
|
+
|
|
205
|
+
#### `publish(action)`
|
|
206
|
+
Publish the message with the specified action.
|
|
207
|
+
|
|
208
|
+
**Parameters:**
|
|
209
|
+
- `action` - Action name as string or symbol
|
|
210
|
+
|
|
211
|
+
**Returns:**
|
|
212
|
+
- `true` if published successfully, `false` otherwise
|
|
213
|
+
|
|
214
|
+
**Example:**
|
|
215
|
+
```ruby
|
|
216
|
+
message = OrderMessage.new
|
|
217
|
+
message[:order_id] = 12345
|
|
218
|
+
message[:customer] = { name: "John", email: "john@example.com" }
|
|
219
|
+
|
|
220
|
+
if message.publish('process')
|
|
221
|
+
puts "Message published successfully"
|
|
222
|
+
else
|
|
223
|
+
puts "Failed to publish: #{message.errors.join(', ')}"
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Routing Key:**
|
|
228
|
+
The message will be routed using: `MessageClassName.action`
|
|
229
|
+
|
|
230
|
+
#### `to_json`
|
|
231
|
+
Convert the message data to JSON string.
|
|
232
|
+
|
|
233
|
+
**Returns:**
|
|
234
|
+
- JSON string representation of the message data
|
|
235
|
+
|
|
236
|
+
**Example:**
|
|
237
|
+
```ruby
|
|
238
|
+
message[:name] = "John"
|
|
239
|
+
message[:email] = "john@example.com"
|
|
240
|
+
json = message.to_json
|
|
241
|
+
# => '{"name":"John","email":"john@example.com"}'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Instance Variables
|
|
245
|
+
|
|
246
|
+
### `@items`
|
|
247
|
+
Hash containing the validated and structured message data based on field definitions.
|
|
248
|
+
|
|
249
|
+
**Access:**
|
|
250
|
+
- Direct: `@items[:field_name]`
|
|
251
|
+
- Hash-like: `message[:field_name]`
|
|
252
|
+
|
|
253
|
+
### `@elements`
|
|
254
|
+
Hash containing the raw message data as received from the message broker.
|
|
255
|
+
|
|
256
|
+
### `@payload`
|
|
257
|
+
String containing the original JSON payload as received from RabbitMQ.
|
|
258
|
+
|
|
259
|
+
### `@errors`
|
|
260
|
+
Array containing error messages accumulated during processing.
|
|
261
|
+
|
|
262
|
+
## Action Method Implementation
|
|
263
|
+
|
|
264
|
+
When you define actions, you must implement corresponding methods:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
class OrderMessage < BunnyFarm::Message
|
|
268
|
+
actions :validate, :process, :ship
|
|
269
|
+
|
|
270
|
+
def validate
|
|
271
|
+
# Validation logic here
|
|
272
|
+
validate_customer_info
|
|
273
|
+
validate_order_items
|
|
274
|
+
validate_shipping_address
|
|
275
|
+
|
|
276
|
+
# Mark as successful if no errors
|
|
277
|
+
success! if errors.empty?
|
|
278
|
+
|
|
279
|
+
# Return true for ACK, false for NACK
|
|
280
|
+
successful?
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def process
|
|
284
|
+
# Processing logic
|
|
285
|
+
charge_payment
|
|
286
|
+
update_inventory
|
|
287
|
+
|
|
288
|
+
if payment_successful? && inventory_updated?
|
|
289
|
+
success!
|
|
290
|
+
else
|
|
291
|
+
failure("Order processing failed")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
successful?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ship
|
|
298
|
+
# Shipping logic
|
|
299
|
+
create_shipping_label
|
|
300
|
+
notify_carrier
|
|
301
|
+
send_tracking_info
|
|
302
|
+
|
|
303
|
+
success!
|
|
304
|
+
successful?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
private
|
|
308
|
+
|
|
309
|
+
def validate_customer_info
|
|
310
|
+
failure("Customer name required") if @items[:customer][:name].blank?
|
|
311
|
+
failure("Customer email required") if @items[:customer][:email].blank?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def charge_payment
|
|
315
|
+
# Payment processing logic
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Error Handling Patterns
|
|
321
|
+
|
|
322
|
+
### Basic Error Handling
|
|
323
|
+
```ruby
|
|
324
|
+
def risky_operation
|
|
325
|
+
begin
|
|
326
|
+
perform_external_call
|
|
327
|
+
success!
|
|
328
|
+
rescue ExternalServiceError => e
|
|
329
|
+
failure("External service failed: #{e.message}")
|
|
330
|
+
rescue StandardError => e
|
|
331
|
+
failure("Unexpected error: #{e.message}")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
successful?
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Validation with Multiple Errors
|
|
339
|
+
```ruby
|
|
340
|
+
def validate
|
|
341
|
+
failure("Name required") if @items[:name].blank?
|
|
342
|
+
failure("Email required") if @items[:email].blank?
|
|
343
|
+
failure("Invalid email format") unless valid_email?
|
|
344
|
+
|
|
345
|
+
success! if errors.empty?
|
|
346
|
+
successful?
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Conditional Processing
|
|
351
|
+
```ruby
|
|
352
|
+
def process_order
|
|
353
|
+
validate_order
|
|
354
|
+
return unless successful?
|
|
355
|
+
|
|
356
|
+
authorize_payment
|
|
357
|
+
return unless successful?
|
|
358
|
+
|
|
359
|
+
fulfill_order
|
|
360
|
+
return unless successful?
|
|
361
|
+
|
|
362
|
+
send_confirmation
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Best Practices
|
|
367
|
+
|
|
368
|
+
### 1. Always Return Success Status
|
|
369
|
+
Action methods should always return the result of `successful?`:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
def my_action
|
|
373
|
+
# ... processing logic
|
|
374
|
+
success! # or failure(...)
|
|
375
|
+
successful? # Always return this
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 2. Use Descriptive Error Messages
|
|
380
|
+
Provide clear, actionable error messages:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# Good
|
|
384
|
+
failure("Customer email format is invalid: #{email}")
|
|
385
|
+
|
|
386
|
+
# Avoid
|
|
387
|
+
failure("Invalid input")
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 3. Implement Idempotent Operations
|
|
391
|
+
Make operations safe to retry:
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
def charge_payment
|
|
395
|
+
return success! if already_charged?
|
|
396
|
+
|
|
397
|
+
# Perform charge only if not already done
|
|
398
|
+
result = payment_gateway.charge(@items[:amount])
|
|
399
|
+
|
|
400
|
+
if result.success?
|
|
401
|
+
success!
|
|
402
|
+
else
|
|
403
|
+
failure("Payment failed: #{result.error}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
successful?
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 4. Keep Actions Focused
|
|
411
|
+
Each action should have a single, clear responsibility:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
# Good: Focused actions
|
|
415
|
+
actions :validate_order, :process_payment, :ship_order
|
|
416
|
+
|
|
417
|
+
# Avoid: Overly broad actions
|
|
418
|
+
actions :handle_order # Too generic
|
|
419
|
+
```
|