Skip to content

Commit b8faad7

Browse files
feat: Implement validation for OpenAPI composition structures and add integration tests
1 parent a70fd9a commit b8faad7

File tree

5 files changed

+1064
-11
lines changed

5 files changed

+1064
-11
lines changed

internal/transform/flatten.go

Lines changed: 154 additions & 1 deletion
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

@@ -381,7 +387,11 @@ func flattenSchemaNode(node *yaml.Node, schemaName, path string, result *Flatten
381387

382388
switch key {
383389
case "oneOf", "anyOf", "allOf":
384-
if refValue := getSingleRefFromArray(value); refValue != "" {
390+
if isEmptyComposition(value) {
391+
// Handle empty composition by removing it entirely
392+
handleEmptyComposition(node, i, schemaName, key, path, result)
393+
changed = true
394+
} else if refValue := getSingleRefFromArray(value); refValue != "" {
385395
// Replace the oneOf/anyOf/allOf with direct $ref
386396
node.Content[i] = &yaml.Node{Kind: yaml.ScalarNode, Value: "$ref"}
387397
node.Content[i+1] = &yaml.Node{Kind: yaml.ScalarNode, Value: refValue}
@@ -393,6 +403,54 @@ func flattenSchemaNode(node *yaml.Node, schemaName, path string, result *Flatten
393403
}
394404
result.FlattenedRefs[path] = append(result.FlattenedRefs[path], flattenedPath)
395405
changed = true
406+
} else if singleSchema := getSingleSchemaFromArray(value); singleSchema != nil {
407+
// Replace the oneOf/anyOf/allOf with the single inline schema
408+
flattenCompositionWithInlineSchema(node, i, singleSchema, schemaName, key, path, result)
409+
changed = true
410+
}
411+
case "properties":
412+
// Special handling for properties - process each property individually
413+
if value.Kind == yaml.MappingNode {
414+
propertiesToRemove := []int{}
415+
416+
for j := 0; j < len(value.Content); j += 2 {
417+
propName := value.Content[j].Value
418+
propNode := value.Content[j+1]
419+
420+
if propNode.Kind == yaml.MappingNode {
421+
propSchemaName := fmt.Sprintf("%s.properties.%s", schemaName, propName)
422+
if flattenSchemaNode(propNode, propSchemaName, path, result) {
423+
changed = true
424+
}
425+
426+
// Check if property became empty after flattening
427+
if len(propNode.Content) == 0 {
428+
propertiesToRemove = append(propertiesToRemove, j)
429+
}
430+
}
431+
}
432+
433+
// Remove empty properties (in reverse order to maintain indices)
434+
for i := len(propertiesToRemove) - 1; i >= 0; i-- {
435+
propIndex := propertiesToRemove[i]
436+
437+
// Remove property key-value pair
438+
newContent := make([]*yaml.Node, 0, len(value.Content)-2)
439+
440+
// Copy content before the property
441+
for k := 0; k < propIndex; k++ {
442+
newContent = append(newContent, value.Content[k])
443+
}
444+
445+
// Copy content after the property (skip the key-value pair)
446+
for k := propIndex + 2; k < len(value.Content); k++ {
447+
newContent = append(newContent, value.Content[k])
448+
}
449+
450+
// Replace the properties node's content
451+
value.Content = newContent
452+
changed = true
453+
}
396454
}
397455
default:
398456
switch value.Kind {
@@ -517,3 +575,98 @@ func getSingleRefFromArray(arrayNode *yaml.Node) string {
517575

518576
return refValue
519577
}
578+
579+
// getSingleSchemaFromArray checks if an array contains only one schema (not $ref) and returns it
580+
func getSingleSchemaFromArray(arrayNode *yaml.Node) *yaml.Node {
581+
if arrayNode == nil || arrayNode.Kind != yaml.SequenceNode {
582+
return nil
583+
}
584+
585+
// Check if array has exactly one element
586+
if len(arrayNode.Content) != 1 {
587+
return nil
588+
}
589+
590+
element := arrayNode.Content[0]
591+
if element.Kind != yaml.MappingNode {
592+
return nil
593+
}
594+
595+
// Check if the element is an inline schema (not a $ref)
596+
for i := 0; i < len(element.Content); i += 2 {
597+
key := element.Content[i].Value
598+
if key == "$ref" {
599+
// This is a $ref, not an inline schema
600+
return nil
601+
}
602+
}
603+
604+
// It's an inline schema
605+
return element
606+
}
607+
608+
// flattenCompositionWithInlineSchema replaces oneOf/anyOf/allOf with the single inline schema
609+
func flattenCompositionWithInlineSchema(parentNode *yaml.Node, keyIndex int, singleSchema *yaml.Node, schemaName, compositionType, path string, result *FlattenResult) {
610+
// Remove the composition key and replace with the inline schema's properties
611+
// We need to merge the single schema's content into the parent node
612+
613+
// First, remove the composition key-value pair
614+
newContent := make([]*yaml.Node, 0, len(parentNode.Content)-2+len(singleSchema.Content))
615+
616+
// Copy content before the composition
617+
for i := 0; i < keyIndex; i++ {
618+
newContent = append(newContent, parentNode.Content[i])
619+
}
620+
621+
// Add the single schema's content (all its key-value pairs)
622+
newContent = append(newContent, singleSchema.Content...)
623+
624+
// Copy content after the composition
625+
for i := keyIndex + 2; i < len(parentNode.Content); i++ {
626+
newContent = append(newContent, parentNode.Content[i])
627+
}
628+
629+
// Replace the parent node's content
630+
parentNode.Content = newContent
631+
632+
// Record the flattening
633+
flattenedPath := fmt.Sprintf("%s.%s -> inline schema", schemaName, compositionType)
634+
if result.FlattenedRefs[path] == nil {
635+
result.FlattenedRefs[path] = []string{}
636+
}
637+
result.FlattenedRefs[path] = append(result.FlattenedRefs[path], flattenedPath)
638+
}
639+
640+
// isEmptyComposition checks if a composition array is empty
641+
func isEmptyComposition(arrayNode *yaml.Node) bool {
642+
if arrayNode == nil || arrayNode.Kind != yaml.SequenceNode {
643+
return false
644+
}
645+
return len(arrayNode.Content) == 0
646+
}
647+
648+
// handleEmptyComposition removes empty composition from schema
649+
func handleEmptyComposition(parentNode *yaml.Node, keyIndex int, schemaName, compositionType, path string, result *FlattenResult) {
650+
// Remove the empty composition key-value pair
651+
newContent := make([]*yaml.Node, 0, len(parentNode.Content)-2)
652+
653+
// Copy content before the composition
654+
for i := 0; i < keyIndex; i++ {
655+
newContent = append(newContent, parentNode.Content[i])
656+
}
657+
658+
// Copy content after the composition (skip the key-value pair)
659+
for i := keyIndex + 2; i < len(parentNode.Content); i++ {
660+
newContent = append(newContent, parentNode.Content[i])
661+
}
662+
663+
// Replace the parent node's content
664+
parentNode.Content = newContent
665+
666+
// Record the removal
667+
flattenedPath := fmt.Sprintf("%s.%s -> removed (empty)", schemaName, compositionType)
668+
if result.FlattenedRefs[path] == nil {
669+
result.FlattenedRefs[path] = []string{}
670+
}
671+
result.FlattenedRefs[path] = append(result.FlattenedRefs[path], flattenedPath)
672+
}

0 commit comments

Comments
 (0)