Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions converter/ref_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@
package converter

import (
"regexp"
"strings"

"github.com/erraggy/oastools/parser"
)

var (
// Map OAS 3.x locations to the Regexp for the prefix of the OAS 2.0 locations
refRegxMapWithOAS3AsNew = map[string]*regexp.Regexp{
"#/components/schemas/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/definitions/")),
"#/components/parameters/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/parameters/")),
"#/components/responses/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/responses/")),
"#/components/securitySchemes/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/securityDefinitions/")),
}
// refMapping defines a prefix substitution for $ref rewriting.
type refMapping struct {
from string
to string
}

// Map OAS 2.0 locations to the Regexp for the prefix of the OAS 3.x locations
refRegxMapWithSwaggerAsNew = map[string]*regexp.Regexp{
"#/definitions/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/components/schemas/")),
"#/parameters/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/components/parameters/")),
"#/responses/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/components/responses/")),
"#/securityDefinitions/": regexp.MustCompile(`^` + regexp.QuoteMeta("#/components/securitySchemes/")),
}
)
// oas2ToOAS3Mappings maps OAS 2.0 $ref prefixes to their OAS 3.x equivalents.
var oas2ToOAS3Mappings = []refMapping{
{"#/definitions/", "#/components/schemas/"},
{"#/parameters/", "#/components/parameters/"},
{"#/responses/", "#/components/responses/"},
{"#/securityDefinitions/", "#/components/securitySchemes/"},
}

// oas3ToOAS2Mappings maps OAS 3.x $ref prefixes to their OAS 2.0 equivalents.
var oas3ToOAS2Mappings = []refMapping{
{"#/components/schemas/", "#/definitions/"},
{"#/components/parameters/", "#/parameters/"},
{"#/components/responses/", "#/responses/"},
{"#/components/securitySchemes/", "#/securityDefinitions/"},
}

// rewriteRefOAS2ToOAS3 rewrites an OAS 2.0 $ref to OAS 3.x format
// Only rewrites local references (starting with #/)
Expand All @@ -36,10 +39,9 @@ func rewriteRefOAS2ToOAS3(ref string) string {
return ref
}

