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
@@ -0,0 +1,144 @@
1
+ # Client y Middleware
2
+
3
+ ## Client
4
+
5
+ API de alto nivel para publicar mensajes. Usa un pool de conexiones y una pila de middlewares.
6
+
7
+ ### Métodos Principales
8
+
9
+ ```ruby
10
+ # RPC síncrono — bloquea hasta respuesta
11
+ response = client.request('users/123', method: :get, timeout: 30)
12
+ response = client.request('users', method: :post, body: { name: 'John' })
13
+ # → { 'status' => 200, 'body' => {...} }
14
+
15
+ # Fire-and-Forget — no bloquea
16
+ client.publish('events/user_created', method: :post, body: { user_id: 42 })
17
+ # → { 'status' => 202, 'body' => nil }
18
+
19
+ # General con bloque
20
+ client.send(url) { |req| req.delivery_mode = :publish }
21
+ ```
22
+
23
+ ### Argumentos de Request
24
+
25
+ | Argumento | Tipo | Default | Propósito |
26
+ |-----------|------|---------|-----------|
27
+ | `method` | Symbol | `:get` | Verbo HTTP |
28
+ | `body` | Hash/String | nil | Payload del mensaje |
29
+ | `headers` | Hash | {} | Headers AMQP custom |
30
+ | `params` | Hash | {} | Query string params |
31
+ | `timeout` | Integer | config | Override de RPC timeout |
32
+ | `exchange` | String | config | Exchange destino |
33
+ | `exchange_type` | String | 'direct' | Tipo de exchange |
34
+ | `routing_key` | String | path | Override de routing key |
35
+ | `exchange_options` | Hash | {} | Cascade level 3 |
36
+ | `queue_options` | Hash | {} | Cascade level 3 |
37
+
38
+ ## Producer (bajo nivel)
39
+
40
+ El `Producer` es usado internamente por el `Client`. Implementa los dos patrones de entrega.
41
+
42
+ ### RPC
43
+
44
+ - Usa `amq.rabbitmq.reply-to` (direct reply-to pattern).
45
+ - Tracking de `correlation_id` en `Concurrent::Map` (thread-safe).
46
+ - Reply listener (`basic_consume`) auto-iniciado en el primer RPC.
47
+ - Double-checked locking mutex para seguridad del listener.
48
+ - Timeout lanza `BugBunny::RequestTimeout`.
49
+
50
+ ### Fire-and-Forget
51
+
52
+ - Publica en el exchange y retorna `{ 'status' => 202 }` inmediatamente.
53
+ - Sin confirmación de procesamiento.
54
+
55
+ ## Middleware Stack (Client-side, Onion Architecture)
56
+
57
+ ```
58
+ Request ─→ RaiseError ─→ JsonResponse ─→ Custom ─→ Producer
59
+ Response ←─ RaiseError ←─ JsonResponse ←─ Custom ←─
60
+ ```
61
+
62
+ ### Registrar Middlewares
63
+
64
+ ```ruby
65
+ class Order < BugBunny::Resource
66
+ client_middleware do |stack|
67
+ stack.use BugBunny::Middleware::RaiseError
68
+ stack.use BugBunny::Middleware::JsonResponse
69
+ stack.use MyCustomMiddleware
70
+ end
71
+ end
72
+ ```
73
+
74
+ ### Crear un Middleware Custom
75
+
76
+ ```ruby
77
+ class MyMiddleware < BugBunny::Middleware::Base
78
+ def on_request(env)
79
+ # Modificar request antes de enviar
80
+ env.headers['X-Custom'] = 'value'
81
+ end
82
+
83
+ def on_complete(response)
84
+ # Modificar response después de recibir
85
+ response['body'] = transform(response['body'])
86
+ end
87
+ end
88
+ ```
89
+
90
+ El método `call` del `Base` invoca `on_request`, delega a `@app.call`, y luego `on_complete`.
91
+
92
+ ### Middlewares Incluidos
93
+
94
+ **RaiseError** — Mapea status HTTP a excepciones:
95
+
96
+ | Status | Excepción |
97
+ |--------|-----------|
98
+ | 200-299 | (ninguna) |
99
+ | 400 | `BadRequest` |
100
+ | 404 | `NotFound` |
101
+ | 406 | `NotAcceptable` |
102
+ | 408 | `RequestTimeout` |
103
+ | 409 | `Conflict` |
104
+ | 422 | `UnprocessableEntity` (con smart extraction de errors) |
105
+ | 500+ | `InternalServerError` / `ServerError` |
106
+ | 4xx otros | `ClientError` |
107
+
108
+ **JsonResponse** — Auto-parsea `response['body']` de String a Hash/Array. Aplica `HashWithIndifferentAccess` si disponible.
109
+
110
+ ## Request Object
111
+
112
+ Value object con toda la metadata AMQP:
113
+
114
+ ```ruby
115
+ req.path # 'users/123'
116
+ req.method # :get
117
+ req.body # Hash, Array, String o nil
118
+ req.headers # Hash custom
119
+ req.params # Hash query string
120
+ req.full_path # path + query string
121
+ req.delivery_mode # :rpc o :publish
122
+ req.exchange # String destino
123
+ req.exchange_type # 'direct', 'topic', 'fanout'
124
+ req.correlation_id # UUID auto-generado
125
+ req.reply_to # 'amq.rabbitmq.reply-to' (auto para RPC)
126
+ req.timestamp # Time.now.to_i
127
+ req.content_type # 'application/json'
128
+ ```
129
+
130
+ ## Cascada de Configuración (3 niveles)
131
+
132
+ ```ruby
133
+ # Level 1: Gem defaults
134
+ { durable: false, auto_delete: false } # exchanges
135
+ { exclusive: false, durable: false, auto_delete: true } # queues
136
+
137
+ # Level 2: Global config
138
+ BugBunny.configure { |c| c.exchange_options = { durable: true } }
139
+
140
+ # Level 3: Per-request
141
+ client.request('users', exchange_options: { durable: true })
142
+
143
+ # Merge final: Level1.merge(Level2).merge(Level3)
144
+ ```
@@ -0,0 +1,104 @@
1
+ # Consumer
2
+
3
+ ## Subscribe
4
+
5
+ ```ruby
6
+ consumer = BugBunny::Consumer.subscribe(
7
+ connection: bunny_session,
8
+ queue_name: 'my_app_queue',
9
+ exchange_name: 'my_exchange',
10
+ routing_key: 'users.*',
11
+ exchange_type: 'topic',
12
+ exchange_opts: { durable: true },
13
+ queue_opts: { auto_delete: false },
14
+ block: true # Si false, retorna inmediatamente
15
+ )
16
+ ```
17
+
18
+ ## Flujo de Procesamiento
19
+
20
+ 1. Escucha en la queue con `manual_ack: true`.
21
+ 2. Valida que el mensaje tenga header `type` (path).
22
+ 3. Parsea el método HTTP de headers (`x-http-method` o `method`).
23
+ 4. Reconoce la ruta con `BugBunny.routes.recognize(method, path)`.
24
+ 5. Resuelve el controlador validando herencia de `BugBunny::Controller`.
25
+ 6. Ejecuta consumer middlewares → controller callbacks → acción.
26
+ 7. Responde via `reply_to` si está presente (RPC).
27
+ 8. Hace `ack` del mensaje. En caso de error, `reject`.
28
+
29
+ ## Lifecycle
30
+
31
+ ```ruby
32
+ consumer.shutdown # Cierra canal, detiene health check
33
+ consumer.session # Accede al Session subyacente
34
+ ```
35
+
36
+ ## Consumer Middleware
37
+
38
+ ### Registrar
39
+
40
+ ```ruby
41
+ BugBunny.configuration.consumer_middlewares.use MyTracing::Middleware
42
+ BugBunny.configuration.consumer_middlewares.use MyAuth::Middleware
43
+ ```
44
+
45
+ ### Crear Middleware
46
+
47
+ ```ruby
48
+ class MyMiddleware < BugBunny::ConsumerMiddleware::Base
49
+ def call(delivery_info, properties, body)
50
+ # Pre-procesamiento (ej: hidratar trace context)
51
+ @app.call(delivery_info, properties, body)
52
+ # Post-procesamiento (ej: cleanup)
53
+ end
54
+ end
55
+ ```
56
+
57
+ ### Comportamiento del Stack
58
+
59
+ - El stack toma un **snapshot** al inicio de `call()`.
60
+ - Registros concurrentes durante la ejecución NO afectan la cadena actual.
61
+ - Thread-safe para registros con `use()`.
62
+ - Orden FIFO: el primero registrado es el primero en ejecutar.
63
+
64
+ ```ruby
65
+ stack.use(A) # A.call → B.call → core
66
+ stack.use(B)
67
+ stack.empty? # false
68
+ ```
69
+
70
+ ## Health Check
71
+
72
+ - **Intervalo:** Configurable (default 60s).
73
+ - **Verificación:** `queue.declare(passive: true)` para confirmar conexión.
74
+ - **Touchfile:** Si `config.health_check_file` está configurado, actualiza mtime.
75
+ - **Fallo:** Cierra canal, dispara loop de reconexión.
76
+
77
+ ### Kubernetes Integration
78
+
79
+ ```yaml
80
+ livenessProbe:
81
+ exec:
82
+ command:
83
+ - test
84
+ - -f
85
+ - /app/tmp/bb_health
86
+ initialDelaySeconds: 30
87
+ periodSeconds: 60
88
+ ```
89
+
90
+ ## Reconexión
91
+
92
+ - Exponential backoff desde `network_recovery_interval` hasta `max_reconnect_interval`.
93
+ - Intentos limitados por `max_reconnect_attempts` (nil = infinito).
94
+ - Logs estructurados en cada intento: `event=session.reconnect_attempt`.
95
+ - Si se agotan intentos: `event=consumer.reconnect_exhausted`, lanza `CommunicationError`.
96
+
97
+ ## Manejo de Errores
98
+
99
+ | Situación | Respuesta |
100
+ |-----------|-----------|
101
+ | Ruta no encontrada | 404 + log `event=consumer.route_not_found` |
102
+ | Controller no encontrado (namespace) | 404 + log `event=consumer.controller_not_found` |
103
+ | Controller no hereda de BugBunny::Controller | `SecurityError` |
104
+ | Excepción no capturada en controller | 500 + log `event=controller.unhandled_exception` con backtrace |
@@ -0,0 +1,105 @@
1
+ # Controllers
2
+
3
+ ## Estructura Base
4
+
5
+ ```ruby
6
+ class UsersController < BugBunny::Controller
7
+ before_action :authenticate, only: [:create, :update, :destroy]
8
+ around_action :with_tracing
9
+ after_action :log_response, only: [:index, :show]
10
+ rescue_from BugBunny::NotFound, with: :render_not_found
11
+
12
+ def index
13
+ users = UserService.list(params[:filter])
14
+ render status: :ok, json: { users: users }
15
+ end
16
+
17
+ def show
18
+ user = UserService.find(params[:id])
19
+ render status: :ok, json: user
20
+ end
21
+
22
+ private
23
+
24
+ def authenticate
25
+ render status: :forbidden, json: { error: 'Unauthorized' } unless valid_token?
26
+ end
27
+
28
+ def with_tracing
29
+ Tracer.start
30
+ yield
31
+ ensure
32
+ Tracer.finish
33
+ end
34
+
35
+ def log_response
36
+ logger.info("Response: #{rendered_response[:status]}")
37
+ end
38
+
39
+ def render_not_found(exception)
40
+ render status: :not_found, json: { error: exception.message }
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Callbacks — Orden de Ejecución
46
+
47
+ 1. `around_action` blocks (capa externa, envuelve todo con `yield`)
48
+ 2. `before_action` callbacks (se detiene si se llama `render`)
49
+ 3. Acción del controlador
50
+ 4. `after_action` callbacks (NO se ejecuta si before_action haltó o si hubo excepción)
51
+
52
+ ## Filtros: only / except
53
+
54
+ ```ruby
55
+ before_action :authenticate, only: [:create, :update]
56
+ after_action :audit, except: [:index]
57
+ ```
58
+
59
+ ## rescue_from
60
+
61
+ Captura excepciones con handler method o bloque:
62
+
63
+ ```ruby
64
+ rescue_from BugBunny::UnprocessableEntity, with: :handle_validation
65
+ rescue_from StandardError do |e|
66
+ render status: :internal_server_error, json: { error: e.message }
67
+ end
68
+ ```
69
+
70
+ ## Atributos Disponibles en el Controller
71
+
72
+ ```ruby
73
+ @headers # Hash — metadata del mensaje AMQP (method, routing_key, id, etc.)
74
+ @params # HashWithIndifferentAccess — body JSON + query params unificados
75
+ @raw_string # String — body crudo si no es JSON
76
+ @response_headers # Hash — headers para el reply RPC
77
+ @rendered_response # Hash o nil — respuesta renderizada
78
+ ```
79
+
80
+ ## Render
81
+
82
+ ```ruby
83
+ render(status: :ok, json: { users: [...] })
84
+ render(status: 201, json: @user)
85
+ render(status: :unprocessable_entity, json: { errors: @resource.errors }, headers: { 'X-Custom' => 'val' })
86
+ ```
87
+
88
+ Si no se llama `render`, el response default es `{ status: 204, body: nil }`.
89
+
90
+ ## Log Tags
91
+
92
+ ```ruby
93
+ # Global
94
+ BugBunny.configuration.log_tags = [:uuid, :user_id, ->(c) { c.current_user }]
95
+
96
+ # Por controller
97
+ class UsersController < BugBunny::Controller
98
+ self.log_tags = [:uuid]
99
+ end
100
+ ```
101
+
102
+ Tipos soportados:
103
+ - **Symbol:** Llama al método del controller (ej: `:uuid` → `self.uuid`)
104
+ - **Proc:** Ejecuta con el controller como argumento
105
+ - **String:** Valor literal
@@ -0,0 +1,97 @@
1
+ # Catálogo de Errores
2
+
3
+ ## Jerarquía
4
+
5
+ ```
6
+ StandardError
7
+ └── BugBunny::Error
8
+ ├── BugBunny::CommunicationError
9
+ ├── BugBunny::ConfigurationError
10
+ ├── BugBunny::SecurityError
11
+ ├── BugBunny::ClientError (4xx)
12
+ │ ├── BugBunny::BadRequest (400)
13
+ │ ├── BugBunny::NotFound (404)
14
+ │ ├── BugBunny::NotAcceptable (406)
15
+ │ ├── BugBunny::RequestTimeout (408)
16
+ │ ├── BugBunny::Conflict (409)
17
+ │ └── BugBunny::UnprocessableEntity (422)
18
+ └── BugBunny::ServerError (5xx)
19
+ └── BugBunny::InternalServerError (500+)
20
+ ```
21
+
22
+ ## Errores de Infraestructura
23
+
24
+ ### BugBunny::CommunicationError
25
+ **Causa:** Fallo de conexión TCP/AMQP o reconexión agotada (`max_reconnect_attempts`).
26
+ **Cuándo:** Al intentar publicar o consumir sin conexión activa, o tras agotar intentos de reconexión.
27
+ **Resolución:** Verificar conectividad a RabbitMQ (host, port, firewall). Revisar logs `event=session.reconnect_failed`. Ajustar `max_reconnect_attempts` y `max_reconnect_interval`.
28
+
29
+ ### BugBunny::ConfigurationError
30
+ **Causa:** Campo requerido faltante o valor fuera de rango en `BugBunny.configure`.
31
+ **Validaciones:** host (String no vacío), port (1-65535), username/password (no nil), heartbeat (0-3600), rpc_timeout (>0), channel_prefetch (1-10000).
32
+ **Resolución:** Revisar el bloque `BugBunny.configure` y corregir valores.
33
+
34
+ ### BugBunny::SecurityError
35
+ **Causa:** Un mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
36
+ **Cuándo:** El consumer resuelve la clase pero falla la validación `is_a?(BugBunny::Controller)`.
37
+ **Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
38
+
39
+ ## Errores de Cliente (4xx)
40
+
41
+ ### BugBunny::BadRequest (400)
42
+ **Causa:** Request malformado o sintaxis inválida.
43
+ **Resolución:** Verificar formato del body y headers.
44
+
45
+ ### BugBunny::NotFound (404)
46
+ **Causa:** El recurso solicitado no existe en el servicio remoto.
47
+ **Resolución:** Verificar ID del recurso y que el endpoint exista.
48
+
49
+ ### BugBunny::NotAcceptable (406)
50
+ **Causa:** Negociación de contenido falló.
51
+ **Resolución:** Verificar `content_type` del request.
52
+
53
+ ### BugBunny::RequestTimeout (408)
54
+ **Causa:** No hubo respuesta en `config.rpc_timeout` segundos.
55
+ **Cuándo:** El `Concurrent::IVar` expira sin recibir reply.
56
+ **Resolución:** Verificar que el worker esté activo. Revisar saturación de prefetch. Aumentar `rpc_timeout` si el procesamiento es legítimamente lento.
57
+
58
+ ### BugBunny::Conflict (409)
59
+ **Causa:** Conflicto de regla de negocio (ej: recurso ya existe, versión desactualizada).
60
+ **Resolución:** Reintentar tras refrescar el estado del recurso.
61
+
62
+ ### BugBunny::UnprocessableEntity (422)
63
+ **Causa:** Fallo de validación en el servicio remoto.
64
+ **Acceso a errores:**
65
+ ```ruby
66
+ begin
67
+ order.save
68
+ rescue BugBunny::UnprocessableEntity => e
69
+ e.error_messages # Hash, Array o String con detalles
70
+ e.raw_response # Response original completo
71
+ end
72
+ ```
73
+ **Smart extraction:** Busca `errors`, `error`, `detail`, `message` en el body. Formatea como Hash descriptivo si no encuentra convención.
74
+ **En Resource:** `save` captura 422 automáticamente, carga `resource.errors` y retorna `false`.
75
+
76
+ ## Errores de Servidor (5xx)
77
+
78
+ ### BugBunny::InternalServerError (500+)
79
+ **Causa:** Error no controlado en el servicio remoto.
80
+ **Resolución:** Revisar logs del servicio remoto. Verificar backtrace en `event=controller.unhandled_exception`.
81
+
82
+ ### BugBunny::ServerError (base 5xx)
83
+ **Causa:** Cualquier error de servidor no mapeado a InternalServerError.
84
+ **Resolución:** Similar a InternalServerError.
85
+
86
+ ## Formato de Mensajes de Error
87
+
88
+ El middleware `RaiseError` construye el mensaje así:
89
+ 1. Busca `{ "error": "...", "detail": "..." }` en el body.
90
+ 2. Si no encuentra, usa el Hash completo como JSON.
91
+ 3. Si el body está vacío, usa `"Unknown Error"`.
92
+
93
+ ## Connection Pool Missing
94
+
95
+ **No es una excepción BugBunny**, pero es un error común:
96
+ **Causa:** Se intentó usar un Resource sin asignar el pool global.
97
+ **Resolución:** Asegurar que `BugBunny::Resource.connection_pool = MY_POOL` se ejecute en el arranque.
@@ -0,0 +1,116 @@
1
+ # Resources
2
+
3
+ ## Definición
4
+
5
+ ```ruby
6
+ class Order < BugBunny::Resource
7
+ # Infraestructura AMQP
8
+ @connection_pool = MY_POOL
9
+ @exchange = 'orders_ex'
10
+ @exchange_type = 'topic'
11
+ @resource_name = 'orders' # path en la URL
12
+ @routing_key = 'orders.#'
13
+ @param_key = 'order' # wrapper key en payloads
14
+ @exchange_options = { durable: true }
15
+ @queue_options = { auto_delete: false }
16
+
17
+ # Atributos tipados (ActiveModel::Attributes)
18
+ attribute :id, :integer
19
+ attribute :status, :string
20
+ attribute :total, :decimal
21
+ attribute :active, :boolean
22
+
23
+ # Validaciones (ActiveModel::Validations)
24
+ validates :status, presence: true
25
+
26
+ # Callbacks
27
+ before_save :normalize_status
28
+ after_create :notify_warehouse
29
+ around_destroy :audit_deletion
30
+
31
+ # Middleware client-side
32
+ client_middleware do |stack|
33
+ stack.use BugBunny::Middleware::RaiseError
34
+ stack.use BugBunny::Middleware::JsonResponse
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## Operaciones CRUD
40
+
41
+ ### Class Methods
42
+
43
+ ```ruby
44
+ Order.find(42) # GET orders/42 → Order
45
+ Order.where(status: 'active') # GET orders?status=active → [Order, ...]
46
+ Order.all # GET orders → [Order, ...]
47
+ Order.create(status: 'pending', total: 100) # POST orders → Order
48
+ ```
49
+
50
+ ### Instance Methods
51
+
52
+ ```ruby
53
+ order = Order.new(status: 'pending')
54
+ order.save # POST orders (nuevo) o PUT orders/42 (existente)
55
+ order.update(status: 'shipped') # assign + save
56
+ order.destroy # DELETE orders/42
57
+ order.persisted? # true si fue guardado
58
+ order.changed? # true si tiene cambios sin guardar
59
+ order.errors # ActiveModel::Errors
60
+ ```
61
+
62
+ ### Save: Create vs Update
63
+
64
+ - **Nuevo** (`persisted? == false`): Envía POST con todos los atributos.
65
+ - **Existente** (`persisted? == true`): Envía PUT solo con atributos cambiados (`changes_to_send`).
66
+ - Captura `BugBunny::UnprocessableEntity` (422) y carga `resource.errors`. Retorna `false`.
67
+
68
+ ## Contexto Dinámico (.with)
69
+
70
+ ### Forma de bloque (recomendada)
71
+
72
+ ```ruby
73
+ Order.with(exchange: 'priority_ex', routing_key: 'priority.orders') do
74
+ Order.all # Usa config temporal
75
+ Order.find(1) # También usa config temporal
76
+ end
77
+ # Config restaurada automáticamente
78
+ ```
79
+
80
+ ### Forma de cadena (single use)
81
+
82
+ ```ruby
83
+ order = Order.with(pool: special_pool).find(42)
84
+ # Siguiente llamada requiere nuevo .with()
85
+ ```
86
+
87
+ **Antipatrón:** No guardar el proxy en variable para múltiples llamadas → lanza error.
88
+
89
+ ## Change Tracking
90
+
91
+ Combina `ActiveModel::Dirty` con atributos dinámicos:
92
+
93
+ ```ruby
94
+ order = Order.find(42)
95
+ order.name = 'New Name' # Atributo definido
96
+ order.custom_field = 'value' # Atributo dinámico
97
+ order.changed # → ['name', 'custom_field']
98
+ order.changes_to_send # → { 'name' => 'New Name', 'custom_field' => 'value' }
99
+ ```
100
+
101
+ ## Callbacks Disponibles
102
+
103
+ Definidos con `define_model_callbacks`:
104
+ - `:save` — before/after/around save (create o update)
105
+ - `:create` — before/after/around create (recurso nuevo)
106
+ - `:update` — before/after/around update (recurso existente)
107
+ - `:destroy` — before/after/around destroy
108
+
109
+ ## Coerción de Tipos
110
+
111
+ Los atributos tipados usan `ActiveModel::Attributes`:
112
+ - `'25.50'` → `BigDecimal` (con `:decimal`)
113
+ - `'1'` / `'true'` → `true` (con `:boolean`)
114
+ - `'2026-04-01T...'` → `Time` (con `:time`)
115
+
116
+ Los atributos dinámicos (no declarados) se almacenan sin coerción.
@@ -0,0 +1,82 @@
1
+ # Routing
2
+
3
+ ## DSL Completo
4
+
5
+ ```ruby
6
+ BugBunny.routes.draw do
7
+ # Verbos HTTP individuales
8
+ get 'health', to: 'health#check'
9
+ post 'events', to: 'events#create'
10
+ put 'settings', to: 'settings#update'
11
+ patch 'settings', to: 'settings#patch'
12
+ delete 'cache', to: 'cache#clear'
13
+
14
+ # Resources genera 5 rutas REST
15
+ resources :users
16
+ # GET users → UsersController#index
17
+ # GET users/:id → UsersController#show
18
+ # POST users → UsersController#create
19
+ # PUT users/:id → UsersController#update
20
+ # DELETE users/:id → UsersController#destroy
21
+
22
+ # Filtros
23
+ resources :orders, only: [:index, :show]
24
+ resources :products, except: [:destroy]
25
+
26
+ # Namespaces (anidables)
27
+ namespace :admin do
28
+ namespace :v2 do
29
+ resources :reports # → Admin::V2::ReportsController
30
+ end
31
+ end
32
+
33
+ # Member y Collection
34
+ resources :nodes do
35
+ member do
36
+ put :drain # PUT nodes/:id/drain → NodesController#drain
37
+ post :restart # POST nodes/:id/restart → NodesController#restart
38
+ end
39
+ collection do
40
+ get :stats # GET nodes/stats → NodesController#stats
41
+ get :health # GET nodes/health → NodesController#health
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ ## Route Matching
48
+
49
+ El `RouteSet#recognize(method, path)` busca la primera ruta que matchee:
50
+
51
+ - **Normalización:** Strips leading/trailing slashes del path.
52
+ - **Parámetros:** `:id` se compila a regex `(?<id>[^/]+)`. Los valores se extraen como Hash.
53
+ - **No match:** Retorna `nil` → el consumer responde 404.
54
+
55
+ ```ruby
56
+ route = BugBunny.routes.recognize('GET', 'users/42')
57
+ route.controller # => "users"
58
+ route.action # => "show"
59
+ route.namespace # => nil
60
+ route.params # => { 'id' => '42' }
61
+ ```
62
+
63
+ ## Resolución de Controller
64
+
65
+ El consumer resuelve el controlador concatenando:
66
+ 1. `config.controller_namespace` (default: `BugBunny::Controllers`)
67
+ 2. `route.namespace` (si existe)
68
+ 3. `route.controller.classify + "Controller"`
69
+
70
+ Ejemplo: namespace `:admin`, controller `:reports` → `BugBunny::Controllers::Admin::ReportsController`
71
+
72
+ Valida que el controlador sea subclase de `BugBunny::Controller`. Si no, lanza `SecurityError`.
73
+
74
+ ## Route Object
75
+
76
+ ```ruby
77
+ route.http_method # String: "GET", "POST", etc.
78
+ route.path_pattern # String: "users/:id"
79
+ route.controller # String: "users"
80
+ route.action # String: "show"
81
+ route.namespace # String o nil: "Admin::V2"
82
+ ```