import type { ApiSpec, Emitter, EmitterContext, GeneratedFile, Operation, Service } from "@workos/oagen" type EffectEmitterOptions = { serverSentEvents?: string[] } export const effectEmitter: Emitter = { language: "effect", generateModels(): GeneratedFile[] { return [] }, generateEnums(): GeneratedFile[] { return [] }, generateResources(): GeneratedFile[] { return [] }, generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] { return [generateEffectClient(spec, ctx)] }, generateErrors(): GeneratedFile[] { return [] }, generateTests(): GeneratedFile[] { return [] }, fileHeader() { return "// This file is auto-generated by oagen. Do not edit." }, } function generateEffectClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile { const sse = new Set((ctx.emitterOptions as EffectEmitterOptions | undefined)?.serverSentEvents ?? []) const operations = spec.services.flatMap((service) => service.operations.map((operation) => ({ service, operation }))) const typeImports = operations.flatMap(({ operation }) => { const type = operationType(operation) if (operation.errors.length === 0) return [`${type}Data`, `${type}Response`, `${type}Responses`] return [`${type}Data`, `${type}Error`, `${type}Errors`, `${type}Response`, `${type}Responses`] }) return { path: "effect.gen.ts", content: [ `import { Effect } from "effect"`, `import type { Client, Options, TDataShape } from "./client/index.js"`, `import type { ServerSentEventsResult } from "./core/serverSentEvents.gen.js"`, `import type {`, ...Array.from(new Set(typeImports)) .sort() .map((name) => ` ${name},`), `} from "./types.gen.js"`, ``, `export type ${ctx.namespacePascal}EffectOptions = Omit<`, ` Options,`, ` "responseStyle" | "throwOnError"`, `>`, ``, `export interface ${ctx.namespacePascal}EffectClient {`, ...spec.services.flatMap((service) => serviceShape(service, sse, `${ctx.namespacePascal}EffectOptions`)), `}`, ``, `export function create${ctx.namespacePascal}EffectClient(client: Client): ${ctx.namespacePascal}EffectClient {`, ` return {`, ...spec.services.flatMap((service) => serviceFactory(service, sse)), ` }`, `}`, ``, `function request(evaluate: () => Promise) {`, ` return Effect.tryPromise({`, ` try: evaluate,`, ` catch: (error) => error as E | Error,`, ` })`, `}`, ].join("\n"), } } function serviceShape(service: Service, sse: Set, optionsType: string) { return [ ` ${propertyName(service.name)}: {`, ...service.operations.flatMap((operation) => [ doc(operation, " "), ` ${methodSignature(operation, sse, optionsType)}`, ]), ` }`, ] } function serviceFactory(service: Service, sse: Set) { return [ ` ${propertyName(service.name)}: {`, ...service.operations.map( (operation) => ` ${propertyName(operation.name)}: ${methodFactory(operation, sse)},`, ), ` },`, ] } function methodSignature(operation: Operation, sse: Set, optionsType: string) { const optional = hasRequiredOptions(operation) ? "" : "?" return `${propertyName(operation.name)}(options${optional}: ${optionsType}<${operationType( operation, )}Data, ${operationType(operation)}Responses>): Effect.Effect<${operationResponse(operation, sse)}, ${operationError( operation, )}>` } function methodFactory(operation: Operation, sse: Set) { const type = operationType(operation) const args = [ `url: ${JSON.stringify(operation.path)}`, `...options`, `throwOnError: true`, `responseStyle: "data"`, contentType(operation), ].filter((line): line is string => Boolean(line)) const request = isSse(operation, sse) ? `client.sse.${operation.httpMethod}<${type}Responses, ${operationErrorTypes(operation)}, true, "data">({ ${args.join( ", ", )} })` : `client.${operation.httpMethod}<${type}Responses, ${operationErrorTypes(operation)}, true, "data">({ ${args.join( ", ", )} })` return `(options${hasRequiredOptions(operation) ? "" : "?"}) => request<${operationResponse( operation, sse, )}, ${operationErrorValue(operation)}>(() => ${request})` } function operationResponse(operation: Operation, sse: Set) { const type = operationType(operation) if (isSse(operation, sse)) return `ServerSentEventsResult<${type}Responses>` return `${type}Response` } function operationError(operation: Operation) { if (operation.errors.length === 0) return "Error" return `${operationType(operation)}Error | Error` } function operationErrorValue(operation: Operation) { if (operation.errors.length === 0) return "never" return `${operationType(operation)}Error` } function operationErrorTypes(operation: Operation) { if (operation.errors.length === 0) return "unknown" return `${operationType(operation)}Errors` } function contentType(operation: Operation) { if (!operation.requestBody) return const value = { binary: "application/octet-stream", json: "application/json", text: "text/plain", "form-urlencoded": "application/x-www-form-urlencoded", "form-data": undefined, }[operation.requestBodyEncoding ?? "json"] if (!value) return return `headers: { "Content-Type": ${JSON.stringify(value)}, ...options?.headers }` } function hasRequiredOptions(operation: Operation) { return ( operation.pathParams.length > 0 || operation.queryParams.some((item) => item.required) || operation.headerParams.some((item) => item.required) ) } function isSse(operation: Operation, sse: Set) { return sse.has(`${operation.httpMethod.toUpperCase()} ${operation.path}`) } function operationType(operation: Operation) { return identifier(operation.name) } function propertyName(value: string) { return value.charAt(0).toLowerCase() + value.slice(1) } function identifier(value: string) { return value .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/[^A-Za-z0-9]+/g, " ") .trim() .split(/\s+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join("") } function doc(operation: Operation, indent: string) { if (!operation.description) return `${indent}/** ${operation.httpMethod.toUpperCase()} ${operation.path} */` return [ `${indent}/**`, ...operation.description .replaceAll("*/", "* /") .split("\n") .map((line) => `${indent} * ${line}`), `${indent} */`, ].join("\n") }