11import { FormProperty , Resource , SwaggerDefinition } from "@src/core/types/index.js" ;
22import { SwaggerParser } from '@src/core/parser.js' ;
3- import { getTypeScriptType , pascalCase , singular } from "@src/core/utils/index.js" ;
3+ import { camelCase , getTypeScriptType , pascalCase , singular } from "@src/core/utils/index.js" ;
44import { analyzeValidationRules } from "./validation.analyzer.js" ;
55
6- import { FormAnalysisResult , FormControlModel } from "./form-types.js" ;
6+ import { FormAnalysisResult , FormControlModel , PolymorphicPropertyConfig } from "./form-types.js" ;
77
88export class FormModelBuilder {
99 private parser : SwaggerParser ;
@@ -13,7 +13,10 @@ export class FormModelBuilder {
1313 usesCustomValidators : false ,
1414 hasFormArrays : false ,
1515 hasFileUploads : false ,
16- isPolymorphic : false
16+ hasMaps : false ,
17+ isPolymorphic : false ,
18+ polymorphicProperties : [ ] ,
19+ dependencyRules : [ ]
1720 } ;
1821
1922 constructor ( parser : SwaggerParser ) {
@@ -22,30 +25,75 @@ export class FormModelBuilder {
2225
2326 public build ( resource : Resource ) : FormAnalysisResult {
2427 const formInterfaceName = `${ pascalCase ( resource . modelName ) } Form` ;
28+ const definitions = this . parser . schemas ;
2529
2630 // 1. Detect Polymorphism
27- const oneOfProp = resource . formProperties . find ( p => p . schema . oneOf && p . schema . discriminator ) ;
28- if ( oneOfProp ) {
31+ const polymorphicProps = resource . formProperties . filter ( p => p . schema . oneOf && p . schema . discriminator ) ;
32+
33+ if ( polymorphicProps . length > 0 ) {
2934 this . result . isPolymorphic = true ;
30- this . result . discriminatorPropName = oneOfProp . schema . discriminator ! . propertyName ;
31- this . analyzePolymorphism ( oneOfProp ) ;
35+
36+ for ( const prop of polymorphicProps ) {
37+ const config = this . analyzePolymorphism ( prop ) ;
38+ if ( config ) {
39+ this . result . polymorphicProperties . push ( config ) ;
40+ }
41+ }
3242 }
3343
34- // 2. Build Top Level Controls & Interfaces
35- // This method recursively populates this.result.interfaces
44+ // 2. Detect Dependent Schemas (Optimization: check generic schema lookup for this model)
45+ const modelDef = definitions . find ( d => d . name === resource . modelName ) ?. definition ;
46+ if ( modelDef && modelDef . dependentSchemas ) {
47+ this . analyzeDependentSchemas ( modelDef ) ;
48+ }
49+ // Also check if the resource properties themselves originated from a schema with dependentSchemas
50+ // (Handling cases where the Resource object was built from flattened paths)
51+ /*for (const prop of resource.formProperties) {
52+ // We can't easily climb back up to the parent form schema from a property alone here without context,
53+ // but the previous check covers the main model definition which is the primary source for forms.
54+ }*/
55+
56+ // 3. Build Top Level Controls & Interfaces
3657 this . result . topLevelControls = this . analyzeControls (
3758 resource . formProperties ,
3859 formInterfaceName ,
3960 true
4061 ) ;
4162
42- // 3 . Global Flags
63+ // 4 . Global Flags
4364 this . result . hasFileUploads = resource . formProperties . some ( p => p . schema . format === 'binary' ) ;
44- // hasFormArrays logic is handled inside analyzeControls where recursion happens
4565
4666 return this . result ;
4767 }
4868
69+ private analyzeDependentSchemas ( modelSchema : SwaggerDefinition ) {
70+ if ( ! modelSchema . dependentSchemas ) return ;
71+
72+ Object . entries ( modelSchema . dependentSchemas ) . forEach ( ( [ triggerProp , schemaOrRef ] ) => {
73+ const dependentSchema = this . parser . resolve ( schemaOrRef ) ;
74+ if ( ! dependentSchema ) return ;
75+
76+ // Start with 'required' array
77+ if ( dependentSchema . required ) {
78+ dependentSchema . required . forEach ( reqProp => {
79+ this . result . dependencyRules . push ( {
80+ triggerField : triggerProp ,
81+ targetField : reqProp ,
82+ type : 'required'
83+ } ) ;
84+ } ) ;
85+ }
86+
87+ // Also check for nested properties that implicitly become required
88+ if ( dependentSchema . properties ) {
89+ // For simply defining the property structure, we don't necessarily force 'required'
90+ // unless it's in the 'required' array. However, if the property only exists via dependent schema,
91+ // UI might want to toggle visibility.
92+ // For now, we strictly follow JSON Schema 'required' semantics for validation logic.
93+ }
94+ } ) ;
95+ }
96+
4997 /**
5098 * Recursively analyzes properties to build Control Models and Interface Definitions.
5199 */
@@ -58,12 +106,10 @@ export class FormModelBuilder {
58106 const interfaceProps : { name : string } [ ] = [ ] ;
59107
60108 for ( const prop of properties ) {
61- if ( prop . schema . readOnly ) continue ;
62-
63109 const schema = prop . schema ;
64110 const validationRules = analyzeValidationRules ( schema ) ;
65111
66- if ( validationRules . some ( r => [ 'exclusiveMinimum' , 'exclusiveMaximum' , 'multipleOf' , 'uniqueItems' ] . includes ( r . type ) ) ) {
112+ if ( validationRules . some ( r => [ 'exclusiveMinimum' , 'exclusiveMaximum' , 'multipleOf' , 'uniqueItems' , 'not' ] . includes ( r . type ) ) ) {
67113 this . result . usesCustomValidators = true ;
68114 }
69115
@@ -93,16 +139,61 @@ export class FormModelBuilder {
93139 schema
94140 } ;
95141 }
96- // 2b. Form Array
142+ // 2b. Map / Dictionary (additionalProperties OR patternProperties)
143+ else if ( schema . type === 'object' && ! schema . properties && ( schema . additionalProperties || schema . unevaluatedProperties || schema . patternProperties ) ) {
144+ this . result . hasMaps = true ;
145+
146+ // extract the schema for map values
147+ let rawValueSchema : any = { } ;
148+ let keyPattern : string | undefined ;
149+
150+ if ( schema . patternProperties ) {
151+ const patterns = Object . keys ( schema . patternProperties ) ;
152+ if ( patterns . length > 0 ) {
153+ keyPattern = patterns [ 0 ] ; // Using first pattern as constraint for the KEY
154+ rawValueSchema = schema . patternProperties [ keyPattern ] ;
155+ }
156+ }
157+
158+ if ( Object . keys ( rawValueSchema ) . length === 0 ) {
159+ rawValueSchema = ( typeof schema . additionalProperties === 'object' ? schema . additionalProperties : undefined )
160+ || ( typeof schema . unevaluatedProperties === 'object' ? schema . unevaluatedProperties : undefined )
161+ || { } ;
162+ }
163+
164+ const valuePropName = 'value' ;
165+ const valueInterfacePrefix = `${ pascalCase ( prop . name ) } Value` ;
166+
167+ const valueControls = this . analyzeControls (
168+ [ { name : valuePropName , schema : rawValueSchema as SwaggerDefinition } ] ,
169+ `${ valueInterfacePrefix } Form` ,
170+ false
171+ ) ;
172+
173+ const valueControl = valueControls [ 0 ] ! ;
174+ const valueTsType = valueControl . dataType ;
175+
176+ controlModel = {
177+ name : prop . name ,
178+ propertyName : prop . name ,
179+ dataType : isValidTsType ( valueTsType ) ? `Record<string, ${ valueTsType } >` : `Record<string, any>` ,
180+ defaultValue : defaultValue || { } ,
181+ validationRules,
182+ controlType : 'map' ,
183+ mapValueControl : valueControl ,
184+ schema,
185+ ...( valueControl . nestedFormInterface && { nestedFormInterface : valueControl . nestedFormInterface } ) ,
186+ ...( keyPattern && { keyPattern } ) // Attach the pattern for renderer usage
187+ } ;
188+ }
189+ // 2c. Form Array
97190 else if ( schema . type === 'array' ) {
98191 const itemSchema = schema . items as SwaggerDefinition ;
99192
100193 if ( itemSchema ?. properties ) {
101- // Array of Objects (Complex)
102194 this . result . hasFormArrays = true ;
103195 const arrayItemInterfaceName = `${ pascalCase ( singular ( prop . name ) ) } Form` ;
104196
105- // Recurse for item structure (phantom call to generate interface)
106197 const nestedItemControls = this . analyzeControls (
107198 Object . entries ( itemSchema . properties ) . map ( ( [ k , v ] ) => ( { name : k , schema : v } ) ) ,
108199 arrayItemInterfaceName ,
@@ -116,12 +207,11 @@ export class FormModelBuilder {
116207 defaultValue,
117208 validationRules,
118209 controlType : 'array' ,
119- nestedFormInterface : arrayItemInterfaceName , // References the Item interface
120- nestedControls : nestedItemControls , // Stored for "createItem" helper generation
210+ nestedFormInterface : arrayItemInterfaceName ,
211+ nestedControls : nestedItemControls ,
121212 schema
122213 } ;
123214 } else {
124- // Array of Primitives
125215 const itemTsType = this . getFormControlTypeString ( itemSchema ) ;
126216
127217 controlModel = {
@@ -135,7 +225,7 @@ export class FormModelBuilder {
135225 } ;
136226 }
137227 }
138- // 2c . Primitive Control
228+ // 2d . Primitive Control
139229 else {
140230 const tsType = this . getFormControlTypeString ( schema ) ;
141231
@@ -153,7 +243,6 @@ export class FormModelBuilder {
153243 controls . push ( controlModel ) ;
154244 }
155245
156- // Register the interface
157246 this . result . interfaces . push ( {
158247 name : interfaceName ,
159248 properties : interfaceProps ,
@@ -163,32 +252,36 @@ export class FormModelBuilder {
163252 return controls ;
164253 }
165254
166- private analyzePolymorphism ( prop : FormProperty ) {
255+ private analyzePolymorphism ( prop : FormProperty ) : PolymorphicPropertyConfig | null {
256+ if ( ! prop . schema . discriminator ) return null ;
257+
167258 const options = this . parser . getPolymorphicSchemaOptions ( prop . schema ) ;
168- this . result . discriminatorOptions = options . map ( o => o . name ) ;
169- this . result . polymorphicOptions = [ ] ;
259+ const dPropName = prop . schema . discriminator . propertyName ;
170260
171- const dPropName = prop . schema . discriminator ! . propertyName ;
261+ const config : PolymorphicPropertyConfig = {
262+ propertyName : dPropName ,
263+ discriminatorOptions : options . map ( o => o . name ) ,
264+ options : [ ]
265+ } ;
172266
173- const oneOfHasObjects = prop . schema . oneOf ! . some ( s => this . parser . resolve ( s ) ?. properties ) ;
174- if ( ! oneOfHasObjects ) return ;
267+ const explicitMapping = prop . schema . discriminator ?. mapping || { } ;
175268
176- for ( const subSchemaRef of prop . schema . oneOf ! ) {
177- if ( ! subSchemaRef . $ref ) continue ;
269+ for ( const subSchemaRef of prop . schema . oneOf || [ ] ) {
270+ const refString = subSchemaRef . $ref || subSchemaRef . $dynamicRef ;
271+ if ( ! refString ) continue ;
178272
179273 const subSchema = this . parser . resolve ( subSchemaRef ) ;
180274 if ( ! subSchema ) continue ;
181275
182276 const allProperties : Record < string , SwaggerDefinition > = { ...( subSchema . properties || { } ) } ;
183277
184- // **THE FIX**: Recursively collect properties from allOf references
185278 const collectAllOfProps = ( schema : SwaggerDefinition ) => {
186279 if ( schema . allOf ) {
187280 for ( const inner of schema . allOf ) {
188281 const resolved = this . parser . resolve ( inner ) ;
189282 if ( resolved ) {
190283 Object . assign ( allProperties , resolved . properties || { } ) ;
191- collectAllOfProps ( resolved ) ; // Recurse
284+ collectAllOfProps ( resolved ) ;
192285 }
193286 }
194287 }
@@ -197,42 +290,64 @@ export class FormModelBuilder {
197290
198291 if ( Object . keys ( allProperties ) . length === 0 ) continue ;
199292
200- const typeName = allProperties [ dPropName ] ?. enum ?. [ 0 ] as string ;
293+ if ( ! allProperties [ dPropName ] ) continue ;
294+
295+ let typeName = allProperties [ dPropName ] ?. enum ?. [ 0 ] as string ;
296+
297+ if ( ! typeName ) {
298+ const mappedKey = Object . keys ( explicitMapping ) . find ( key => explicitMapping [ key ] === refString ) ;
299+ if ( mappedKey ) typeName = mappedKey ;
300+ }
301+
302+ if ( ! typeName ) {
303+ typeName = refString . split ( '/' ) . pop ( ) || '' ;
304+ }
305+
201306 if ( ! typeName ) continue ;
202307
203- const refName = pascalCase ( subSchemaRef . $ref . split ( '/' ) . pop ( ) ! ) ;
308+ const refName = pascalCase ( refString . split ( '/' ) . pop ( ) ! ) ;
204309
205310 const subProperties = Object . entries ( allProperties )
206- . filter ( ( [ key , schema ] ) => key !== dPropName && ! schema . readOnly )
311+ . filter ( ( [ key , _schema ] ) => key !== dPropName )
207312 . map ( ( [ key , s ] ) => ( { name : key , schema : s as SwaggerDefinition } ) ) ;
208313
209314 const subControls : FormControlModel [ ] = [ ] ;
210315 for ( const subProp of subProperties ) {
211316 const controls = this . analyzeControls ( [ subProp ] , `Temp${ refName } ` , false ) ;
212- this . result . interfaces . pop ( ) ;
317+ this . result . interfaces . pop ( ) ; // Remove temp interface
213318 subControls . push ( ...controls ) ;
214319 }
215320
216- this . result . polymorphicOptions . push ( {
321+ if ( ! config . discriminatorOptions . includes ( typeName ) ) {
322+ config . discriminatorOptions . push ( typeName ) ;
323+ }
324+
325+ config . options . push ( {
217326 discriminatorValue : typeName ,
218327 modelName : refName ,
219- subFormName : typeName . toLowerCase ( ) , // Ensure sub-form name is consistent (e.g., 'cat', 'dog')
328+ subFormName : camelCase ( typeName ) ,
220329 controls : subControls
221330 } ) ;
222331 }
223332
224- // Detect default mapping
225333 if ( prop . schema . discriminator ?. defaultMapping ) {
226334 const defaultName = pascalCase ( prop . schema . discriminator . defaultMapping . split ( '/' ) . pop ( ) || '' ) ;
227- if ( defaultName && this . result . polymorphicOptions . some ( p => p . modelName === defaultName ) ) {
228- this . result . defaultPolymorphicOption = defaultName ;
335+ if ( defaultName && config . options . some ( p => p . modelName === defaultName ) ) {
336+ config . defaultOption = defaultName ;
229337 }
230338 }
339+
340+ return config ;
231341 }
232342
233343 private getFormControlTypeString ( schema : SwaggerDefinition ) : string {
234344 const knownTypes = this . parser . schemas . map ( s => s . name ) ;
235- const type = getTypeScriptType ( schema , { options : { dateType : 'Date' } } as any , knownTypes ) ;
345+ const dummyConfig = { options : { dateType : 'Date' , enumStyle : 'enum' } } as any ;
346+ const type = getTypeScriptType ( schema , dummyConfig , knownTypes ) ;
236347 return `${ type } | null` ;
237348 }
238349}
350+
351+ function isValidTsType ( type : string ) : boolean {
352+ return type != null && type !== 'any' && type !== 'void' ;
353+ }
0 commit comments