mcpField
T
typed-service-contracts

typed-service-contracts

github.com/google-labs-code開発1.8k96 回インストール3 日前に更新
リポジトリ

typed-service-contracts について

CLI やライブラリの開発向けに、契約と垂直スライスでアプリケーションロジックを構成する設計スキルです。Zod で入力を解析し、ハンドラーでは例外を投げずに Result に変換します。

主な機能

  1. 01Spec で入出力とエラーの契約を定義
  2. 02Zod による入力の解析と変換
  3. 03Handler で副作用のある処理を実装
  4. 04Result で成功と失敗を表現
  5. 05契約テストとロジックテストを分離

活用シーン

  1. 01CLI の入力境界を厳密に定義
  2. 02ファイルパスなどの入力を解析・検証
  3. 03エラー変換と業務ロジックの結果をテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/google-labs-code/stitch-sdk --skill typed-service-contracts

インストール後、Claude Code で/typed-service-contractsを入力して読み込まれたか確認します。

詳細

型付きサービスコントラクト(Spec & Handler パターン)

このスキルでは、Design by Contract(DbC)の原則に基づく垂直スライスアーキテクチャを定義します。アプリケーションロジックを厳密に定義された作業単位として扱い、入力は単に検証するのではなく解析し、エラーは例外ではなく値(Result パターン)として扱います。

このスキルを使用する場面

  • CLI やライブラリの構築: ユーザー入力とシステムロジックの間に厳格な境界が必要な場合。
  • 複雑な検証: 入力を使用可能な状態にする前に変換(解析)が必要な場合(例:文字列が有効なファイルパスであることの確認)。
  • 高い信頼性が求められる要件: 未処理のランタイム例外を許容できず、網羅的なエラー処理が必要な場合。
  • テスト重視: データ検証テストとビジネスロジックのテストを分離したい場合。

アーキテクチャの構成要素

1. Spec(spec.ts)

「コントラクト」または「ポート」です。_何をするか_を定義します。次の内容を含める必要があります。

  • 入力スキーマ: 生の入力を有効な DTO に解析する Zod スキーマ。
  • 出力スキーマ: 成功時のデータ構造を定義する Zod スキーマ。
  • エラースキーマ: 特定の失敗モードを表す判別可能なユニオン(汎用エラーではありません)。
  • Result 型: Success | Failure の DiscriminatedUnion。
  • インターフェース: 機能の定義(例:interface ConfigureSpec)。
2. Handler(handler.ts)

「実装」または「アダプター」です。_どのように実現するか_を定義します。次の要件を満たす必要があります。

  • Spec で定義されたインターフェースを実装する。
  • 副作用(ファイルシステム、API 呼び出し)を処理する「不純な」クラスである。
  • 例外を決してスローしない。内部エラーを捕捉し、Result 型にマッピングする必要があります。

使用方法

ステップ 1:コントラクトを定義する(spec.ts)

次のテンプレートに従って境界を定義します。

import { z } from "zod";

// 1. VALIDATION HELPERS (Reusable Refinements)
export const SafePathSchema = z
  .string()
  .min(1)
  .refine((p) => !p.includes(".."), "No traversal allowed");

// 2. INPUT (The Command) - "Parse, don't validate"
export const MyTaskInputSchema = z.object({
  path: SafePathSchema,
  force: z.boolean().default(false),
});
export type MyTaskInput = z.infer<typeof MyTaskInputSchema>;

// 3. ERROR CODES (Exhaustive)
export const MyTaskErrorCode = z.enum([
  "FILE_NOT_FOUND",
  "PERMISSION_DENIED",
  "UNKNOWN_ERROR",
]);

// 4. RESULT (The Monad)
export const MyTaskSuccess = z.object({
  success: z.literal(true),
  data: z.string(), // The output payload
});

export const MyTaskFailure = z.object({
  success: z.literal(false),
  error: z.object({
    code: MyTaskErrorCode,
    message: z.string(),
    suggestion: z.string().optional(),
    recoverable: z.boolean(),
  }),
});

export type MyTaskResult =
  | z.infer<typeof MyTaskSuccess>
  | z.infer<typeof MyTaskFailure>;

// 5. INTERFACE (The Capability)
export interface MyTaskSpec {
  execute(input: MyTaskInput): Promise<MyTaskResult>;
}
ステップ 2:Handler を実装する(handler.ts)

次のテンプレートに従ってロジックを実装します。

import { MyTaskSpec, MyTaskInput, MyTaskResult } from "./spec.js";
import * as fs from "fs";

export class MyTaskHandler implements MyTaskSpec {
  async execute(input: MyTaskInput): Promise<MyTaskResult> {
    try {
      // 1. Business Logic
      if (!fs.existsSync(input.path)) {
        // 2. Explicit Error Return (No Throwing)
        return {
          success: false,
          error: {
            code: "FILE_NOT_FOUND",
            message: `Path does not exist: ${input.path}`,
            recoverable: true,
          },
        };
      }

      // 3. Success Return
      return {
        success: true,
        data: "Operation complete",
      };
    } catch (error) {
      // 4. Safety Net: Catch unknown runtime errors
      return {
        success: false,
        error: {
          code: "UNKNOWN_ERROR",
          message: error instanceof Error ? error.message : String(error),
          recoverable: false,
        },
      };
    }
  }
}
ステップ 3:テスト戦略

モノリシックなテストは書かないでください。コントラクトテストとロジックテストに分割します。

A. コントラクトテスト(スキーマ)

_門番_をテストします。無効なデータがハンドラーに到達する前に拒否されることを確認します。

  • 重点項目: エッジケース、検証ルール、Zod のリファインメント。
  • スタイル: データ駆動型(テーブルテスト)。
// spec.test.ts
import { MyTaskInputSchema } from "./spec";

const invalidCases = [
  { val: "../etc/passwd", err: "No traversal allowed" },
  { val: "", err: "min(1)" },
];

test.each(invalidCases)("validates paths", ({ val, err }) => {
  const result = MyTaskInputSchema.safeParse({ path: val });
  expect(result.success).toBe(false);
});
B. ロジックテスト(Handler)

_シェフ_をテストします。外部依存関係(fs、ネットワーク)をモックし、Result オブジェクトをアサートします。

  • 重点項目: ビジネスロジックのフロー、エラーマッピング、成功状態。
  • スタイル: モックを使用した単体テスト、またはシナリオランナー。
// handler.test.ts
import { MyTaskHandler } from "./handler";
import { vi } from "vitest"; // or jest

test("returns FILE_NOT_FOUND if path missing", async () => {
  // MOCK
  vi.mocked(fs.existsSync).mockReturnValue(false);

  // EXECUTE
  const handler = new MyTaskHandler();
  const result = await handler.execute({ path: "/fake" });

  // ASSERT (Check the Result Object)
  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error.code).toBe("FILE_NOT_FOUND");
  }
});

コメント