Únete a la beta de Hanzo y consigue un 50 % de descuento en cualquier plan de pago

El router es el registro

Hanzo Cloud no tiene ningún archivo OpenAPI versionado. La tabla de rutas en vivo es la fuente, proyectada de tres maneras, y un test de biyección hace fallar la compilación si alguna proyección se desvía. 1,362 operaciones en 963 rutas, generadas por proceso en el momento de la petición.

En esta página

No hay ningún openapi.yaml en el repositorio de Hanzo Cloud. No hay un segundo registro que enumere endpoints, ni un fichero de anotaciones, ni un paso generador en CI que escriba una especificación a disco. Si queréis saber qué es la API, preguntáis a un proceso en ejecución, y os lo dice leyendo su propio router.

Este texto explica por qué ese acabó siendo el único arreglo que se sostiene, y también las partes que no funcionan, que resultan ser la mitad más útil.

Una tabla, tres proyecciones

serve.go compone una sola tabla de rutas. Tres cosas la leen después, todas tras MountAll, de modo que cada una ve una tabla completa:

  • /zap reproduce los handlers de /v1 sobre el transporte ZAP.
  • La consola los pinta.
  • GET /v1/openapi.json los describe.

Ninguna de las tres guarda una copia de nada. Esa es toda la restricción de diseño, y lo demás se sigue de ella. Una proyección que guardara su propia lista de endpoints sería una segunda fuente de verdad, y una segunda fuente de verdad se desvía: no a veces, siempre, con el calendario que marca cada vez que alguien se olvida.

La proyección que describe es la que la gente pregunta, así que: openapi.Live(app) llama a app.Fiber().GetRoutes(true), y cualquier otra función de openapi/ es una función pura del []Route que vuelve. El documento no se construye a partir del router. El documento es el router, con otra forma.

Por qué leer el router en vivo es la única fuente total

La alternativa obvia es escanear el código fuente. No funciona, y merece la pena decir con precisión por qué, porque es la razón de que mucha herramienta de API informe de menos, en silencio.

POST /v1/kms/auth/login se registra así:

Group("/v1/kms/auth").Post("/login")

Ningún grep encuentra esa ruta. La cadena /v1/kms/auth/login no existe en el código. Solo existe en el router ensamblado, en tiempo de ejecución, después de que el framework haya concatenado el prefijo del grupo y la hoja.

Luego empeora, de una forma que en realidad es correcta. El conjunto de rutas es una función de la configuración del despliegue: cfg.Enabled, más compuertas internas como el if kc != nil de KMS. Un despliegue que no monta admin no sirve rutas de admin. Así que la especificación honesta de ese despliegue no las anuncia.

Por eso el documento se genera por proceso, en el momento de la petición, y no se construye una vez en CI. Una especificación construida en CI describe un despliegue máximo hipotético, que puede no ser aquel con el que estáis hablando. Una especificación leída del router en vivo describe el proceso que os contestó.

El test que lo hace verdad

Una arquitectura en la que la especificación no puede desviarse es una afirmación, y las afirmaciones necesitan una compuerta. La nuestra es una biyección, en cmd/cloud/openapi_test.go:

