188 lines · 6.1 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import {  HttpApi,  HttpApiEndpoint,  HttpApiGroup,  OpenApi,} from "@effect/platform";import { Effect, Option, Schema } from "effect"; import { parse } from "./parse";import { extract } from "./extract";import { compileToolDefinitions } from "./definitions"; // ---------------------------------------------------------------------------// Define a test API using Effect's HttpApi// --------------------------------------------------------------------------- class Pet extends Schema.Class<Pet>("Pet")({  id: Schema.Number,  name: Schema.String,  tag: Schema.optional(Schema.String),}) {} class CreatePetInput extends Schema.Class<CreatePetInput>("CreatePetInput")({  name: Schema.String,  tag: Schema.optional(Schema.String),}) {} class PetNotFound extends Schema.TaggedError<PetNotFound>()("PetNotFound", {  message: Schema.String,}) {} const PetstoreGroup = HttpApiGroup.make("pets")  .add(    HttpApiEndpoint.get("listPets", "/pets")      .addSuccess(Schema.Array(Pet)),  )  .add(    HttpApiEndpoint.post("createPet", "/pets")      .setPayload(CreatePetInput)      .addSuccess(Pet),  )  .add(    HttpApiEndpoint.get("getPet", "/pets/:petId")      .addSuccess(Pet)      .addError(PetNotFound, { status: 404 }),  ); const PetstoreApi = HttpApi.make("petstore").add(PetstoreGroup); // Generate OpenAPI spec from the Effect API definitionconst spec = OpenApi.fromApi(PetstoreApi); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("OpenAPI plugin", () => {  it.effect("parses and extracts operations from Effect HttpApi spec", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       expect(Option.getOrElse(result.title, () => "")).toBe("Api");      expect(result.operations.length).toBeGreaterThanOrEqual(3);    }),  );   it.effect("extracts listPets with output schema", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       const listPets = result.operations.find(        (op) => op.operationId === "pets.listPets",      );      expect(listPets).toBeDefined();      expect(listPets!.method).toBe("get");      expect(listPets!.pathTemplate).toBe("/pets");      expect(listPets!.tags).toContain("pets");       // Has output schema (array of pets, dereferenced)      expect(Option.isSome(listPets!.outputSchema)).toBe(true);      const outputSchema = Option.getOrThrow(listPets!.outputSchema) as Record<        string,        unknown      >;      expect(outputSchema.type).toBe("array");    }),  );   it.effect("extracts createPet with request body from payload", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       const createPet = result.operations.find(        (op) => op.operationId === "pets.createPet",      );      expect(createPet).toBeDefined();      expect(createPet!.method).toBe("post");       // Has request body      expect(Option.isSome(createPet!.requestBody)).toBe(true);      const rb = Option.getOrThrow(createPet!.requestBody);      expect(rb.contentType).toBe("application/json");       // Input schema includes body      expect(Option.isSome(createPet!.inputSchema)).toBe(true);      const inputSchema = Option.getOrThrow(        createPet!.inputSchema,      ) as Record<string, unknown>;      expect(inputSchema.properties).toHaveProperty("body");    }),  );   it.effect("extracts getPet with path template", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       const getPet = result.operations.find(        (op) => op.operationId === "pets.getPet",      );      expect(getPet).toBeDefined();      expect(getPet!.method).toBe("get");      expect(getPet!.pathTemplate).toBe("/pets/{petId}");    }),  );   it.effect("extracts error responses", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       const getPet = result.operations.find(        (op) => op.operationId === "pets.getPet",      );      expect(getPet).toBeDefined();       // Should have a success output schema      expect(Option.isSome(getPet!.outputSchema)).toBe(true);    }),  );   it.effect("round-trips: generated spec paths match HttpApi definition", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);       // Should have all 3 operations      const paths = result.operations.map((op) => `${op.method} ${op.pathTemplate}`);      expect(paths).toContain("get /pets");      expect(paths).toContain("post /pets");      expect(paths).toContain("get /pets/{petId}");    }),  );   it.effect("compileToolDefinitions produces nested group.leaf paths", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const doc = yield* parse(JSON.stringify(spec));      const result = yield* extract(doc);      const defs = compileToolDefinitions(result.operations);       // All 3 operations compiled      expect(defs).toHaveLength(3);       // All should be under the "pets" group      for (const def of defs) {        expect(def.group).toBe("pets");        expect(def.toolPath).toMatch(/^pets\./);      }       // Specific tool paths      const paths = defs.map((d) => d.toolPath);      expect(paths).toContain("pets.listPets");      expect(paths).toContain("pets.createPet");      expect(paths).toContain("pets.getPet");    }),  );});