@@ -16,7 +16,7 @@ export interface EnumT {
16
16
options : string [ ]
17
17
}
18
18
19
- export type TypeNode = Base | MappingType | ArrayT | Obj | EnumT | UnknownT ;
19
+ export type TypeNode = Base | MappingType | ArrayT | Obj | EnumT | UnionT | UnknownT ;
20
20
21
21
export interface MappingType {
22
22
node : "MappingType" ;
@@ -39,6 +39,11 @@ export interface Obj {
39
39
fields : ReadonlyArray < Field > ;
40
40
}
41
41
42
+ export interface UnionT {
43
+ node : "Union" ;
44
+ members : ReadonlyArray < TypeNode > ;
45
+ }
46
+
42
47
/**
43
48
* Tokenize
44
49
*/
@@ -74,9 +79,6 @@ export function tokenize(src: string): Tok[] {
74
79
while ( i < s . length ) {
75
80
const c = s [ i ] ! ;
76
81
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
- }
80
82
if ( c === "{" ) { out . push ( { kind : "LBRACE" , value : c , pos : i } ) ; i ++ ; continue ; }
81
83
if ( c === "}" ) { out . push ( { kind : "RBRACE" , value : c , pos : i } ) ; i ++ ; continue ; }
82
84
if ( c === ":" ) { out . push ( { kind : "COLON" , value : c , pos : i } ) ; i ++ ; continue ; }
@@ -184,11 +186,35 @@ class Parser {
184
186
const name_tok = this . want ( "IDENT" ) ;
185
187
const optional = ! ! this . accept ( "QUESTION_MARK" ) ;
186
188
this . want ( "COLON" ) ;
187
- const typ = this . parse_primary ( ) ;
189
+ const typ = this . parse_type ( ) ;
188
190
return { name : name_tok . value , typ, optional } ;
189
191
}
190
192
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 {
192
218
const t = this . peek ( ) ;
193
219
if ( ! t ) throw new SyntaxError ( "Unexpected EOF while parsing Type" ) ;
194
220
@@ -218,7 +244,7 @@ class Parser {
218
244
if ( t . kind === "IDENT" && t . value === "Array" ) {
219
245
this . want ( "IDENT" )
220
246
this . want ( "LESS_THAN" )
221
- const val = this . parse_primary ( )
247
+ const val = this . parse_atomic ( )
222
248
this . want ( "GREATER_THAN" )
223
249
return { node : "Array" , item : val }
224
250
}
@@ -230,7 +256,7 @@ class Parser {
230
256
if ( k . value !== "string" )
231
257
throw new SyntaxError ( `Only Record<string, ...> supported at pos ${ k . pos } ` ) ;
232
258
this . want ( "COMMA" ) ;
233
- const val = this . parse_primary ( ) ;
259
+ const val = this . parse_atomic ( ) ;
234
260
this . want ( "GREATER_THAN" ) ;
235
261
return { node : "MappingType" , value : val } ;
236
262
}
@@ -279,6 +305,7 @@ export type Sig =
279
305
| [ "obj" , ReadonlyArray < [ string , boolean , Sig ] > ]
280
306
| [ "mapping" , Sig ]
281
307
| [ "enum" , string [ ] ]
308
+ | [ "union" , Sig [ ] ]
282
309
| [ "unknown" ] ;
283
310
284
311
export function type_signature ( t : TypeNode ) : Sig {
@@ -305,6 +332,13 @@ export function type_signature(t: TypeNode): Sig {
305
332
const opts = [ ...t . options ] . sort ( ) ;
306
333
return [ "enum" , opts ] ;
307
334
}
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
+
308
342
throw new TypeError ( `Unknown TypeNode: ${ ( t as any ) && ( t as any ) . node } ` ) ;
309
343
}
310
344
@@ -373,6 +407,11 @@ export function collect_objects(root: Obj, root_hint = "Root"): TDClass[] {
373
407
374
408
visit ( t . value , path ) ;
375
409
410
+ } else if ( t . node === "Union" ) {
411
+
412
+ for ( const m of t . members ) {
413
+ visit ( m , path ) ;
414
+ }
376
415
}
377
416
} ;
378
417
@@ -415,6 +454,11 @@ export function py_type(t: TypeNode, name_of: Map<string, string>): string {
415
454
return `Literal[${ opts } ]` ;
416
455
}
417
456
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
+
418
462
if ( t . node === "Unknown" ) {
419
463
return "Any" ;
420
464
}
0 commit comments