Skip to content

Commit a29b6df

Browse files
committed
feat: add support for Unions
1 parent 0bf9344 commit a29b6df

File tree

2 files changed

+53
-8
lines changed

2 files changed

+53
-8
lines changed

packages/core/src/types/generate-python-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const generateApiRequest = (
4444
generated +=
4545
`${handlerName}_ApiRequest_type: TypeAlias = ApiRequest[${api_req_root_name}]` + "\n\n";
4646
} catch (error) {
47+
console.log(`[ERROR]: ${error}`)
4748
generated +=
4849
`${handlerName}_ApiRequest_type: TypeAlias = ApiRequest[Dict[str, Any]]` + "\n\n";
4950
}

packages/core/src/types/schema-to-typedDict.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface EnumT {
1616
options: string[]
1717
}
1818

19-
export type TypeNode = Base | MappingType | ArrayT | Obj | EnumT | UnknownT;
19+
export type TypeNode = Base | MappingType | ArrayT | Obj | EnumT | UnionT | UnknownT;
2020

2121
export interface MappingType {
2222
node: "MappingType";
@@ -39,6 +39,11 @@ export interface Obj {
3939
fields: ReadonlyArray<Field>;
4040
}
4141

42+
export interface UnionT {
43+
node: "Union";
44+
members: ReadonlyArray<TypeNode>;
45+
}
46+
4247
/**
4348
* Tokenize
4449
*/
@@ -74,9 +79,6 @@ export function tokenize(src: string): Tok[] {
7479
while (i < s.length) {
7580
const c = s[i]!;
7681
if (/\s/.test(c)) { i++; continue; }
77-
if (c === "[" && i + 1 < s.length && s[i + 1] === "]") {
78-
out.push({ kind: "SQUARE_BRACKETS", value: "[]", pos: i }); i += 2; continue;
79-
}
8082
if (c === "{") { out.push({ kind: "LBRACE", value: c, pos: i }); i++; continue; }
8183
if (c === "}") { out.push({ kind: "RBRACE", value: c, pos: i }); i++; continue; }
8284
if (c === ":") { out.push({ kind: "COLON", value: c, pos: i }); i++; continue; }
@@ -184,11 +186,35 @@ class Parser {
184186
const name_tok = this.want("IDENT");
185187
const optional = !!this.accept("QUESTION_MARK");
186188
this.want("COLON");
187-
const typ = this.parse_primary();
189+
const typ = this.parse_type();
188190
return { name: name_tok.value, typ, optional };
189191
}
190192

191-
private parse_primary(): TypeNode {
193+
private parse_type(): TypeNode {
194+
const members: TypeNode[] = [this.parse_atomic()];
195+
196+
while (this.accept("PIPE")) {
197+
members.push(this.parse_atomic());
198+
}
199+
200+
if (members.length === 1) return members[0];
201+
202+
// Collapse nested unions
203+
const flat: TypeNode[] = [];
204+
for (const m of members) {
205+
if (m.node === "Union") flat.push(...m.members);
206+
else flat.push(m);
207+
}
208+
209+
// If all are string enums → make single Enum
210+
if (flat.every(m => m.node === "Enum" && m.options.length === 1)) {
211+
return { node: "Enum", options: flat.map(e => (e as EnumT).options[0]) };
212+
}
213+
214+
return { node: "Union", members: flat };
215+
}
216+
217+
private parse_atomic(): TypeNode {
192218
const t = this.peek();
193219
if (!t) throw new SyntaxError("Unexpected EOF while parsing Type");
194220

@@ -218,7 +244,7 @@ class Parser {
218244
if(t.kind === "IDENT" && t.value === "Array"){
219245
this.want("IDENT")
220246
this.want("LESS_THAN")
221-
const val = this.parse_primary()
247+
const val = this.parse_atomic()
222248
this.want("GREATER_THAN")
223249
return {node: "Array", item: val}
224250
}
@@ -230,7 +256,7 @@ class Parser {
230256
if (k.value !== "string")
231257
throw new SyntaxError(`Only Record<string, ...> supported at pos ${k.pos}`);
232258
this.want("COMMA");
233-
const val = this.parse_primary();
259+
const val = this.parse_atomic();
234260
this.want("GREATER_THAN");
235261
return { node: "MappingType", value: val };
236262
}
@@ -279,6 +305,7 @@ export type Sig =
279305
| ["obj", ReadonlyArray<[string, boolean, Sig]>]
280306
| ["mapping", Sig]
281307
| ["enum", string[]]
308+
| ["union", Sig[]]
282309
| ["unknown"];
283310

284311
export function type_signature(t: TypeNode): Sig {
@@ -305,6 +332,13 @@ export function type_signature(t: TypeNode): Sig {
305332
const opts = [...t.options].sort();
306333
return ["enum", opts];
307334
}
335+
336+
if (t.node === "Union") {
337+
const parts = t.members.map(type_signature);
338+
const sorted = parts.map(p => JSON.stringify(p)).sort().map(s => JSON.parse(s));
339+
return ["union", sorted];
340+
}
341+
308342
throw new TypeError(`Unknown TypeNode: ${(t as any) && (t as any).node}`);
309343
}
310344

@@ -373,6 +407,11 @@ export function collect_objects(root: Obj, root_hint = "Root"): TDClass[] {
373407

374408
visit(t.value, path);
375409

410+
} else if (t.node === "Union") {
411+
412+
for (const m of t.members){
413+
visit(m, path);
414+
}
376415
}
377416
};
378417

@@ -415,6 +454,11 @@ export function py_type(t: TypeNode, name_of: Map<string, string>): string {
415454
return `Literal[${opts}]`;
416455
}
417456

457+
if (t.node === "Union") {
458+
const parts = t.members.map(m => py_type(m, name_of));
459+
return parts.join(" | "); // PEP 604 syntax
460+
}
461+
418462
if (t.node === "Unknown") {
419463
return "Any";
420464
}

0 commit comments

Comments
 (0)