Skip to content

Commit 093eadb

Browse files
committed
Continue implementing OpenAPI 3.2.0
1 parent 249186b commit 093eadb

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// src/service/emit/utility/server-url.generator.ts
2+
3+
import * as path from "node:path";
4+
import { Project, VariableDeclarationKind } from "ts-morph";
5+
import { UTILITY_GENERATOR_HEADER_COMMENT } from "../../../core/constants.js";
6+
import { SwaggerParser } from "../../../core/parser.js";
7+
8+
/**
9+
* Generates the `utils/server-url.ts` file.
10+
* This file provides constants and a helper function to access and construct
11+
* server URLs as defined in the OpenAPI spec, including variable substitution logic.
12+
*/
13+
export class ServerUrlGenerator {
14+
constructor(private parser: SwaggerParser, private project: Project) {
15+
}
16+
17+
public generate(outputDir: string): void {
18+
if (this.parser.servers.length === 0) {
19+
// No servers defined in spec, skip generation.
20+
return;
21+
}
22+
23+
const utilsDir = path.join(outputDir, "utils");
24+
const filePath = path.join(utilsDir, "server-url.ts");
25+
26+
const sourceFile = this.project.createSourceFile(filePath, "", { overwrite: true });
27+
28+
sourceFile.insertText(0, UTILITY_GENERATOR_HEADER_COMMENT);
29+
30+
// Define the type for Server Object to keep the generated code clean and typed
31+
sourceFile.addInterface({
32+
name: "ServerConfiguration",
33+
isExported: true,
34+
properties: [
35+
{ name: "url", type: "string" },
36+
{ name: "description", type: "string", hasQuestionToken: true },
37+
{
38+
name: "variables",
39+
hasQuestionToken: true,
40+
type: "Record<string, { enum?: string[]; default: string; description?: string; }>"
41+
}
42+
]
43+
});
44+
45+
// Export the servers array
46+
sourceFile.addVariableStatement({
47+
isExported: true,
48+
declarationKind: VariableDeclarationKind.Const,
49+
declarations: [{
50+
name: "API_SERVERS",
51+
type: "ServerConfiguration[]",
52+
initializer: JSON.stringify(this.parser.servers, null, 2)
53+
}],
54+
docs: ["The list of servers defined in the OpenAPI specification."]
55+
});
56+
57+
// Helper function to build the URL
58+
sourceFile.addFunction({
59+
name: "getServerUrl",
60+
isExported: true,
61+
parameters: [
62+
{ name: "indexOrDescription", type: "number | string", initializer: "0" },
63+
{ name: "variables", type: "Record<string, string>", hasQuestionToken: true }
64+
],
65+
returnType: "string",
66+
docs: [
67+
"Gets the URL for a specific server definition, substituting variables if needed.",
68+
"@param indexOrDescription The index of the server in `API_SERVERS` or its description. Defaults to 0.",
69+
"@param variables A map of variable names to values to override the defaults.",
70+
"@throws Error if the server is not found.",
71+
"@returns The fully constructed URL."
72+
],
73+
statements: writer => {
74+
writer.writeLine("let server: ServerConfiguration | undefined;");
75+
writer.writeLine("if (typeof indexOrDescription === 'number') {").indent(() => {
76+
writer.writeLine("server = API_SERVERS[indexOrDescription];");
77+
}).writeLine("} else {").indent(() => {
78+
writer.writeLine("server = API_SERVERS.find(s => s.description === indexOrDescription);");
79+
}).writeLine("}");
80+
81+
writer.writeLine("if (!server) {").indent(() => {
82+
writer.writeLine("throw new Error(`Server not found: ${indexOrDescription}`);");
83+
}).writeLine("}");
84+
85+
writer.writeLine("let url = server.url;");
86+
writer.writeLine("if (server.variables) {").indent(() => {
87+
writer.writeLine("Object.entries(server.variables).forEach(([key, config]) => {").indent(() => {
88+
writer.writeLine("const value = variables?.[key] ?? config.default;");
89+
writer.writeLine("// Simple substitution (e.g., {port})");
90+
writer.writeLine("url = url.replace(new RegExp(`{${key}}`, 'g'), value);");
91+
}).writeLine("});");
92+
}).writeLine("}");
93+
writer.writeLine("return url;");
94+
}
95+
});
96+
97+
sourceFile.formatText();
98+
}
99+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// tests/40-emit-utility/09-server-url-generator.spec.ts
2+
3+
import { describe, expect, it } from 'vitest';
4+
import { Project } from 'ts-morph';
5+
import { SwaggerParser } from '@src/core/parser.js';
6+
import { ServerUrlGenerator } from '@src/service/emit/utility/server-url.generator.js';
7+
import { createTestProject } from '../shared/helpers.js';
8+
import ts from 'typescript';
9+
10+
describe('Emitter: ServerUrlGenerator', () => {
11+
12+
const runGenerator = (servers: any[]) => {
13+
const project = createTestProject();
14+
const parser = new SwaggerParser({
15+
openapi: '3.0.0',
16+
info: { title: 'Test', version: '1.0' },
17+
paths: {},
18+
servers
19+
} as any, { options: {} } as any);
20+
21+
new ServerUrlGenerator(parser, project).generate('/out');
22+
return project;
23+
};
24+
25+
const compileHelper = (project: Project) => {
26+
const sourceFile = project.getSourceFileOrThrow('/out/utils/server-url.ts');
27+
const startText = sourceFile.getText();
28+
// Removing export keywords to evaluate in this context as strict CommonJS/Script
29+
const jsCode = ts.transpile(startText.replace(/export /g, ''), {
30+
target: ts.ScriptTarget.ESNext,
31+
module: ts.ModuleKind.CommonJS
32+
});
33+
const moduleScope = { API_SERVERS: [], getServerUrl: null as any };
34+
// Evaluate in simple scope
35+
new Function('scope', `
36+
${jsCode}
37+
scope.API_SERVERS = API_SERVERS;
38+
scope.getServerUrl = getServerUrl;
39+
`)(moduleScope);
40+
return moduleScope;
41+
};
42+
43+
it('should not generate file if no servers are defined', () => {
44+
const project = runGenerator([]);
45+
expect(project.getSourceFile('/out/utils/server-url.ts')).toBeUndefined();
46+
});
47+
48+
it('should generate API_SERVERS constant', () => {
49+
const project = runGenerator([
50+
{ url: 'https://api.example.com', description: 'Production' }
51+
]);
52+
const text = project.getSourceFileOrThrow('/out/utils/server-url.ts').getText();
53+
expect(text).toContain('export const API_SERVERS: ServerConfiguration[] = [');
54+
expect(text).toContain('"url": "https://api.example.com"');
55+
});
56+
57+
it('should generate logic to substitute simple variables', () => {
58+
const project = runGenerator([
59+
{
60+
url: 'https://{env}.example.com/v1',
61+
variables: { env: { default: 'dev' } }
62+
}
63+
]);
64+
65+
const { getServerUrl } = compileHelper(project);
66+
67+
// Use default
68+
expect(getServerUrl(0)).toBe('https://dev.example.com/v1');
69+
// Override
70+
expect(getServerUrl(0, { env: 'prod' })).toBe('https://prod.example.com/v1');
71+
});
72+
73+
it('should generate logic to look up server by description', () => {
74+
const project = runGenerator([
75+
{ url: 'https://dev.api.com', description: 'Development' },
76+
{ url: 'https://prod.api.com', description: 'Production' }
77+
]);
78+
79+
const { getServerUrl } = compileHelper(project);
80+
81+
expect(getServerUrl('Production')).toBe('https://prod.api.com');
82+
expect(getServerUrl('Development')).toBe('https://dev.api.com');
83+
});
84+
85+
it('should throw error if server is not found', () => {
86+
const project = runGenerator([{ url: '/' }]);
87+
const { getServerUrl } = compileHelper(project);
88+
expect(() => getServerUrl(99)).toThrow('Server not found: 99');
89+
expect(() => getServerUrl('Unknown')).toThrow('Server not found: Unknown');
90+
});
91+
92+
it('should handle multiple variables', () => {
93+
const project = runGenerator([
94+
{
95+
url: '{protocol}://{host}:{port}/{base}',
96+
variables: {
97+
protocol: { default: 'https' },
98+
host: { default: 'localhost' },
99+
port: { default: '8080' },
100+
base: { default: 'api' }
101+
}
102+
}
103+
]);
104+
const { getServerUrl } = compileHelper(project);
105+
106+
// Defaults
107+
expect(getServerUrl(0)).toBe('https://localhost:8080/api');
108+
// Partial override
109+
expect(getServerUrl(0, { port: '3000', protocol: 'http' })).toBe('http://localhost:3000/api');
110+
});
111+
});

0 commit comments

Comments
 (0)