@@ -28,6 +28,8 @@ interface ResolveTypeOptions {
2828 quoteForwardReferences ?: boolean
2929}
3030
31+ const STRING_RECORD_KEY_TYPES = new Set ( [ 'str' , 'text' , 'tstr' ] )
32+
3133export function transform ( assignments : Assignment [ ] , options ?: TransformOptions ) : string {
3234 const ctx : Context = {
3335 pydantic : options ?. pydantic ?? false ,
@@ -135,6 +137,7 @@ function generateGroup (group: Group, ctx: Context): string {
135137 }
136138
137139 const props = properties as Property [ ]
140+ const extraItemsType = getExtraItemsType ( props , ctx )
138141
139142 if ( props . length === 1 ) {
140143 const prop = props [ 0 ]
@@ -150,7 +153,7 @@ function generateGroup (group: Group, ctx: Context): string {
150153 }
151154
152155 const mixins = props . filter ( isUnNamedProperty )
153- const ownProps = props . filter ( p => ! isUnNamedProperty ( p ) )
156+ const ownProps = props . filter ( p => ! isUnNamedProperty ( p ) && ! isExtensibleRecordProperty ( p ) )
154157
155158 const simpleMixinBases : string [ ] = [ ]
156159 const unionMixinGroups : string [ ] [ ] = [ ]
@@ -186,10 +189,10 @@ function generateGroup (group: Group, ctx: Context): string {
186189 }
187190
188191 if ( unionMixinGroups . length > 0 ) {
189- return comments + generateGroupWithUnionMixins ( name , simpleMixinBases , unionMixinGroups , ownProps , ctx )
192+ return comments + generateGroupWithUnionMixins ( name , simpleMixinBases , unionMixinGroups , ownProps , extraItemsType , ctx )
190193 }
191194
192- return comments + generateClass ( name , simpleMixinBases , ownProps , ctx )
195+ return comments + generateClass ( name , simpleMixinBases , ownProps , ctx , extraItemsType )
193196}
194197
195198function generateGroupWithChoices ( name : string , properties : ( Property | Property [ ] ) [ ] , ctx : Context ) : string {
@@ -259,6 +262,7 @@ function generateGroupWithUnionMixins (
259262 simpleBases : string [ ] ,
260263 unionGroups : string [ ] [ ] ,
261264 ownProps : Property [ ] ,
265+ extraItemsType : string | undefined ,
262266 ctx : Context
263267) : string {
264268 if ( ownProps . length === 0 && simpleBases . length === 0 ) {
@@ -278,7 +282,7 @@ function generateGroupWithUnionMixins (
278282
279283 if ( ownProps . length > 0 ) {
280284 const baseName = `_${ name } Fields`
281- blocks . push ( generateClass ( baseName , [ ] , ownProps , ctx ) )
285+ blocks . push ( generateClass ( baseName , [ ] , ownProps , ctx , extraItemsType ) )
282286
283287 for ( let i = 0 ; i < unionTypes . length ; i ++ ) {
284288 const variantName = `_${ name } Variant${ i } `
@@ -291,7 +295,7 @@ function generateGroupWithUnionMixins (
291295 const variantName = `_${ name } Variant${ i } `
292296 variantNames . push ( variantName )
293297 const bases = [ unionTypes [ i ] , ...simpleBases ]
294- blocks . push ( generateClass ( variantName , bases , [ ] , ctx ) )
298+ blocks . push ( generateClass ( variantName , bases , [ ] , ctx , extraItemsType ) )
295299 }
296300 }
297301 } else {
@@ -366,7 +370,13 @@ function generateArrayAssignment (arr: CDDLArray, ctx: Context): string {
366370// Class generation (TypedDict or Pydantic BaseModel)
367371// ---------------------------------------------------------------------------
368372
369- function generateClass ( name : string , bases : string [ ] , props : Property [ ] , ctx : Context ) : string {
373+ function generateClass (
374+ name : string ,
375+ bases : string [ ] ,
376+ props : Property [ ] ,
377+ ctx : Context ,
378+ extraItemsType ?: string
379+ ) : string {
370380 const lines : string [ ] = [ ]
371381
372382 let classDecl : string
@@ -381,17 +391,25 @@ function generateClass (name: string, bases: string[], props: Property[], ctx: C
381391 } else {
382392 ctx . typingExtensionsImports . add ( 'TypedDict' )
383393 const typedDictBases = bases . filter ( ( base ) => isModelCompatibleBase ( base , ctx ) )
384- if ( typedDictBases . length > 0 ) {
385- classDecl = `class ${ name } (${ typedDictBases . join ( ', ' ) } ):`
386- } else {
387- classDecl = `class ${ name } (TypedDict):`
388- }
394+ const baseList = typedDictBases . length > 0 ? typedDictBases . join ( ', ' ) : 'TypedDict'
395+ classDecl = extraItemsType
396+ ? `class ${ name } (${ baseList } , extra_items=${ extraItemsType } ):`
397+ : `class ${ name } (${ baseList } ):`
389398 }
390399
391400 lines . push ( classDecl )
392401
402+ if ( ctx . pydantic && extraItemsType ) {
403+ ctx . pydanticImports . add ( 'ConfigDict' )
404+ ctx . pydanticImports . add ( 'Field' )
405+ lines . push ( ` __pydantic_extra__: dict[str, ${ extraItemsType } ] = Field(init=False)` )
406+ lines . push ( ` model_config = ConfigDict(extra='allow')` )
407+ }
408+
393409 if ( props . length === 0 ) {
394- lines . push ( ' pass' )
410+ if ( lines . length === 1 ) {
411+ lines . push ( ' pass' )
412+ }
395413 return lines . join ( '\n' )
396414 }
397415
@@ -470,6 +488,34 @@ function generateField (prop: Property, ctx: Context): string | null {
470488 return ` ${ propName } : ${ typeStr } ${ commentSuffix } `
471489}
472490
491+ function isExtensibleRecordProperty ( prop : Property ) : boolean {
492+ return ! isUnNamedProperty ( prop ) &&
493+ prop . Occurrence . m === Infinity &&
494+ ! prop . HasCut &&
495+ STRING_RECORD_KEY_TYPES . has ( prop . Name )
496+ }
497+
498+ function getExtraItemsType ( props : Property [ ] , ctx : Context ) : string | undefined {
499+ const types = props
500+ . filter ( isExtensibleRecordProperty )
501+ . flatMap ( ( prop ) => {
502+ const cddlTypes = Array . isArray ( prop . Type ) ? prop . Type : [ prop . Type ]
503+ return cddlTypes . map ( ( type ) => resolveType ( type , ctx ) )
504+ } )
505+
506+ if ( types . length === 0 ) {
507+ return
508+ }
509+
510+ const uniqueTypes = [ ...new Set ( types ) ]
511+ if ( uniqueTypes . length === 1 ) {
512+ return uniqueTypes [ 0 ]
513+ }
514+
515+ ctx . typingImports . add ( 'Union' )
516+ return `Union[${ uniqueTypes . join ( ', ' ) } ]`
517+ }
518+
473519// ---------------------------------------------------------------------------
474520// Type resolution
475521// ---------------------------------------------------------------------------
0 commit comments