@@ -77,6 +77,12 @@ func processFlatteningInFile(path string, opts FlattenOptions, result *FlattenRe
7777
7878// processDocumentFlattening processes flattening in a document
7979func processDocumentFlattening (doc , root * yaml.Node , path string , opts FlattenOptions , result * FlattenResult ) (bool , error ) {
80+ // Validate composition structures before processing
81+ if validationErrors := ValidateAndReportCompositions (root , path ); validationErrors != "" {
82+ // Log validation warnings but continue processing
83+ fmt .Printf ("⚠️ Validation warnings for %s:\n %s" , path , validationErrors )
84+ }
85+
8086 // Track component references before flattening to identify unused ones later
8187 componentsBefore := extractComponentRefs (root )
8288
@@ -374,47 +380,151 @@ func flattenSchemaNode(node *yaml.Node, schemaName, path string, result *Flatten
374380
375381 changed := false
376382
377- // Check for oneOf, anyOf, allOf and try to flatten them
383+ // Process composition keys ( oneOf, anyOf, allOf) and other properties
378384 for i := 0 ; i < len (node .Content ); i += 2 {
379385 key := node .Content [i ].Value
380386 value := node .Content [i + 1 ]
381387
382- switch key {
383- case "oneOf" , "anyOf" , "allOf" :
384- if refValue := getSingleRefFromArray (value ); refValue != "" {
385- // Replace the oneOf/anyOf/allOf with direct $ref
386- node .Content [i ] = & yaml.Node {Kind : yaml .ScalarNode , Value : "$ref" }
387- node .Content [i + 1 ] = & yaml.Node {Kind : yaml .ScalarNode , Value : refValue }
388-
389- // Record the flattening
390- flattenedPath := fmt .Sprintf ("%s.%s -> $ref: %s" , schemaName , key , refValue )
391- if result .FlattenedRefs [path ] == nil {
392- result .FlattenedRefs [path ] = []string {}
393- }
394- result .FlattenedRefs [path ] = append (result .FlattenedRefs [path ], flattenedPath )
388+ switch {
389+ case isCompositionKey (key ):
390+ if processCompositionKey (node , i , key , value , schemaName , path , result ) {
391+ changed = true
392+ }
393+ case key == "properties" :
394+ if processPropertiesNode (value , schemaName , path , result ) {
395395 changed = true
396396 }
397397 default :
398- switch value .Kind {
399- case yaml .MappingNode :
400- // Recursively process nested objects
401- if flattenSchemaNode (value , schemaName , path , result ) {
402- changed = true
403- }
404- case yaml .SequenceNode :
405- // Process arrays
406- for _ , item := range value .Content {
407- if flattenSchemaNode (item , schemaName , path , result ) {
408- changed = true
409- }
410- }
398+ if processOtherNodes (value , schemaName , path , result ) {
399+ changed = true
411400 }
412401 }
413402 }
414403
415404 return changed
416405}
417406
407+ // processCompositionKey handles oneOf/anyOf/allOf keys
408+ func processCompositionKey (parentNode * yaml.Node , keyIndex int , key string , value * yaml.Node , schemaName , path string , result * FlattenResult ) bool {
409+ if isEmptyComposition (value ) {
410+ // Handle empty composition by removing it entirely
411+ handleEmptyComposition (parentNode , keyIndex , schemaName , key , path , result )
412+ return true
413+ }
414+
415+ if refValue := getSingleRefFromArray (value ); refValue != "" {
416+ // Replace the oneOf/anyOf/allOf with direct $ref
417+ parentNode .Content [keyIndex ] = & yaml.Node {Kind : yaml .ScalarNode , Value : "$ref" }
418+ parentNode .Content [keyIndex + 1 ] = & yaml.Node {Kind : yaml .ScalarNode , Value : refValue }
419+
420+ // Record the flattening
421+ recordFlattening (result , path , fmt .Sprintf ("%s.%s -> $ref: %s" , schemaName , key , refValue ))
422+ return true
423+ }
424+
425+ if singleSchema := getSingleSchemaFromArray (value ); singleSchema != nil {
426+ // Replace the oneOf/anyOf/allOf with the single inline schema
427+ flattenCompositionWithInlineSchema (parentNode , keyIndex , singleSchema , schemaName , key , path , result )
428+ return true
429+ }
430+
431+ return false
432+ }
433+
434+ // processPropertiesNode handles the properties section
435+ func processPropertiesNode (value * yaml.Node , schemaName , path string , result * FlattenResult ) bool {
436+ if value .Kind != yaml .MappingNode {
437+ return false
438+ }
439+
440+ changed := false
441+ propertiesToRemove := []int {}
442+
443+ for j := 0 ; j < len (value .Content ); j += 2 {
444+ propName := value .Content [j ].Value
445+ propNode := value .Content [j + 1 ]
446+
447+ if propNode .Kind == yaml .MappingNode {
448+ propSchemaName := fmt .Sprintf ("%s.properties.%s" , schemaName , propName )
449+ if flattenSchemaNode (propNode , propSchemaName , path , result ) {
450+ changed = true
451+ }
452+
453+ // Check if property became empty after flattening
454+ if len (propNode .Content ) == 0 {
455+ propertiesToRemove = append (propertiesToRemove , j )
456+ }
457+ }
458+ }
459+
460+ // Remove empty properties (in reverse order to maintain indices)
461+ if removeEmptyProperties (value , propertiesToRemove ) {
462+ changed = true
463+ }
464+
465+ return changed
466+ }
467+
468+ // processOtherNodes handles other node types (mappings, sequences)
469+ func processOtherNodes (value * yaml.Node , schemaName , path string , result * FlattenResult ) bool {
470+ changed := false
471+
472+ switch value .Kind {
473+ case yaml .MappingNode :
474+ // Recursively process nested objects
475+ if flattenSchemaNode (value , schemaName , path , result ) {
476+ changed = true
477+ }
478+ case yaml .SequenceNode :
479+ // Process arrays
480+ for _ , item := range value .Content {
481+ if flattenSchemaNode (item , schemaName , path , result ) {
482+ changed = true
483+ }
484+ }
485+ }
486+
487+ return changed
488+ }
489+
490+ // removeEmptyProperties removes empty properties from a properties node
491+ func removeEmptyProperties (propertiesNode * yaml.Node , propertiesToRemove []int ) bool {
492+ if len (propertiesToRemove ) == 0 {
493+ return false
494+ }
495+
496+ // Remove empty properties (in reverse order to maintain indices)
497+ for i := len (propertiesToRemove ) - 1 ; i >= 0 ; i -- {
498+ propIndex := propertiesToRemove [i ]
499+
500+ // Remove property key-value pair
501+ newContent := make ([]* yaml.Node , 0 , len (propertiesNode .Content )- 2 )
502+
503+ // Copy content before the property
504+ for k := 0 ; k < propIndex ; k ++ {
505+ newContent = append (newContent , propertiesNode .Content [k ])
506+ }
507+
508+ // Copy content after the property (skip the key-value pair)
509+ for k := propIndex + 2 ; k < len (propertiesNode .Content ); k ++ {
510+ newContent = append (newContent , propertiesNode .Content [k ])
511+ }
512+
513+ // Replace the properties node's content
514+ propertiesNode .Content = newContent
515+ }
516+
517+ return true
518+ }
519+
520+ // recordFlattening records a flattening operation in the result
521+ func recordFlattening (result * FlattenResult , path , flattenedPath string ) {
522+ if result .FlattenedRefs [path ] == nil {
523+ result .FlattenedRefs [path ] = []string {}
524+ }
525+ result .FlattenedRefs [path ] = append (result .FlattenedRefs [path ], flattenedPath )
526+ }
527+
418528// flattenPathNode flattens oneOf/anyOf/allOf in path responses
419529func flattenPathNode (node * yaml.Node , pathName , path string , result * FlattenResult ) bool {
420530 if node == nil || node .Kind != yaml .MappingNode {
@@ -517,3 +627,94 @@ func getSingleRefFromArray(arrayNode *yaml.Node) string {
517627
518628 return refValue
519629}
630+
631+ // getSingleSchemaFromArray checks if an array contains only one schema (not $ref) and returns it
632+ func getSingleSchemaFromArray (arrayNode * yaml.Node ) * yaml.Node {
633+ if arrayNode == nil || arrayNode .Kind != yaml .SequenceNode {
634+ return nil
635+ }
636+
637+ // Check if array has exactly one element
638+ if len (arrayNode .Content ) != 1 {
639+ return nil
640+ }
641+
642+ element := arrayNode .Content [0 ]
643+ if element .Kind != yaml .MappingNode {
644+ return nil
645+ }
646+
647+ // Check if the element is an inline schema (not a $ref)
648+ for i := 0 ; i < len (element .Content ); i += 2 {
649+ key := element .Content [i ].Value
650+ if key == "$ref" {
651+ // This is a $ref, not an inline schema
652+ return nil
653+ }
654+ }
655+
656+ // It's an inline schema
657+ return element
658+ }
659+
660+ // flattenCompositionWithInlineSchema replaces oneOf/anyOf/allOf with the single inline schema
661+ func flattenCompositionWithInlineSchema (parentNode * yaml.Node , keyIndex int , singleSchema * yaml.Node , schemaName , compositionType , path string , result * FlattenResult ) {
662+ // Remove the composition key and replace with the inline schema's properties
663+ // We need to merge the single schema's content into the parent node
664+
665+ // First, remove the composition key-value pair
666+ newContent := make ([]* yaml.Node , 0 , len (parentNode .Content )- 2 + len (singleSchema .Content ))
667+
668+ // Copy content before the composition
669+ for i := 0 ; i < keyIndex ; i ++ {
670+ newContent = append (newContent , parentNode .Content [i ])
671+ }
672+
673+ // Add the single schema's content (all its key-value pairs)
674+ newContent = append (newContent , singleSchema .Content ... )
675+
676+ // Copy content after the composition
677+ for i := keyIndex + 2 ; i < len (parentNode .Content ); i ++ {
678+ newContent = append (newContent , parentNode .Content [i ])
679+ }
680+
681+ // Replace the parent node's content
682+ parentNode .Content = newContent
683+
684+ // Record the flattening
685+ recordFlattening (result , path , fmt .Sprintf ("%s.%s -> inline schema" , schemaName , compositionType ))
686+ }
687+
688+ // isEmptyComposition checks if a composition array is empty
689+ func isEmptyComposition (arrayNode * yaml.Node ) bool {
690+ if arrayNode == nil || arrayNode .Kind != yaml .SequenceNode {
691+ return false
692+ }
693+ return len (arrayNode .Content ) == 0
694+ }
695+
696+ // handleEmptyComposition removes empty composition from schema
697+ func handleEmptyComposition (parentNode * yaml.Node , keyIndex int , schemaName , compositionType , path string , result * FlattenResult ) {
698+ // Remove the empty composition key-value pair
699+ newContent := make ([]* yaml.Node , 0 , len (parentNode .Content )- 2 )
700+
701+ // Copy content before the composition
702+ for i := 0 ; i < keyIndex ; i ++ {
703+ newContent = append (newContent , parentNode .Content [i ])
704+ }
705+
706+ // Copy content after the composition (skip the key-value pair)
707+ for i := keyIndex + 2 ; i < len (parentNode .Content ); i ++ {
708+ newContent = append (newContent , parentNode .Content [i ])
709+ }
710+
711+ // Replace the parent node's content
712+ parentNode .Content = newContent
713+
714+ // Record the removal
715+ flattenedPath := fmt .Sprintf ("%s.%s -> removed (empty)" , schemaName , compositionType )
716+ if result .FlattenedRefs [path ] == nil {
717+ result .FlattenedRefs [path ] = []string {}
718+ }
719+ result .FlattenedRefs [path ] = append (result .FlattenedRefs [path ], flattenedPath )
720+ }
0 commit comments