Unisciti alla beta di Hanzo e ottieni il 50% di sconto su qualsiasi piano a pagamento

Il router è il registro

Hanzo Cloud non ha alcun file OpenAPI versionato. La tabella di route viva è la fonte, proiettata in tre modi, e un test di biiezione fa fallire la build se una proiezione deriva. 1,362 operazioni su 963 path, generate per processo al momento della richiesta.

In questa pagina

Non c'è alcun openapi.yaml nel repository di Hanzo Cloud. Non c'è un secondo registro che elenchi gli endpoint, nessun file di annotazioni e nessun passo di generatore in CI che scriva una specifica su disco. Se vuoi sapere che cos'è l'API, lo chiedi a un processo in esecuzione, e te lo dice leggendo il proprio router.

Questo testo spiega perché quell'assetto è finito per essere l'unico che regge, e anche le parti che non funzionano — che si rivelano la metà più utile.

Una tabella, tre proiezioni

serve.go compone una sola tabella di route. Tre cose la leggono poi, tutte dopo MountAll, così ognuna vede una tabella completa:

  • /zap ripete gli handler di /v1 sul trasporto ZAP.
  • La console li disegna.
  • GET /v1/openapi.json li descrive.

Nessuna delle tre tiene una copia di alcunché. È l'intero vincolo di progetto, e il resto ne discende. Una proiezione che tenesse un proprio elenco di endpoint sarebbe una seconda fonte di verità, e una seconda fonte di verità deriva — non a volte, sempre, col ritmo con cui qualcuno dimentica.

La proiezione che descrive è quella su cui la gente chiede, quindi: openapi.Live(app) chiama app.Fiber().GetRoutes(true), e ogni altra funzione in openapi/ è una funzione pura del []Route che torna. Il documento non è costruito a partire dal router. Il documento è il router, in un'altra forma.

Perché leggere il router vivo è l'unica fonte totale

L'alternativa ovvia è scandire il sorgente. Non funziona, e il motivo vale la pena di dirlo con precisione, perché è la ragione per cui molta strumentazione di API sotto-riporta, in silenzio.

POST /v1/kms/auth/login è registrato così:

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

Nessun grep trova quel percorso. La stringa /v1/kms/auth/login non esiste nel codice. Esiste solo nel router assemblato, a runtime, dopo che il framework ha concatenato il prefisso del gruppo e la foglia.

Poi peggiora, in un modo che in realtà è corretto. L'insieme delle route è una funzione della configurazione di deployment — cfg.Enabled, più cancelli interni come l'if kc != nil di KMS. Un deployment che non monta admin non serve le route di admin. La specifica onesta di quel deployment non le annuncia.

Per questo il documento è generato per processo, al momento della richiesta, invece di essere costruito una volta in CI. Una specifica costruita in CI descrive un deployment massimo ipotetico, che può non essere quello con cui stai parlando. Una specifica letta dal router vivo descrive il processo che ti ha risposto.

Il test che lo rende vero

Un'architettura in cui la specifica non può derivare è un'affermazione, e le affermazioni hanno bisogno di un cancello. Il nostro è una biiezione, in cmd/cloud/openapi_test.go:

