212 lines
6.6 KiB
TypeScript
212 lines
6.6 KiB
TypeScript
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<TData extends TDataShape, TResponse> = Omit<`,
|
|
` Options<TData, true, TResponse, "data">,`,
|
|
` "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<T, E>(evaluate: () => Promise<T>) {`,
|
|
` return Effect.tryPromise({`,
|
|
` try: evaluate,`,
|
|
` catch: (error) => error as E | Error,`,
|
|
` })`,
|
|
`}`,
|
|
].join("\n"),
|
|
}
|
|
}
|
|
|
|
function serviceShape(service: Service, sse: Set<string>, optionsType: string) {
|
|
return [
|
|
` ${propertyName(service.name)}: {`,
|
|
...service.operations.flatMap((operation) => [
|
|
doc(operation, " "),
|
|
` ${methodSignature(operation, sse, optionsType)}`,
|
|
]),
|
|
` }`,
|
|
]
|
|
}
|
|
|
|
function serviceFactory(service: Service, sse: Set<string>) {
|
|
return [
|
|
` ${propertyName(service.name)}: {`,
|
|
...service.operations.map(
|
|
(operation) => ` ${propertyName(operation.name)}: ${methodFactory(operation, sse)},`,
|
|
),
|
|
` },`,
|
|
]
|
|
}
|
|
|
|
function methodSignature(operation: Operation, sse: Set<string>, 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<string>) {
|
|
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<string>) {
|
|
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<string>) {
|
|
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")
|
|
}
|