Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/openapi-generator-client-generics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/openapi-generator": minor
---

Thread HttpClient error (`E`) and requirements (`R`) channels through generated client interfaces and the `make()` constructor, preserving middleware-introduced error types at the call site.
56 changes: 30 additions & 26 deletions packages/tools/openapi-generator/src/OpenApiTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export const makeTransformerSchema = () => {
methods.push(operationToBinaryMethod(name, op))
}
}
return `export interface ${name} {
readonly httpClient: HttpClient.HttpClient
return `export interface ${name}<E = HttpClientError.HttpClientError, R = never> {
readonly httpClient: HttpClient.HttpClient.With<E, R>
${methods.join("\n ")}
}

Expand Down Expand Up @@ -129,7 +129,7 @@ ${clientErrorSource(name)}`
.map((schema) => `typeof ${schema}.Type`)
.join(" | ")
}
const errors = ["HttpClientError.HttpClientError", "SchemaError"]
const errors = ["E", "SchemaError"]
if (operation.errorSchemas.size > 0) {
Utils.spreadElementsInto(
Array.from(operation.errorSchemas.values()).map(
Expand All @@ -143,7 +143,7 @@ ${clientErrorSource(name)}`
const methodKey = `readonly "${operation.id}"`
const generic = `<Config extends OperationConfig>`
const parameters = args.join(", ")
const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}>`
const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}, R>`
return `${jsdoc}${methodKey}: ${generic}(${parameters}) => ${returnType}`
}

Expand Down Expand Up @@ -174,7 +174,7 @@ ${clientErrorSource(name)}`
const methodKey = `readonly "${operation.id}Sse"`
const parameters = args.join(", ")
const returnType =
`Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof ${operation.sseSchema}.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof ${operation.sseSchema}.DecodingServices>`
`Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof ${operation.sseSchema}.Type }, E | HttpClientError.HttpClientError | SchemaError | Sse.Retry, R | typeof ${operation.sseSchema}.DecodingServices>`
return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
}

Expand Down Expand Up @@ -204,7 +204,7 @@ ${clientErrorSource(name)}`
const jsdoc = Utils.toComment(operation.description)
const methodKey = `readonly "${operation.id}Stream"`
const parameters = args.join(", ")
const returnType = `Stream.Stream<Uint8Array, HttpClientError.HttpClientError>`
const returnType = `Stream.Stream<Uint8Array, E | HttpClientError.HttpClientError, R>`
return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
}

Expand Down Expand Up @@ -255,12 +255,12 @@ export type WithOptionalResponse<A, Config extends OperationConfig> = Config ext
readonly includeResponse: true
} ? [A, HttpClientResponse.HttpClientResponse] : A

export const make = (
httpClient: HttpClient.HttpClient,
export const make = <E, R>(
httpClient: HttpClient.HttpClient.With<E, R>,
options: {
readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
readonly transformClient?: ((client: HttpClient.HttpClient.With<E, R>) => Effect.Effect<HttpClient.HttpClient.With<E, R>>) | undefined
} = {}
): ${name} => {
): ${name}<E, R> => {
${helpers.join("\n ")}
const decodeSuccess =
<Schema extends ${importName}.Constraint>(schema: Schema) =>
Expand Down Expand Up @@ -499,8 +499,8 @@ export const makeTransformerTs = () => {
methods.push(operationToBinaryMethod(op))
}
}
return `export interface ${name} {
readonly httpClient: HttpClient.HttpClient
return `export interface ${name}<E = HttpClientError.HttpClientError, R = never> {
readonly httpClient: HttpClient.HttpClient.With<E, R>
${methods.join("\n ")}
}

Expand Down Expand Up @@ -537,7 +537,7 @@ ${clientErrorSource(name)}`
success = Array.from(operation.successSchemas.values()).join(" | ")
}

const errors = ["HttpClientError.HttpClientError"]
const errors = ["E"]
if (operation.errorSchemas.size > 0) {
for (const schema of operation.errorSchemas.values()) {
errors.push(`${name}Error<"${schema}", ${schema}>`)
Expand All @@ -548,7 +548,7 @@ ${clientErrorSource(name)}`
const methodKey = `readonly "${operation.id}"`
const generic = `<Config extends OperationConfig>`
const parameters = args.join(", ")
const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}>`
const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}, R>`
return `${jsdoc}${methodKey}: ${generic}(${parameters}) => ${returnType}`
}