// iterate all regexp mappings and if found on the specified ref, replace it with the new prefix
for newOAS3Prefix, swaggerPrefixRegX := range refRegxMapWithOAS3AsNew {
if swaggerPrefixRegX.MatchString(ref) {
return swaggerPrefixRegX.ReplaceAllString(ref, newOAS3Prefix)
for _, m := range oas2ToOAS3Mappings {
if strings.HasPrefix(ref, m.from) {
return m.to + ref[len(m.from):]
}
}

Expand All @@ -54,10 +56,9 @@ func rewriteRefOAS3ToOAS2(ref string) string {
return ref
}

// iterate all regexp mappings and if found on the specified ref, replace it with the new prefix
for newSwaggerPrefix, oas3PrefixRegX := range refRegxMapWithSwaggerAsNew {
if oas3PrefixRegX.MatchString(ref) {
return oas3PrefixRegX.ReplaceAllString(ref, newSwaggerPrefix)
for _, m := range oas3ToOAS2Mappings {
if strings.HasPrefix(ref, m.from) {
return m.to + ref[len(m.from):]
}
}

Expand Down
88 changes: 41 additions & 47 deletions fixer/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ import (
// that can be *parser.Schema, bool, or map[string]any. This helper reduces duplication
// in collectSchemaRefsRecursive for AdditionalProperties, Items, AdditionalItems,
// UnevaluatedProperties, and UnevaluatedItems.
func collectPolymorphicSchemaRefs(field any, prefix string, visited map[*parser.Schema]bool) []string {
func collectPolymorphicSchemaRefs(refs *[]string, field any, prefix string, visited map[*parser.Schema]bool) {
if field == nil {
return nil
return
}
if schema, ok := field.(*parser.Schema); ok {
return collectSchemaRefsRecursive(schema, prefix, visited)
collectSchemaRefsRecursive(refs, schema, prefix, visited)
return
}
if mapField, ok := field.(map[string]any); ok {
return collectRefsFromMap(mapField, prefix)
collectRefsFromMap(refs, mapField, prefix)
}
return nil
}

// buildReferencedSchemaSet builds the transitive closure of referenced schemas.
Expand Down Expand Up @@ -99,96 +99,94 @@ func extractSchemaName(ref, prefix string) string {
// prefix should be "#/definitions/" for OAS 2.0 or "#/components/schemas/" for OAS 3.x
func collectSchemaRefs(schema *parser.Schema, prefix string) []string {
visited := make(map[*parser.Schema]bool)
return collectSchemaRefsRecursive(schema, prefix, visited)
refs := make([]string, 0, 32)
collectSchemaRefsRecursive(&refs, schema, prefix, visited)
return refs
}

// appendOptionalSchemaRefs appends refs from an optional schema field if non-nil.
// This helper reduces duplication in collectSchemaRefsRecursive.
func appendOptionalSchemaRefs(refs []string, s *parser.Schema, prefix string, visited map[*parser.Schema]bool) []string {
func appendOptionalSchemaRefs(refs *[]string, s *parser.Schema, prefix string, visited map[*parser.Schema]bool) {
if s != nil {
refs = append(refs, collectSchemaRefsRecursive(s, prefix, visited)...)
collectSchemaRefsRecursive(refs, s, prefix, visited)
}
return refs
}

// collectSchemaRefsRecursive is the internal implementation with circular reference protection.
func collectSchemaRefsRecursive(schema *parser.Schema, prefix string, visited map[*parser.Schema]bool) []string {
// It appends found refs to the pre-allocated slice pointed to by refs.
func collectSchemaRefsRecursive(refs *[]string, schema *parser.Schema, prefix string, visited map[*parser.Schema]bool) {
if schema == nil || visited[schema] {
return nil
return
}
visited[schema] = true

var refs []string

// Direct schema ref
if name := extractSchemaName(schema.Ref, prefix); name != "" {
refs = append(refs, name)
*refs = append(*refs, name)
}

// Properties
for _, propSchema := range schema.Properties {
refs = append(refs, collectSchemaRefsRecursive(propSchema, prefix, visited)...)
collectSchemaRefsRecursive(refs, propSchema, prefix, visited)
}

// Polymorphic fields (can be *Schema, bool, or map[string]any)
refs = append(refs, collectPolymorphicSchemaRefs(schema.AdditionalProperties, prefix, visited)...)
refs = append(refs, collectPolymorphicSchemaRefs(schema.Items, prefix, visited)...)
refs = append(refs, collectPolymorphicSchemaRefs(schema.AdditionalItems, prefix, visited)...)
collectPolymorphicSchemaRefs(refs, schema.AdditionalProperties, prefix, visited)
collectPolymorphicSchemaRefs(refs, schema.Items, prefix, visited)
collectPolymorphicSchemaRefs(refs, schema.AdditionalItems, prefix, visited)

// Schema composition
for _, s := range schema.AllOf {
refs = append(refs, collectSchemaRefsRecursive(s, prefix, visited)...)
collectSchemaRefsRecursive(refs, s, prefix, visited)
}
for _, s := range schema.AnyOf {
refs = append(refs, collectSchemaRefsRecursive(s, prefix, visited)...)
collectSchemaRefsRecursive(refs, s, prefix, visited)
}
for _, s := range schema.OneOf {
refs = append(refs, collectSchemaRefsRecursive(s, prefix, visited)...)
collectSchemaRefsRecursive(refs, s, prefix, visited)
}
refs = appendOptionalSchemaRefs(refs, schema.Not, prefix, visited)
appendOptionalSchemaRefs(refs, schema.Not, prefix, visited)

// OAS 3.1+ / JSON Schema Draft 2020-12 fields
for _, s := range schema.PrefixItems {
refs = append(refs, collectSchemaRefsRecursive(s, prefix, visited)...)
collectSchemaRefsRecursive(refs, s, prefix, visited)
}
refs = appendOptionalSchemaRefs(refs, schema.Contains, prefix, visited)
refs = appendOptionalSchemaRefs(refs, schema.PropertyNames, prefix, visited)
appendOptionalSchemaRefs(refs, schema.Contains, prefix, visited)
appendOptionalSchemaRefs(refs, schema.PropertyNames, prefix, visited)
for _, depSchema := range schema.DependentSchemas {
refs = append(refs, collectSchemaRefsRecursive(depSchema, prefix, visited)...)
collectSchemaRefsRecursive(refs, depSchema, prefix, visited)
}

// JSON Schema 2020-12 unevaluated keywords (polymorphic: *Schema or bool)
refs = append(refs, collectPolymorphicSchemaRefs(schema.UnevaluatedProperties, prefix, visited)...)
refs = append(refs, collectPolymorphicSchemaRefs(schema.UnevaluatedItems, prefix, visited)...)
collectPolymorphicSchemaRefs(refs, schema.UnevaluatedProperties, prefix, visited)
collectPolymorphicSchemaRefs(refs, schema.UnevaluatedItems, prefix, visited)

// JSON Schema 2020-12 content keywords
refs = appendOptionalSchemaRefs(refs, schema.ContentSchema, prefix, visited)
appendOptionalSchemaRefs(refs, schema.ContentSchema, prefix, visited)

// Conditional schemas (OAS 3.1+)
refs = appendOptionalSchemaRefs(refs, schema.If, prefix, visited)
refs = appendOptionalSchemaRefs(refs, schema.Then, prefix, visited)
refs = appendOptionalSchemaRefs(refs, schema.Else, prefix, visited)
appendOptionalSchemaRefs(refs, schema.If, prefix, visited)
appendOptionalSchemaRefs(refs, schema.Then, prefix, visited)
appendOptionalSchemaRefs(refs, schema.Else, prefix, visited)

// $defs (OAS 3.1+)
for _, defSchema := range schema.Defs {
refs = append(refs, collectSchemaRefsRecursive(defSchema, prefix, visited)...)
collectSchemaRefsRecursive(refs, defSchema, prefix, visited)
}

// Pattern properties
for _, propSchema := range schema.PatternProperties {
refs = append(refs, collectSchemaRefsRecursive(propSchema, prefix, visited)...)
collectSchemaRefsRecursive(refs, propSchema, prefix, visited)
}

// Discriminator mapping values are references
if schema.Discriminator != nil {
for _, mappingRef := range schema.Discriminator.Mapping {
if name := extractSchemaName(mappingRef, prefix); name != "" {
refs = append(refs, name)
*refs = append(*refs, name)
}
}
}

return refs
}

// isPathItemEmpty returns true if the path item has no operations defined.
Expand Down Expand Up @@ -319,45 +317,41 @@ func isComponentsEmpty(comp *parser.Components) bool {
// This handles polymorphic schema fields (Items, AdditionalProperties, etc.) that may
// remain as untyped maps after YAML/JSON unmarshaling. These fields are declared as
// `any` in parser.Schema to support both *Schema and bool values per the OAS spec.
func collectRefsFromMap(m map[string]any, prefix string) []string {
var refs []string

func collectRefsFromMap(refs *[]string, m map[string]any, prefix string) {
// Check for direct $ref
if refStr, ok := m["$ref"].(string); ok {
if name := extractSchemaName(refStr, prefix); name != "" {
refs = append(refs, name)
*refs = append(*refs, name)
}
}

// Check nested properties
if props, ok := m["properties"].(map[string]any); ok {
for _, propVal := range props {
if propMap, ok := propVal.(map[string]any); ok {
refs = append(refs, collectRefsFromMap(propMap, prefix)...)
collectRefsFromMap(refs, propMap, prefix)
}
}
}

// Check items
if items, ok := m["items"].(map[string]any); ok {
refs = append(refs, collectRefsFromMap(items, prefix)...)
collectRefsFromMap(refs, items, prefix)
}

// Check additionalProperties
if addProps, ok := m["additionalProperties"].(map[string]any); ok {
refs = append(refs, collectRefsFromMap(addProps, prefix)...)
collectRefsFromMap(refs, addProps, prefix)
}

// Check allOf, anyOf, oneOf
for _, key := range []string{"allOf", "anyOf", "oneOf"} {
if arr, ok := m[key].([]any); ok {
for _, item := range arr {
if itemMap, ok := item.(map[string]any); ok {
refs = append(refs, collectRefsFromMap(itemMap, prefix)...)
collectRefsFromMap(refs, itemMap, prefix)
}
}
}
}

return refs
}
25 changes: 13 additions & 12 deletions internal/schemautil/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"hash/fnv"
"reflect"
"sort"
"strconv"

"github.com/erraggy/oastools/parser"
)
Expand All @@ -28,7 +29,7 @@ func NewSchemaHasher() *SchemaHasher {
// Schemas with identical structural properties will have the same hash.
// Note: Hash collisions are possible; use deep comparison to verify equivalence.
func (h *SchemaHasher) Hash(schema *parser.Schema) uint64 {
h.visited = make(map[uintptr]bool) // Reset visited map
clear(h.visited) // Reset visited map without reallocating
hasher := fnv.New64a()
h.hashSchema(hasher, schema)
return hasher.Sum64()
Expand Down Expand Up @@ -323,10 +324,10 @@ func (h *SchemaHasher) hashSchemaOrBool(hasher hash.Hash64, v any) {
// hashNumericConstraints hashes numeric validation fields.
func (h *SchemaHasher) hashNumericConstraints(hasher hash.Hash64, schema *parser.Schema) {
if schema.Minimum != nil {
h.writeString(hasher, fmt.Sprintf("minimum:%v", *schema.Minimum))
h.writeString(hasher, "minimum:"+strconv.FormatFloat(*schema.Minimum, 'g', -1, 64))
}
if schema.Maximum != nil {
h.writeString(hasher, fmt.Sprintf("maximum:%v", *schema.Maximum))
h.writeString(hasher, "maximum:"+strconv.FormatFloat(*schema.Maximum, 'g', -1, 64))
}
if schema.ExclusiveMinimum != nil {
h.writeString(hasher, fmt.Sprintf("exclusiveMinimum:%v", schema.ExclusiveMinimum))
Expand All @@ -335,46 +336,46 @@ func (h *SchemaHasher) hashNumericConstraints(hasher hash.Hash64, schema *parser
h.writeString(hasher, fmt.Sprintf("exclusiveMaximum:%v", schema.ExclusiveMaximum))
}
if schema.MultipleOf != nil {
h.writeString(hasher, fmt.Sprintf("multipleOf:%v", *schema.MultipleOf))
h.writeString(hasher, "multipleOf:"+strconv.FormatFloat(*schema.MultipleOf, 'g', -1, 64))
}
}

// hashStringConstraints hashes string validation fields.
func (h *SchemaHasher) hashStringConstraints(hasher hash.Hash64, schema *parser.Schema) {
if schema.MinLength != nil {
h.writeString(hasher, fmt.Sprintf("minLength:%d", *schema.MinLength))
h.writeString(hasher, "minLength:"+strconv.Itoa(*schema.MinLength))
}
if schema.MaxLength != nil {
h.writeString(hasher, fmt.Sprintf("maxLength:%d", *schema.MaxLength))
h.writeString(hasher, "maxLength:"+strconv.Itoa(*schema.MaxLength))
}
}

// hashArrayConstraints hashes array validation fields.
func (h *SchemaHasher) hashArrayConstraints(hasher hash.Hash64, schema *parser.Schema) {
if schema.MinItems != nil {
h.writeString(hasher, fmt.Sprintf("minItems:%d", *schema.MinItems))
h.writeString(hasher, "minItems:"+strconv.Itoa(*schema.MinItems))
}
if schema.MaxItems != nil {
h.writeString(hasher, fmt.Sprintf("maxItems:%d", *schema.MaxItems))
h.writeString(hasher, "maxItems:"+strconv.Itoa(*schema.MaxItems))
}
if schema.UniqueItems {
h.writeString(hasher, "uniqueItems:true")
}
if schema.MinContains != nil {
h.writeString(hasher, fmt.Sprintf("minContains:%d", *schema.MinContains))
h.writeString(hasher, "minContains:"+strconv.Itoa(*schema.MinContains))
}
if schema.MaxContains != nil {
h.writeString(hasher, fmt.Sprintf("maxContains:%d", *schema.MaxContains))
h.writeString(hasher, "maxContains:"+strconv.Itoa(*schema.MaxContains))
}
}

// hashObjectConstraints hashes object validation fields.
func (h *SchemaHasher) hashObjectConstraints(hasher hash.Hash64, schema *parser.Schema) {
if schema.MinProperties != nil {
h.writeString(hasher, fmt.Sprintf("minProperties:%d", *schema.MinProperties))
h.writeString(hasher, "minProperties:"+strconv.Itoa(*schema.MinProperties))
}
if schema.MaxProperties != nil {
h.writeString(hasher, fmt.Sprintf("maxProperties:%d", *schema.MaxProperties))
h.writeString(hasher, "maxProperties:"+strconv.Itoa(*schema.MaxProperties))
}
}

Expand Down
Loading