698 lines · 21.9 KB · TypeScript
RawDownload
import { Context, Data, Effect, Layer, Option, 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 { isToolResult } 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" | "name" | "namespace"> & {  readonly baseUrl?: string | null;  readonly name?: string;  readonly namespace?: string;}; export type OpenApiHttpApiTestSourceOptions = Omit<  OpenApiSpecConfig,  "spec" | "name" | "namespace" | "baseUrl"> & {  readonly name?: string;  readonly namespace?: string;  readonly baseUrl?: string | null;  readonly specBaseUrl?: string;  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}; type OpenApiHttpApiAddSpecCredentialInput =  | string  | {      readonly kind: "secret";      readonly prefix?: string;    }; type OpenApiHttpApiAddSpecCredentialsInput = {  readonly headers?: Record<string, OpenApiHttpApiAddSpecCredentialInput>;  readonly queryParams?: Record<string, OpenApiHttpApiAddSpecCredentialInput>;}; export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit<  OpenApiHttpApiTestSourceOptions,  "scope" | "headers" | "queryParams" | "specFetchCredentials"> &  OpenApiHttpApiAddSpecCredentialsInput & {    readonly specFetchCredentials?: OpenApiHttpApiAddSpecCredentialsInput;  }; export type OpenApiTestSourceExecutor = {  readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;}; 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, ...rest } = options;  return {    ...rest,    spec: { kind: "blob", value: server.specJson },    name: rest.name ?? "Test API",    namespace: rest.namespace ?? "test_api",    ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? server.baseUrl }),  } as 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, ...config } = options;  return {    ...config,    spec: {      kind: "blob",      value: makeOpenApiTestSpecJson(api, { baseUrl: specBaseUrl, transformSpec }),    },    name: config.name ?? "Test API",    namespace: config.namespace ?? "test_api",    ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? specBaseUrl ?? "https://api.example.test" }),  } as 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 { ...sourceOptions } = options;  const config = makeOpenApiHttpApiTestSourceConfig(api, {    ...sourceOptions,    scope: "unused-http-helper-scope",  });  return {    spec: config.spec,    namespace: config.namespace,    name: config.name,    baseUrl: config.baseUrl,    ...(sourceOptions.headers !== undefined ? { headers: sourceOptions.headers } : {}),    ...(sourceOptions.queryParams !== undefined ? { queryParams: sourceOptions.queryParams } : {}),    ...(config.oauth2 !== undefined ? { oauth2: config.oauth2 } : {}),    ...(sourceOptions.specFetchCredentials !== undefined      ? { specFetchCredentials: sourceOptions.specFetchCredentials }      : {}),  };}; 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: {            type: "oauth2",            flows: {              authorizationCode: {                authorizationUrl: oauth2.authorizationUrl,                tokenUrl: oauth2.tokenUrl,                scopes,              },            },          },        },      },    };  }; const composeSpecTransforms =  (    ...transforms: readonly (      | ((spec: Record<string, unknown>) => Record<string, unknown>)      | undefined    )[]  ) =>  (spec: Record<string, unknown>): Record<string, unknown> =>    transforms.reduce((current, transform) => (transform ? transform(current) : current), spec); const recordOpenApiRequest = (  requests: Ref.Ref<readonly OpenApiTestRequest[]>,  request: HttpServerRequest.HttpServerRequest,) =>  Effect.gen(function* () {    const url = new URL(request.url, "http://executor.test");    const body = yield* request.text.pipe(Effect.catch(() => Effect.succeed("")));    yield* Ref.update(requests, (all) => [      ...all,      {        method: request.method,        url: request.url,        path: url.pathname,        headers: request.headers,        body,      },    ]);    return request;  }); const captureOpenApiRequest = (requests: Ref.Ref<readonly OpenApiTestRequest[]>) =>  Effect.gen(function* () {    const request = yield* HttpServerRequest.HttpServerRequest;    return yield* recordOpenApiRequest(requests, request);  }); const openApiUnauthorizedResponse = (  options: OpenApiEchoTestServerOptions,  authorization: string | null,): Effect.Effect<HttpServerResponse.HttpServerResponse | null> =>  options.oauth2?.validateAuthorization    ? options.oauth2.validateAuthorization(authorization).pipe(        Effect.map((accepted) =>          accepted            ? null            : HttpServerResponse.jsonUnsafe(                { error: "invalid_token" },

Lines 1–500 of 698.