Skip to content

Commit 79252b0

Browse files
authored
refactor: schema-builders use ISchemaProvider instead of Input (#420)
(following on from #419) - schema builders no longer depend on `Input`, but rather use `ISchemaProvider` to make testing easier - reworks the schema builder unit tests to use the IRModel fixtures
1 parent 5421817 commit 79252b0

20 files changed

+1757
-1560
lines changed

packages/openapi-code-generator/src/core/dependency-graph.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Input} from "./input"
1+
import type {ISchemaProvider} from "./input"
22
import {logger} from "./logger"
33
import type {Reference} from "./openapi-types"
44
import type {MaybeIRModel} from "./openapi-types-normalized"
@@ -94,11 +94,11 @@ export type DependencyGraph = {order: string[]; circular: Set<string>}
9494
*
9595
* It's not perfect though:
9696
* - doesn't discover schemas declared in external specifications (eg: shared definition files)
97-
* @param input
97+
* @param schemaProvider
9898
* @param getNameForRef
9999
*/
100100
export function buildDependencyGraph(
101-
input: Input,
101+
schemaProvider: ISchemaProvider,
102102
getNameForRef: (reference: Reference) => string,
103103
): DependencyGraph {
104104
logger.time("calculate schema dependency graph")
@@ -110,7 +110,7 @@ export function buildDependencyGraph(
110110
const order: string[] = []
111111

112112
// TODO: this may miss extracted in-line schemas
113-
for (const [name, schema] of Object.entries(input.allSchemas())) {
113+
for (const [name, schema] of Object.entries(schemaProvider.allSchemas())) {
114114
remaining.set(
115115
getNameForRef({$ref: name}),
116116
getDependenciesFromSchema(schema, getNameForRef),

packages/openapi-code-generator/src/core/input.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export type InputConfig = {
3838
}
3939

4040
export interface ISchemaProvider {
41-
schema(maybeRef: MaybeIRModel): IRModel
41+
schema(maybeRef: MaybeIRModel | Reference): IRModel
42+
allSchemas(): Record<string, MaybeIRModel>
43+
preprocess(maybePreprocess: Reference | xInternalPreproccess): IRPreprocess
4244
}
4345

4446
export class Input implements ISchemaProvider {

packages/openapi-code-generator/src/core/normalization/parameter-normalizer.spec.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,14 @@ describe("ParameterNormalizer", () => {
8686
})
8787

8888
it("throws on unsupported style", () => {
89-
const param = {
90-
name: "id",
91-
in: "path",
92-
style: "form",
93-
schema: {type: "string"},
94-
} as any
95-
expect(() => parameterNormalizer.normalizeParameter(param)).toThrow(
96-
"unsupported parameter style: 'form' for in: 'path'",
97-
)
89+
expect(() =>
90+
parameterNormalizer.normalizeParameter({
91+
name: "id",
92+
in: "path",
93+
style: "form",
94+
schema: {type: "string"},
95+
}),
96+
).toThrow("unsupported parameter style: 'form' for in: 'path'")
9897
})
9998
})
10099

@@ -226,7 +225,7 @@ describe("ParameterNormalizer", () => {
226225
}
227226

228227
loader.parameter.mockImplementation((it) => it as Parameter)
229-
loader.schema.mockImplementation((it) => it as any)
228+
loader.schema.mockImplementation((it) => it)
230229
loader.addVirtualType.mockImplementation((_opId, name) =>
231230
ir.ref(name, "virtual"),
232231
)
@@ -300,7 +299,7 @@ describe("ParameterNormalizer", () => {
300299
}
301300

302301
loader.parameter.mockReturnValue(queryParam)
303-
loader.schema.mockReturnValue({type: "string"} as any)
302+
loader.schema.mockReturnValue({type: "string"})
304303
loader.addVirtualType.mockImplementation((_operationId, name) =>
305304
ir.ref(name, "virtual"),
306305
)
@@ -328,7 +327,7 @@ describe("ParameterNormalizer", () => {
328327
loader.schema.mockReturnValue({
329328
type: "array",
330329
items: {type: "string"},
331-
} as any)
330+
})
332331
loader.addVirtualType.mockImplementation((_opId, name) =>
333332
ir.ref(name, "virtual"),
334333
)

packages/openapi-code-generator/src/core/openapi-utils.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {describe, expect, it} from "@jest/globals"
2-
import {extractPlaceholders, getNameFromRef, isRef} from "./openapi-utils"
2+
import {
3+
extractPlaceholders,
4+
getNameFromRef,
5+
getRawNameFromRef,
6+
isRef,
7+
} from "./openapi-utils"
38

49
describe("core/openapi-utils", () => {
510
describe("#isRef", () => {
@@ -17,6 +22,20 @@ describe("core/openapi-utils", () => {
1722
})
1823
})
1924

25+
describe("#getRawNameFromRef", () => {
26+
it("returns the raw name", () => {
27+
expect(
28+
getRawNameFromRef({$ref: "#/components/schemas/Something"}),
29+
).toEqual("Something")
30+
})
31+
32+
it("throws on an invalid $ref", () => {
33+
expect(() => getRawNameFromRef({$ref: "#/"})).toThrow(
34+
"no name found in $ref: '#/'",
35+
)
36+
})
37+
})
38+
2039
describe("#getNameFromRef", () => {
2140
it("includes the given prefix", () => {
2241
expect(getNameFromRef({$ref: "#/components/schemas/Foo"}, "t_")).toBe(

packages/openapi-code-generator/src/core/openapi-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ export function isRef(it: unknown | Reference): it is Reference {
77
return Reflect.has(it, "$ref")
88
}
99

10-
export function getNameFromRef({$ref}: Reference, prefix: string): string {
10+
export function getRawNameFromRef({$ref}: Reference): string {
1111
const name = $ref.split("/").pop()
1212

1313
if (!name) {
1414
throw new Error(`no name found in $ref: '${$ref}'`)
1515
}
1616

17+
return name
18+
}
19+
20+
export function getNameFromRef({$ref}: Reference, prefix: string): string {
21+
const name = getRawNameFromRef({$ref})
1722
// todo: this is a hack to workaround reserved words being used as names
1823
// can likely improve to selectively apply when a reserved word is used.
1924
return prefix + name.replace(/[-.]+/g, "_")
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {ISchemaProvider} from "../core/input"
2+
import type {
3+
IRModel,
4+
IRPreprocess,
5+
IRRef,
6+
MaybeIRModel,
7+
} from "../core/openapi-types-normalized"
8+
import {getRawNameFromRef, isRef} from "../core/openapi-utils"
9+
10+
export class FakeSchemaProvider implements ISchemaProvider {
11+
private readonly testRefs: Record<string, IRModel> = {}
12+
13+
registerTestRef(ref: IRRef, model: IRModel) {
14+
this.testRefs[ref.$ref] = model
15+
}
16+
17+
schema(maybeRef: MaybeIRModel): IRModel {
18+
if (isRef(maybeRef)) {
19+
const result = this.testRefs[maybeRef.$ref]
20+
21+
if (!result) {
22+
throw new Error(
23+
`FakeSchemaProvider: $ref '${maybeRef.$ref}' is not registered`,
24+
)
25+
}
26+
27+
return result
28+
}
29+
30+
return maybeRef
31+
}
32+
33+
allSchemas(): Record<string, MaybeIRModel> {
34+
return Object.fromEntries(
35+
Object.entries(this.testRefs).map(([$ref, value]) => {
36+
return [getRawNameFromRef({$ref}), value]
37+
}),
38+
)
39+
}
40+
41+
preprocess(): IRPreprocess {
42+
return {}
43+
}
44+
}

packages/openapi-code-generator/src/test/input.test-utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {GenericLoader} from "../core/loaders/generic.loader"
77
import {OpenapiLoader} from "../core/loaders/openapi-loader"
88
import {TypespecLoader} from "../core/loaders/typespec.loader"
99
import {logger} from "../core/logger"
10-
import {SchemaNormalizer} from "../core/normalization/schema-normalizer"
1110
import {OpenapiValidator} from "../core/openapi-validator"
1211

1312
export type OpenApiVersion = "3.0.x" | "3.1.x"
@@ -59,7 +58,6 @@ export async function unitTestInput(
5958

6059
return {
6160
input: new Input(loader, config),
62-
schemaNormalizer: new SchemaNormalizer(config),
6361
file,
6462
}
6563
}

packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
buildDependencyGraph,
33
type DependencyGraph,
44
} from "../../../core/dependency-graph"
5-
import type {Input} from "../../../core/input"
5+
import type {ISchemaProvider} from "../../../core/input"
66
import {logger} from "../../../core/logger"
77
import type {Reference} from "../../../core/openapi-types"
88
import type {
@@ -38,7 +38,7 @@ export abstract class AbstractSchemaBuilder<
3838

3939
protected constructor(
4040
public readonly filename: string,
41-
protected readonly input: Input,
41+
protected readonly schemaProvider: ISchemaProvider,
4242
protected readonly config: SchemaBuilderConfig,
4343
protected readonly schemaBuilderImports: ImportBuilder,
4444
typeBuilder: TypeBuilder,
@@ -50,7 +50,9 @@ export abstract class AbstractSchemaBuilder<
5050
) {
5151
this.graph =
5252
parent?.graph ??
53-
buildDependencyGraph(this.input, (it) => this.getSchemaNameFromRef(it))
53+
buildDependencyGraph(this.schemaProvider, (it) =>
54+
this.getSchemaNameFromRef(it),
55+
)
5456
this.importHelpers(this.schemaBuilderImports)
5557
this.typeBuilder = typeBuilder.withImports(this.schemaBuilderImports)
5658
}
@@ -123,6 +125,7 @@ export abstract class AbstractSchemaBuilder<
123125
})
124126
}
125127

128+
/* istanbul ignore next */
126129
throw new Error("unreachable")
127130
})
128131
)
@@ -184,7 +187,7 @@ export abstract class AbstractSchemaBuilder<
184187
}
185188

186189
if (maybeModel["x-internal-preprocess"]) {
187-
const dereferenced = this.input.preprocess(
190+
const dereferenced = this.schemaProvider.preprocess(
188191
maybeModel["x-internal-preprocess"],
189192
)
190193
if (dereferenced.deserialize) {
@@ -211,6 +214,7 @@ export abstract class AbstractSchemaBuilder<
211214
// todo: byte is base64 encoded string, https://spec.openapis.org/registry/format/byte.html
212215
// model.format === "byte"
213216
if (model.format === "binary") {
217+
// todo: check instanceof Blob?
214218
result = this.any()
215219
} else {
216220
result = this.string(model)
@@ -234,7 +238,7 @@ export abstract class AbstractSchemaBuilder<
234238
// Note: for zod in particular it's desirable to use merge over intersection
235239
// where possible, as it returns a more malleable schema
236240
const isMergable = model.schemas
237-
.map((it) => this.input.schema(it))
241+
.map((it) => this.schemaProvider.schema(it))
238242
.every((it) => it.type === "object" && !it.additionalProperties)
239243

240244
result = isMergable ? this.merge(schemas) : this.intersect(schemas)
@@ -304,7 +308,9 @@ export abstract class AbstractSchemaBuilder<
304308
}
305309

306310
if (model["x-internal-preprocess"]) {
307-
const dereferenced = this.input.preprocess(model["x-internal-preprocess"])
311+
const dereferenced = this.schemaProvider.preprocess(
312+
model["x-internal-preprocess"],
313+
)
308314
if (dereferenced.deserialize) {
309315
result = this.preprocess(result, dereferenced.deserialize.fn)
310316
}

packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.integration.spec.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import vm from "node:vm"
2-
import {describe, expect, it} from "@jest/globals"
3-
import type {
4-
SchemaArray,
5-
SchemaBoolean,
6-
SchemaNumber,
7-
SchemaObject,
8-
SchemaString,
9-
} from "../../../core/openapi-types"
2+
import {beforeAll, describe, expect, it} from "@jest/globals"
103
import {testVersions} from "../../../test/input.test-utils"
11-
import type {SchemaBuilderConfig} from "./abstract-schema-builder"
4+
import {TypescriptFormatterBiome} from "../typescript-formatter.biome"
125
import {
13-
schemaBuilderTestHarness,
14-
schemaNumber,
15-
schemaObject,
16-
schemaString,
6+
type SchemaBuilderIntegrationTestHarness,
7+
schemaBuilderIntegrationTestHarness,
178
} from "./schema-builder.test-utils"
189

1910
describe.each(
@@ -32,11 +23,19 @@ describe.each(
3223
)
3324
}
3425

35-
const {getActual, getActualFromModel} = schemaBuilderTestHarness(
36-
"joi",
37-
version,
38-
executeParseSchema,
39-
)
26+
let getActual: SchemaBuilderIntegrationTestHarness["getActual"]
27+
28+
beforeAll(async () => {
29+
const formatter = await TypescriptFormatterBiome.createNodeFormatter()
30+
const harness = schemaBuilderIntegrationTestHarness(
31+
"joi",
32+
formatter,
33+
version,
34+
executeParseSchema,
35+
)
36+
37+
getActual = harness.getActual
38+
})
4039

4140
it("supports the SimpleObject", async () => {
4241
const {code, schemas} = await getActual("components/schemas/SimpleObject")

packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Input} from "../../../core/input"
1+
import type {ISchemaProvider} from "../../../core/input"
22
import type {Reference} from "../../../core/openapi-types"
33
import type {
44
IRModel,
@@ -34,16 +34,16 @@ export class JoiBuilder extends AbstractSchemaBuilder<
3434

3535
private includeIntersectHelper = false
3636

37-
static async fromInput(
37+
static async fromSchemaProvider(
3838
filename: string,
39-
input: Input,
39+
schemaProvider: ISchemaProvider,
4040
schemaBuilderConfig: SchemaBuilderConfig,
4141
schemaBuilderImports: ImportBuilder,
4242
typeBuilder: TypeBuilder,
4343
): Promise<JoiBuilder> {
4444
return new JoiBuilder(
4545
filename,
46-
input,
46+
schemaProvider,
4747
schemaBuilderConfig,
4848
schemaBuilderImports,
4949
typeBuilder,
@@ -54,7 +54,7 @@ export class JoiBuilder extends AbstractSchemaBuilder<
5454
override withImports(imports: ImportBuilder): JoiBuilder {
5555
return new JoiBuilder(
5656
this.filename,
57-
this.input,
57+
this.schemaProvider,
5858
this.config,
5959
this.schemaBuilderImports,
6060
this.typeBuilder,
@@ -89,7 +89,7 @@ export class JoiBuilder extends AbstractSchemaBuilder<
8989

9090
protected schemaFromRef(reference: Reference): ExportDefinition {
9191
const name = this.getSchemaNameFromRef(reference)
92-
const schemaObject = this.input.schema(reference)
92+
const schemaObject = this.schemaProvider.schema(reference)
9393

9494
const value = this.fromModel(schemaObject, true)
9595

0 commit comments

Comments
 (0)