Skip to content

Commit 36823ec

Browse files
feat: Implement validation for OpenAPI composition structures and add integration tests
feat: Enhance composition handling and validation in flattening process
1 parent a70fd9a commit 36823ec

File tree

5 files changed

+1207
-37
lines changed

5 files changed

+1207
-37
lines changed

internal/transform/flatten.go

Lines changed: 228 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ func processFlatteningInFile(path string, opts FlattenOptions, result *FlattenRe
7777

7878
// processDocumentFlattening processes flattening in a document
7979
func 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
419529
func 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

Comments
 (0)