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: introduce (Paths).InMatchingOrder() paths iterator #719

Merged
merged 3 commits into from Dec 19, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Expand Up @@ -105,7 +105,7 @@ jobs:
- if: runner.os == 'Linux'
name: Missing specification object link to definition
run: |
[[ 30 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]]
[[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]]

- if: runner.os == 'Linux'
name: Style around ExtensionProps embedding
Expand Down
10 changes: 4 additions & 6 deletions openapi2/openapi2.go
Expand Up @@ -40,15 +40,13 @@ func (doc *T) UnmarshalJSON(data []byte) error {
}

func (doc *T) AddOperation(path string, method string, operation *Operation) {
paths := doc.Paths
if paths == nil {
paths = make(map[string]*PathItem)
doc.Paths = paths
if doc.Paths == nil {
doc.Paths = make(map[string]*PathItem)
}
pathItem := paths[path]
pathItem := doc.Paths[path]
if pathItem == nil {
pathItem = &PathItem{}
paths[path] = pathItem
doc.Paths[path] = pathItem
}
pathItem.SetOperation(method, operation)
}
Expand Down
10 changes: 4 additions & 6 deletions openapi2conv/openapi2_conv.go
Expand Up @@ -1204,15 +1204,13 @@ func stripNonCustomExtensions(extensions map[string]interface{}) {
}

func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) {
paths := doc2.Paths
if paths == nil {
paths = make(map[string]*openapi2.PathItem)
doc2.Paths = paths
if doc2.Paths == nil {
doc2.Paths = make(map[string]*openapi2.PathItem)
}
pathItem := paths[path]
pathItem := doc2.Paths[path]
if pathItem == nil {
pathItem = &openapi2.PathItem{}
paths[path] = pathItem
doc2.Paths[path] = pathItem
}
pathItem.ExtensionProps = extensionProps
}
5 changes: 1 addition & 4 deletions openapi3/internalize_refs.go
Expand Up @@ -193,14 +193,11 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver,
return false
}
name := refNameResolver(c.Ref)
if _, ok := doc.Components.Callbacks[name]; ok {
c.Ref = "#/components/callbacks/" + name
}
if doc.Components.Callbacks == nil {
doc.Components.Callbacks = make(Callbacks)
}
doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
c.Ref = "#/components/callbacks/" + name
doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
return true
}

Expand Down
18 changes: 5 additions & 13 deletions openapi3/loader.go
Expand Up @@ -281,12 +281,7 @@ func isSingleRefElement(ref string) bool {
return !strings.Contains(ref, "#")
}

func (loader *Loader) resolveComponent(
doc *T,
ref string,
path *url.URL,
resolved interface{},
) (
func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved interface{}) (
componentDoc *T,
componentPath *url.URL,
err error,
Expand Down Expand Up @@ -928,11 +923,10 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
}
id := unescapeRefString(rest)

definitions := doc.Components.Callbacks
if definitions == nil {
if doc.Components.Callbacks == nil {
return failedToResolveRefFragmentPart(ref, "callbacks")
}
resolved := definitions[id]
resolved := doc.Components.Callbacks[id]
if resolved == nil {
return failedToResolveRefFragmentPart(ref, id)
}
Expand Down Expand Up @@ -1022,15 +1016,13 @@ func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *Pa
}
id := unescapeRefString(rest)

definitions := doc.Paths
if definitions == nil {
if doc.Paths == nil {
return failedToResolveRefFragmentPart(ref, "paths")
}
resolved := definitions[id]
resolved := doc.Paths[id]
if resolved == nil {
return failedToResolveRefFragmentPart(ref, id)
}

*pathItem = *resolved
}
}
Expand Down
10 changes: 4 additions & 6 deletions openapi3/openapi3.go
Expand Up @@ -36,15 +36,13 @@ func (doc *T) UnmarshalJSON(data []byte) error {
}

func (doc *T) AddOperation(path string, method string, operation *Operation) {
paths := doc.Paths
if paths == nil {
paths = make(Paths)
doc.Paths = paths
if doc.Paths == nil {
doc.Paths = make(Paths)
}
pathItem := paths[path]
pathItem := doc.Paths[path]
if pathItem == nil {
pathItem = &PathItem{}
paths[path] = pathItem
doc.Paths[path] = pathItem
}
pathItem.SetOperation(method, operation)
}
Expand Down
35 changes: 33 additions & 2 deletions openapi3/paths.go
Expand Up @@ -29,8 +29,8 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error
}

if pathItem == nil {
paths[path] = &PathItem{}
pathItem = paths[path]
pathItem = &PathItem{}
paths[path] = pathItem
}

normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
Expand Down Expand Up @@ -109,6 +109,37 @@ func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error
return nil
}

// InMatchingOrder returns paths in the order they are matched against URLs.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
// When matching URLs, concrete (non-templated) paths would be matched
// before their templated counterparts.
func (paths Paths) InMatchingOrder() []string {
// NOTE: sorting by number of variables ASC then by descending lexicographical
// order seems to be a good heuristic.
if paths == nil {
return nil
}

vars := make(map[int][]string)
max := 0
for path := range paths {
count := strings.Count(path, "}")
vars[count] = append(vars[count], path)
if count > max {
max = count
}
}

ordered := make([]string, 0, len(paths))
for c := 0; c <= max; c++ {
if ps, ok := vars[c]; ok {
sort.Sort(sort.Reverse(sort.StringSlice(ps)))
ordered = append(ordered, ps...)
}
}
return ordered
}

// Find returns a path that matches the key.
//
// The method ignores differences in template variable names (except possible "*" suffix).
Expand Down
27 changes: 1 addition & 26 deletions routers/gorillamux/router.go
Expand Up @@ -56,7 +56,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) {

muxRouter := mux.NewRouter().UseEncodedPath()
r := &Router{}
for _, path := range orderedPaths(doc.Paths) {
for _, path := range doc.Paths.InMatchingOrder() {
pathItem := doc.Paths[path]
if len(pathItem.Servers) > 0 {
if servers, err = makeServers(pathItem.Servers); err != nil {
Expand Down Expand Up @@ -203,31 +203,6 @@ func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv,
return svr, nil
}

func orderedPaths(paths map[string]*openapi3.PathItem) []string {
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject
// When matching URLs, concrete (non-templated) paths would be matched
// before their templated counterparts.
// NOTE: sorting by number of variables ASC then by descending lexicographical
// order seems to be a good heuristic.
vars := make(map[int][]string)
max := 0
for path := range paths {
count := strings.Count(path, "}")
vars[count] = append(vars[count], path)
if count > max {
max = count
}
}
ordered := make([]string, 0, len(paths))
for c := 0; c <= max; c++ {
if ps, ok := vars[c]; ok {
sort.Sort(sort.Reverse(sort.StringSlice(ps)))
ordered = append(ordered, ps...)
}
}
return ordered
}

// Magic strings that temporarily replace "{}" so net/url.Parse() works
var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50)

Expand Down