fork of hey-api/openapi-ts because I need some additional things
1import type { RenderContext, Renderer } from '@hey-api/codegen-core';
2import type { MaybeArray, MaybeFunc } from '@hey-api/types';
3
4import type { PyDsl } from '../../py-dsl';
5import { py } from '../../ts-python';
6import type { ModuleExport, ModuleImport, SortGroup, SortKey, SortModule } from './render-utils';
7import { astToString, moduleSortKey } from './render-utils';
8
9type Exports = ReadonlyArray<ReadonlyArray<ModuleExport>>;
10type ExportsOptions = {
11 preferExportAll?: boolean;
12};
13type Header = MaybeArray<string> | null | undefined;
14type Imports = ReadonlyArray<ReadonlyArray<ModuleImport>>;
15
16function headerToLines(header: Header): ReadonlyArray<string> {
17 if (!header) return [];
18 const lines: Array<string> = [];
19 if (typeof header === 'string') {
20 lines.push(...header.split(/\r?\n/));
21 return lines;
22 }
23 for (const line of header) {
24 lines.push(...line.split(/\r?\n/));
25 }
26 return lines;
27}
28
29export class PythonRenderer implements Renderer {
30 /**
31 * Function to generate a file header.
32 *
33 * @private
34 */
35 private _header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>;
36 /**
37 * Whether `export * from 'module'` should be used when possible instead of named exports.
38 *
39 * @private
40 */
41 private _preferExportAll: boolean;
42 /**
43 * Controls whether imports/exports include a file extension (e.g., '.ts' or '.js').
44 *
45 * @private
46 */
47 private _preferFileExtension: string;
48 /**
49 * Optional function to transform module specifiers.
50 *
51 * @private
52 */
53 private _resolveModuleName?: (moduleName: string) => string | undefined;
54
55 constructor(
56 args: {
57 header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>;
58 preferExportAll?: boolean;
59 preferFileExtension?: string;
60 resolveModuleName?: (moduleName: string) => string | undefined;
61 } = {},
62 ) {
63 this._header = args.header;
64 this._preferExportAll = args.preferExportAll ?? false;
65 this._preferFileExtension = args.preferFileExtension ?? '';
66 this._resolveModuleName = args.resolveModuleName;
67 }
68
69 render(ctx: RenderContext<PyDsl>): string {
70 const header = typeof this._header === 'function' ? this._header(ctx) : this._header;
71 return PythonRenderer.astToString({
72 // exports: this.getExports(ctx),
73 exportsOptions: {
74 preferExportAll: this._preferExportAll,
75 },
76 header,
77 imports: this.getImports(ctx),
78 nodes: ctx.file.nodes,
79 });
80 }
81
82 supports(ctx: RenderContext): boolean {
83 return ctx.file.language === 'python';
84 }
85
86 static astToString(args: {
87 exports?: Exports;
88 exportsOptions?: ExportsOptions;
89 header?: Header;
90 imports?: Imports;
91 nodes?: ReadonlyArray<PyDsl>;
92 /**
93 * Whether to include a trailing newline at the end of the file.
94 *
95 * @default true
96 */
97 trailingNewline?: boolean;
98 }): string {
99 let text = '';
100 for (const header of headerToLines(args.header)) {
101 text += `${header}\n`;
102 }
103
104 let imports = '';
105 for (const group of args.imports ?? []) {
106 if (imports) imports += '\n';
107 for (const imp of group) {
108 imports += `${astToString(PythonRenderer.toImportAst(imp))}\n`;
109 }
110 }
111 text = `${text}${text && imports ? '\n' : ''}${imports}`;
112
113 let nodes = '';
114 for (const node of args.nodes ?? []) {
115 if (nodes) nodes += '\n';
116 nodes += `${astToString(node.toAst())}\n`;
117 }
118 text = `${text}${text && nodes ? '\n' : ''}${nodes}`;
119
120 const exports = '';
121 // let exports = '';
122 // for (const group of args.exports ?? []) {
123 // if ((!exports && nodes) || exports) exports += '\n';
124 // for (const exp of group) {
125 // exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}\n`;
126 // }
127 // }
128 text = `${text}${text && exports ? '\n' : ''}${exports}`;
129
130 if (args.trailingNewline === false && text.endsWith('\n')) {
131 text = text.slice(0, -1);
132 }
133
134 return text;
135 }
136
137 // static toExportAst(group: ModuleExport, options?: ExportsOptions): ts.ExportDeclaration {
138 // const specifiers = group.exports.map((exp) => {
139 // const specifier = ts.factory.createExportSpecifier(
140 // exp.isTypeOnly,
141 // exp.sourceName !== exp.exportedName ? $.id(exp.sourceName).toAst() : undefined,
142 // $.id(exp.exportedName).toAst(),
143 // );
144 // return specifier;
145 // });
146 // const exportClause = group.namespaceExport
147 // ? ts.factory.createNamespaceExport($.id(group.namespaceExport).toAst())
148 // : (!group.canExportAll || !options?.preferExportAll) && specifiers.length
149 // ? ts.factory.createNamedExports(specifiers)
150 // : undefined;
151 // return ts.factory.createExportDeclaration(
152 // undefined,
153 // group.isTypeOnly,
154 // exportClause,
155 // $.literal(group.modulePath).toAst(),
156 // );
157 // }
158
159 static toImportAst(group: ModuleImport): py.ImportStatement {
160 const names: Array<{
161 alias?: string;
162 name: string;
163 }> = group.imports.map((imp) => ({
164 alias: imp.localName !== imp.sourceName ? imp.localName : undefined,
165 name: imp.sourceName,
166 }));
167 return py.factory.createImportStatement(group.modulePath, names, group.imports.length > 0);
168 }
169
170 // private getExports(ctx: RenderContext): Exports {
171 // type ModuleEntry = {
172 // group: ModuleExport;
173 // sortKey: SortKey;
174 // };
175
176 // const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
177
178 // for (const exp of ctx.file.exports) {
179 // const sortKey = moduleSortKey({
180 // file: ctx.file,
181 // fromFile: exp.from,
182 // preferFileExtension: this._preferFileExtension,
183 // root: ctx.project.root,
184 // });
185 // const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2];
186 // const [groupIndex] = sortKey;
187
188 // if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
189 // const moduleMap = groups.get(groupIndex)!;
190
191 // if (!moduleMap.has(modulePath)) {
192 // moduleMap.set(modulePath, {
193 // group: {
194 // canExportAll: exp.canExportAll,
195 // exports: exp.exports,
196 // isTypeOnly: exp.isTypeOnly,
197 // modulePath,
198 // namespaceExport: exp.namespaceExport,
199 // },
200 // sortKey,
201 // });
202 // }
203 // }
204
205 // const exports: Array<Array<ModuleExport>> = Array.from(groups.entries())
206 // .sort((a, b) => a[0] - b[0])
207 // .map(([, moduleMap]) => {
208 // const entries = Array.from(moduleMap.values());
209
210 // entries.sort((a, b) => {
211 // const d = a.sortKey[1] - b.sortKey[1];
212 // return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
213 // });
214
215 // return entries.map((e) => {
216 // const group = e.group;
217 // if (group.namespaceExport) {
218 // group.exports = [];
219 // } else {
220 // const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly);
221 // if (isTypeOnly) {
222 // group.isTypeOnly = true;
223 // for (const exp of group.exports) {
224 // exp.isTypeOnly = false;
225 // }
226 // }
227 // group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName));
228 // }
229 // return group;
230 // });
231 // });
232
233 // return exports;
234 // }
235
236 private getImports(ctx: RenderContext): Imports {
237 type ModuleEntry = {
238 group: ModuleImport;
239 sortKey: SortKey;
240 };
241
242 const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
243
244 for (const imp of ctx.file.imports) {
245 const sortKey = moduleSortKey({
246 file: ctx.file,
247 fromFile: imp.from,
248 preferFileExtension: this._preferFileExtension,
249 root: ctx.project.root,
250 });
251 const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2];
252 const [groupIndex] = sortKey;
253
254 if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
255 const moduleMap = groups.get(groupIndex)!;
256
257 if (!moduleMap.has(modulePath)) {
258 moduleMap.set(modulePath, {
259 group: {
260 imports: [],
261 isTypeOnly: false,
262 kind: imp.kind,
263 modulePath,
264 },
265 sortKey,
266 });
267 }
268
269 const entry = moduleMap.get(modulePath)!;
270 const group = entry.group;
271
272 if (imp.kind !== 'named') {
273 group.isTypeOnly = imp.isTypeOnly;
274 group.kind = imp.kind;
275 group.localName = imp.localName;
276 } else {
277 group.imports.push(...imp.imports);
278 }
279 }
280
281 const imports: Array<Array<ModuleImport>> = Array.from(groups.entries())
282 .sort((a, b) => a[0] - b[0])
283 .map(([, moduleMap]) => {
284 const entries = Array.from(moduleMap.values());
285
286 entries.sort((a, b) => {
287 const d = a.sortKey[1] - b.sortKey[1];
288 return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
289 });
290
291 return entries.map((e) => {
292 const group = e.group;
293 if (group.kind === 'namespace') {
294 group.imports = [];
295 } else {
296 const isTypeOnly = !group.imports.find((imp) => !imp.isTypeOnly);
297 if (isTypeOnly) {
298 group.isTypeOnly = true;
299 for (const imp of group.imports) {
300 imp.isTypeOnly = false;
301 }
302 }
303 group.imports.sort((a, b) => a.localName.localeCompare(b.localName));
304 }
305 return group;
306 });
307 });
308
309 return imports;
310 }
311}