import { Context, Data, Effect, Layer, Predicate, Ref, Schema, Scope } from "effect";import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse,} from "effect/unstable/http";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi,} from "effect/unstable/httpapi";import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing";import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, isToolResult, type Connection, type CreateConnectionInput, type Owner, type ToolAddress as ToolAddressType,} from "@executor-js/sdk/core";import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; export class OpenApiTestServerAddressError extends Data.TaggedError( "OpenApiTestServerAddressError",)<{ readonly address: unknown;}> {} export class OpenApiTestServerSpecError extends Data.TaggedError("OpenApiTestServerSpecError")<{ readonly cause: unknown;}> {} export interface OpenApiTestServerShape { readonly baseUrl: string; readonly specJson: string; readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>;} export interface OpenApiHttpApiTestServerOptions { readonly api: HttpApi.Any; readonly handlersLayer: Layer.Layer<any, any, any>; readonly specPath?: `/${string}`; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; readonly captureSpecRequest?: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<void>; readonly guardSpecRequest?: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<HttpServerResponse.HttpServerResponse | null>;} export interface OpenApiHttpApiTestServerShape extends OpenApiTestServerShape { readonly specUrl: string;} export interface MutableOpenApiSpecTestServerShape extends OpenApiTestServerShape { readonly specUrl: string; readonly setApi: (api: HttpApi.Any) => Effect.Effect<void, OpenApiTestServerSpecError>; readonly requestCount: Effect.Effect<number>;} export interface OpenApiTestRequest { readonly method: string; readonly url: string; readonly path: string; readonly headers: Readonly<Record<string, string>>; readonly body: string;} export interface OpenApiEchoTestServerOptions { readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; readonly oauth2?: { readonly authorizationUrl: string; readonly tokenUrl: string; readonly scopes?: Readonly<Record<string, string>>; readonly validateAuthorization?: (authorization: string | null) => Effect.Effect<boolean>; readonly wwwAuthenticate?: string; };} export interface OpenApiEchoTestServerShape extends OpenApiTestServerShape { readonly specUrl: string; readonly requests: Effect.Effect<readonly OpenApiTestRequest[]>; readonly clearRequests: Effect.Effect<void>;} export type OpenApiTestSourceOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & { readonly baseUrl?: string | null; readonly slug?: string;}; export type OpenApiHttpApiTestSourceOptions = Omit< OpenApiSpecConfig, "spec" | "slug" | "baseUrl"> & { readonly slug?: string; readonly baseUrl?: string | null; readonly specBaseUrl?: string; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}; type OpenApiHttpApiAddSpecHeadersInput = { readonly headers?: Record<string, string>; readonly queryParams?: Record<string, string>;}; export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit< OpenApiHttpApiTestSourceOptions, "headers" | "queryParams"> & OpenApiHttpApiAddSpecHeadersInput; export type OpenApiTestSourceExecutor = { readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;}; // ---------------------------------------------------------------------------// v2 connection helper. In v2 a connection IS the credential and tools are// produced per-connection — so a test that invokes an openapi tool must// (1) addSpec to register the integration and (2) create a connection so the// tools are stamped + persisted. This helper does both and returns the// invokable address builder.// --------------------------------------------------------------------------- export type OpenApiTestConnectionExecutor = { readonly openapi: Pick<OpenApiPluginExtension, "addSpec">; readonly connections: { readonly create: (input: CreateConnectionInput) => Effect.Effect<Connection, unknown>; };}; export interface OpenApiTestConnectionOptions { readonly slug?: string; readonly owner?: Owner; readonly connection?: string; readonly template?: string; /** Inline credential value. Defaults to a throwaway token via the in-memory * provider's `from` path is avoided — pass `value` to store a real secret. */ readonly value?: string; readonly baseUrl?: string | null;} export interface OpenApiTestConnection { readonly slug: string; readonly owner: Owner; readonly connection: string; readonly address: (tool: string) => ToolAddressType;} /** addSpec + connections.create from a raw server spec; returns an address * builder for `executor.execute`. */export const addOpenApiTestConnection = ( executor: OpenApiTestConnectionExecutor, server: OpenApiTestServerShape, sourceOptions: OpenApiTestSourceOptions, connectionOptions: OpenApiTestConnectionOptions = {},): Effect.Effect<OpenApiTestConnection, unknown> => Effect.gen(function* () { const config = makeOpenApiTestSourceConfig(server, sourceOptions); const result = yield* executor.openapi.addSpec(config); const slug = String(result.slug); const owner: Owner = connectionOptions.owner ?? "org"; const connection = connectionOptions.connection ?? "main"; const template = connectionOptions.template ?? "apiKey"; yield* executor.connections.create({ owner, name: ConnectionName.make(connection), integration: IntegrationSlug.make(slug), template: AuthTemplateSlug.make(template), ...(connectionOptions.value !== undefined ? { value: connectionOptions.value } : { from: { provider: ProviderKey.make("memory"), id: ProviderItemId.make(`${slug}-token`), }, }), } satisfies CreateConnectionInput); return { slug, owner, connection, address: (tool: string) => ToolAddress.make(`tools.${slug}.${owner}.${connection}.${tool}`), }; }); export interface OpenApiTestSpecOptions { readonly baseUrl?: string; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;} export const makeOpenApiTestSpecJson = ( api: HttpApi.Any, options: OpenApiTestSpecOptions = {},): string => { const annotations = OpenApi.annotations({ ...(options.baseUrl !== undefined ? { servers: [{ url: options.baseUrl }] } : {}), transform: options.transformSpec, }); const annotated = (api as HttpApi.AnyWithProps).annotateMerge(annotations); return JSON.stringify(OpenApi.fromApi(annotated));}; export const makeOpenApiTestSourceConfig = ( server: OpenApiTestServerShape, options: OpenApiTestSourceOptions,): OpenApiSpecConfig => { const { baseUrl, slug, ...rest } = options; return { ...rest, spec: { kind: "blob", value: server.specJson }, slug: slug ?? "test_api", ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? server.baseUrl }), } satisfies OpenApiSpecConfig;}; export const addOpenApiTestSource = ( executor: OpenApiTestSourceExecutor, server: OpenApiTestServerShape, options: OpenApiTestSourceOptions,) => executor.openapi.addSpec(makeOpenApiTestSourceConfig(server, options)); export const makeOpenApiHttpApiTestSourceConfig = ( api: HttpApi.Any, options: OpenApiHttpApiTestSourceOptions,): OpenApiSpecConfig => { const { baseUrl, specBaseUrl, transformSpec, slug, ...config } = options; return { ...config, spec: { kind: "blob", value: makeOpenApiTestSpecJson(api, { baseUrl: specBaseUrl, transformSpec }), }, slug: slug ?? "test_api", ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? specBaseUrl ?? "https://api.example.test" }), } satisfies OpenApiSpecConfig;}; export const addOpenApiHttpApiTestSource = ( executor: OpenApiTestSourceExecutor, api: HttpApi.Any, options: OpenApiHttpApiTestSourceOptions,) => executor.openapi.addSpec(makeOpenApiHttpApiTestSourceConfig(api, options)); export const makeOpenApiHttpApiTestAddSpecPayload = ( api: HttpApi.Any, options: OpenApiHttpApiTestAddSpecPayloadOptions,) => { const { headers, queryParams, ...sourceOptions } = options; const config = makeOpenApiHttpApiTestSourceConfig(api, sourceOptions); return { spec: config.spec, slug: config.slug, baseUrl: config.baseUrl, ...(headers !== undefined ? { headers } : {}), ...(queryParams !== undefined ? { queryParams } : {}), ...(config.authenticationTemplate !== undefined ? { authenticationTemplate: config.authenticationTemplate } : {}), };}; export const makeOpenApiHttpApiTestSpecPayload = ( api: HttpApi.Any, options: OpenApiTestSpecOptions = {},) => ({ spec: makeOpenApiTestSpecJson(api, options),}); const isJsonObject = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null && !Array.isArray(value); const OpenApiEchoItem = Schema.Struct({ id: Schema.Number, name: Schema.String,}); const OpenApiEchoHeaders = Schema.Struct({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String),}); const OpenApiEchoMessage = Schema.Struct({ message: Schema.String, suffix: Schema.optional(Schema.String), path: Schema.String,}); const OpenApiEchoItemsGroup = HttpApiGroup.make("items") .add( HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(OpenApiEchoItem), }), ) .add( HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: OpenApiEchoHeaders, }), ); const OpenApiEchoGroup = HttpApiGroup.make("echo").add( HttpApiEndpoint.get("echoMessage", "/echo/:message", { params: Schema.Struct({ message: Schema.String }), query: Schema.Struct({ suffix: Schema.optional(Schema.String) }), success: OpenApiEchoMessage, }),); const OpenApiEchoApi = HttpApi.make("executorOpenApiTest") .add(OpenApiEchoItemsGroup) .add(OpenApiEchoGroup) .annotateMerge( OpenApi.annotations({ title: "Executor OpenAPI Test Server", version: "1.0.0", }), ); const openApiSpecJsonFromHttpApi = ( api: HttpApi.Any, baseUrl: string, transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>,): Effect.Effect<string, OpenApiTestServerSpecError> => Effect.try({ try: () => makeOpenApiTestSpecJson(api, { baseUrl, transformSpec }), catch: (cause) => new OpenApiTestServerSpecError({ cause }), }); export const serveOpenApiHttpApiTestServer = ( options: OpenApiHttpApiTestServerOptions,): Effect.Effect< OpenApiHttpApiTestServerShape, OpenApiTestServerAddressError | OpenApiTestServerSpecError, Scope.Scope> => Effect.gen(function* () { const specPath = options.specPath ?? "/spec.json"; let specJson = ""; const SpecRoute = HttpRouter.addAll([ HttpRouter.route( "GET", specPath, Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; if (options.captureSpecRequest) { yield* options.captureSpecRequest(request); } const guardResponse = options.guardSpecRequest ? yield* options.guardSpecRequest(request) : null; if (guardResponse) return guardResponse; return HttpServerResponse.text(specJson, { status: 200, contentType: "application/json", }); }), ), ]); const ApiLive = HttpApiBuilder.layer(options.api as HttpApi.AnyWithProps).pipe( Layer.provide(options.handlersLayer), ); const ServerLayer = HttpRouter.serve(Layer.mergeAll(ApiLive, SpecRoute), { disableListenLog: true, disableLogger: true, }); const server = yield* serveTestHttpServerLayer(ServerLayer).pipe( Effect.mapError((error) => Predicate.isTagged(error, "TestHttpServerAddressError") ? new OpenApiTestServerAddressError({ address: error.address }) : new OpenApiTestServerSpecError({ cause: error.cause }), ), ); specJson = yield* openApiSpecJsonFromHttpApi( options.api, server.baseUrl, options.transformSpec, ); return { baseUrl: server.baseUrl, specUrl: server.url(specPath), specJson, httpClientLayer: server.httpClientLayer, }; }); export class OpenApiHttpApiTestServer extends Context.Service< OpenApiHttpApiTestServer, OpenApiHttpApiTestServerShape>()("@executor-js/plugin-openapi/testing/OpenApiHttpApiTestServer") { static readonly layer = (options: OpenApiHttpApiTestServerOptions) => Layer.effect(OpenApiHttpApiTestServer, serveOpenApiHttpApiTestServer(options));} export const serveMutableOpenApiSpecTestServer = (options: { readonly initialApi: HttpApi.Any; readonly specPath?: `/${string}`; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}): Effect.Effect< MutableOpenApiSpecTestServerShape, OpenApiTestServerAddressError | OpenApiTestServerSpecError, Scope.Scope> => Effect.gen(function* () { const specPath = options.specPath ?? "/spec.json"; const specJson = yield* Ref.make(""); const requests = yield* Ref.make(0); const SpecRoute = HttpRouter.addAll([ HttpRouter.route( "GET", specPath, Effect.gen(function* () { yield* Ref.update(requests, (count) => count + 1); const current = yield* Ref.get(specJson); return HttpServerResponse.text(current, { status: 200, contentType: "application/json", }); }), ), ]); const server = yield* serveTestHttpServerLayer( HttpRouter.serve(SpecRoute, { disableListenLog: true, disableLogger: true, }), ).pipe( Effect.mapError((error) => Predicate.isTagged(error, "TestHttpServerAddressError") ? new OpenApiTestServerAddressError({ address: error.address }) : new OpenApiTestServerSpecError({ cause: error.cause }), ), ); const renderSpec = (api: HttpApi.Any) => openApiSpecJsonFromHttpApi(api, server.baseUrl, options.transformSpec); yield* Ref.set(specJson, yield* renderSpec(options.initialApi)); return { baseUrl: server.baseUrl, specUrl: server.url(specPath), specJson: yield* Ref.get(specJson), httpClientLayer: server.httpClientLayer, setApi: (api) => renderSpec(api).pipe(Effect.flatMap((next) => Ref.set(specJson, next))), requestCount: Ref.get(requests), }; }); const openApiOperationMethods = new Set([ "get", "put", "post", "delete", "options", "head", "patch",]); const withOAuth2Security = (oauth2: NonNullable<OpenApiEchoTestServerOptions["oauth2"]>) => (spec: Record<string, unknown>): Record<string, unknown> => { const scopes = oauth2.scopes ?? { read: "Read test resources" }; const security = [{ oauth2: Object.keys(scopes) }]; const paths = isJsonObject(spec.paths) ? Object.fromEntries( Object.entries(spec.paths).map(([path, pathItem]) => [ path, isJsonObject(pathItem) ? Object.fromEntries( Object.entries(pathItem).map(([method, operation]) => [ method, openApiOperationMethods.has(method) && isJsonObject(operation) ? { ...operation, security } : operation, ]), ) : pathItem, ]), ) : spec.paths; const components = isJsonObject(spec.components) ? spec.components : {}; const securitySchemes = isJsonObject(components.securitySchemes) ? components.securitySchemes : {}; return { ...spec, paths, components: { ...components, securitySchemes: { ...securitySchemes, oauth2: {Lines 1–500 of 743. Load more
import { Context, Data, Effect, Layer, Predicate, Ref, Schema, Scope } from "effect"; import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse, } from "effect/unstable/http"; import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi, } from "effect/unstable/httpapi"; import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, isToolResult, type Connection, type CreateConnectionInput, type Owner, type ToolAddress as ToolAddressType, } from "@executor-js/sdk/core"; import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; export class OpenApiTestServerAddressError extends Data.TaggedError( "OpenApiTestServerAddressError", )<{ readonly address: unknown; }> {} export class OpenApiTestServerSpecError extends Data.TaggedError("OpenApiTestServerSpecError")<{ readonly cause: unknown; }> {} export interface OpenApiTestServerShape { readonly baseUrl: string; readonly specJson: string; readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>; } export interface OpenApiHttpApiTestServerOptions { readonly api: HttpApi.Any; readonly handlersLayer: Layer.Layer<any, any, any>; readonly specPath?: `/${string}`; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; readonly captureSpecRequest?: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<void>; readonly guardSpecRequest?: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<HttpServerResponse.HttpServerResponse | null>; } export interface OpenApiHttpApiTestServerShape extends OpenApiTestServerShape { readonly specUrl: string; } export interface MutableOpenApiSpecTestServerShape extends OpenApiTestServerShape { readonly specUrl: string; readonly setApi: (api: HttpApi.Any) => Effect.Effect<void, OpenApiTestServerSpecError>; readonly requestCount: Effect.Effect<number>; } export interface OpenApiTestRequest { readonly method: string; readonly url: string; readonly path: string; readonly headers: Readonly<Record<string, string>>; readonly body: string; } export interface OpenApiEchoTestServerOptions { readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; readonly oauth2?: { readonly authorizationUrl: string; readonly tokenUrl: string; readonly scopes?: Readonly<Record<string, string>>; readonly validateAuthorization?: (authorization: string | null) => Effect.Effect<boolean>; readonly wwwAuthenticate?: string; }; } export interface OpenApiEchoTestServerShape extends OpenApiTestServerShape { readonly specUrl: string; readonly requests: Effect.Effect<readonly OpenApiTestRequest[]>; readonly clearRequests: Effect.Effect<void>; } export type OpenApiTestSourceOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & { readonly baseUrl?: string | null; readonly slug?: string; }; export type OpenApiHttpApiTestSourceOptions = Omit< OpenApiSpecConfig, "spec" | "slug" | "baseUrl" > & { readonly slug?: string; readonly baseUrl?: string | null; readonly specBaseUrl?: string; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; }; type OpenApiHttpApiAddSpecHeadersInput = { readonly headers?: Record<string, string>; readonly queryParams?: Record<string, string>; }; export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit< OpenApiHttpApiTestSourceOptions, "headers" | "queryParams" > & OpenApiHttpApiAddSpecHeadersInput; export type OpenApiTestSourceExecutor = { readonly openapi: Pick<OpenApiPluginExtension, "addSpec">; }; // --------------------------------------------------------------------------- // v2 connection helper. In v2 a connection IS the credential and tools are // produced per-connection — so a test that invokes an openapi tool must // (1) addSpec to register the integration and (2) create a connection so the // tools are stamped + persisted. This helper does both and returns the // invokable address builder. // --------------------------------------------------------------------------- export type OpenApiTestConnectionExecutor = { readonly openapi: Pick<OpenApiPluginExtension, "addSpec">; readonly connections: { readonly create: (input: CreateConnectionInput) => Effect.Effect<Connection, unknown>; }; }; export interface OpenApiTestConnectionOptions { readonly slug?: string; readonly owner?: Owner; readonly connection?: string; readonly template?: string; /** Inline credential value. Defaults to a throwaway token via the in-memory * provider's `from` path is avoided — pass `value` to store a real secret. */ readonly value?: string; readonly baseUrl?: string | null; } export interface OpenApiTestConnection { readonly slug: string; readonly owner: Owner; readonly connection: string; readonly address: (tool: string) => ToolAddressType; } /** addSpec + connections.create from a raw server spec; returns an address * builder for `executor.execute`. */ export const addOpenApiTestConnection = ( executor: OpenApiTestConnectionExecutor, server: OpenApiTestServerShape, sourceOptions: OpenApiTestSourceOptions, connectionOptions: OpenApiTestConnectionOptions = {}, ): Effect.Effect<OpenApiTestConnection, unknown> => Effect.gen(function* () { const config = makeOpenApiTestSourceConfig(server, sourceOptions); const result = yield* executor.openapi.addSpec(config); const slug = String(result.slug); const owner: Owner = connectionOptions.owner ?? "org"; const connection = connectionOptions.connection ?? "main"; const template = connectionOptions.template ?? "apiKey"; yield* executor.connections.create({ owner, name: ConnectionName.make(connection), integration: IntegrationSlug.make(slug), template: AuthTemplateSlug.make(template), ...(connectionOptions.value !== undefined ? { value: connectionOptions.value } : { from: { provider: ProviderKey.make("memory"), id: ProviderItemId.make(`${slug}-token`), }, }), } satisfies CreateConnectionInput); return { slug, owner, connection, address: (tool: string) => ToolAddress.make(`tools.${slug}.${owner}.${connection}.${tool}`), }; }); export interface OpenApiTestSpecOptions { readonly baseUrl?: string; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; } export const makeOpenApiTestSpecJson = ( api: HttpApi.Any, options: OpenApiTestSpecOptions = {}, ): string => { const annotations = OpenApi.annotations({ ...(options.baseUrl !== undefined ? { servers: [{ url: options.baseUrl }] } : {}), transform: options.transformSpec, }); const annotated = (api as HttpApi.AnyWithProps).annotateMerge(annotations); return JSON.stringify(OpenApi.fromApi(annotated)); }; export const makeOpenApiTestSourceConfig = ( server: OpenApiTestServerShape, options: OpenApiTestSourceOptions, ): OpenApiSpecConfig => { const { baseUrl, slug, ...rest } = options; return { ...rest, spec: { kind: "blob", value: server.specJson }, slug: slug ?? "test_api", ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? server.baseUrl }), } satisfies OpenApiSpecConfig; }; export const addOpenApiTestSource = ( executor: OpenApiTestSourceExecutor, server: OpenApiTestServerShape, options: OpenApiTestSourceOptions, ) => executor.openapi.addSpec(makeOpenApiTestSourceConfig(server, options)); export const makeOpenApiHttpApiTestSourceConfig = ( api: HttpApi.Any, options: OpenApiHttpApiTestSourceOptions, ): OpenApiSpecConfig => { const { baseUrl, specBaseUrl, transformSpec, slug, ...config } = options; return { ...config, spec: { kind: "blob", value: makeOpenApiTestSpecJson(api, { baseUrl: specBaseUrl, transformSpec }), }, slug: slug ?? "test_api", ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? specBaseUrl ?? "https://api.example.test" }), } satisfies OpenApiSpecConfig; }; export const addOpenApiHttpApiTestSource = ( executor: OpenApiTestSourceExecutor, api: HttpApi.Any, options: OpenApiHttpApiTestSourceOptions, ) => executor.openapi.addSpec(makeOpenApiHttpApiTestSourceConfig(api, options)); export const makeOpenApiHttpApiTestAddSpecPayload = ( api: HttpApi.Any, options: OpenApiHttpApiTestAddSpecPayloadOptions, ) => { const { headers, queryParams, ...sourceOptions } = options; const config = makeOpenApiHttpApiTestSourceConfig(api, sourceOptions); return { spec: config.spec, slug: config.slug, baseUrl: config.baseUrl, ...(headers !== undefined ? { headers } : {}), ...(queryParams !== undefined ? { queryParams } : {}), ...(config.authenticationTemplate !== undefined ? { authenticationTemplate: config.authenticationTemplate } : {}), }; }; export const makeOpenApiHttpApiTestSpecPayload = ( api: HttpApi.Any, options: OpenApiTestSpecOptions = {}, ) => ({ spec: makeOpenApiTestSpecJson(api, options), }); const isJsonObject = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null && !Array.isArray(value); const OpenApiEchoItem = Schema.Struct({ id: Schema.Number, name: Schema.String, }); const OpenApiEchoHeaders = Schema.Struct({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String), }); const OpenApiEchoMessage = Schema.Struct({ message: Schema.String, suffix: Schema.optional(Schema.String), path: Schema.String, }); const OpenApiEchoItemsGroup = HttpApiGroup.make("items") .add( HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(OpenApiEchoItem), }), ) .add( HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: OpenApiEchoHeaders, }), ); const OpenApiEchoGroup = HttpApiGroup.make("echo").add( HttpApiEndpoint.get("echoMessage", "/echo/:message", { params: Schema.Struct({ message: Schema.String }), query: Schema.Struct({ suffix: Schema.optional(Schema.String) }), success: OpenApiEchoMessage, }), ); const OpenApiEchoApi = HttpApi.make("executorOpenApiTest") .add(OpenApiEchoItemsGroup) .add(OpenApiEchoGroup) .annotateMerge( OpenApi.annotations({ title: "Executor OpenAPI Test Server", version: "1.0.0", }), ); const openApiSpecJsonFromHttpApi = ( api: HttpApi.Any, baseUrl: string, transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>, ): Effect.Effect<string, OpenApiTestServerSpecError> => Effect.try({ try: () => makeOpenApiTestSpecJson(api, { baseUrl, transformSpec }), catch: (cause) => new OpenApiTestServerSpecError({ cause }), }); export const serveOpenApiHttpApiTestServer = ( options: OpenApiHttpApiTestServerOptions, ): Effect.Effect< OpenApiHttpApiTestServerShape, OpenApiTestServerAddressError | OpenApiTestServerSpecError, Scope.Scope > => Effect.gen(function* () { const specPath = options.specPath ?? "/spec.json"; let specJson = ""; const SpecRoute = HttpRouter.addAll([ HttpRouter.route( "GET", specPath, Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; if (options.captureSpecRequest) { yield* options.captureSpecRequest(request); } const guardResponse = options.guardSpecRequest ? yield* options.guardSpecRequest(request) : null; if (guardResponse) return guardResponse; return HttpServerResponse.text(specJson, { status: 200, contentType: "application/json", }); }), ), ]); const ApiLive = HttpApiBuilder.layer(options.api as HttpApi.AnyWithProps).pipe( Layer.provide(options.handlersLayer), ); const ServerLayer = HttpRouter.serve(Layer.mergeAll(ApiLive, SpecRoute), { disableListenLog: true, disableLogger: true, }); const server = yield* serveTestHttpServerLayer(ServerLayer).pipe( Effect.mapError((error) => Predicate.isTagged(error, "TestHttpServerAddressError") ? new OpenApiTestServerAddressError({ address: error.address }) : new OpenApiTestServerSpecError({ cause: error.cause }), ), ); specJson = yield* openApiSpecJsonFromHttpApi( options.api, server.baseUrl, options.transformSpec, ); return { baseUrl: server.baseUrl, specUrl: server.url(specPath), specJson, httpClientLayer: server.httpClientLayer, }; }); export class OpenApiHttpApiTestServer extends Context.Service< OpenApiHttpApiTestServer, OpenApiHttpApiTestServerShape >()("@executor-js/plugin-openapi/testing/OpenApiHttpApiTestServer") { static readonly layer = (options: OpenApiHttpApiTestServerOptions) => Layer.effect(OpenApiHttpApiTestServer, serveOpenApiHttpApiTestServer(options)); } export const serveMutableOpenApiSpecTestServer = (options: { readonly initialApi: HttpApi.Any; readonly specPath?: `/${string}`; readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>; }): Effect.Effect< MutableOpenApiSpecTestServerShape, OpenApiTestServerAddressError | OpenApiTestServerSpecError, Scope.Scope > => Effect.gen(function* () { const specPath = options.specPath ?? "/spec.json"; const specJson = yield* Ref.make(""); const requests = yield* Ref.make(0); const SpecRoute = HttpRouter.addAll([ HttpRouter.route( "GET", specPath, Effect.gen(function* () { yield* Ref.update(requests, (count) => count + 1); const current = yield* Ref.get(specJson); return HttpServerResponse.text(current, { status: 200, contentType: "application/json", }); }), ), ]); const server = yield* serveTestHttpServerLayer( HttpRouter.serve(SpecRoute, { disableListenLog: true, disableLogger: true, }), ).pipe( Effect.mapError((error) => Predicate.isTagged(error, "TestHttpServerAddressError") ? new OpenApiTestServerAddressError({ address: error.address }) : new OpenApiTestServerSpecError({ cause: error.cause }), ), ); const renderSpec = (api: HttpApi.Any) => openApiSpecJsonFromHttpApi(api, server.baseUrl, options.transformSpec); yield* Ref.set(specJson, yield* renderSpec(options.initialApi)); return { baseUrl: server.baseUrl, specUrl: server.url(specPath), specJson: yield* Ref.get(specJson), httpClientLayer: server.httpClientLayer, setApi: (api) => renderSpec(api).pipe(Effect.flatMap((next) => Ref.set(specJson, next))), requestCount: Ref.get(requests), }; }); const openApiOperationMethods = new Set([ "get", "put", "post", "delete", "options", "head", "patch", ]); const withOAuth2Security = (oauth2: NonNullable<OpenApiEchoTestServerOptions["oauth2"]>) => (spec: Record<string, unknown>): Record<string, unknown> => { const scopes = oauth2.scopes ?? { read: "Read test resources" }; const security = [{ oauth2: Object.keys(scopes) }]; const paths = isJsonObject(spec.paths) ? Object.fromEntries( Object.entries(spec.paths).map(([path, pathItem]) => [ path, isJsonObject(pathItem) ? Object.fromEntries( Object.entries(pathItem).map(([method, operation]) => [ method, openApiOperationMethods.has(method) && isJsonObject(operation) ? { ...operation, security } : operation, ]), ) : pathItem, ]), ) : spec.paths; const components = isJsonObject(spec.components) ? spec.components : {}; const securitySchemes = isJsonObject(components.securitySchemes) ? components.securitySchemes : {}; return { ...spec, paths, components: { ...components, securitySchemes: { ...securitySchemes, oauth2: {