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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +114 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/sentry/SKILL.md +135 -0
  6. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  7. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  8. data/.agents/skills/skill-builder/SKILL.md +232 -0
  9. data/.agents/skills/skill-manager/SKILL.md +172 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +8 -0
  14. data/CLAUDE.md +28 -231
  15. data/lib/bug_bunny/version.rb +1 -1
  16. data/skill/SKILL.md +230 -0
  17. data/skill/references/client-middleware.md +144 -0
  18. data/skill/references/consumer.md +104 -0
  19. data/skill/references/controller.md +105 -0
  20. data/skill/references/errores.md +97 -0
  21. data/skill/references/resource.md +116 -0
  22. data/skill/references/routing.md +82 -0
  23. data/skill/references/testing.md +138 -0
  24. data/skills.lock +24 -0
  25. data/skills.yml +19 -0
  26. metadata +24 -28
  27. data/.claude/commands/gem-ai-setup.md +0 -174
  28. data/.claude/commands/pr.md +0 -53
  29. data/.claude/commands/release.md +0 -52
  30. data/.claude/commands/rubocop.md +0 -22
  31. data/.claude/commands/service-ai-setup.md +0 -168
  32. data/.claude/commands/test.md +0 -28
  33. data/.claude/commands/yard.md +0 -46
  34. data/docs/_index.md +0 -50
  35. data/docs/ai/_index.md +0 -56
  36. data/docs/ai/antipatterns.md +0 -166
  37. data/docs/ai/api.md +0 -251
  38. data/docs/ai/architecture.md +0 -92
  39. data/docs/ai/errors.md +0 -158
  40. data/docs/ai/faq_external.md +0 -133
  41. data/docs/ai/faq_internal.md +0 -86
  42. data/docs/ai/glossary.md +0 -45
  43. data/docs/concepts.md +0 -140
  44. data/docs/howto/controller.md +0 -194
  45. data/docs/howto/middleware_client.md +0 -119
  46. data/docs/howto/middleware_consumer.md +0 -127
  47. data/docs/howto/rails.md +0 -214
  48. data/docs/howto/resource.md +0 -200
  49. data/docs/howto/routing.md +0 -133
  50. data/docs/howto/testing.md +0 -259
  51. 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
- ```
@@ -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
- ```
@@ -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.