Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame^] | 1 | package restful |
| 2 | |
| 3 | // Copyright 2013 Ernest Micklei. All rights reserved. |
| 4 | // Use of this source code is governed by a license |
| 5 | // that can be found in the LICENSE file. |
| 6 | |
| 7 | import ( |
| 8 | "net/http" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | // RouteFunction declares the signature of a function that can be bound to a Route. |
| 13 | type RouteFunction func(*Request, *Response) |
| 14 | |
| 15 | // RouteSelectionConditionFunction declares the signature of a function that |
| 16 | // can be used to add extra conditional logic when selecting whether the route |
| 17 | // matches the HTTP request. |
| 18 | type RouteSelectionConditionFunction func(httpRequest *http.Request) bool |
| 19 | |
| 20 | // Route binds a HTTP Method,Path,Consumes combination to a RouteFunction. |
| 21 | type Route struct { |
| 22 | Method string |
| 23 | Produces []string |
| 24 | Consumes []string |
| 25 | Path string // webservice root path + described path |
| 26 | Function RouteFunction |
| 27 | Filters []FilterFunction |
| 28 | If []RouteSelectionConditionFunction |
| 29 | |
| 30 | // cached values for dispatching |
| 31 | relativePath string |
| 32 | pathParts []string |
| 33 | pathExpr *pathExpression // cached compilation of relativePath as RegExp |
| 34 | |
| 35 | // documentation |
| 36 | Doc string |
| 37 | Notes string |
| 38 | Operation string |
| 39 | ParameterDocs []*Parameter |
| 40 | ResponseErrors map[int]ResponseError |
| 41 | ReadSample, WriteSample interface{} // structs that model an example request or response payload |
| 42 | |
| 43 | // Extra information used to store custom information about the route. |
| 44 | Metadata map[string]interface{} |
| 45 | |
| 46 | // marks a route as deprecated |
| 47 | Deprecated bool |
| 48 | } |
| 49 | |
| 50 | // Initialize for Route |
| 51 | func (r *Route) postBuild() { |
| 52 | r.pathParts = tokenizePath(r.Path) |
| 53 | } |
| 54 | |
| 55 | // Create Request and Response from their http versions |
| 56 | func (r *Route) wrapRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request, pathParams map[string]string) (*Request, *Response) { |
| 57 | wrappedRequest := NewRequest(httpRequest) |
| 58 | wrappedRequest.pathParameters = pathParams |
| 59 | wrappedRequest.selectedRoutePath = r.Path |
| 60 | wrappedResponse := NewResponse(httpWriter) |
| 61 | wrappedResponse.requestAccept = httpRequest.Header.Get(HEADER_Accept) |
| 62 | wrappedResponse.routeProduces = r.Produces |
| 63 | return wrappedRequest, wrappedResponse |
| 64 | } |
| 65 | |
| 66 | // dispatchWithFilters call the function after passing through its own filters |
| 67 | func (r *Route) dispatchWithFilters(wrappedRequest *Request, wrappedResponse *Response) { |
| 68 | if len(r.Filters) > 0 { |
| 69 | chain := FilterChain{Filters: r.Filters, Target: r.Function} |
| 70 | chain.ProcessFilter(wrappedRequest, wrappedResponse) |
| 71 | } else { |
| 72 | // unfiltered |
| 73 | r.Function(wrappedRequest, wrappedResponse) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Return whether the mimeType matches to what this Route can produce. |
| 78 | func (r Route) matchesAccept(mimeTypesWithQuality string) bool { |
| 79 | parts := strings.Split(mimeTypesWithQuality, ",") |
| 80 | for _, each := range parts { |
| 81 | var withoutQuality string |
| 82 | if strings.Contains(each, ";") { |
| 83 | withoutQuality = strings.Split(each, ";")[0] |
| 84 | } else { |
| 85 | withoutQuality = each |
| 86 | } |
| 87 | // trim before compare |
| 88 | withoutQuality = strings.Trim(withoutQuality, " ") |
| 89 | if withoutQuality == "*/*" { |
| 90 | return true |
| 91 | } |
| 92 | for _, producibleType := range r.Produces { |
| 93 | if producibleType == "*/*" || producibleType == withoutQuality { |
| 94 | return true |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | return false |
| 99 | } |
| 100 | |
| 101 | // Return whether this Route can consume content with a type specified by mimeTypes (can be empty). |
| 102 | func (r Route) matchesContentType(mimeTypes string) bool { |
| 103 | |
| 104 | if len(r.Consumes) == 0 { |
| 105 | // did not specify what it can consume ; any media type (“*/*”) is assumed |
| 106 | return true |
| 107 | } |
| 108 | |
| 109 | if len(mimeTypes) == 0 { |
| 110 | // idempotent methods with (most-likely or guaranteed) empty content match missing Content-Type |
| 111 | m := r.Method |
| 112 | if m == "GET" || m == "HEAD" || m == "OPTIONS" || m == "DELETE" || m == "TRACE" { |
| 113 | return true |
| 114 | } |
| 115 | // proceed with default |
| 116 | mimeTypes = MIME_OCTET |
| 117 | } |
| 118 | |
| 119 | parts := strings.Split(mimeTypes, ",") |
| 120 | for _, each := range parts { |
| 121 | var contentType string |
| 122 | if strings.Contains(each, ";") { |
| 123 | contentType = strings.Split(each, ";")[0] |
| 124 | } else { |
| 125 | contentType = each |
| 126 | } |
| 127 | // trim before compare |
| 128 | contentType = strings.Trim(contentType, " ") |
| 129 | for _, consumeableType := range r.Consumes { |
| 130 | if consumeableType == "*/*" || consumeableType == contentType { |
| 131 | return true |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | return false |
| 136 | } |
| 137 | |
| 138 | // Tokenize an URL path using the slash separator ; the result does not have empty tokens |
| 139 | func tokenizePath(path string) []string { |
| 140 | if "/" == path { |
| 141 | return []string{} |
| 142 | } |
| 143 | return strings.Split(strings.Trim(path, "/"), "/") |
| 144 | } |
| 145 | |
| 146 | // for debugging |
| 147 | func (r Route) String() string { |
| 148 | return r.Method + " " + r.Path |
| 149 | } |