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")({ id: Schema.Number, name: Schema.String, tag: Schema.optional(Schema.String), }) {} class CreatePetInput extends Schema.Class("CreatePetInput")({ name: Schema.String, tag: Schema.optional(Schema.String), }) {} class PetNotFound extends Schema.TaggedError()("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 definition const 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; 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"); }), ); });