Hanzoのベータに参加すると、有料プランが50%オフになります

ルーターがレジストリである

Hanzo Cloud にチェックインされた OpenAPI ファイルはない。生きているルート表がソースであり、三通りに射影され、どの射影がずれても全単射のテストがビルドを落とす。963 パスにわたる 1,362 操作が、リクエスト時にプロセスごとに生成される。

このページの内容

Hanzo Cloud のリポジトリに openapi.yaml はない。エンドポイントを列挙する第二のレジストリもなく、注釈ファイルもなく、仕様をディスクへ書く CI の生成ステップもない。API が何かを知りたければ、動いているプロセスに尋ねる。それは自分のルーターを読んで答える。

この記事は、なぜその配置だけが最後に持ったのか、そして動かない部分についてである。動かない部分のほうが、より役に立つ半分だと分かる。

一つの表、三つの射影

serve.go が一つのルート表を組む。三つのものがそれを読む。いずれも MountAll のあとなので、それぞれが完全な表を見る:

  • /zap は ZAP トランスポートの上で /v1 のハンドラを 再生する。
  • コンソールはそれらを 描画する。
  • GET /v1/openapi.json はそれらを 記述する。

三つとも、何のコピーも持たない。それが設計制約のすべてであり、ほかはすべてそこから従う。エンドポイントの自分のリストを持つ射影は第二の真実の源になり、第二の真実の源はずれる。ときどきではなく、いつも、誰かが忘れる頻度が決める日程で。

記述の射影が人の尋ねるものなので、こうなる。openapi.Live(app) が app.Fiber().GetRoutes(true) を呼び、openapi/ のほかの関数はすべて、戻ってくる []Route の純粋関数である。文書はルーターから組み立てられない。文書は、別の形をしたルーター そのもの である。

生きているルーターを読むことだけが完全なソースである理由

明らかな代替はソースを走査することだ。それは働かない。理由は正確に述べる価値がある。多くの API ツールが静かに過少報告する理由だからだ。

POST /v1/kms/auth/login はこう登録される:

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

どの grep もそのパスを見つけない。文字列 /v1/kms/auth/login はコードベースに存在しない。それは組み立てられたルーターの中、実行時に、グループ接頭辞と葉をフレームワークが連結したあとにだけ存在する。

それから、実は正しいやり方で、さらに悪くなる。ルート集合はデプロイ設定の関数である。cfg.Enabled に加え、KMS の if kc != nil のような内部ゲートがある。管理をマウントしないデプロイは管理ルートを出さない。だからそのデプロイの正直な仕様は、それらを宣伝しない。

だから文書はプロセスごとに、リクエスト時に生成され、CI で一度だけ作られない。CI で作った仕様は、仮説上の最大デプロイを記述し、それは今話している相手ではないかもしれない。生きているルーターから読んだ仕様は、あなたに答えたプロセスを記述する。

それを真にするテスト

仕様がずれ得ないアーキテクチャは主張であり、主張には門が要る。私たちの門は全単射で、cmd/cloud/openapi_test.go にある:

