mcpField
T
typed-service-contracts

typed-service-contracts

github.com/google-labs-code开发1.8k96 次安装3 天前更新
代码仓库

关于 typed-service-contracts

一种面向 CLI 和库开发的架构技能,以契约和垂直切片组织应用逻辑。它通过 Zod 解析输入,并要求处理器将错误映射为 Result,而非抛出异常。

主要功能

  1. 01以 Spec 定义输入、输出和错误契约
  2. 02用 Zod 解析并转换输入
  3. 03用 Handler 实现副作用逻辑
  4. 04通过 Result 表示成功或失败
  5. 05分离契约测试与逻辑测试

适用场景

  1. 01为 CLI 定义严格的输入边界
  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 模式)

此技能定义了一种基于契约式设计(DbC)原则的垂直切片架构。它将应用逻辑视为经过严格定义的工作单元:输入会被解析(而不只是验证),错误则作为值处理(结果模式),而非异常。

何时使用此技能

  • **构建 CLI 或库:**需要在用户输入和系统逻辑之间设立严格边界时。
  • **复杂验证:**输入在可用之前需要转换(解析)时(例如,确保字符串是有效的文件路径)。
  • **高可靠性要求:**无法承受未处理的运行时异常,且需要穷尽式错误处理时。
  • **测试重点:**希望将数据验证测试与业务逻辑测试分开时。

架构组件

1. Spec(spec.ts)

“契约”或“端口”。它定义_做什么_,必须包含:

  • **输入 Schema:**一个 Zod Schema,用于将原始输入解析为有效的 DTO。
  • **输出 Schema:**一个定义成功数据结构的 Zod Schema。
  • **错误 Schema:**由特定失败模式组成的判别联合(而非通用错误)。
  • 结果类型: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. 契约测试(Schema)

测试_把关者_。确保无效数据在到达 Handler 之前就被拒绝。

  • **重点:**边缘情况、验证规则、Zod refinements。
  • **风格:**数据驱动(表格测试)。
// 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 Object。

  • **重点:**业务逻辑流程、错误映射、成功状态。
  • **风格:**模拟单元测试或场景运行器。
// 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");
  }
});

评论

支持 Markdown