2022-05-05 02:47:06 +00:00
|
|
|
package restful
|
|
|
|
|
|
|
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
|
|
|
// Use of this source code is governed by a license
|
|
|
|
// that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// RouteFunction declares the signature of a function that can be bound to a Route.
|
|
|
|
type RouteFunction func(*Request, *Response)
|
|
|
|
|
|
|
|
// RouteSelectionConditionFunction declares the signature of a function that
|
|
|
|
// can be used to add extra conditional logic when selecting whether the route
|
|
|
|
// matches the HTTP request.
|
|
|
|
type RouteSelectionConditionFunction func(httpRequest *http.Request) bool
|
|
|
|
|
|
|
|
// Route binds a HTTP Method,Path,Consumes combination to a RouteFunction.
|
|
|
|
type Route struct {
|
2022-08-24 02:24:25 +00:00
|
|
|
ExtensionProperties
|
2022-05-05 02:47:06 +00:00
|
|
|
Method string
|
|
|
|
Produces []string
|
|
|
|
Consumes []string
|
|
|
|
Path string // webservice root path + described path
|
|
|
|
Function RouteFunction
|
|
|
|
Filters []FilterFunction
|
|
|
|
If []RouteSelectionConditionFunction
|
|
|
|
|
|
|
|
// cached values for dispatching
|
|
|
|
relativePath string
|
|
|
|
pathParts []string
|
|
|
|
pathExpr *pathExpression // cached compilation of relativePath as RegExp
|
|
|
|
|
|
|
|
// documentation
|
|
|
|
Doc string
|
|
|
|
Notes string
|
|
|
|
Operation string
|
|
|
|
ParameterDocs []*Parameter
|
|
|
|
ResponseErrors map[int]ResponseError
|
|
|
|
DefaultResponse *ResponseError
|
2023-10-25 09:53:23 +00:00
|
|
|
ReadSample, WriteSample interface{} // structs that model an example request or response payload
|
|
|
|
WriteSamples []interface{} // if more than one return types is possible (oneof) then this will contain multiple values
|
2022-05-05 02:47:06 +00:00
|
|
|
|
|
|
|
// Extra information used to store custom information about the route.
|
|
|
|
Metadata map[string]interface{}
|
|
|
|
|
|
|
|
// marks a route as deprecated
|
|
|
|
Deprecated bool
|
|
|
|
|
|
|
|
//Overrides the container.contentEncodingEnabled
|
|
|
|
contentEncodingEnabled *bool
|
2022-08-24 02:24:25 +00:00
|
|
|
|
|
|
|
// indicate route path has custom verb
|
|
|
|
hasCustomVerb bool
|
|
|
|
|
|
|
|
// if a request does not include a content-type header then
|
|
|
|
// depending on the method, it may return a 415 Unsupported Media
|
|
|
|
// Must have uppercase HTTP Method names such as GET,HEAD,OPTIONS,...
|
|
|
|
allowedMethodsWithoutContentType []string
|
2022-05-05 02:47:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize for Route
|
|
|
|
func (r *Route) postBuild() {
|
|
|
|
r.pathParts = tokenizePath(r.Path)
|
2022-08-24 02:24:25 +00:00
|
|
|
r.hasCustomVerb = hasCustomVerb(r.Path)
|
2022-05-05 02:47:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create Request and Response from their http versions
|
|
|
|
func (r *Route) wrapRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request, pathParams map[string]string) (*Request, *Response) {
|
|
|
|
wrappedRequest := NewRequest(httpRequest)
|
|
|
|
wrappedRequest.pathParameters = pathParams
|
2022-08-24 02:24:25 +00:00
|
|
|
wrappedRequest.selectedRoute = r
|
2022-05-05 02:47:06 +00:00
|
|
|
wrappedResponse := NewResponse(httpWriter)
|
|
|
|
wrappedResponse.requestAccept = httpRequest.Header.Get(HEADER_Accept)
|
|
|
|
wrappedResponse.routeProduces = r.Produces
|
|
|
|
return wrappedRequest, wrappedResponse
|
|
|
|
}
|
|
|
|
|
|
|
|
func stringTrimSpaceCutset(r rune) bool {
|
|
|
|
return r == ' '
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return whether the mimeType matches to what this Route can produce.
|
|
|
|
func (r Route) matchesAccept(mimeTypesWithQuality string) bool {
|
|
|
|
remaining := mimeTypesWithQuality
|
|
|
|
for {
|
|
|
|
var mimeType string
|
|
|
|
if end := strings.Index(remaining, ","); end == -1 {
|
|
|
|
mimeType, remaining = remaining, ""
|
|
|
|
} else {
|
|
|
|
mimeType, remaining = remaining[:end], remaining[end+1:]
|
|
|
|
}
|
|
|
|
if quality := strings.Index(mimeType, ";"); quality != -1 {
|
|
|
|
mimeType = mimeType[:quality]
|
|
|
|
}
|
|
|
|
mimeType = strings.TrimFunc(mimeType, stringTrimSpaceCutset)
|
|
|
|
if mimeType == "*/*" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for _, producibleType := range r.Produces {
|
|
|
|
if producibleType == "*/*" || producibleType == mimeType {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(remaining) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return whether this Route can consume content with a type specified by mimeTypes (can be empty).
|
|
|
|
func (r Route) matchesContentType(mimeTypes string) bool {
|
|
|
|
|
|
|
|
if len(r.Consumes) == 0 {
|
|
|
|
// did not specify what it can consume ; any media type (“*/*”) is assumed
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(mimeTypes) == 0 {
|
|
|
|
// idempotent methods with (most-likely or guaranteed) empty content match missing Content-Type
|
|
|
|
m := r.Method
|
2022-08-24 02:24:25 +00:00
|
|
|
// if route specifies less or non-idempotent methods then use that
|
|
|
|
if len(r.allowedMethodsWithoutContentType) > 0 {
|
|
|
|
for _, each := range r.allowedMethodsWithoutContentType {
|
|
|
|
if m == each {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if m == "GET" || m == "HEAD" || m == "OPTIONS" || m == "DELETE" || m == "TRACE" {
|
|
|
|
return true
|
|
|
|
}
|
2022-05-05 02:47:06 +00:00
|
|
|
}
|
|
|
|
// proceed with default
|
|
|
|
mimeTypes = MIME_OCTET
|
|
|
|
}
|
|
|
|
|
|
|
|
remaining := mimeTypes
|
|
|
|
for {
|
|
|
|
var mimeType string
|
|
|
|
if end := strings.Index(remaining, ","); end == -1 {
|
|
|
|
mimeType, remaining = remaining, ""
|
|
|
|
} else {
|
|
|
|
mimeType, remaining = remaining[:end], remaining[end+1:]
|
|
|
|
}
|
|
|
|
if quality := strings.Index(mimeType, ";"); quality != -1 {
|
|
|
|
mimeType = mimeType[:quality]
|
|
|
|
}
|
|
|
|
mimeType = strings.TrimFunc(mimeType, stringTrimSpaceCutset)
|
|
|
|
for _, consumeableType := range r.Consumes {
|
|
|
|
if consumeableType == "*/*" || consumeableType == mimeType {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(remaining) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tokenize an URL path using the slash separator ; the result does not have empty tokens
|
|
|
|
func tokenizePath(path string) []string {
|
|
|
|
if "/" == path {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-25 09:53:23 +00:00
|
|
|
if TrimRightSlashEnabled {
|
|
|
|
// 3.9.0
|
|
|
|
return strings.Split(strings.Trim(path, "/"), "/")
|
|
|
|
} else {
|
|
|
|
// 3.10.2
|
|
|
|
return strings.Split(strings.TrimLeft(path, "/"), "/")
|
|
|
|
}
|
2022-05-05 02:47:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// for debugging
|
2022-08-24 02:24:25 +00:00
|
|
|
func (r *Route) String() string {
|
2022-05-05 02:47:06 +00:00
|
|
|
return r.Method + " " + r.Path
|
|
|
|
}
|
|
|
|
|
|
|
|
// EnableContentEncoding (default=false) allows for GZIP or DEFLATE encoding of responses. Overrides the container.contentEncodingEnabled value.
|
2022-08-24 02:24:25 +00:00
|
|
|
func (r *Route) EnableContentEncoding(enabled bool) {
|
2022-05-05 02:47:06 +00:00
|
|
|
r.contentEncodingEnabled = &enabled
|
|
|
|
}
|
2023-09-18 20:36:03 +00:00
|
|
|
|
2023-10-25 09:53:23 +00:00
|
|
|
// TrimRightSlashEnabled controls whether
|
|
|
|
// - path on route building is using path.Join
|
|
|
|
// - the path of the incoming request is trimmed of its slash suffux.
|
|
|
|
// Value of true matches the behavior of <= 3.9.0
|
|
|
|
var TrimRightSlashEnabled = true
|