Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

openapi3: improve internalization ref naming to avoid collisions #955

Merged
merged 16 commits into from
Jul 3, 2024
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
42 changes: 30 additions & 12 deletions .github/docs/openapi3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ const (

VARIABLES

var (
IdentifierRegExp = regexp.MustCompile(`^[` + identifierChars + `]+$`)
InvalidIdentifierCharRegExp = regexp.MustCompile(`[^` + identifierChars + `]`)
)
IdentifierRegExp verifies whether Component object key matches contains just
'identifierChars', according to OpenAPI v3.x. InvalidIdentifierCharRegExp
matches all characters not contained in 'identifierChars'. However, to be
able supporting legacy OpenAPI v2.x, there is a need to customize above
pattern in order not to fail converted v2-v3 validation

var (
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
SchemaErrorDetailsDisabled = false
Expand All @@ -63,12 +73,6 @@ var ErrURINotSupported = errors.New("unsupported URI")
ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle
a given URI.

var IdentifierRegExp = regexp.MustCompile(identifierPattern)
IdentifierRegExp verifies whether Component object key matches
'identifierPattern' pattern, according to OpenAPI v3.x. However, to be able
supporting legacy OpenAPI v2.x, there is a need to customize above pattern
in order not to fail converted v2-v3 validation

var SchemaStringFormats = make(map[string]Format, 4)
SchemaStringFormats allows for validating string formats

Expand All @@ -78,13 +82,22 @@ FUNCTIONS
func BoolPtr(value bool) *bool
BoolPtr is a helper for defining OpenAPI schemas.

func DefaultRefNameResolver(ref string) string
func DefaultRefNameResolver(doc *T, ref componentRef) string
DefaultRefResolver is a default implementation of refNameResolver for the
InternalizeRefs function.

If a reference points to an element inside a document, it returns the last
element in the reference using filepath.Base. Otherwise if the reference
points to a file, it returns the file name trimmed of all extensions.
The external reference is internalized to (hopefully) a unique name.
If the external reference matches (by path) to another reference in the root
document then the name of that component is used.

The transformation involves:
- Cutting the "#/components/<type>" part.
- Cutting the file extensions (.yaml/.json) from documents.
- Trimming the common directory with the root spec.
- Replace invalid characters with with underscores.

This is an injective mapping over a "reasonable" amount of the possible
openapi spec domain space but is not perfect. There might be edge cases.

func DefineIPv4Format()
DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
Expand Down Expand Up @@ -1171,7 +1184,12 @@ type Ref struct {
Ref is specified by OpenAPI/Swagger 3.0 standard. See
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object

type RefNameResolver func(string) string
type RefNameResolver func(*T, componentRef) string
RefNameResolver maps a component to an name that is used as it's
internalized name.

The function should avoid name collisions (i.e. be a injective mapping). It
must only contain characters valid for fixed field names: IdentifierRegExp.

type RequestBodies map[string]*RequestBodyRef

Expand Down Expand Up @@ -1922,7 +1940,7 @@ func (doc *T) AddServer(server *Server)

func (doc *T) AddServers(servers ...*Server)

func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string)
func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, componentRef) string)
InternalizeRefs removes all references to external files from the spec and
moves them to the components section.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ for _, path := range doc.Paths.InMatchingOrder() {

### v0.126.0
* `openapi3.CircularReferenceError` and `openapi3.CircularReferenceCounter` are removed. `openapi3.Loader` now implements reference backtracking, so any kind of circular references should be properly resolved.
* `InternalizeRefs` now takes a refNameResolver that has access to `openapi3.T` and more properties of the reference needing resolving.
* The `DefaultRefNameResolver` has been updated, choosing names that will be less likely to collide with each other. Because of this internalized specs will likely change slightly.

### v0.125.0
* The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument.
Expand Down
35 changes: 30 additions & 5 deletions openapi3/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,29 @@ import (
"path"
"reflect"
"regexp"
"sort"
"strings"

"github.com/go-openapi/jsonpointer"
)

const identifierPattern = `^[a-zA-Z0-9._-]+$`
const identifierChars = `a-zA-Z0-9._-`

// IdentifierRegExp verifies whether Component object key matches 'identifierPattern' pattern, according to OpenAPI v3.x.
// IdentifierRegExp verifies whether Component object key matches contains just 'identifierChars', according to OpenAPI v3.x.
// InvalidIdentifierCharRegExp matches all characters not contained in 'identifierChars'.
// However, to be able supporting legacy OpenAPI v2.x, there is a need to customize above pattern in order not to fail
// converted v2-v3 validation
var IdentifierRegExp = regexp.MustCompile(identifierPattern)
var (
IdentifierRegExp = regexp.MustCompile(`^[` + identifierChars + `]+$`)
InvalidIdentifierCharRegExp = regexp.MustCompile(`[^` + identifierChars + `]`)
)

// ValidateIdentifier returns an error if the given component name does not match IdentifierRegExp.
// ValidateIdentifier returns an error if the given component name does not match [IdentifierRegExp].
func ValidateIdentifier(value string) error {
if IdentifierRegExp.MatchString(value) {
return nil
}
return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern)
return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (charset: [%q])", value, identifierChars)
}

// Float64Ptr is a helper for defining OpenAPI schemas.
Expand All @@ -46,6 +51,26 @@ func Uint64Ptr(value uint64) *uint64 {
return &value
}

// componentNames returns the map keys in a sorted slice.
func componentNames[E any](s map[string]E) []string {
out := make([]string, 0, len(s))
for i := range s {
out = append(out, i)
}
sort.Strings(out)
return out
}

// copyURI makes a copy of the pointer.
func copyURI(u *url.URL) *url.URL {
if u == nil {
return nil
}

c := *u // shallow-copy
return &c
}

type componentRef interface {
RefString() string
RefPath() *url.URL
Expand Down
Loading
Loading