Expand Down Expand Up @@ -608,7 +608,7 @@ ${clientErrorSource(name)}`
const jsdoc = Utils.toComment(operation.description)
const methodKey = `readonly "${operation.id}Stream"`
const parameters = args.join(", ")
const returnType = `Stream.Stream<Uint8Array, HttpClientError.HttpClientError>`
const returnType = `Stream.Stream<Uint8Array, E | HttpClientError.HttpClientError, R>`
return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
}

Expand Down Expand Up @@ -659,12 +659,12 @@ export type WithOptionalResponse<A, Config extends OperationConfig> = Config ext
readonly includeResponse: true
} ? [A, HttpClientResponse.HttpClientResponse] : A

export const make = (
httpClient: HttpClient.HttpClient,
export const make = <E, R>(
httpClient: HttpClient.HttpClient.With<E, R>,
options: {
readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
readonly transformClient?: ((client: HttpClient.HttpClient.With<E, R>) => Effect.Effect<HttpClient.HttpClient.With<E, R>>) | undefined
} = {}
): ${name} => {
): ${name}<E, R> => {
${helpers.join("\n ")}
const decodeSuccess = <A>(response: HttpClientResponse.HttpClientResponse) =>
response.json as Effect.Effect<A, HttpClientError.HttpClientError>
Expand Down Expand Up @@ -895,9 +895,13 @@ const commonSource = `const unexpectedStatus = (response: HttpClientResponse.Htt
}),
),
)
const withResponse = <Config extends OperationConfig>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<any, any>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<any, any> => {
const withResponse = <Config extends OperationConfig, A, E2, R2>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<A, E2, R2>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<
Config extends { readonly includeResponse: true } ? [A, HttpClientResponse.HttpClientResponse] : A,
E | E2,
R | R2
> => {
const withOptionalResponse = (
config?.includeResponse
? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response])
Expand All @@ -923,8 +927,8 @@ const sseRequestSource = (_importName: string) =>
request: HttpClientRequest.HttpClientRequest
): Stream.Stream<
{ readonly event: string; readonly id: string | undefined; readonly data: Type },
HttpClientError.HttpClientError | SchemaError | Sse.Retry,
DecodingServices
E | HttpClientError.HttpClientError | SchemaError | Sse.Retry,
R | DecodingServices
> =>
HttpClient.filterStatusOk(httpClient).execute(request).pipe(
Effect.map((response) => response.stream),
Expand All @@ -934,7 +938,7 @@ const sseRequestSource = (_importName: string) =>
)`

const binaryRequestSource =
`const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, HttpClientError.HttpClientError> =>
`const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, E | HttpClientError.HttpClientError, R> =>
HttpClient.filterStatusOk(httpClient).execute(request).pipe(
Effect.map((response) => response.stream),
Stream.unwrap
Expand All @@ -953,7 +957,7 @@ const sseRequestSourceTs =
)`

const binaryRequestSourceTs =
`const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, HttpClientError.HttpClientError> =>
`const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, E | HttpClientError.HttpClientError, R> =>
HttpClient.filterStatusOk(httpClient).execute(request).pipe(
Effect.map((response) => response.stream),
Stream.unwrap
Expand Down
50 changes: 29 additions & 21 deletions packages/tools/openapi-generator/test/OpenApiGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,12 @@ export type WithOptionalResponse<A, Config extends OperationConfig> = Config ext
readonly includeResponse: true
} ? [A, HttpClientResponse.HttpClientResponse] : A

export const make = (
httpClient: HttpClient.HttpClient,
export const make = <E, R>(
httpClient: HttpClient.HttpClient.With<E, R>,
options: {
readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
readonly transformClient?: ((client: HttpClient.HttpClient.With<E, R>) => Effect.Effect<HttpClient.HttpClient.With<E, R>>) | undefined
} = {}
): TestClient => {
): TestClient<E, R> => {
const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) =>
Effect.flatMap(
Effect.orElseSucceed(response.json, () => "Unexpected status code"),
Expand All @@ -399,9 +399,13 @@ export const make = (
}),
),
)
const withResponse = <Config extends OperationConfig>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<any, any>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<any, any> => {
const withResponse = <Config extends OperationConfig, A, E2, R2>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<A, E2, R2>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<
Config extends { readonly includeResponse: true } ? [A, HttpClientResponse.HttpClientResponse] : A,
E | E2,
R | R2
> => {
const withOptionalResponse = (
config?.includeResponse
? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response])
Expand Down Expand Up @@ -437,9 +441,9 @@ export const make = (
}
}

