bug_bunny 4.8.0 → 4.8.1
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/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +114 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +232 -0
- data/.agents/skills/skill-manager/SKILL.md +172 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +28 -231
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +230 -0
- data/skill/references/client-middleware.md +144 -0
- data/skill/references/consumer.md +104 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills.lock +24 -0
- data/skills.yml +19 -0
- metadata +24 -28
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- data/docs/howto/tracing.md +0 -119
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# Consumer Middleware
|
|
2
|
-
|
|
3
|
-
The consumer middleware stack runs before every message reaches the router. It is the right place for cross-cutting concerns: distributed tracing, authentication, audit logging, rate limiting.
|
|
4
|
-
|
|
5
|
-
## Execution Order
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
RabbitMQ message arrives
|
|
9
|
-
|
|
|
10
|
-
v
|
|
11
|
-
ConsumerMiddleware::Stack
|
|
12
|
-
Middleware A (first registered)
|
|
13
|
-
Middleware B
|
|
14
|
-
Middleware C
|
|
15
|
-
process_message (router → controller → action)
|
|
16
|
-
Middleware C (post-processing)
|
|
17
|
-
Middleware B (post-processing)
|
|
18
|
-
Middleware A (post-processing)
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Middlewares execute FIFO. If no middlewares are registered, the overhead is zero.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Writing a Middleware
|
|
26
|
-
|
|
27
|
-
Inherit from `BugBunny::ConsumerMiddleware::Base` and implement `call`:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
class TracingMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
31
|
-
def call(delivery_info, properties, body)
|
|
32
|
-
trace_id = properties.headers&.dig('X-Trace-Id') || SecureRandom.uuid
|
|
33
|
-
MyTracer.with_trace(trace_id) do
|
|
34
|
-
@app.call(delivery_info, properties, body)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`@app.call(delivery_info, properties, body)` passes control to the next middleware. Always call it (unless you intentionally want to halt processing).
|
|
41
|
-
|
|
42
|
-
### Available data
|
|
43
|
-
|
|
44
|
-
| Argument | Type | Contents |
|
|
45
|
-
|---|---|---|
|
|
46
|
-
| `delivery_info` | `Bunny::DeliveryInfo` | `routing_key`, `exchange`, `delivery_tag`, `redelivered` |
|
|
47
|
-
| `properties` | `Bunny::MessageProperties` | `headers` (custom AMQP headers), `correlation_id`, `reply_to`, `content_type` |
|
|
48
|
-
| `body` | `String` | Raw message payload (typically JSON) |
|
|
49
|
-
|
|
50
|
-
### Post-processing
|
|
51
|
-
|
|
52
|
-
Code after `@app.call` runs once the message has been fully processed (controller action completed):
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
class AuditMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
56
|
-
def call(delivery_info, properties, body)
|
|
57
|
-
@app.call(delivery_info, properties, body)
|
|
58
|
-
rescue => e
|
|
59
|
-
AuditLog.record_failure(routing_key: delivery_info.routing_key, error: e.class.name)
|
|
60
|
-
raise
|
|
61
|
-
ensure
|
|
62
|
-
AuditLog.record_received(routing_key: delivery_info.routing_key)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Registering Middlewares
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
# After BugBunny.configure
|
|
73
|
-
BugBunny.consumer_middlewares.use TracingMiddleware
|
|
74
|
-
BugBunny.consumer_middlewares.use AuditMiddleware
|
|
75
|
-
BugBunny.consumer_middlewares.use AuthenticationMiddleware
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Registrations are thread-safe. You can register middlewares at any point before the Consumer starts subscribing.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Auto-registration from External Gems
|
|
83
|
-
|
|
84
|
-
Integration gems can register themselves transparently when required, without the user modifying the `configure` block:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
# lib/my_tracing_gem/bug_bunny.rb
|
|
88
|
-
require 'my_tracing_gem/bug_bunny/consumer_middleware'
|
|
89
|
-
BugBunny.consumer_middlewares.use MyTracingGem::BugBunny::ConsumerMiddleware
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The user only needs:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
require 'my_tracing_gem/bug_bunny'
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Halting Message Processing
|
|
101
|
-
|
|
102
|
-
To reject a message without routing it (e.g., authentication failure):
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
class AuthMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
106
|
-
def call(delivery_info, properties, body)
|
|
107
|
-
token = properties.headers&.dig('X-Service-Token')
|
|
108
|
-
unless TokenValidator.valid?(token)
|
|
109
|
-
# Do not call @app — message is effectively dropped from this consumer's perspective
|
|
110
|
-
# The channel will nack/reject it based on the consumer's error handling
|
|
111
|
-
return
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
@app.call(delivery_info, properties, body)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Note: halting in a middleware skips all routing and controller logic, but the message is still acknowledged at the AMQP level (the Consumer's normal ack happens in `process_message`, which was never reached). If you need to nack/reject, interact with `delivery_info.delivery_tag` directly — but this is an advanced use case.
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Inspecting the Stack
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
BugBunny.consumer_middlewares.empty? # => false if any middleware registered
|
|
127
|
-
```
|
data/docs/howto/rails.md
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Rails Setup
|
|
2
|
-
|
|
3
|
-
Complete integration guide for BugBunny in a Rails application using Puma and/or Sidekiq.
|
|
4
|
-
|
|
5
|
-
## Gemfile
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
gem 'bug_bunny'
|
|
9
|
-
gem 'connection_pool'
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Generator
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
rails generate bug_bunny:install
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Creates `config/initializers/bug_bunny.rb` with a commented template.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Initializer
|
|
23
|
-
|
|
24
|
-
```ruby
|
|
25
|
-
# config/initializers/bug_bunny.rb
|
|
26
|
-
|
|
27
|
-
BugBunny.configure do |config|
|
|
28
|
-
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
29
|
-
config.port = ENV.fetch('RABBITMQ_PORT', '5672').to_i
|
|
30
|
-
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
31
|
-
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
32
|
-
config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
|
|
33
|
-
|
|
34
|
-
config.rpc_timeout = 30
|
|
35
|
-
config.max_reconnect_attempts = 10
|
|
36
|
-
config.max_reconnect_interval = 60
|
|
37
|
-
config.network_recovery_interval = 5
|
|
38
|
-
|
|
39
|
-
config.exchange_options = { durable: true }
|
|
40
|
-
config.queue_options = { durable: true }
|
|
41
|
-
|
|
42
|
-
config.logger = Rails.logger
|
|
43
|
-
|
|
44
|
-
# Kubernetes / Docker Swarm liveness probe
|
|
45
|
-
config.health_check_file = Rails.root.join('tmp', 'bug_bunny_health').to_s
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Shared connection pool — size should match Puma/Sidekiq thread count
|
|
49
|
-
BUG_BUNNY_POOL = ConnectionPool.new(
|
|
50
|
-
size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i,
|
|
51
|
-
timeout: 5
|
|
52
|
-
) do
|
|
53
|
-
BugBunny.create_connection
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Make the pool available to all Resource classes
|
|
57
|
-
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Routes
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
# config/initializers/bug_bunny_routes.rb
|
|
66
|
-
BugBunny.routes.draw do
|
|
67
|
-
resources :users
|
|
68
|
-
resources :orders do
|
|
69
|
-
member { post :cancel }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Keep routes in a dedicated initializer so they load independently from `config/routes.rb`.
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Directory Layout
|
|
79
|
-
|
|
80
|
-
The Rails generator configures Zeitwerk to autoload `app/rabbit`:
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
app/
|
|
84
|
-
rabbit/
|
|
85
|
-
controllers/ # BugBunny controllers (loaded under BugBunny::Controllers)
|
|
86
|
-
application_controller.rb
|
|
87
|
-
users_controller.rb
|
|
88
|
-
workers/ # Consumer processes (optional)
|
|
89
|
-
inventory_worker.rb
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Or use the configurable controller namespace:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
config.controller_namespace = 'Rabbit::Controllers'
|
|
96
|
-
# → app/rabbit/controllers/ maps to Rabbit::Controllers
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## Consumer Workers
|
|
102
|
-
|
|
103
|
-
The Consumer is a blocking subscribe loop. Run it in a dedicated process (not inside Puma).
|
|
104
|
-
|
|
105
|
-
### Rake task
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
# lib/tasks/rabbit.rake
|
|
109
|
-
namespace :rabbit do
|
|
110
|
-
desc 'Start the RabbitMQ consumer'
|
|
111
|
-
task inventory: :environment do
|
|
112
|
-
consumer = BugBunny::Consumer.new
|
|
113
|
-
trap('TERM') { consumer.shutdown; exit }
|
|
114
|
-
trap('INT') { consumer.shutdown; exit }
|
|
115
|
-
|
|
116
|
-
consumer.subscribe(
|
|
117
|
-
queue_name: 'inventory_queue',
|
|
118
|
-
exchange_name: 'inventory_exchange',
|
|
119
|
-
routing_key: 'inventory',
|
|
120
|
-
block: true
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
bundle exec rake rabbit:inventory
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Dockerfile entrypoint (separate service)
|
|
131
|
-
|
|
132
|
-
```dockerfile
|
|
133
|
-
# Dockerfile.worker
|
|
134
|
-
CMD ["bundle", "exec", "rake", "rabbit:inventory"]
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Multiple consumers
|
|
138
|
-
|
|
139
|
-
Run one process per queue. Each process has its own connection pool.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Puma Fork Safety
|
|
144
|
-
|
|
145
|
-
When Puma forks workers, open AMQP connections in the parent process become invalid in children. BugBunny's Railtie handles this automatically for Puma via `on_worker_boot`:
|
|
146
|
-
|
|
147
|
-
```ruby
|
|
148
|
-
# Railtie registers this automatically:
|
|
149
|
-
Puma::Server.on_worker_boot do
|
|
150
|
-
BugBunny.reconnect! if defined?(BUG_BUNNY_POOL)
|
|
151
|
-
end
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
If you use a custom pool variable name, add the hook manually:
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
# config/puma.rb
|
|
158
|
-
on_worker_boot do
|
|
159
|
-
MY_CUSTOM_POOL.reload_connections { BugBunny.create_connection }
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Sidekiq Integration
|
|
166
|
-
|
|
167
|
-
Sidekiq uses threads, not forks, so no special handling is needed. The shared `BUG_BUNNY_POOL` is thread-safe. Sidekiq jobs can call `BugBunny::Resource` methods directly.
|
|
168
|
-
|
|
169
|
-
Set the pool size to match Sidekiq's concurrency:
|
|
170
|
-
|
|
171
|
-
```ruby
|
|
172
|
-
BUG_BUNNY_POOL = ConnectionPool.new(
|
|
173
|
-
size: ENV.fetch('SIDEKIQ_CONCURRENCY', 10).to_i,
|
|
174
|
-
timeout: 5
|
|
175
|
-
) { BugBunny.create_connection }
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Health Checks (Kubernetes / Docker Swarm)
|
|
181
|
-
|
|
182
|
-
The Consumer's internal heartbeat timer touches `config.health_check_file` every 30 seconds after verifying the RabbitMQ connection and queue are alive.
|
|
183
|
-
|
|
184
|
-
```yaml
|
|
185
|
-
# docker-compose.yml
|
|
186
|
-
healthcheck:
|
|
187
|
-
test: ["CMD", "test", "-f", "/app/tmp/bug_bunny_health"]
|
|
188
|
-
interval: 60s
|
|
189
|
-
timeout: 5s
|
|
190
|
-
retries: 3
|
|
191
|
-
start_period: 30s # allow time for Rails boot + first heartbeat
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
```yaml
|
|
195
|
-
# Kubernetes
|
|
196
|
-
livenessProbe:
|
|
197
|
-
exec:
|
|
198
|
-
command: ["test", "-f", "/app/tmp/bug_bunny_health"]
|
|
199
|
-
initialDelaySeconds: 30
|
|
200
|
-
periodSeconds: 60
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
## Graceful Shutdown
|
|
206
|
-
|
|
207
|
-
The `Consumer#shutdown` method stops the heartbeat timer and closes the AMQP channel cleanly. It is also called automatically when `subscribe` exits for any reason.
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
consumer = BugBunny::Consumer.new
|
|
211
|
-
trap('TERM') { consumer.shutdown; exit 0 }
|
|
212
|
-
trap('INT') { consumer.shutdown; exit 0 }
|
|
213
|
-
consumer.subscribe(...)
|
|
214
|
-
```
|
data/docs/howto/resource.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Resource ORM
|
|
2
|
-
|
|
3
|
-
`BugBunny::Resource` provides an ActiveRecord-like interface for remote services. Each Resource class represents a resource type in another microservice, reachable via RabbitMQ.
|
|
4
|
-
|
|
5
|
-
## Defining a Resource
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
class RemoteNode < BugBunny::Resource
|
|
9
|
-
# AMQP infrastructure
|
|
10
|
-
self.exchange = 'inventory_exchange'
|
|
11
|
-
self.exchange_type = 'direct' # default
|
|
12
|
-
self.resource_name = 'nodes' # used as the path prefix and routing key
|
|
13
|
-
|
|
14
|
-
# Typed attributes (ActiveModel::Attributes)
|
|
15
|
-
attribute :name, :string
|
|
16
|
-
attribute :status, :string
|
|
17
|
-
attribute :cpu_cores, :integer
|
|
18
|
-
attribute :active, :boolean
|
|
19
|
-
|
|
20
|
-
# Validations (ActiveModel::Validations)
|
|
21
|
-
validates :name, presence: true
|
|
22
|
-
validates :status, inclusion: { in: %w[pending active draining decommissioned] }
|
|
23
|
-
end
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Connection Pool
|
|
27
|
-
|
|
28
|
-
All Resource classes in a service typically share one pool:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
# config/initializers/bug_bunny.rb
|
|
32
|
-
BUG_BUNNY_POOL = ConnectionPool.new(size: 5, timeout: 5) do
|
|
33
|
-
BugBunny.create_connection
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Individual classes can override:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
RemoteNode.connection_pool = OTHER_POOL
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## CRUD
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
# Find by ID — returns nil on 404
|
|
51
|
-
node = RemoteNode.find('node-123')
|
|
52
|
-
|
|
53
|
-
# List all
|
|
54
|
-
nodes = RemoteNode.all
|
|
55
|
-
|
|
56
|
-
# Filter — query params forwarded to the consumer
|
|
57
|
-
nodes = RemoteNode.where(status: 'active')
|
|
58
|
-
nodes = RemoteNode.where(q: { cpu_cores: 4 }, page: 2)
|
|
59
|
-
|
|
60
|
-
# Create
|
|
61
|
-
node = RemoteNode.create(name: 'web-01', status: 'pending')
|
|
62
|
-
node.persisted? # => true if save succeeded
|
|
63
|
-
|
|
64
|
-
# Update
|
|
65
|
-
node = RemoteNode.find('node-123')
|
|
66
|
-
node.status = 'active'
|
|
67
|
-
node.save # PUT nodes/node-123
|
|
68
|
-
|
|
69
|
-
# Update (shorthand)
|
|
70
|
-
node.update(status: 'active', name: 'web-01')
|
|
71
|
-
|
|
72
|
-
# Destroy
|
|
73
|
-
node.destroy # DELETE nodes/node-123
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Typed vs Dynamic Attributes
|
|
79
|
-
|
|
80
|
-
### Typed attributes
|
|
81
|
-
|
|
82
|
-
Declared with `attribute :name, :type`. Benefit from ActiveModel coercions, dirty tracking, and validations:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
attribute :cpu_cores, :integer
|
|
86
|
-
attribute :enabled, :boolean
|
|
87
|
-
attribute :score, :decimal
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Dynamic attributes
|
|
91
|
-
|
|
92
|
-
Any key received from the remote service that is not declared as a typed attribute is stored dynamically and accessible via `method_missing`:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
node = RemoteNode.find('node-123')
|
|
96
|
-
node.docker_id # => "abc123xyz" (not declared, but present in the response)
|
|
97
|
-
node.docker_id = 'new-id'
|
|
98
|
-
node.changed? # => true
|
|
99
|
-
node.changed # => ['docker_id']
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Dynamic attributes participate in `changed?`, `changed`, and `changes_to_send` — so they are serialized correctly on `save`.
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## Change Tracking
|
|
107
|
-
|
|
108
|
-
BugBunny::Resource merges ActiveModel::Dirty (for typed attributes) with its own tracking for dynamic attributes.
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
node = RemoteNode.find('node-123') # changes cleared after find
|
|
112
|
-
node.changed? # => false
|
|
113
|
-
|
|
114
|
-
node.status = 'draining'
|
|
115
|
-
node.changed? # => true
|
|
116
|
-
node.changed # => ['status']
|
|
117
|
-
|
|
118
|
-
node.save # sends only changed attrs
|
|
119
|
-
node.changed? # => false (cleared after save)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`save` on a new record (not persisted) sends all attributes.
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Validations
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
node = RemoteNode.new(name: '', status: 'invalid')
|
|
130
|
-
node.valid? # => false
|
|
131
|
-
node.errors.full_messages
|
|
132
|
-
# => ["Name can't be blank", "Status is not included in the list"]
|
|
133
|
-
|
|
134
|
-
node.save # => false (does not send the request)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Remote validation errors (422 responses) are loaded back into the object:
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
node = RemoteNode.create(name: 'duplicate-name')
|
|
141
|
-
node.persisted? # => false
|
|
142
|
-
node.errors[:name] # => ["has already been taken"]
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Callbacks
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
class RemoteNode < BugBunny::Resource
|
|
151
|
-
before_save :normalize_name
|
|
152
|
-
after_create :notify_provisioner
|
|
153
|
-
around_save :with_timing
|
|
154
|
-
|
|
155
|
-
private
|
|
156
|
-
|
|
157
|
-
def normalize_name
|
|
158
|
-
self.name = name.to_s.downcase.strip
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Available callbacks: `before_save`, `after_save`, `before_create`, `after_create`, `before_update`, `after_update`, `before_destroy`, `after_destroy`.
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## Dynamic Exchange Configuration with `.with`
|
|
168
|
-
|
|
169
|
-
Override AMQP settings for a single operation without changing the class defaults:
|
|
170
|
-
|
|
171
|
-
```ruby
|
|
172
|
-
# Different exchange for a single call
|
|
173
|
-
RemoteNode.with(exchange: 'us-east-inventory').where(status: 'active')
|
|
174
|
-
|
|
175
|
-
# Different routing key
|
|
176
|
-
RemoteNode.with(routing_key: 'nodes.priority').find('node-123')
|
|
177
|
-
|
|
178
|
-
# Chain is single-use — call .with again for the next operation
|
|
179
|
-
RemoteNode.with(exchange: 'staging').create(name: 'test-node')
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`.with` sets thread-local values that are cleaned up after the single operation completes, even if an exception is raised.
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## Payload Wrapping
|
|
187
|
-
|
|
188
|
-
By default, `save` wraps the payload under a root key derived from the model name:
|
|
189
|
-
|
|
190
|
-
```ruby
|
|
191
|
-
# RemoteNode → root key: 'node'
|
|
192
|
-
node.save
|
|
193
|
-
# Sends: { "node" => { "name" => "web-01", "status" => "active" } }
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
The consumer can then use `params.require(:node)` in the controller. Override the root key:
|
|
197
|
-
|
|
198
|
-
```ruby
|
|
199
|
-
self.param_key = 'server'
|
|
200
|
-
```
|
data/docs/howto/routing.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# Routing
|
|
2
|
-
|
|
3
|
-
Routes declare how incoming AMQP messages map to controllers and actions. They are evaluated on every message received by the Consumer.
|
|
4
|
-
|
|
5
|
-
## Drawing Routes
|
|
6
|
-
|
|
7
|
-
Define routes in an initializer (or any file loaded at boot):
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
BugBunny.routes.draw do
|
|
11
|
-
# routes here
|
|
12
|
-
end
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Call `draw` only once. Multiple calls replace the previous route set.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## HTTP Verbs
|
|
20
|
-
|
|
21
|
-
```ruby
|
|
22
|
-
BugBunny.routes.draw do
|
|
23
|
-
get 'status', to: 'health#show'
|
|
24
|
-
post 'events', to: 'events#create'
|
|
25
|
-
put 'users/:id', to: 'users#update'
|
|
26
|
-
delete 'users/:id', to: 'users#destroy'
|
|
27
|
-
end
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The verb is set by the producer via the `x-http-method` AMQP header. `BugBunny::Resource` and `BugBunny::Client` set it automatically.
|
|
31
|
-
|
|
32
|
-
Dynamic segments (`:id`) are extracted from the path and available in `params` inside the controller.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Resources Macro
|
|
37
|
-
|
|
38
|
-
`resources` generates the standard seven CRUD routes in one line:
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
resources :users
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Generates:
|
|
45
|
-
|
|
46
|
-
| Verb | Path | Action |
|
|
47
|
-
|--------|---------------|-----------|
|
|
48
|
-
| GET | users | index |
|
|
49
|
-
| POST | users | create |
|
|
50
|
-
| GET | users/:id | show |
|
|
51
|
-
| PUT | users/:id | update |
|
|
52
|
-
| DELETE | users/:id | destroy |
|
|
53
|
-
|
|
54
|
-
### Filtering actions
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
resources :orders, only: [:index, :show, :create]
|
|
58
|
-
resources :logs, except: [:update, :destroy]
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Member and Collection Routes
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
resources :nodes do
|
|
67
|
-
member do
|
|
68
|
-
put :drain # PUT nodes/:id/drain → NodesController#drain
|
|
69
|
-
post :reboot # POST nodes/:id/reboot → NodesController#reboot
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
collection do
|
|
73
|
-
post :rebalance # POST nodes/rebalance → NodesController#rebalance
|
|
74
|
-
get :summary # GET nodes/summary → NodesController#summary
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Member routes receive `params[:id]` automatically. Collection routes do not.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Namespace Blocks
|
|
84
|
-
|
|
85
|
-
Group routes under a controller namespace. Namespaces stack: nested `namespace` blocks accumulate with `::`.
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
BugBunny.routes.draw do
|
|
89
|
-
namespace :api do
|
|
90
|
-
namespace :v1 do
|
|
91
|
-
resources :metrics # → Api::V1::MetricsController
|
|
92
|
-
resources :alerts # → Api::V1::AlertsController
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
resources :health # → Api::HealthController
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
resources :nodes # → BugBunny::Controllers::NodesController (global namespace)
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The namespace in the route takes precedence over `config.controller_namespace`. Routes without a namespace block use the global controller namespace.
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## Nested Resources
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
resources :clusters do
|
|
110
|
-
resources :nodes do # → nodes/:id nested under clusters/:cluster_id
|
|
111
|
-
member { put :drain }
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Nested resource routes inject all parent IDs into params. Example: `PUT clusters/c1/nodes/n2/drain` → `params[:cluster_id] = 'c1'`, `params[:id] = 'n2'`.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Inspecting Routes
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
BugBunny.routes.recognize('GET', 'nodes/123')
|
|
124
|
-
# => { controller: 'nodes', action: 'show', params: { 'id' => '123' }, namespace: nil }
|
|
125
|
-
|
|
126
|
-
BugBunny.routes.recognize('POST', 'api/v1/metrics')
|
|
127
|
-
# => { controller: 'metrics', action: 'create', params: {}, namespace: 'Api::V1' }
|
|
128
|
-
|
|
129
|
-
BugBunny.routes.recognize('GET', 'unknown/path')
|
|
130
|
-
# => nil
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Useful in tests to verify route definitions without sending real messages.
|