1// Package legacy implements a router.
2//
3// It differs from the gorilla/mux router:
4// * it provides granular errors: "path not found", "method not allowed", "variable missing from path"
5// * it does not handle matching routes with extensions (e.g. /books/{id}.json)
6// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*})
7package legacy
8
9import (
10	"context"
11	"errors"
12	"fmt"
13	"net/http"
14	"strings"
15
16	"github.com/getkin/kin-openapi/openapi3"
17	"github.com/getkin/kin-openapi/routers"
18	"github.com/getkin/kin-openapi/routers/legacy/pathpattern"
19)
20
21// Routers maps a HTTP request to a Router.
22type Routers []*Router
23
24// FindRoute extracts the route and parameters of an http.Request
25func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) {
26	for _, router := range rs {
27		// Skip routers that have DO NOT have servers
28		if len(router.doc.Servers) == 0 {
29			continue
30		}
31		route, pathParams, err := router.FindRoute(req)
32		if err == nil {
33			return router, route, pathParams, nil
34		}
35	}
36	for _, router := range rs {
37		// Skip routers that DO have servers
38		if len(router.doc.Servers) > 0 {
39			continue
40		}
41		route, pathParams, err := router.FindRoute(req)
42		if err == nil {
43			return router, route, pathParams, nil
44		}
45	}
46	return nil, nil, nil, &routers.RouteError{
47		Reason: "none of the routers match",
48	}
49}
50
51// Router maps a HTTP request to an OpenAPI operation.
52type Router struct {
53	doc      *openapi3.T
54	pathNode *pathpattern.Node
55}
56
57// NewRouter creates a new router.
58//
59// If the given OpenAPIv3 document has servers, router will use them.
60// All operations of the document will be added to the router.
61func NewRouter(doc *openapi3.T) (routers.Router, error) {
62	if err := doc.Validate(context.Background()); err != nil {
63		return nil, fmt.Errorf("validating OpenAPI failed: %v", err)
64	}
65	router := &Router{doc: doc}
66	root := router.node()
67	for path, pathItem := range doc.Paths {
68		for method, operation := range pathItem.Operations() {
69			method = strings.ToUpper(method)
70			if err := root.Add(method+" "+path, &routers.Route{
71				Spec:      doc,
72				Path:      path,
73				PathItem:  pathItem,
74				Method:    method,
75				Operation: operation,
76			}, nil); err != nil {
77				return nil, err
78			}
79		}
80	}
81	return router, nil
82}
83
84// AddRoute adds a route in the router.
85func (router *Router) AddRoute(route *routers.Route) error {
86	method := route.Method
87	if method == "" {
88		return errors.New("route is missing method")
89	}
90	method = strings.ToUpper(method)
91	path := route.Path
92	if path == "" {
93		return errors.New("route is missing path")
94	}
95	return router.node().Add(method+" "+path, router, nil)
96}
97
98func (router *Router) node() *pathpattern.Node {
99	root := router.pathNode
100	if root == nil {
101		root = &pathpattern.Node{}
102		router.pathNode = root
103	}
104	return root
105}
106
107// FindRoute extracts the route and parameters of an http.Request
108func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
109	method, url := req.Method, req.URL
110	doc := router.doc
111
112	// Get server
113	servers := doc.Servers
114	var server *openapi3.Server
115	var remainingPath string
116	var pathParams map[string]string
117	if len(servers) == 0 {
118		remainingPath = url.Path
119	} else {
120		var paramValues []string
121		server, paramValues, remainingPath = servers.MatchURL(url)
122		if server == nil {
123			return nil, nil, &routers.RouteError{
124				Reason: routers.ErrPathNotFound.Error(),
125			}
126		}
127		pathParams = make(map[string]string, 8)
128		paramNames, err := server.ParameterNames()
129		if err != nil {
130			return nil, nil, err
131		}
132		for i, value := range paramValues {
133			name := paramNames[i]
134			pathParams[name] = value
135		}
136	}
137
138	// Get PathItem
139	root := router.node()
140	var route *routers.Route
141	node, paramValues := root.Match(method + " " + remainingPath)
142	if node != nil {
143		route, _ = node.Value.(*routers.Route)
144	}
145	if route == nil {
146		pathItem := doc.Paths[remainingPath]
147		if pathItem == nil {
148			return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()}
149		}
150		if pathItem.GetOperation(method) == nil {
151			return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()}
152		}
153	}
154
155	if pathParams == nil {
156		pathParams = make(map[string]string, len(paramValues))
157	}
158	paramKeys := node.VariableNames
159	for i, value := range paramValues {
160		key := paramKeys[i]
161		if strings.HasSuffix(key, "*") {
162			key = key[:len(key)-1]
163		}
164		pathParams[key] = value
165	}
166	return route, pathParams, nil
167}
168