Skip to content
Closed
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/fix-openapi-type-only-schema-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/openapi-generator": patch
---

Fix missing Schema imports in generated type-only HTTP clients with open object schemas.
12 changes: 10 additions & 2 deletions packages/tools/openapi-generator/src/JsonSchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,14 @@ function makeWithRepresentation() {
renderSchemaTypeAndRuntime(name, generated.codeDocument.codes[i], typeOnly, options)
)

return renderImportArtifacts(generated.codeDocument, !typeOnly) +
render("recursive declarations", recursiveDeclarations) +
const declarations = render("recursive declarations", recursiveDeclarations) +
render("non-recursive definitions", nonRecursives) +
render("recursive definitions", recursives) +
render("schemas", codes)
const schemaImport = typeOnly && referencesSchema(declarations)
? "import type * as Schema from \"effect/Schema\"\n"
: ""
return renderImportArtifacts(generated.codeDocument, !typeOnly) + schemaImport + declarations
}

function generateHttpApi(
Expand Down Expand Up @@ -343,6 +346,11 @@ function render(title: string, as: ReadonlyArray<string>) {
return "// " + title + "\n" + as.join("\n") + "\n"
}

// Quoted property names and literal types can contain "Schema." without referencing the namespace.
function referencesSchema(declarations: string): boolean {
return /\bSchema\./.test(declarations.replace(/"(?:\\.|[^"\\])*"/g, ""))
}

function renderImportArtifacts(codeDocument: SchemaRepresentation.CodeDocument, enabled: boolean): string {
if (!enabled) return ""
const imports = codeDocument.artifacts.flatMap((artifact) =>
Expand Down
52 changes: 46 additions & 6 deletions packages/tools/openapi-generator/test/OpenApiGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const assertTypeOnly = Effect.fnUntraced(function*(spec: OpenAPISpec, expected:
})

// console.log(result)
const failures = yield* typeCheckCases([{ sources: [result], exactOptionalPropertyTypes: true }], true)
assert.deepStrictEqual(failures, [[]])
assert.strictEqual(result, expected)
}, Effect.provide(OpenApiGenerator.layerTransformerTs))

Expand Down Expand Up @@ -148,7 +150,8 @@ const parseDiagnostics = (output: string) => {
// which cases are clean; each of its cases is then checked on its own.
const typeCheckCases = Effect.fnUntraced(
function*(
cases: ReadonlyArray<{ readonly sources: ReadonlyArray<string>; readonly exactOptionalPropertyTypes: boolean }>
cases: ReadonlyArray<{ readonly sources: ReadonlyArray<string>; readonly exactOptionalPropertyTypes: boolean }>,
noUnusedLocals = false
) {
const directory = mkdtempSync(join(dirname(fileURLToPath(import.meta.url)), ".generated-clients-"))
return yield* Effect.gen(function*() {
Expand All @@ -171,6 +174,7 @@ const typeCheckCases = Effect.fnUntraced(
lib: ["ESNext", "DOM"],
module: "NodeNext",
noEmit: true,
noUnusedLocals,
skipLibCheck: true,
strict: true,
target: "ES2022",
Expand Down Expand Up @@ -1376,11 +1380,12 @@ export const TestClientError = <Tag extends string, E>(
id: {
type: "string"
},
name: {
type: "string"
"Schema.property": {
type: "string",
enum: ["Schema.Json"]
}
},
required: ["id", "name"],
required: ["id", "Schema.property"],
additionalProperties: false
}
}
Expand All @@ -1406,7 +1411,7 @@ import * as HttpClientError from "effect/http/HttpClientError"
import * as HttpClientRequest from "effect/http/HttpClientRequest"
import * as HttpClientResponse from "effect/http/HttpClientResponse"
// schemas
export type GetUser200 = { readonly "id": string, readonly "name": string }
export type GetUser200 = { readonly "id": string, readonly "Schema.property": "Schema.Json" }

export interface OperationConfig {
/**
Expand Down Expand Up @@ -1574,7 +1579,7 @@ export const TestClientError = <Tag extends string, E>(
response,
request: response.request,
}) as any`
))
), compilationTimeout)

it.effect("preserves response variants and routes binary and bodiless statuses", () =>
assertTypeOnlyIncludes(responseMatchingSpec, [
Expand Down Expand Up @@ -3858,6 +3863,41 @@ export const withoutLocale = client.getGreeting({ params: {} })
}
]))

itGeneratedClientsCompile("emits compilable type-only clients with implicit additional properties", {
openapi: "3.1.0",
info: { title: "Open object API", version: "1.0.0" },
paths: {
"/users": {
get: {
operationId: "getUser",
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": { schema: { $ref: "#/components/schemas/User" } }
}
}
},
tags: ["Users"],
security: []
}
}
},
components: {
schemas: {
User: {
type: "object",
properties: { Name: { type: "string" } },
required: ["Name"]
}
},
securitySchemes: {}
},
security: [],
tags: []
}, { formats: ["httpclient-type-only"] })

itGeneratedClientsCompile("emits compilable open objects with optional properties", {
openapi: "3.1.0",
info: {
Expand Down
Loading