Skip to content

Commit c670ca3

Browse files
committed
feat: partial support external refs in paths (#447)
1 parent a56ded2 commit c670ca3

18 files changed

+521
-213
lines changed

.changeset/silent-beds-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swagger-typescript-api": minor
3+
---
4+
5+
partial support external paths by ref (#447)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"swagger-schema-official": "2.0.0-bab6bed",
6262
"swagger2openapi": "^7.0.8",
6363
"typescript": "~5.9.2",
64-
"yaml": "^2.8.1"
64+
"yaml": "^2.8.1",
65+
"yummies": "5.7.0"
6566
},
6667
"devDependencies": {
6768
"@biomejs/biome": "2.2.4",

src/code-gen-process.ts

Lines changed: 23 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import SwaggerParser, { type resolve } from "@apidevtools/swagger-parser";
1+
import type { resolve } from "@apidevtools/swagger-parser";
22
import { consola } from "consola";
33
import lodash from "lodash";
44
import * as typescript from "typescript";
@@ -11,7 +11,6 @@ import { CodeGenConfig } from "./configuration.js";
1111
import { SchemaComponentsMap } from "./schema-components-map.js";
1212
import { SchemaParserFabric } from "./schema-parser/schema-parser-fabric.js";
1313
import { SchemaRoutes } from "./schema-routes/schema-routes.js";
14-
import { SchemaWalker } from "./schema-walker.js";
1514
import { SwaggerSchemaResolver } from "./swagger-schema-resolver.js";
1615
import { TemplatesWorker } from "./templates-worker.js";
1716
import { JavascriptTranslator } from "./translators/javascript.js";
@@ -45,24 +44,17 @@ export class CodeGenProcess {
4544
fileSystem: FileSystem;
4645
codeFormatter: CodeFormatter;
4746
templatesWorker: TemplatesWorker;
48-
schemaWalker: SchemaWalker;
4947
javascriptTranslator: JavascriptTranslator;
50-
swaggerParser: SwaggerParser;
5148
swaggerRefs: Awaited<ReturnType<typeof resolve>> | undefined | null;
5249

5350
constructor(config: Partial<GenerateApiConfiguration["config"]>) {
5451
this.config = new CodeGenConfig(config);
55-
this.swaggerParser = new SwaggerParser();
5652
this.fileSystem = new FileSystem();
5753
this.swaggerSchemaResolver = new SwaggerSchemaResolver(
5854
this.config,
5955
this.fileSystem,
6056
);
61-
this.schemaWalker = new SchemaWalker(
62-
this.config,
63-
this.swaggerSchemaResolver,
64-
);
65-
this.schemaComponentsMap = new SchemaComponentsMap(this.config, this);
57+
this.schemaComponentsMap = new SchemaComponentsMap(this.config);
6658
this.typeNameFormatter = new TypeNameFormatter(this.config);
6759
this.templatesWorker = new TemplatesWorker(
6860
this.config,
@@ -75,11 +67,9 @@ export class CodeGenProcess {
7567
this.templatesWorker,
7668
this.schemaComponentsMap,
7769
this.typeNameFormatter,
78-
this.schemaWalker,
7970
);
8071
this.schemaRoutes = new SchemaRoutes(
8172
this.config,
82-
this,
8373
this.schemaParserFabric,
8474
this.schemaComponentsMap,
8575
this.templatesWorker,
@@ -99,73 +89,35 @@ export class CodeGenProcess {
9989
templatesToRender: this.templatesWorker.getTemplates(this.config),
10090
});
10191

102-
const swagger = await this.swaggerSchemaResolver.create();
103-
104-
this.swaggerSchemaResolver.fixSwaggerSchema(swagger);
105-
106-
try {
107-
this.swaggerRefs = await this.swaggerParser.resolve(
108-
this.config.url || this.config.input || (this.config.spec as any),
109-
{
110-
continueOnError: true,
111-
mutateInputSchema: true,
112-
validate: {
113-
schema: false,
114-
spec: false,
115-
},
116-
resolve: {
117-
external: true,
118-
http: {
119-
...this.config.requestOptions,
120-
headers: Object.assign(
121-
{},
122-
this.config.authorizationToken
123-
? {
124-
Authorization: this.config.authorizationToken,
125-
}
126-
: {},
127-
this.config.requestOptions?.headers ?? {},
128-
),
129-
},
130-
},
131-
},
132-
);
133-
this.swaggerRefs.set("fixed-swagger-schema", swagger.usageSchema as any);
134-
this.swaggerRefs.set(
135-
"original-swagger-schema",
136-
swagger.originalSchema as any,
137-
);
138-
} catch (e) {
139-
consola.error(e);
140-
}
92+
const resolvedSwaggerSchema = await this.swaggerSchemaResolver.create();
14193

14294
this.config.update({
143-
swaggerSchema: swagger.usageSchema,
144-
originalSchema: swagger.originalSchema,
95+
resolvedSwaggerSchema: resolvedSwaggerSchema,
96+
swaggerSchema: resolvedSwaggerSchema.usageSchema,
97+
originalSchema: resolvedSwaggerSchema.originalSchema,
14598
});
14699

147-
this.schemaWalker.addSchema("$usage", swagger.usageSchema);
148-
this.schemaWalker.addSchema("$original", swagger.originalSchema);
149-
150100
consola.info("start generating your typescript api");
151101

152102
this.config.update(
153-
this.config.hooks.onInit(this.config, this) || this.config,
103+
this.config.hooks.onInit?.(this.config, this) || this.config,
154104
);
155105

156106
this.schemaComponentsMap.clear();
157107

158-
lodash.each(swagger.usageSchema.components, (component, componentName) =>
159-
lodash.each(component, (rawTypeData, typeName) => {
160-
this.schemaComponentsMap.createComponent(
161-
this.schemaComponentsMap.createRef([
162-
"components",
163-
componentName,
164-
typeName,
165-
]),
166-
rawTypeData,
167-
);
168-
}),
108+
lodash.each(
109+
resolvedSwaggerSchema.usageSchema.components,
110+
(component, componentName) =>
111+
lodash.each(component, (rawTypeData, typeName) => {
112+
this.schemaComponentsMap.createComponent(
113+
this.schemaComponentsMap.createRef([
114+
"components",
115+
componentName,
116+
typeName,
117+
]),
118+
rawTypeData,
119+
);
120+
}),
169121
);
170122

171123
// Set all discriminators at the top
@@ -190,13 +142,10 @@ export class CodeGenProcess {
190142
return parsed;
191143
});
192144

193-
this.schemaRoutes.attachSchema({
194-
usageSchema: swagger.usageSchema,
195-
parsedSchemas,
196-
});
145+
this.schemaRoutes.attachSchema(resolvedSwaggerSchema, parsedSchemas);
197146

198147
const rawConfiguration = {
199-
apiConfig: this.createApiConfig(swagger.usageSchema),
148+
apiConfig: this.createApiConfig(resolvedSwaggerSchema.usageSchema),
200149
config: this.config,
201150
modelTypes: this.collectModelTypes(),
202151
hasSecurityRoutes: this.schemaRoutes.hasSecurityRoutes,
@@ -214,7 +163,7 @@ export class CodeGenProcess {
214163
};
215164

216165
const configuration =
217-
this.config.hooks.onPrepareConfig(rawConfiguration) || rawConfiguration;
166+
this.config.hooks.onPrepareConfig?.(rawConfiguration) || rawConfiguration;
218167

219168
if (this.fileSystem.pathIsExist(this.config.output)) {
220169
if (this.config.cleanOutput) {

src/configuration.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import type {
99
} from "../types/index.js";
1010
import { ComponentTypeNameResolver } from "./component-type-name-resolver.js";
1111
import * as CONSTANTS from "./constants.js";
12+
import type { ResolvedSwaggerSchema } from "./resolved-swagger-schema.js";
1213
import type { MonoSchemaParser } from "./schema-parser/mono-schema-parser.js";
1314
import type { SchemaParser } from "./schema-parser/schema-parser.js";
1415
import type { Translator } from "./translators/translator.js";
1516
import { objectAssign } from "./util/object-assign.js";
16-
import SwaggerParser from "@apidevtools/swagger-parser";
1717

1818
const TsKeyword = {
1919
Number: "number",
@@ -111,6 +111,7 @@ export class CodeGenConfig {
111111
) => {},
112112
onFormatRouteName: (_routeInfo: unknown, _templateRouteName: unknown) => {},
113113
};
114+
resolvedSwaggerSchema!: ResolvedSwaggerSchema;
114115
defaultResponseType;
115116
singleHttpClient = false;
116117
httpClientType = CONSTANTS.HTTP_CLIENT.FETCH;
@@ -440,7 +441,13 @@ export class CodeGenConfig {
440441
this.componentTypeNameResolver = new ComponentTypeNameResolver(this, []);
441442
}
442443

443-
update = (update: Partial<GenerateApiConfiguration["config"]>) => {
444+
update = (
445+
update: Partial<
446+
GenerateApiConfiguration["config"] & {
447+
resolvedSwaggerSchema: ResolvedSwaggerSchema;
448+
}
449+
>,
450+
) => {
444451
objectAssign(this, update);
445452
if (this.enumNamesAsValues) {
446453
this.extractEnums = true;

src/resolved-swagger-schema.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { resolve } from "@apidevtools/swagger-parser";
2+
import consola from "consola";
3+
import type { OpenAPI } from "openapi-types";
4+
import type { AnyObject, Maybe, Primitive } from "yummies/utils/types";
5+
import type { CodeGenConfig } from "./configuration.js";
6+
7+
export interface RefDetails {
8+
ref: string;
9+
isLocal: boolean;
10+
externalUrlOrPath: Maybe<string>;
11+
externalOpenapiFileName?: string;
12+
}
13+
14+
export class ResolvedSwaggerSchema {
15+
private parsedRefsCache = new Map<string, RefDetails>();
16+
17+
constructor(
18+
private config: CodeGenConfig,
19+
public usageSchema: OpenAPI.Document,
20+
public originalSchema: OpenAPI.Document,
21+
private resolvers: Awaited<ReturnType<typeof resolve>>[],
22+
) {
23+
this.usageSchema = usageSchema;
24+
this.originalSchema = originalSchema;
25+
}
26+
27+
getRefDetails(ref: string): RefDetails {
28+
if (!this.parsedRefsCache.has(ref)) {
29+
const isLocal = ref.startsWith("#");
30+
31+
if (isLocal) {
32+
this.parsedRefsCache.set(ref, {
33+
ref,
34+
isLocal,
35+
externalUrlOrPath: null,
36+
});
37+
} else {
38+
const externalUrlOrPath = ref.split("#")[0]!
39+
let externalOpenapiFileName = (externalUrlOrPath.split('/').at(-1) || '')
40+
41+
if (externalOpenapiFileName.endsWith('.json') || externalOpenapiFileName.endsWith('.yaml')) {
42+
externalOpenapiFileName = externalOpenapiFileName.slice(0, -5);
43+
} else if (externalOpenapiFileName.endsWith('.yml')) {
44+
externalOpenapiFileName = externalOpenapiFileName.slice(0, -4);
45+
}
46+
47+
48+
this.parsedRefsCache.set(ref, {
49+
ref,
50+
isLocal,
51+
externalUrlOrPath,
52+
externalOpenapiFileName,
53+
});
54+
}
55+
}
56+
57+
return this.parsedRefsCache.get(ref)!;
58+
}
59+
60+
isLocalRef(ref: string): boolean {
61+
return this.getRefDetails(ref).isLocal;
62+
}
63+
64+
getRef(ref: Maybe<string>): Maybe<AnyObject | Primitive> {
65+
if (!ref) {
66+
return null;
67+
}
68+
69+
const resolvedByOrigRef = this.tryToResolveRef(ref);
70+
71+
if (resolvedByOrigRef) {
72+
return resolvedByOrigRef;
73+
}
74+
75+
// const ref.match(/\#[a-z]/)
76+
if (/#[a-z]/.test(ref)) {
77+
const fixedRef = ref.replace(/#[a-z]/, (match) => {
78+
const [hashtag, char] = match.split("");
79+
return `${hashtag}/${char}`;
80+
});
81+
82+
return this.tryToResolveRef(fixedRef);
83+
}
84+
85+
// this.tryToResolveRef(`@usage${ref}`) ??
86+
// this.tryToResolveRef(`@original${ref}`)
87+
}
88+
89+
private tryToResolveRef(ref: Maybe<string>) {
90+
if (!this.resolvers || !ref) {
91+
return null;
92+
}
93+
94+
for (const resolver of this.resolvers) {
95+
try {
96+
const resolvedAsIs = resolver.get(ref);
97+
return resolvedAsIs;
98+
} catch (e) {
99+
consola.debug(e);
100+
}
101+
}
102+
103+
return null;
104+
}
105+
}

0 commit comments

Comments
 (0)