210 lines
5.3 KiB
Go
210 lines
5.3 KiB
Go
package openapi3
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Paths is specified by OpenAPI/Swagger standard version 3.
|
|
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
|
|
type Paths map[string]*PathItem
|
|
|
|
// Validate returns an error if Paths does not comply with the OpenAPI spec.
|
|
func (paths Paths) Validate(ctx context.Context) error {
|
|
normalizedPaths := make(map[string]string, len(paths))
|
|
|
|
keys := make([]string, 0, len(paths))
|
|
for key := range paths {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, path := range keys {
|
|
pathItem := paths[path]
|
|
if path == "" || path[0] != '/' {
|
|
return fmt.Errorf("path %q does not start with a forward slash (/)", path)
|
|
}
|
|
|
|
if pathItem == nil {
|
|
paths[path] = &PathItem{}
|
|
pathItem = paths[path]
|
|
}
|
|
|
|
normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
|
|
if oldPath, ok := normalizedPaths[normalizedPath]; ok {
|
|
return fmt.Errorf("conflicting paths %q and %q", path, oldPath)
|
|
}
|
|
normalizedPaths[path] = path
|
|
|
|
var commonParams []string
|
|
for _, parameterRef := range pathItem.Parameters {
|
|
if parameterRef != nil {
|
|
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
|
|
commonParams = append(commonParams, parameter.Name)
|
|
}
|
|
}
|
|
}
|
|
operations := pathItem.Operations()
|
|
methods := make([]string, 0, len(operations))
|
|
for method := range operations {
|
|
methods = append(methods, method)
|
|
}
|
|
sort.Strings(methods)
|
|
for _, method := range methods {
|
|
operation := operations[method]
|
|
var setParams []string
|
|
for _, parameterRef := range operation.Parameters {
|
|
if parameterRef != nil {
|
|
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
|
|
setParams = append(setParams, parameter.Name)
|
|
}
|
|
}
|
|
}
|
|
if expected := len(setParams) + len(commonParams); expected != len(varsInPath) {
|
|
expected -= len(varsInPath)
|
|
if expected < 0 {
|
|
expected *= -1
|
|
}
|
|
missing := make(map[string]struct{}, expected)
|
|
definedParams := append(setParams, commonParams...)
|
|
for _, name := range definedParams {
|
|
if _, ok := varsInPath[name]; !ok {
|
|
missing[name] = struct{}{}
|
|
}
|
|
}
|
|
for name := range varsInPath {
|
|
got := false
|
|
for _, othername := range definedParams {
|
|
if othername == name {
|
|
got = true
|
|
break
|
|
}
|
|
}
|
|
if !got {
|
|
missing[name] = struct{}{}
|
|
}
|
|
}
|
|
if len(missing) != 0 {
|
|
missings := make([]string, 0, len(missing))
|
|
for name := range missing {
|
|
missings = append(missings, name)
|
|
}
|
|
return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := pathItem.Validate(ctx); err != nil {
|
|
return fmt.Errorf("invalid path %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
if err := paths.validateUniqueOperationIDs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Find returns a path that matches the key.
|
|
//
|
|
// The method ignores differences in template variable names (except possible "*" suffix).
|
|
//
|
|
// For example:
|
|
//
|
|
// paths := openapi3.Paths {
|
|
// "/person/{personName}": &openapi3.PathItem{},
|
|
// }
|
|
// pathItem := path.Find("/person/{name}")
|
|
//
|
|
// would return the correct path item.
|
|
func (paths Paths) Find(key string) *PathItem {
|
|
// Try directly access the map
|
|
pathItem := paths[key]
|
|
if pathItem != nil {
|
|
return pathItem
|
|
}
|
|
|
|
normalizedPath, expected, _ := normalizeTemplatedPath(key)
|
|
for path, pathItem := range paths {
|
|
pathNormalized, got, _ := normalizeTemplatedPath(path)
|
|
if got == expected && pathNormalized == normalizedPath {
|
|
return pathItem
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (paths Paths) validateUniqueOperationIDs() error {
|
|
operationIDs := make(map[string]string)
|
|
for urlPath, pathItem := range paths {
|
|
if pathItem == nil {
|
|
continue
|
|
}
|
|
for httpMethod, operation := range pathItem.Operations() {
|
|
if operation == nil || operation.OperationID == "" {
|
|
continue
|
|
}
|
|
endpoint := httpMethod + " " + urlPath
|
|
if endpointDup, ok := operationIDs[operation.OperationID]; ok {
|
|
if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
|
|
endpoint, endpointDup = endpointDup, endpoint
|
|
}
|
|
return fmt.Errorf("operations %q and %q have the same operation id %q",
|
|
endpoint, endpointDup, operation.OperationID)
|
|
}
|
|
operationIDs[operation.OperationID] = endpoint
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
|
|
if strings.IndexByte(path, '{') < 0 {
|
|
return path, 0, nil
|
|
}
|
|
|
|
var buffTpl strings.Builder
|
|
buffTpl.Grow(len(path))
|
|
|
|
var (
|
|
cc rune
|
|
count uint
|
|
isVariable bool
|
|
vars = make(map[string]struct{})
|
|
buffVar strings.Builder
|
|
)
|
|
for i, c := range path {
|
|
if isVariable {
|
|
if c == '}' {
|
|
// End path variable
|
|
isVariable = false
|
|
|
|
vars[buffVar.String()] = struct{}{}
|
|
buffVar = strings.Builder{}
|
|
|
|
// First append possible '*' before this character
|
|
// The character '}' will be appended
|
|
if i > 0 && cc == '*' {
|
|
buffTpl.WriteRune(cc)
|
|
}
|
|
} else {
|
|
buffVar.WriteRune(c)
|
|
continue
|
|
}
|
|
|
|
} else if c == '{' {
|
|
// Begin path variable
|
|
isVariable = true
|
|
|
|
// The character '{' will be appended
|
|
count++
|
|
}
|
|
|
|
// Append the character
|
|
buffTpl.WriteRune(c)
|
|
cc = c
|
|
}
|
|
return buffTpl.String(), count, vars
|
|
}
|