Skip to content

Commit db7be98

Browse files
authored
refactor: simplify dependency graph (#417)
1 parent 01bc328 commit db7be98

File tree

2 files changed

+84
-60
lines changed

2 files changed

+84
-60
lines changed

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import {describe, expect, it} from "@jest/globals"
2-
import {testVersions, unitTestInput} from "../test/input.test-utils"
2+
import {
3+
createTestInputFromYamlString,
4+
testVersions,
5+
unitTestInput,
6+
} from "../test/input.test-utils"
37
import {buildDependencyGraph} from "./dependency-graph"
48
import {getNameFromRef} from "./openapi-utils"
59

@@ -50,4 +54,27 @@ describe.each(testVersions)("%s - core/dependency-graph", (version) => {
5054

5155
expect(graph.circular).toStrictEqual(new Set(["s_Recursive"]))
5256
})
57+
58+
it("handles recursion in intersection types", async () => {
59+
const yaml = `
60+
openapi: 3.0.3
61+
info:
62+
title: Test
63+
version: 1.0.0
64+
paths: {}
65+
components:
66+
schemas:
67+
RecursiveIntersection:
68+
allOf:
69+
- type: object
70+
properties:
71+
foo:
72+
type: string
73+
- $ref: '#/components/schemas/RecursiveIntersection'
74+
`
75+
const input = await createTestInputFromYamlString(yaml)
76+
const graph = buildDependencyGraph(input, (it) => getNameFromRef(it, "s_"))
77+
78+
expect(graph.circular).toContain("s_RecursiveIntersection")
79+
})
5380
})

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

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {Input} from "./input"
22
import {logger} from "./logger"
33
import type {Reference} from "./openapi-types"
4-
import type {IRModel, MaybeIRModel} from "./openapi-types-normalized"
4+
import type {MaybeIRModel} from "./openapi-types-normalized"
55
import {isRef} from "./openapi-utils"
66

77
function intersect<T>(a: Set<T>, b: Set<T>) {
@@ -16,72 +16,69 @@ function remove(a: Set<unknown>, b: Set<unknown>) {
1616
}
1717
}
1818

19-
const getAllSourcesFromSchema = (it: IRModel) => {
20-
const allSources: Array<MaybeIRModel> = [it as MaybeIRModel]
21-
22-
if (it.type === "object") {
23-
if (it.additionalProperties) {
24-
allSources.push(it.additionalProperties.key)
25-
allSources.push(it.additionalProperties.value)
26-
}
27-
} else if (it.type === "array") {
28-
allSources.push(it.items)
29-
} else if (it.type === "record") {
30-
allSources.push(it.key)
31-
allSources.push(it.value)
32-
} else if (it.type === "intersection" || it.type === "union") {
33-
allSources.push(...it.schemas)
34-
}
35-
36-
return allSources
37-
}
38-
3919
const getDependenciesFromSchema = (
40-
schema: IRModel,
20+
schema: MaybeIRModel,
4121
getNameForRef: (ref: Reference) => string,
22+
visited = new Set<MaybeIRModel>(),
4223
): Set<string> => {
43-
const allSources = getAllSourcesFromSchema(schema)
44-
const result = new Set<string>()
24+
if (visited.has(schema)) {
25+
logger.error(
26+
"circular dependency visited for schema - stopping graph walk",
27+
{schema},
28+
)
29+
return new Set(
30+
visited
31+
.values()
32+
.filter((it) => isRef(it))
33+
.map(getNameForRef),
34+
)
35+
}
36+
37+
visited.add(schema)
4538

46-
for (const it of allSources) {
47-
if (isRef(it)) {
48-
result.add(getNameForRef(it))
49-
} else if (it.type === "object") {
50-
const innerSources = Object.values(it.properties)
39+
const result = new Set<string>()
5140

52-
if (it.additionalProperties) {
53-
innerSources.push(it.additionalProperties.key)
54-
innerSources.push(it.additionalProperties.value)
41+
if (isRef(schema)) {
42+
result.add(getNameForRef(schema))
43+
} else {
44+
if (schema.type === "object") {
45+
for (const property of Object.values(schema.properties)) {
46+
intersect(
47+
result,
48+
getDependenciesFromSchema(property, getNameForRef, visited),
49+
)
5550
}
5651

57-
for (const prop of innerSources) {
58-
if (isRef(prop)) {
59-
result.add(getNameForRef(prop))
60-
} else if (
61-
prop.type === "object" ||
62-
prop.type === "union" ||
63-
prop.type === "intersection" ||
64-
prop.type === "record"
65-
) {
66-
intersect(result, getDependenciesFromSchema(prop, getNameForRef))
67-
} else if (prop.type === "array") {
68-
if (isRef(prop.items)) {
69-
result.add(getNameForRef(prop.items))
70-
} else {
71-
intersect(
72-
result,
73-
getDependenciesFromSchema(prop.items, getNameForRef),
74-
)
75-
}
76-
}
52+
if (schema.additionalProperties) {
53+
intersect(
54+
result,
55+
getDependenciesFromSchema(
56+
schema.additionalProperties,
57+
getNameForRef,
58+
visited,
59+
),
60+
)
7761
}
78-
} else if (it.type === "intersection" || it.type === "union") {
79-
for (const prop of it.schemas) {
80-
if (isRef(prop)) {
81-
result.add(getNameForRef(prop))
82-
} else {
83-
intersect(result, getDependenciesFromSchema(prop, getNameForRef))
84-
}
62+
} else if (schema.type === "array") {
63+
intersect(
64+
result,
65+
getDependenciesFromSchema(schema.items, getNameForRef, visited),
66+
)
67+
} else if (schema.type === "record") {
68+
intersect(
69+
result,
70+
getDependenciesFromSchema(schema.key, getNameForRef, visited),
71+
)
72+
intersect(
73+
result,
74+
getDependenciesFromSchema(schema.value, getNameForRef, visited),
75+
)
76+
} else if (schema.type === "intersection" || schema.type === "union") {
77+
for (const subSchema of schema.schemas) {
78+
intersect(
79+
result,
80+
getDependenciesFromSchema(subSchema, getNameForRef, visited),
81+
)
8582
}
8683
}
8784
}

0 commit comments

Comments
 (0)