func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {

Monta ogni sottosistema in apps.Wire(), genera il documento e afferma la corrispondenza nei due sensi: ogni route viva compare come operazione, e ogni operazione è retta da una route viva. Non un sottoinsieme in nessuna delle due direzioni.

Eseguito oggi:

bijection holds over 1362 operations / 963 paths / 138 products

È l'unico test del repository il cui fallimento significa che il documento pubblicato mente. Tutto il resto che si rompe rende sbagliato il prodotto; questo rende sbagliato il contratto, che è peggio, perché chi integra ha già costruito contro di esso.

Che il conteggio si muova non è un problema — si muove ogni volta che si aggiunge un sottosistema. Il numero che conta è la differenza tra i due lati, e quel numero è zero oppure la build fallisce.

L'asse di prodotto è meccanico

Il primo segmento di percorso dopo /v1/ è il prodotto. openapi.Product lo legge e ne etichetta ogni operazione, ed è ciò che permette a una CLI di costruire hanzo <product> <resource> <verb> senza che nessuno giudichi dove un comando debba vivere.

Di proposito non è il nome del sottosistema. clients/billing serve anche /v1/finance/*, e l'URL è ciò che vede chi chiama, quindi vince l'URL. Nominare l'asse secondo il package interno avrebbe fatto della CLI un artefatto curato — una cosa con opinioni dentro, che qualcuno deve mantenere — invece di una proiezione.

Quello che il router non può dirti

Ecco il limite, e non intendiamo aggirarlo.

Metodo, percorso, parametri di percorso e prodotto si ricavano dal router. Gli schemi di richiesta e di risposta, i parametri di query e di header, i codici di stato e l'autenticazione no. Il router tiene un func(*zip.Ctx) error. Il tipo della richiesta è una variabile locale nel corpo dell'handler:

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

Go non può riflettere da un valore di funzione nel suo corpo. Qui non c'è un trucco, e tentarne uno nel generatore significherebbe indovinare — ed è così che una specifica acquisisce voci sicure e sbagliate.

L'unica via agli schemi è un'op tipizzata:

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

In e Out stanno sulla registrazione, quindi lo schema è disponibile senza leggere nel pensiero di nessuno. E, poiché stanno sulla registrazione, la stessa voce produce anche uno strumento MCP — zip/openapi.go e zip/mcp.go sono due lettori di un solo registro. È la quarta proiezione, ed è arrivata senza un quarto meccanismo.

La migrazione è incrementale per costruzione: GetRoutes() è un superinsieme di app.ops, quindi convertire un handler in un'op tipizzata aggiunge lo schema senza cambiare nulla a monte.

Le route che non si possono tipizzare, e perché

clients/integrations è convertito fin dove il filo lo consente: 20 route su 41 sono op tipizzate. Le altre 21 restano handler grezzi, e ciascuna è elencata in una mappa rawRoutes in ops_projection_test.go con la ragione per cui non può spostarsi:

  • 13 redirect del browser. La callback OAuth e ogni tratto del collegamento rispondono 302. Un'op tipizzata restituisce un valore, non un header Location e uno stato nei 300.
  • 6 webhook. HMAC di Slack e di GitHub, Ed25519 di Discord, JWT del Bot Framework di Teams, l'header secret-token di Telegram. Ognuno autentica una firma sui byte grezzi della richiesta. Un'op tipizzata riceve un In già decodificato e non vede mai i byte su cui la firma è stata calcolata, quindi tipizzarli ne romperebbe l'autenticazione.
  • 2 creatori di 202 Accepted. cloud.Created ci dà una giuntura 201. Non c'è una giuntura 202. Due route ne hanno bisogno, quindi due route aspettano.

Pensiamo che quell'elenco sia l'artefatto più prezioso di tutto il sistema. È un inventario enumerato, fissato dai test, di dove esattamente l'astrazione si ferma, con una ragione per voce che un revisore può contestare. Il modo di fallire che previene è quello in cui una route torna in silenzio a un handler grezzo e sparisce insieme dal documento, dall'insieme di strumenti MCP e dalla CLI — in modo invisibile, perché nulla affermava che ci fosse stata.

Un'architettura non sono le parti che si generalizzano. Sono le parti che si generalizzano più un resoconto onesto, e imposto, delle parti che non lo fanno.

Che cosa compra

Chi integra contro Hanzo Cloud legge un documento prodotto dal processo che sta rispondendo alle sue richieste, nel deployment con cui parla davvero, pochi millisecondi fa. Un agente che chiama via MCP raggiunge lo stesso handler, con lo stesso controllo di tenant, di chi chiama via REST — una chiamata anonima a /mcp riceve lo stesso 403 che dà REST, perché è lo stesso percorso di codice e un test lo fissa.

Niente di tutto ciò ha richiesto che qualcuno si ricordasse di rigenerare alcunché.

La forma generale: se due artefatti devono concordare, non generarne uno dall'altro e sperare. Fai di uno una vista dell'altro, e scrivi il test che fallisce quando non lo sono.

Commenti

Accedi con il tuo Hanzo ID per commentare.

Caricamento dei commenti…

Leggi di più