import { describe, expect, it } from "@effect/vitest";import { Effect, Option } from "effect";import { FetchHttpClient } from "effect/unstable/http"; import { extract } from "./extract";import { resolveServerUrl } from "./openapi-utils";import { parse } from "./parse";import { previewSpec as previewSpecRaw } from "./preview";import { ServerVariable } from "./types"; const previewSpec = (input: string) => previewSpecRaw(input).pipe(Effect.provide(FetchHttpClient.layer)); const serverVariable = (defaultValue: string): ServerVariable => ServerVariable.make({ default: defaultValue, enum: Option.none(), description: Option.none() }); // ---------------------------------------------------------------------------// resolveServerUrl — invoke-time template resolution// --------------------------------------------------------------------------- describe("resolveServerUrl", () => { const vars = { tenant: serverVariable("default-tenant"), region: serverVariable("us-east-1"), }; it("fills placeholders from variable defaults when no override is given", () => { expect(resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, {})).toBe( "https://default-tenant.us-east-1.api.example.com", ); }); it("prefers connection overrides over defaults", () => { expect( resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, { tenant: "acme", region: "eu-west-1", }), ).toBe("https://acme.eu-west-1.api.example.com"); }); it("ignores empty overrides and keeps the default", () => { expect( resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, { tenant: "" }), ).toBe("https://default-tenant.us-east-1.api.example.com"); }); it("returns a URL with no placeholders unchanged", () => { expect(resolveServerUrl("https://api.example.com", undefined, { tenant: "acme" })).toBe( "https://api.example.com", ); });}); // ---------------------------------------------------------------------------// Operation applicable servers — operation/path override else document servers// --------------------------------------------------------------------------- // Raw JSON so the path-level `servers` override is easy to express.const specWithPathOverride = { openapi: "3.0.0", info: { title: "Example", version: "1.0" }, servers: [{ url: "https://api.example.com" }], paths: { "/items": { get: { operationId: "listItems", responses: { "200": { description: "ok" } } }, }, "/query": { servers: [ { url: "https://{tenant}.{region}.api.example.com", variables: { tenant: { default: "default-tenant" }, region: { default: "us-east-1" } }, }, ], post: { operationId: "runQuery", responses: { "200": { description: "ok" } } }, }, },}; describe("extract — operation applicable servers", () => { it.effect("inherits the document servers when there is no override", () => Effect.gen(function* () { // @effect-diagnostics-next-line preferSchemaOverJson:off const doc = yield* parse(JSON.stringify(specWithPathOverride)); const result = yield* extract(doc); const listItems = result.operations.find((op) => op.operationId === "listItems")!; expect(listItems.servers.map((s) => s.url)).toEqual(["https://api.example.com"]); }), ); it.effect("carries a path-level override's servers as templates with variables", () => Effect.gen(function* () { // @effect-diagnostics-next-line preferSchemaOverJson:off const doc = yield* parse(JSON.stringify(specWithPathOverride)); const result = yield* extract(doc); const runQuery = result.operations.find((op) => op.operationId === "runQuery")!; expect(runQuery.servers.map((s) => s.url)).toEqual([ "https://{tenant}.{region}.api.example.com", ]); const vars = Option.getOrThrow(runQuery.servers[0]!.variables); expect(vars.tenant?.default).toBe("default-tenant"); expect(vars.region?.default).toBe("us-east-1"); // The host resolves per call: defaults, or call-supplied overrides. const server = runQuery.servers[0]!; expect(resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {})).toBe( "https://default-tenant.us-east-1.api.example.com", ); expect( resolveServerUrl(server.url, Option.getOrUndefined(server.variables), { tenant: "acme", region: "eu-west-1", }), ).toBe("https://acme.eu-west-1.api.example.com"); }), );}); // ---------------------------------------------------------------------------// Preview surfaces each top-level server with its own variables// --------------------------------------------------------------------------- describe("previewSpec — server variables", () => { it.effect("carries variables per top-level server, excluding operation overrides", () => Effect.gen(function* () { const spec = { openapi: "3.0.0", info: { title: "Example", version: "1.0" }, servers: [ { url: "https://api.example.com", description: "Control plane" }, { url: "https://{branch}.{region}.example.com", variables: { branch: { default: "main" }, region: { default: "us-east-1" } }, }, ], paths: { "/query": { servers: [ { url: "https://{tenant}.gw.example.com", variables: { tenant: { default: "default-tenant" } }, }, ], post: { operationId: "runQuery", responses: { "200": { description: "ok" } } }, }, }, }; // @effect-diagnostics-next-line preferSchemaOverJson:off const preview = yield* previewSpec(JSON.stringify(spec)); // The plain control-plane server has no variables. const control = preview.servers.find((s) => s.url === "https://api.example.com")!; expect(Option.getOrNull(control.variables)).toBeNull(); // The gateway server carries only its own branch/region. const gateway = preview.servers.find((s) => s.url.startsWith("https://{branch}"))!; expect(Object.keys(Option.getOrThrow(gateway.variables))).toEqual(["branch", "region"]); // The operation override's `tenant` never appears in the top-level servers. const names = preview.servers.flatMap((s) => Object.keys(Option.getOrElse(s.variables, () => ({}))), ); expect(names).not.toContain("tenant"); }), );}); // ---------------------------------------------------------------------------// The `server` input property exposes per-call host selection + variables// --------------------------------------------------------------------------- type ServerInputSchema = { readonly required?: readonly string[]; readonly properties: { readonly server?: { readonly properties: { readonly url?: { readonly enum: readonly string[]; readonly default: string }; readonly variables?: { readonly properties: Record< string, { readonly default?: string; readonly enum?: readonly string[] } >; }; }; }; };}; describe("buildInputSchema — server property", () => { it.effect("exposes a server picker and variables for multiple/templated servers", () => Effect.gen(function* () { const spec = { openapi: "3.0.0", info: { title: "Example", version: "1.0" }, servers: [ { url: "https://api.example.com" }, { url: "https://{branch}.{region}.example.com", variables: { branch: { default: "main" }, region: { default: "us", enum: ["us", "eu"] }, }, }, ], paths: { "/items": { get: { operationId: "listItems", responses: { "200": { description: "ok" } } }, }, }, }; // @effect-diagnostics-next-line preferSchemaOverJson:off const result = yield* extract(yield* parse(JSON.stringify(spec))); const op = result.operations.find((o) => o.operationId === "listItems")!; const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema; expect(schema.properties.server?.properties.url?.enum).toEqual([ "https://api.example.com", "https://{branch}.{region}.example.com", ]); const vars = schema.properties.server?.properties.variables?.properties; expect(vars?.branch?.default).toBe("main"); expect(vars?.region?.enum).toEqual(["us", "eu"]); // The host stays optional — defaults apply when the call omits `server`. expect(schema.required ?? []).not.toContain("server"); }), ); it.effect("omits the server property for a single concrete server", () => Effect.gen(function* () { const spec = { openapi: "3.0.0", info: { title: "Example", version: "1.0" }, servers: [{ url: "https://api.example.com" }], paths: { "/items/{id}": { get: { operationId: "getItem", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok" } }, }, }, }, }; // @effect-diagnostics-next-line preferSchemaOverJson:off const result = yield* extract(yield* parse(JSON.stringify(spec))); const op = result.operations.find((o) => o.operationId === "getItem")!; const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema; expect(schema.properties.server).toBeUndefined(); }), );});
import { describe, expect, it } from "@effect/vitest";
import { Effect, Option } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { extract } from "./extract";
import { resolveServerUrl } from "./openapi-utils";
import { parse } from "./parse";
import { previewSpec as previewSpecRaw } from "./preview";
import { ServerVariable } from "./types";
const previewSpec = (input: string) =>
previewSpecRaw(input).pipe(Effect.provide(FetchHttpClient.layer));
const serverVariable = (defaultValue: string): ServerVariable =>
ServerVariable.make({ default: defaultValue, enum: Option.none(), description: Option.none() });
// ---------------------------------------------------------------------------
// resolveServerUrl — invoke-time template resolution
// ---------------------------------------------------------------------------
describe("resolveServerUrl", () => {
const vars = {
tenant: serverVariable("default-tenant"),
region: serverVariable("us-east-1"),
};
it("fills placeholders from variable defaults when no override is given", () => {
expect(resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, {})).toBe(
"https://default-tenant.us-east-1.api.example.com",
);
});
it("prefers connection overrides over defaults", () => {
expect(
resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, {
tenant: "acme",
region: "eu-west-1",
}),
).toBe("https://acme.eu-west-1.api.example.com");
});
it("ignores empty overrides and keeps the default", () => {
expect(
resolveServerUrl("https://{tenant}.{region}.api.example.com", vars, { tenant: "" }),
).toBe("https://default-tenant.us-east-1.api.example.com");
});
it("returns a URL with no placeholders unchanged", () => {
expect(resolveServerUrl("https://api.example.com", undefined, { tenant: "acme" })).toBe(
"https://api.example.com",
);
});
});
// ---------------------------------------------------------------------------
// Operation applicable servers — operation/path override else document servers
// ---------------------------------------------------------------------------
// Raw JSON so the path-level `servers` override is easy to express.
const specWithPathOverride = {
openapi: "3.0.0",
info: { title: "Example", version: "1.0" },
servers: [{ url: "https://api.example.com" }],
paths: {
"/items": {
get: { operationId: "listItems", responses: { "200": { description: "ok" } } },
},
"/query": {
servers: [
{
url: "https://{tenant}.{region}.api.example.com",
variables: { tenant: { default: "default-tenant" }, region: { default: "us-east-1" } },
},
],
post: { operationId: "runQuery", responses: { "200": { description: "ok" } } },
},
},
};
describe("extract — operation applicable servers", () => {
it.effect("inherits the document servers when there is no override", () =>
Effect.gen(function* () {
// @effect-diagnostics-next-line preferSchemaOverJson:off
const doc = yield* parse(JSON.stringify(specWithPathOverride));
const result = yield* extract(doc);
const listItems = result.operations.find((op) => op.operationId === "listItems")!;
expect(listItems.servers.map((s) => s.url)).toEqual(["https://api.example.com"]);
}),
);
it.effect("carries a path-level override's servers as templates with variables", () =>
Effect.gen(function* () {
// @effect-diagnostics-next-line preferSchemaOverJson:off
const doc = yield* parse(JSON.stringify(specWithPathOverride));
const result = yield* extract(doc);
const runQuery = result.operations.find((op) => op.operationId === "runQuery")!;
expect(runQuery.servers.map((s) => s.url)).toEqual([
"https://{tenant}.{region}.api.example.com",
]);
const vars = Option.getOrThrow(runQuery.servers[0]!.variables);
expect(vars.tenant?.default).toBe("default-tenant");
expect(vars.region?.default).toBe("us-east-1");
// The host resolves per call: defaults, or call-supplied overrides.
const server = runQuery.servers[0]!;
expect(resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {})).toBe(
"https://default-tenant.us-east-1.api.example.com",
);
expect(
resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {
tenant: "acme",
region: "eu-west-1",
}),
).toBe("https://acme.eu-west-1.api.example.com");
}),
);
});
// ---------------------------------------------------------------------------
// Preview surfaces each top-level server with its own variables
// ---------------------------------------------------------------------------
describe("previewSpec — server variables", () => {
it.effect("carries variables per top-level server, excluding operation overrides", () =>
Effect.gen(function* () {
const spec = {
openapi: "3.0.0",
info: { title: "Example", version: "1.0" },
servers: [
{ url: "https://api.example.com", description: "Control plane" },
{
url: "https://{branch}.{region}.example.com",
variables: { branch: { default: "main" }, region: { default: "us-east-1" } },
},
],
paths: {
"/query": {
servers: [
{
url: "https://{tenant}.gw.example.com",
variables: { tenant: { default: "default-tenant" } },
},
],
post: { operationId: "runQuery", responses: { "200": { description: "ok" } } },
},
},
};
// @effect-diagnostics-next-line preferSchemaOverJson:off
const preview = yield* previewSpec(JSON.stringify(spec));
// The plain control-plane server has no variables.
const control = preview.servers.find((s) => s.url === "https://api.example.com")!;
expect(Option.getOrNull(control.variables)).toBeNull();
// The gateway server carries only its own branch/region.
const gateway = preview.servers.find((s) => s.url.startsWith("https://{branch}"))!;
expect(Object.keys(Option.getOrThrow(gateway.variables))).toEqual(["branch", "region"]);
// The operation override's `tenant` never appears in the top-level servers.
const names = preview.servers.flatMap((s) =>
Object.keys(Option.getOrElse(s.variables, () => ({}))),
);
expect(names).not.toContain("tenant");
}),
);
});
// ---------------------------------------------------------------------------
// The `server` input property exposes per-call host selection + variables
// ---------------------------------------------------------------------------
type ServerInputSchema = {
readonly required?: readonly string[];
readonly properties: {
readonly server?: {
readonly properties: {
readonly url?: { readonly enum: readonly string[]; readonly default: string };
readonly variables?: {
readonly properties: Record<
string,
{ readonly default?: string; readonly enum?: readonly string[] }
>;
};
};
};
};
};
describe("buildInputSchema — server property", () => {
it.effect("exposes a server picker and variables for multiple/templated servers", () =>
Effect.gen(function* () {
const spec = {
openapi: "3.0.0",
info: { title: "Example", version: "1.0" },
servers: [
{ url: "https://api.example.com" },
{
url: "https://{branch}.{region}.example.com",
variables: {
branch: { default: "main" },
region: { default: "us", enum: ["us", "eu"] },
},
},
],
paths: {
"/items": {
get: { operationId: "listItems", responses: { "200": { description: "ok" } } },
},
},
};
// @effect-diagnostics-next-line preferSchemaOverJson:off
const result = yield* extract(yield* parse(JSON.stringify(spec)));
const op = result.operations.find((o) => o.operationId === "listItems")!;
const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema;
expect(schema.properties.server?.properties.url?.enum).toEqual([
"https://api.example.com",
"https://{branch}.{region}.example.com",
]);
const vars = schema.properties.server?.properties.variables?.properties;
expect(vars?.branch?.default).toBe("main");
expect(vars?.region?.enum).toEqual(["us", "eu"]);
// The host stays optional — defaults apply when the call omits `server`.
expect(schema.required ?? []).not.toContain("server");
}),
);
it.effect("omits the server property for a single concrete server", () =>
Effect.gen(function* () {
const spec = {
openapi: "3.0.0",
info: { title: "Example", version: "1.0" },
servers: [{ url: "https://api.example.com" }],
paths: {
"/items/{id}": {
get: {
operationId: "getItem",
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
responses: { "200": { description: "ok" } },
},
},
},
};
// @effect-diagnostics-next-line preferSchemaOverJson:off
const result = yield* extract(yield* parse(JSON.stringify(spec)));
const op = result.operations.find((o) => o.operationId === "getItem")!;
const schema = Option.getOrThrow(op.inputSchema) as ServerInputSchema;
expect(schema.properties.server).toBeUndefined();
}),
);
});