func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {

apps.Wire() のあらゆるサブシステムをマウントし、文書を生成し、対応を両方向に主張する。生きているルートはすべて操作として現れ、すべての操作は生きているルートに裏打ちされる。どちらの方向にも部分集合ではない。

今日走らせると:

bijection holds over 1362 operations / 963 paths / 138 products

失敗が、公開された文書が嘘をついていることを意味する、リポジトリで唯一のテストである。ほかの壊れは製品を間違わせる。これは契約を間違わせる。そちらのほうが悪い。統合する側はすでにそれに対して組んでいるからだ。

数が動くことは問題ではない。サブシステムが足されるたびに動く。重要な数は両側の差であり、その数は 0 でなければビルドが失敗する。

製品の軸は機械的である

/v1/ のあとの最初のパス部分が製品である。openapi.Product がそれを読み、各操作にタグする。それで CLI は、コマンドがどこに住むべきか誰も判断せずに hanzo <product> <resource> <verb> を組める。

これは意図してサブシステム名ではない。clients/billing は /v1/finance/* も出す。呼び出し側が見るのは URL なので、URL が勝つ。軸を内部パッケージ名にすると、CLI は意見の入った、誰かが維持する選定済みの成果物になり、射影ではなくなる。

ルーターが教えられないこと

これが限界であり、迂回するつもりはない。

メソッド、パス、パスパラメータ、製品はルーターから導ける。リクエストとレスポンスのスキーマ、クエリとヘッダのパラメータ、ステータスコード、認証は 導けない。ルーターが持つのは func(*zip.Ctx) error である。リクエスト型はハンドラ本体の中の局所変数だ:

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

Go は関数値からその本体へ反映できない。ここにちょうどいい修正はなく、ジェネレータで試みることは推測を意味する。仕様が自信たっぷりで間違った項目を得るのは、そのやり方だ。

スキーマへの唯一の道は型付き操作である:

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

In と Out は登録の上にあるので、誰かの心を読まずにスキーマが手に入る。登録の上にあるから、同じ項目は MCP ツールにもなる。zip/openapi.go と zip/mcp.go は一つのレジストリの二人の読み手である。それが第四の射影であり、第四の機構なしに届いた。

移行は構成上インクリメンタルである。GetRoutes() は app.ops の上位集合なので、ハンドラを型付き操作に変えると、その上流を変えずにスキーマが足される。

型を付けられないルートと、その理由

clients/integrations は配線が許すところまで変換されている。41 のうち 20 が型付き操作である。残り 21 は生のハンドラのままで、それぞれ ops_projection_test.go の rawRoutes マップに、動けない理由とともに載っている:

  • ブラウザリダイレクト 13。 OAuth コールバックとすべてのリンク脚は 302 で答える。型付き操作は値を返し、Location ヘッダと 300 番台のステータスは返さない。
  • ウェブフック 6。 Slack と GitHub の HMAC、Discord の Ed25519、Teams Bot Framework の JWT、Telegram の秘密トークンヘッダ。どれも 生のリクエストバイト の上の署名を認証する。型付き操作はデコードされた In を受け取り、署名が計算されたバイトを見ない。だからこれらに型を付けると認証が壊れる。
  • 202 Accepted の作成者 2。 cloud.Created は 201 の継ぎ目をくれる。202 の継ぎ目はない。二つのルートがそれを必要とするので、二つのルートは待つ。

その一覧こそ、システム全体で最も価値のある成果物だと思う。抽象がちょうど止まる場所の、列挙されテストで固定された目録であり、項目ごとの理由はレビュアが反論できる。防いでいる失敗は、ルートが静かに生ハンドラへ戻り、文書と MCP ツール集合と CLI から一度に消えることだ。見えずに消える。そこにあったと主張するものが何もなかったからだ。

アーキテクチャは一般化する部分ではない。一般化する部分に、一般化しない部分の正直で強制された勘定を足したものである。

これで買えるもの

Hanzo Cloud に統合する呼び出し側は、自分のリクエストに答えているプロセスが、実際に話しているデプロイで、数ミリ秒前に作った文書を読む。MCP 越しに呼ぶエージェントは、REST の呼び出し側と同じハンドラに、同じテナンシ検査つきで届く。匿名の /mcp 呼び出しは REST が返すのと同じ 403 を得る。同じコードパスであり、テストがそれを固定しているからだ。

そのどれにも、誰かが何かを再生成することを覚えておく必要はなかった。

一般形はこうだ。二つの成果物が一致しなければならないなら、一方を他方から生成して願うな。一方を他方のビューにし、一致しないときに失敗するテストを書け。

コメント

コメントするには Hanzo ID でサインインしてください。

コメントを読み込み中…

続きを読む