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, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) 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 { pathItem = &PathItem{} paths[path] = pathItem } 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 } // 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). // // 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 }