fork of hey-api/openapi-ts because I need some additional things
1import type { AnalysisContext, NodeName } from '@hey-api/codegen-core';
2import type { MaybeArray } from '@hey-api/types';
3
4import { py } from '../../ts-python';
5import type { MaybePyDsl } from '../base';
6import { PyDsl } from '../base';
7import type { DoExpr } from '../mixins/do';
8import { BlockPyDsl } from './block';
9
10const Mixed = PyDsl<py.TryStatement>;
11
12type ExceptType = string | MaybePyDsl<py.Expression>;
13
14interface ExceptEntry {
15 body: Array<DoExpr>;
16 name?: NodeName;
17 types: Array<ExceptType>;
18}
19
20function exceptKey(types: Array<ExceptType>): string {
21 return types
22 .map((t) => (typeof t === 'string' ? t : '<<expr>>'))
23 .sort()
24 .join(',');
25}
26
27export class TryPyDsl extends Mixed {
28 readonly '~dsl' = 'TryPyDsl';
29
30 /**
31 * Ordered list of except clauses. We also keep a lookup map
32 * (`_exceptIndex`) keyed by the normalised type key so that
33 * repeated `.except()` calls with the same type set merge their
34 * body statements instead of creating duplicate clauses.
35 */
36 protected _excepts: Array<ExceptEntry> = [];
37 protected _exceptIndex: Map<string, number> = new Map();
38
39 protected _finally?: Array<DoExpr>;
40 protected _try?: Array<DoExpr>;
41
42 constructor(...tryBlock: Array<DoExpr>) {
43 super();
44 this.try(...tryBlock);
45 }
46
47 override analyze(ctx: AnalysisContext): void {
48 super.analyze(ctx);
49
50 if (this._try) {
51 ctx.pushScope();
52 try {
53 for (const stmt of this._try) ctx.analyze(stmt);
54 } finally {
55 ctx.popScope();
56 }
57 }
58
59 for (const entry of this._excepts) {
60 ctx.pushScope();
61 try {
62 ctx.analyze(entry.name);
63 for (const t of entry.types) ctx.analyze(t);
64 for (const stmt of entry.body) ctx.analyze(stmt);
65 } finally {
66 ctx.popScope();
67 }
68 }
69
70 if (this._finally) {
71 ctx.pushScope();
72 try {
73 for (const stmt of this._finally) ctx.analyze(stmt);
74 } finally {
75 ctx.popScope();
76 }
77 }
78 }
79
80 /** Returns true when all required builder calls are present. */
81 get isValid(): boolean {
82 return this.missingRequiredCalls().length === 0;
83 }
84
85 /**
86 * Add (or merge into) an except clause.
87 *
88 * ```ts
89 * $.try(...)
90 * .except('ValueError', 'e', body1, body2) // except ValueError as e:
91 * .except(['TypeError', 'KeyError'], 'e', ...) // except (TypeError, KeyError) as e:
92 * .except('ValueError', moreBody) // merges into first clause
93 * ```
94 *
95 * @param types Single exception type or array of types.
96 * @param nameOrBody Either the `as` variable name (`NodeName`) or the
97 * first body expression. If it looks like a `NodeName` (string that
98 * is a valid Python identifier and is *not* a DSL node), it is treated
99 * as the name; pass body items after it.
100 * @param body Remaining body statements.
101 */
102 except(
103 types: MaybeArray<ExceptType>,
104 nameOrBody?: NodeName | DoExpr,
105 ...body: Array<DoExpr>
106 ): this {
107 const typeArr = Array.isArray(types) ? types : [types];
108 const key = exceptKey(typeArr);
109
110 let name: NodeName | undefined;
111 let bodyItems: Array<DoExpr>;
112
113 // Disambiguate: if the second arg is a plain string that looks like
114 // an identifier (no dots, no spaces, not a DSL node) treat it as
115 // the `as` name. Otherwise it's the first body expression.
116 if (nameOrBody !== undefined && this._isNodeName(nameOrBody)) {
117 name = nameOrBody as NodeName;
118 bodyItems = body;
119 } else if (nameOrBody !== undefined) {
120 bodyItems = [nameOrBody as DoExpr, ...body];
121 } else {
122 bodyItems = body;
123 }
124
125 const existing = this._exceptIndex.get(key);
126 if (existing !== undefined) {
127 const entry = this._excepts[existing]!;
128 entry.body.push(...bodyItems);
129 if (name !== undefined) entry.name = name;
130 } else {
131 this._exceptIndex.set(key, this._excepts.length);
132 this._excepts.push({ body: bodyItems, name, types: typeArr });
133 }
134
135 return this;
136 }
137
138 /** Add a bare `except:` clause (catches everything). */
139 exceptAll(...body: Array<DoExpr>): this {
140 const key = '';
141 const existing = this._exceptIndex.get(key);
142 if (existing !== undefined) {
143 this._excepts[existing]!.body.push(...body);
144 } else {
145 this._exceptIndex.set(key, this._excepts.length);
146 this._excepts.push({ body, types: [] });
147 }
148 return this;
149 }
150
151 finally(...items: Array<DoExpr>): this {
152 this._finally = items;
153 return this;
154 }
155
156 try(...items: Array<DoExpr>): this {
157 this._try = items;
158 return this;
159 }
160
161 override toAst() {
162 this.$validate();
163
164 const tryStatements = new BlockPyDsl(...this._try!).$do();
165
166 let exceptClauses: Array<py.ExceptClause> | undefined;
167 if (this._excepts.length) {
168 exceptClauses = this._excepts.map((entry) => {
169 const bodyStatements = new BlockPyDsl(...entry.body).$do();
170
171 let exceptionType: py.Expression | undefined;
172 if (entry.types.length === 1) {
173 exceptionType = this.$node(entry.types[0]!);
174 } else if (entry.types.length > 1) {
175 exceptionType = py.factory.createTupleExpression(
176 entry.types.map((t) => this.$node(t) as py.Expression),
177 );
178 }
179
180 const exceptionName = entry.name
181 ? py.factory.createIdentifier(
182 this.$name({ current: entry.name } as any) || String(entry.name),
183 )
184 : undefined;
185
186 return py.factory.createExceptClause([...bodyStatements], exceptionType, exceptionName);
187 });
188 }
189
190 const finallyStatements = this._finally
191 ? [...new BlockPyDsl(...this._finally).$do()]
192 : undefined;
193
194 return py.factory.createTryStatement(
195 [...tryStatements],
196 exceptClauses,
197 undefined,
198 finallyStatements,
199 );
200 }
201
202 $validate(): asserts this is this & {
203 _try: Array<DoExpr>;
204 } {
205 const missing = this.missingRequiredCalls();
206 if (missing.length === 0) return;
207 throw new Error(`Try statement missing ${missing.join(' and ')}`);
208 }
209
210 private missingRequiredCalls(): ReadonlyArray<string> {
211 const missing: Array<string> = [];
212 if (!this._try || this._try.length === 0) missing.push('.try()');
213 return missing;
214 }
215
216 /**
217 * Heuristic: a value is a `NodeName` (intended as the `as` variable)
218 * if it is a plain string matching a Python identifier pattern, or a
219 * Symbol.
220 */
221 private _isNodeName(value: unknown): boolean {
222 if (typeof value === 'string') {
223 return /^[A-Za-z_]\w*$/.test(value);
224 }
225 // Symbols from codegen-core have `~brand`
226 if (value && typeof value === 'object' && '~brand' in value) {
227 return true;
228 }
229 return false;
230 }
231}