Nimm an der Hanzo-Beta teil und erhalte 50 % auf jeden bezahlten Tarif

Der Router ist das Register

Hanzo Cloud hat keine eingecheckte OpenAPI-Datei. Die lebende Routentabelle ist die Quelle, dreifach projiziert, und ein Bijektionstest lässt den Build scheitern, wenn eine Projektion abweicht. 1,362 Operationen über 963 Pfade, je Prozess zur Anfragezeit erzeugt.

Auf dieser Seite

Es gibt kein openapi.yaml im Repository von Hanzo Cloud. Es gibt kein zweites Register, das Endpunkte aufführt, keine Annotationsdatei und keinen Generatorschritt in der CI, der eine Spezifikation auf die Platte schreibt. Wenn du wissen willst, was die API ist, fragst du einen laufenden Prozess, und er sagt es dir, indem er seinen eigenen Router liest.

Dieser Text handelt davon, warum genau diese Anordnung am Ende die einzige war, die hält, und von den Teilen, die nicht funktionieren — die sich als die nützlichere Hälfte erweisen.

Eine Tabelle, drei Projektionen

serve.go setzt eine Routentabelle zusammen. Drei Dinge lesen sie danach, alle nach MountAll, damit jede eine vollständige Tabelle sieht:

  • /zap spielt die Handler von /v1 über den ZAP-Transport nach.
  • Die Konsole zeichnet sie.
  • GET /v1/openapi.json beschreibt sie.

Keine der drei hält eine Kopie von irgendetwas. Das ist die gesamte Entwurfsbeschränkung, und alles andere folgt daraus. Eine Projektion, die ihre eigene Endpunktliste hielte, wäre eine zweite Quelle der Wahrheit, und eine zweite Quelle der Wahrheit driftet — nicht manchmal, immer, nach dem Takt, in dem jemand vergisst.

Die beschreibende Projektion ist die, nach der Leute fragen, also: openapi.Live(app) ruft app.Fiber().GetRoutes(true) auf, und jede andere Funktion in openapi/ ist eine reine Funktion der []Route, die zurückkommt. Das Dokument wird nicht aus dem Router gebaut. Das Dokument ist der Router, in einer anderen Form.

Warum das Lesen des lebenden Routers die einzige totale Quelle ist

Die naheliegende Alternative ist, den Quelltext zu durchsuchen. Sie funktioniert nicht, und der Grund verdient eine genaue Formulierung, weil er erklärt, warum viel API-Werkzeug still unterberichtet.

POST /v1/kms/auth/login wird so registriert:

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

Kein grep findet diesen Pfad. Die Zeichenkette /v1/kms/auth/login existiert im Code nicht. Sie existiert nur im zusammengesetzten Router, zur Laufzeit, nachdem das Framework den Gruppenpräfix und das Blatt verkettet hat.

Dann wird es schlimmer, auf eine Art, die tatsächlich richtig ist. Die Routenmenge ist eine Funktion der Deployment-Konfiguration — cfg.Enabled, plus interne Tore wie das if kc != nil von KMS. Ein Deployment, das admin nicht einhängt, bedient keine Admin-Routen. Die ehrliche Spezifikation dieses Deployments kündigt sie also nicht an.

Darum wird das Dokument je Prozess zur Anfragezeit erzeugt, statt einmal in der CI gebaut. Eine in der CI gebaute Spezifikation beschreibt ein hypothetisches maximales Deployment, das nicht das sein muss, mit dem du sprichst. Eine vom lebenden Router gelesene Spezifikation beschreibt den Prozess, der dir geantwortet hat.

Der Test, der es wahr macht

Eine Architektur, in der die Spezifikation nicht driften kann, ist eine Behauptung, und Behauptungen brauchen ein Tor. Unseres ist eine Bijektion, in cmd/cloud/openapi_test.go:

