mcpField
T
typed-service-contracts

typed-service-contracts

github.com/google-labs-code開発28.6k707 回インストール2 週間前に更新
リポジトリ

typed-service-contracts について

入力のパースと Result によるエラー処理を取り入れ、契約とハンドラーでサービスを定義する TypeScript 向けアーキテクチャスキルです。境界の明確化や網羅的なエラー処理、検証とロジックの分離テストを重視する CLI・ライブラリ開発に適しています。

主な機能

  1. 01Zod による入出力スキーマ
  2. 02成功・失敗を判別する Result
  3. 03例外を投げずにエラーを変換するハンドラー
  4. 04契約テストとロジックテストの分離

活用シーン

  1. 01CLI の厳密な入力境界の定義
  2. 02複雑な入力のパースと変換
  3. 03スキーマとビジネスロジックの分離テスト

インストール

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

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

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

詳細

型付きサービス契約(仕様とハンドラーパターン)

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

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

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

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

1. 仕様(spec.ts)

「契約」または「ポート」です。何をするかを定義します。以下を含める必要があります。

  • 入力スキーマ: 生の入力を有効な DTO に解析する Zod スキーマ。
  • 出力スキーマ: 成功時のデータ構造を定義する Zod スキーマ。
  • エラースキーマ: 特定の失敗モードを表す判別共用体(汎用エラーではありません)。
  • 結果型: Success | Failure の DiscriminatedUnion。
  • インターフェース: 機能の定義(例:interface ConfigureSpec)。
2. ハンドラー(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.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. ロジックテスト(ハンドラー)

シェフをテストします。外部依存関係(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');
  }
});

コメント