22// Each blueprint resource, along with events, action attempts, and pagination,
33// becomes a dataclass in its own module, re-exported from seam/resources/__init__.py.
44
5- import type { Blueprint , Property , Resource } from '@seamapi/blueprint'
5+ import type { Blueprint , Property } from '@seamapi/blueprint'
66import { pascalCase , snakeCase } from 'change-case'
77
88import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
@@ -14,14 +14,25 @@ export interface ResourceLayoutContext {
1414 description : string
1515 isDeprecated : boolean
1616 deprecationMessage : string
17- properties : Array < {
18- name : string
19- description : string
20- isDeprecated : boolean
21- deprecationMessage : string
22- type : string
23- isDictParam : boolean
24- } >
17+ nestedClasses : ResourceClassLayoutContext [ ]
18+ properties : ResourcePropertyLayoutContext [ ]
19+ }
20+
21+ interface ResourceClassLayoutContext {
22+ className : string
23+ description : string
24+ properties : ResourcePropertyLayoutContext [ ]
25+ }
26+
27+ interface ResourcePropertyLayoutContext {
28+ name : string
29+ description : string
30+ isDeprecated : boolean
31+ deprecationMessage : string
32+ type : string
33+ isDictParam : boolean
34+ isObject : boolean
35+ isObjectList : boolean
2536}
2637
2738export interface ResourcesIndexLayoutContext {
@@ -31,7 +42,9 @@ export interface ResourcesIndexLayoutContext {
3142// The action attempt and event variants each generate a single dataclass with
3243// the union of the variant properties. The first occurrence of a property
3344// name wins.
34- const mergeResourceProperties = ( resources : Resource [ ] ) : Property [ ] => {
45+ const mergeResourceProperties = (
46+ resources : Array < { properties : Property [ ] } > ,
47+ ) : Property [ ] => {
3548 const merged = new Map < string , Property > ( )
3649 for ( const { properties } of resources ) {
3750 for ( const property of properties ) {
@@ -91,6 +104,67 @@ export const getResourceLayoutContexts = (
91104 const { properties, description, isDeprecated, deprecationMessage } =
92105 model
93106 const className = pascalCase ( convertCustomResourceName ( name ) )
107+ const nestedClasses = new Map < string , ResourceClassLayoutContext > ( )
108+
109+ const buildProperties = (
110+ sourceProperties : Property [ ] ,
111+ ) : ResourcePropertyLayoutContext [ ] =>
112+ sourceProperties . map ( ( property ) => {
113+ let nestedClassName : string | undefined
114+ let nestedProperties : Property [ ] | undefined
115+ if ( property . format === 'object' ) {
116+ nestedClassName = `${ className } ${ pascalCase ( property . name ) } `
117+ nestedProperties = property . properties
118+ } else if (
119+ property . format === 'list' &&
120+ property . itemFormat === 'object'
121+ ) {
122+ nestedClassName = `${ className } ${ pascalCase ( property . name ) } `
123+ nestedProperties = property . itemProperties
124+ } else if (
125+ property . format === 'list' &&
126+ property . itemFormat === 'discriminated_object'
127+ ) {
128+ nestedClassName = `${ className } ${ pascalCase ( property . name ) } `
129+ nestedProperties = mergeResourceProperties ( property . variants )
130+ }
131+
132+ if (
133+ nestedClassName != null &&
134+ nestedProperties != null &&
135+ ! nestedClasses . has ( nestedClassName )
136+ ) {
137+ // Reserve the name before recursing so colliding/recursive shapes
138+ // cannot register it twice. Reinsert after children for definition
139+ // order: annotations are evaluated when each class is created.
140+ nestedClasses . set ( nestedClassName , {
141+ className : nestedClassName ,
142+ description : property . description ,
143+ properties : [ ] ,
144+ } )
145+ const childProperties = buildProperties ( nestedProperties )
146+ nestedClasses . delete ( nestedClassName )
147+ nestedClasses . set ( nestedClassName , {
148+ className : nestedClassName ,
149+ description : property . description ,
150+ properties : childProperties ,
151+ } )
152+ }
153+
154+ const type = mapPropertyToPythonType ( property , nestedClassName )
155+ return {
156+ name : property . name ,
157+ description : property . description ,
158+ isDeprecated : property . isDeprecated ,
159+ deprecationMessage : property . deprecationMessage ,
160+ type,
161+ isDictParam : type . startsWith ( 'Dict' ) ,
162+ isObject : nestedClassName != null && property . format === 'object' ,
163+ isObjectList : nestedClassName != null && property . format === 'list' ,
164+ }
165+ } )
166+
167+ const resourceProperties = buildProperties ( properties )
94168 return {
95169 className,
96170 description,
@@ -100,18 +174,8 @@ export const getResourceLayoutContexts = (
100174 // module always matches the dataclass it exports (e.g. the "event"
101175 // resource becomes SeamEvent in seam_event.py).
102176 moduleName : snakeCase ( className ) ,
103- properties : properties . map ( ( property ) => {
104- const type = mapPropertyToPythonType ( property )
105- return {
106- name : property . name ,
107- description : property . description ,
108- isDeprecated : property . isDeprecated ,
109- deprecationMessage : property . deprecationMessage ,
110- type,
111- isDictParam :
112- type . startsWith ( 'Dict' ) || property . name === 'properties' ,
113- }
114- } ) ,
177+ nestedClasses : [ ...nestedClasses . values ( ) ] ,
178+ properties : resourceProperties ,
115179 }
116180 } )
117181 . sort ( ( a , b ) => ( a . moduleName < b . moduleName ? - 1 : 1 ) )
0 commit comments