func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {

Er hängt jedes Subsystem in apps.Wire() ein, erzeugt das Dokument und behauptet die Abbildung in beide Richtungen: Jede lebende Route erscheint als Operation, und jede Operation ist durch eine lebende Route gedeckt. Keine Teilmenge in einer der beiden Richtungen.

Heute ausgeführt:

bijection holds over 1362 operations / 963 paths / 138 products

Es ist der einzige Test im Repository, dessen Fehlschlag bedeutet, dass das veröffentlichte Dokument lügt. Alles andere, das bricht, macht das Produkt falsch; dieser macht den Vertrag falsch, und das ist schlimmer, weil Integratoren bereits dagegen gebaut haben.

Dass die Zahl sich bewegt, ist kein Problem — sie bewegt sich, wann immer ein Subsystem hinzukommt. Die Zahl, auf die es ankommt, ist die Differenz der beiden Seiten, und diese Zahl ist null oder der Build scheitert.

Die Produktachse ist mechanisch

Das erste Pfadsegment nach /v1/ ist das Produkt. openapi.Product liest es und versieht jede Operation damit, wodurch eine CLI hanzo <product> <resource> <verb> bauen kann, ohne dass jemand urteilt, wo ein Befehl leben soll.

Es ist absichtlich nicht der Name des Subsystems. clients/billing bedient auch /v1/finance/*, und die URL ist das, was ein Aufrufer sieht, also gewinnt die URL. Die Achse nach dem internen Paket zu benennen hätte die CLI zu einem kuratierten Artefakt gemacht — einem Ding mit Meinungen darin, das jemand pflegen muss — statt zu einer Projektion.

Was der Router dir nicht sagen kann

Hier ist die Grenze, und wir wollen sie nicht umgehen.

Methode, Pfad, Pfadparameter und Produkt lassen sich aus dem Router ableiten. Anfrage- und Antwortschemata, Query- und Header-Parameter, Statuscodes und Auth nicht. Der Router hält ein func(*zip.Ctx) error. Der Anfragetyp ist eine lokale Variable im Körper des Handlers:

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

Go kann von einem Funktionswert nicht in dessen Körper reflektieren. Hier gibt es keinen klugen Ausweg, und einer im Generator wäre Raten — so sammelt eine Spezifikation sichere, falsche Einträge.

Der einzige Weg zu Schemata ist eine typisierte Op:

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

In und Out stehen auf der Registrierung, also ist das Schema da, ohne jemandem in den Kopf zu sehen. Und weil sie auf der Registrierung stehen, liefert derselbe Eintrag auch ein MCP-Werkzeug — zip/openapi.go und zip/mcp.go sind zwei Leser eines Registers. Das ist die vierte Projektion, und sie kam ohne vierten Mechanismus.

Die Migration ist von der Konstruktion her inkrementell: GetRoutes() ist eine Obermenge von app.ops, also fügt das Umwandeln eines Handlers in eine typisierte Op Schema hinzu, ohne etwas stromaufwärts davon zu ändern.

Die Routen, die sich nicht typisieren lassen, und warum

clients/integrations ist so weit umgestellt, wie der Draht es zulässt: 20 von 41 Routen sind typisierte Ops. Die anderen 21 bleiben rohe Handler, und jede steht in einer rawRoutes-Map in ops_projection_test.go mit dem Grund, warum sie nicht wechseln kann:

  • 13 Browser-Redirects. Der OAuth-Callback und jedes Glied der Verknüpfung antworten 302. Eine typisierte Op gibt einen Wert zurück, keinen Location-Header und keinen Status in den 300ern.
  • 6 Webhooks. HMAC von Slack und GitHub, Ed25519 von Discord, JWT des Bot Framework von Teams, der secret-token-Header von Telegram. Jeder authentifiziert eine Signatur über die rohen Anfragebytes. Eine typisierte Op empfängt ein dekodiertes In und sieht die Bytes nie, über die die Signatur berechnet wurde, also würde Typisieren ihre Auth zerbrechen.
  • 2 Erzeuger von 202 Accepted. cloud.Created gibt uns eine 201-Naht. Eine 202-Naht gibt es nicht. Zwei Routen brauchen eine, also warten zwei Routen.

Wir halten diese Liste für das wertvollste Artefakt im ganzen System. Sie ist ein aufgezähltes, von Tests festgehaltenes Inventar genau der Stelle, an der die Abstraktion aufhört, mit einem Grund je Eintrag, über den ein Reviewer streiten kann. Der Fehlermodus, den sie verhindert, ist der, in dem eine Route still zu einem rohen Handler zurückrutscht und zugleich aus dem Dokument, dem MCP-Werkzeugsatz und der CLI verschwindet — unsichtbar, weil nichts behauptet hatte, dass sie dort gewesen war.

Eine Architektur sind nicht die Teile, die sich verallgemeinern. Es sind die Teile, die sich verallgemeinern, plus eine ehrliche, erzwungene Rechenschaft über die Teile, die es nicht tun.

Was das einbringt

Wer gegen Hanzo Cloud integriert, liest ein Dokument, das der Prozess erzeugt hat, der seine Anfragen beantwortet, in dem Deployment, mit dem er tatsächlich spricht, vor wenigen Millisekunden. Ein Agent, der über MCP ruft, erreicht denselben Handler, mit derselben Tenant-Prüfung, wie ein REST-Aufrufer — ein anonymer Aufruf von /mcp bekommt dasselbe 403, das REST gibt, weil es derselbe Codepfad ist und ein Test ihn festnagelt.

Nichts davon verlangte, dass sich jemand daran erinnert, irgendetwas neu zu erzeugen.

Die allgemeine Form: Wenn zwei Artefakte übereinstimmen müssen, erzeuge nicht das eine aus dem anderen und hoffe. Mach eines zu einer Sicht auf das andere, und schreib den Test, der scheitert, wenn sie es nicht sind.

Kommentare

Melde dich mit deiner Hanzo ID an, um zu kommentieren.

Kommentare werden geladen…

Weiterlesen