func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {

Monta cada subsistema en apps.Wire(), genera el documento y afirma el mapeo en los dos sentidos: cada ruta en vivo aparece como operación, y cada operación está respaldada por una ruta en vivo. No un subconjunto en ninguna de las dos direcciones.

Ejecutado hoy:

bijection holds over 1362 operations / 963 paths / 138 products

Es el único test del repositorio cuyo fallo significa que el documento publicado miente. Todo lo demás que se rompe deja el producto mal; este deja el contrato mal, que es peor, porque quienes integran ya han construido contra él.

Que la cuenta se mueva no es un problema: se mueve cada vez que se añade un subsistema. El número que importa es la diferencia entre los dos lados, y ese número es cero o la compilación falla.

El eje de producto es mecánico

El primer segmento de ruta después de /v1/ es el producto. openapi.Product lo lee y etiqueta con él cada operación, que es lo que permite a una CLI construir hanzo <product> <resource> <verb> sin que nadie ejerza juicio sobre dónde debe vivir un comando.

A propósito, no es el nombre del subsistema. clients/billing también sirve /v1/finance/*, y la URL es lo que ve quien llama, así que gana la URL. Nombrar el eje según el paquete interno habría hecho de la CLI un artefacto curado —una cosa con opiniones dentro, que alguien tiene que mantener— en lugar de una proyección.

Lo que el router no puede deciros

Este es el límite, y no pensamos rodearlo.

El método, la ruta, los parámetros de ruta y el producto se derivan del router. Los esquemas de petición y de respuesta, los parámetros de query y de cabecera, los códigos de estado y la autenticación no. El router guarda un func(*zip.Ctx) error. El tipo de la petición es una variable local dentro del cuerpo del handler:

var req secretPutRequest
json.Unmarshal(ctx.Body(), &req)

Go no puede reflejar desde un valor de función hasta su cuerpo. Aquí no hay un arreglo ingenioso, y intentarlo en el generador significaría adivinar, que es como una especificación adquiere entradas seguras y equivocadas.

El único camino hacia los esquemas es una op tipada:

zip.Get[In, Out](...)

In y Out están en el registro, así que el esquema está disponible sin leerle la mente a nadie. Y, como están en el registro, la misma entrada produce también una herramienta MCP: zip/openapi.go y zip/mcp.go son dos lectores de un solo registro. Esa es la cuarta proyección, y llegó sin un cuarto mecanismo.

La migración es incremental por construcción: GetRoutes() es un superconjunto de app.ops, así que convertir un handler en una op tipada añade esquema sin cambiar nada que esté aguas arriba.

Las rutas que no se pueden tipar, y por qué

clients/integrations está convertido hasta donde el wire lo permite: 20 de 41 rutas son ops tipadas. Las otras 21 siguen como handlers en crudo, y cada una figura en un mapa rawRoutes de ops_projection_test.go con la razón por la que no puede moverse:

  • 13 redirecciones de navegador. La callback de OAuth y cada tramo del enlace responden 302. Una op tipada devuelve un valor, no una cabecera Location y un estado en los 300.
  • 6 webhooks. HMAC de Slack y de GitHub, Ed25519 de Discord, JWT del Bot Framework de Teams, la cabecera secret-token de Telegram. Cada uno autentica una firma sobre los bytes crudos de la petición. Una op tipada recibe un In ya decodificado y no ve nunca los bytes sobre los que se calculó la firma, así que tiparlos rompería su autenticación.
  • 2 creadores de 202 Accepted. cloud.Created nos da una junta de 201. No hay junta de 202. Dos rutas necesitan una, así que dos rutas esperan.

Creemos que esa lista es el artefacto más valioso de todo el sistema. Es un inventario enumerado, fijado por tests, de exactamente dónde se detiene la abstracción, con una razón por entrada que un revisor puede discutir. El fallo que evita es aquel en el que una ruta vuelve en silencio a un handler en crudo y desaparece a la vez del documento, del conjunto de herramientas MCP y de la CLI, de forma invisible, porque nada afirmaba que hubiera estado ahí.

Una arquitectura no son las partes que generalizan. Son las partes que generalizan más un recuento honesto, y exigido, de las partes que no.

Lo que esto compra

Quien integra contra Hanzo Cloud lee un documento producido por el proceso que está contestando a sus peticiones, en el despliegue con el que de verdad habla, hace unos milisegundos. Un agente que llama por MCP llega al mismo handler, con la misma comprobación de tenant, que quien llama por REST: una llamada anónima a /mcp recibe el mismo 403 que da REST, porque es el mismo camino de código y un test lo fija.

Nada de eso exigió que alguien se acordara de regenerar nada.

La forma general: si dos artefactos tienen que coincidir, no generéis uno a partir del otro y esperéis. Haced que uno sea una vista del otro, y escribid el test que falla cuando no lo son.

Comentarios

Entra con tu Hanzo ID para comentar.

Cargando comentarios…

Leer más