export interface TestClient {
readonly httpClient: HttpClient.HttpClient
readonly "getUser": <Config extends OperationConfig>(id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<typeof GetUser200.Type, Config>, HttpClientError.HttpClientError | SchemaError>
export interface TestClient<E = HttpClientError.HttpClientError, R = never> {
readonly httpClient: HttpClient.HttpClient.With<E, R>
readonly "getUser": <Config extends OperationConfig>(id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<typeof GetUser200.Type, Config>, E | SchemaError, R>
}

export interface TestClientError<Tag extends string, E> {
Expand Down Expand Up @@ -518,7 +522,7 @@ export const TestClientError = <Tag extends string, E>(
},
[
`import * as Sse from "effect/unstable/encoding/Sse"`,
`readonly "streamEventsSse": () => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof StreamEvents200Sse.DecodingServices>`,
`readonly "streamEventsSse": () => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, E | HttpClientError.HttpClientError | SchemaError | Sse.Retry, R | typeof StreamEvents200Sse.DecodingServices>`,
`"streamEventsSse": () => HttpClientRequest.get(\`/events\`).pipe(`,
`sseRequest(StreamEvents200Sse)`,
`schema: Schema.ConstraintDecoder<Type, DecodingServices>`
Expand Down Expand Up @@ -678,12 +682,12 @@ export type WithOptionalResponse<A, Config extends OperationConfig> = Config ext
readonly includeResponse: true
} ? [A, HttpClientResponse.HttpClientResponse] : A

export const make = (
httpClient: HttpClient.HttpClient,
export const make = <E, R>(
httpClient: HttpClient.HttpClient.With<E, R>,
options: {
readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
readonly transformClient?: ((client: HttpClient.HttpClient.With<E, R>) => Effect.Effect<HttpClient.HttpClient.With<E, R>>) | undefined
} = {}
): TestClient => {
): TestClient<E, R> => {
const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) =>
Effect.flatMap(
Effect.orElseSucceed(response.json, () => "Unexpected status code"),
Expand All @@ -698,9 +702,13 @@ export const make = (
}),
),
)
const withResponse = <Config extends OperationConfig>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<any, any>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<any, any> => {
const withResponse = <Config extends OperationConfig, A, E2, R2>(config: Config | undefined) => (
f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<A, E2, R2>,
): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<
Config extends { readonly includeResponse: true } ? [A, HttpClientResponse.HttpClientResponse] : A,
E | E2,
R | R2
> => {
const withOptionalResponse = (
config?.includeResponse
? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response])
Expand Down Expand Up @@ -756,9 +764,9 @@ export const make = (
}
}

export interface TestClient {
readonly httpClient: HttpClient.HttpClient
readonly "getUser": <Config extends OperationConfig>(id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<GetUser200, Config>, HttpClientError.HttpClientError>
export interface TestClient<E = HttpClientError.HttpClientError, R = never> {
readonly httpClient: HttpClient.HttpClient.With<E, R>
readonly "getUser": <Config extends OperationConfig>(id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<GetUser200, Config>, E, R>
}

export interface TestClientError<Tag extends string, E> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe("openapigen CLI", () => {
const result = yield* runCli(["--spec", spec, "--name", "CliClient"])

assert.isTrue(Exit.isSuccess(result.exit))
assert.include(result.stdout, "export const make = (")
assert.include(result.stdout, "export const make = <E, R>(")
assert.include(result.stderr, "WARNING [cookie-parameter-dropped]")
assert.include(result.stderr, "cookie-parameter-dropped")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
Expand All @@ -135,7 +135,7 @@ describe("openapigen CLI", () => {
const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"])

assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0))
assert.include(result.stdout, "export const make = (")
assert.include(result.stdout, "export const make = <E, R>(")
assert.notInclude(result.stdout, "WARNING [")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
assert.include(
Expand Down