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"); }), );});
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 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<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");
}),
);
});