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 | "fmt" |
| 9 | "os" |
| 10 | "reflect" |
| 11 | "runtime" |
| 12 | "strings" |
| 13 | "sync/atomic" |
| 14 | |
| 15 | "github.com/emicklei/go-restful/log" |
| 16 | ) |
| 17 | |
| 18 | // RouteBuilder is a helper to construct Routes. |
| 19 | type RouteBuilder struct { |
| 20 | rootPath string |
| 21 | currentPath string |
| 22 | produces []string |
| 23 | consumes []string |
| 24 | httpMethod string // required |
| 25 | function RouteFunction // required |
| 26 | filters []FilterFunction |
| 27 | conditions []RouteSelectionConditionFunction |
| 28 | |
| 29 | typeNameHandleFunc TypeNameHandleFunction // required |
| 30 | |
| 31 | // documentation |
| 32 | doc string |
| 33 | notes string |
| 34 | operation string |
| 35 | readSample, writeSample interface{} |
| 36 | parameters []*Parameter |
| 37 | errorMap map[int]ResponseError |
| 38 | metadata map[string]interface{} |
| 39 | deprecated bool |
| 40 | } |
| 41 | |
| 42 | // Do evaluates each argument with the RouteBuilder itself. |
| 43 | // This allows you to follow DRY principles without breaking the fluent programming style. |
| 44 | // Example: |
| 45 | // ws.Route(ws.DELETE("/{name}").To(t.deletePerson).Do(Returns200, Returns500)) |
| 46 | // |
| 47 | // func Returns500(b *RouteBuilder) { |
| 48 | // b.Returns(500, "Internal Server Error", restful.ServiceError{}) |
| 49 | // } |
| 50 | func (b *RouteBuilder) Do(oneArgBlocks ...func(*RouteBuilder)) *RouteBuilder { |
| 51 | for _, each := range oneArgBlocks { |
| 52 | each(b) |
| 53 | } |
| 54 | return b |
| 55 | } |
| 56 | |
| 57 | // To bind the route to a function. |
| 58 | // If this route is matched with the incoming Http Request then call this function with the *Request,*Response pair. Required. |
| 59 | func (b *RouteBuilder) To(function RouteFunction) *RouteBuilder { |
| 60 | b.function = function |
| 61 | return b |
| 62 | } |
| 63 | |
| 64 | // Method specifies what HTTP method to match. Required. |
| 65 | func (b *RouteBuilder) Method(method string) *RouteBuilder { |
| 66 | b.httpMethod = method |
| 67 | return b |
| 68 | } |
| 69 | |
| 70 | // Produces specifies what MIME types can be produced ; the matched one will appear in the Content-Type Http header. |
| 71 | func (b *RouteBuilder) Produces(mimeTypes ...string) *RouteBuilder { |
| 72 | b.produces = mimeTypes |
| 73 | return b |
| 74 | } |
| 75 | |
| 76 | // Consumes specifies what MIME types can be consumes ; the Accept Http header must matched any of these |
| 77 | func (b *RouteBuilder) Consumes(mimeTypes ...string) *RouteBuilder { |
| 78 | b.consumes = mimeTypes |
| 79 | return b |
| 80 | } |
| 81 | |
| 82 | // Path specifies the relative (w.r.t WebService root path) URL path to match. Default is "/". |
| 83 | func (b *RouteBuilder) Path(subPath string) *RouteBuilder { |
| 84 | b.currentPath = subPath |
| 85 | return b |
| 86 | } |
| 87 | |
| 88 | // Doc tells what this route is all about. Optional. |
| 89 | func (b *RouteBuilder) Doc(documentation string) *RouteBuilder { |
| 90 | b.doc = documentation |
| 91 | return b |
| 92 | } |
| 93 | |
| 94 | // Notes is a verbose explanation of the operation behavior. Optional. |
| 95 | func (b *RouteBuilder) Notes(notes string) *RouteBuilder { |
| 96 | b.notes = notes |
| 97 | return b |
| 98 | } |
| 99 | |
| 100 | // Reads tells what resource type will be read from the request payload. Optional. |
| 101 | // A parameter of type "body" is added ,required is set to true and the dataType is set to the qualified name of the sample's type. |
| 102 | func (b *RouteBuilder) Reads(sample interface{}, optionalDescription ...string) *RouteBuilder { |
| 103 | fn := b.typeNameHandleFunc |
| 104 | if fn == nil { |
| 105 | fn = reflectTypeName |
| 106 | } |
| 107 | typeAsName := fn(sample) |
| 108 | description := "" |
| 109 | if len(optionalDescription) > 0 { |
| 110 | description = optionalDescription[0] |
| 111 | } |
| 112 | b.readSample = sample |
| 113 | bodyParameter := &Parameter{&ParameterData{Name: "body", Description: description}} |
| 114 | bodyParameter.beBody() |
| 115 | bodyParameter.Required(true) |
| 116 | bodyParameter.DataType(typeAsName) |
| 117 | b.Param(bodyParameter) |
| 118 | return b |
| 119 | } |
| 120 | |
| 121 | // ParameterNamed returns a Parameter already known to the RouteBuilder. Returns nil if not. |
| 122 | // Use this to modify or extend information for the Parameter (through its Data()). |
| 123 | func (b RouteBuilder) ParameterNamed(name string) (p *Parameter) { |
| 124 | for _, each := range b.parameters { |
| 125 | if each.Data().Name == name { |
| 126 | return each |
| 127 | } |
| 128 | } |
| 129 | return p |
| 130 | } |
| 131 | |
| 132 | // Writes tells what resource type will be written as the response payload. Optional. |
| 133 | func (b *RouteBuilder) Writes(sample interface{}) *RouteBuilder { |
| 134 | b.writeSample = sample |
| 135 | return b |
| 136 | } |
| 137 | |
| 138 | // Param allows you to document the parameters of the Route. It adds a new Parameter (does not check for duplicates). |
| 139 | func (b *RouteBuilder) Param(parameter *Parameter) *RouteBuilder { |
| 140 | if b.parameters == nil { |
| 141 | b.parameters = []*Parameter{} |
| 142 | } |
| 143 | b.parameters = append(b.parameters, parameter) |
| 144 | return b |
| 145 | } |
| 146 | |
| 147 | // Operation allows you to document what the actual method/function call is of the Route. |
| 148 | // Unless called, the operation name is derived from the RouteFunction set using To(..). |
| 149 | func (b *RouteBuilder) Operation(name string) *RouteBuilder { |
| 150 | b.operation = name |
| 151 | return b |
| 152 | } |
| 153 | |
| 154 | // ReturnsError is deprecated, use Returns instead. |
| 155 | func (b *RouteBuilder) ReturnsError(code int, message string, model interface{}) *RouteBuilder { |
| 156 | log.Print("ReturnsError is deprecated, use Returns instead.") |
| 157 | return b.Returns(code, message, model) |
| 158 | } |
| 159 | |
| 160 | // Returns allows you to document what responses (errors or regular) can be expected. |
| 161 | // The model parameter is optional ; either pass a struct instance or use nil if not applicable. |
| 162 | func (b *RouteBuilder) Returns(code int, message string, model interface{}) *RouteBuilder { |
| 163 | err := ResponseError{ |
| 164 | Code: code, |
| 165 | Message: message, |
| 166 | Model: model, |
| 167 | IsDefault: false, |
| 168 | } |
| 169 | // lazy init because there is no NewRouteBuilder (yet) |
| 170 | if b.errorMap == nil { |
| 171 | b.errorMap = map[int]ResponseError{} |
| 172 | } |
| 173 | b.errorMap[code] = err |
| 174 | return b |
| 175 | } |
| 176 | |
| 177 | // DefaultReturns is a special Returns call that sets the default of the response ; the code is zero. |
| 178 | func (b *RouteBuilder) DefaultReturns(message string, model interface{}) *RouteBuilder { |
| 179 | b.Returns(0, message, model) |
| 180 | // Modify the ResponseError just added/updated |
| 181 | re := b.errorMap[0] |
| 182 | // errorMap is initialized |
| 183 | b.errorMap[0] = ResponseError{ |
| 184 | Code: re.Code, |
| 185 | Message: re.Message, |
| 186 | Model: re.Model, |
| 187 | IsDefault: true, |
| 188 | } |
| 189 | return b |
| 190 | } |
| 191 | |
| 192 | // Metadata adds or updates a key=value pair to the metadata map. |
| 193 | func (b *RouteBuilder) Metadata(key string, value interface{}) *RouteBuilder { |
| 194 | if b.metadata == nil { |
| 195 | b.metadata = map[string]interface{}{} |
| 196 | } |
| 197 | b.metadata[key] = value |
| 198 | return b |
| 199 | } |
| 200 | |
| 201 | // Deprecate sets the value of deprecated to true. Deprecated routes have a special UI treatment to warn against use |
| 202 | func (b *RouteBuilder) Deprecate() *RouteBuilder { |
| 203 | b.deprecated = true |
| 204 | return b |
| 205 | } |
| 206 | |
| 207 | // ResponseError represents a response; not necessarily an error. |
| 208 | type ResponseError struct { |
| 209 | Code int |
| 210 | Message string |
| 211 | Model interface{} |
| 212 | IsDefault bool |
| 213 | } |
| 214 | |
| 215 | func (b *RouteBuilder) servicePath(path string) *RouteBuilder { |
| 216 | b.rootPath = path |
| 217 | return b |
| 218 | } |
| 219 | |
| 220 | // Filter appends a FilterFunction to the end of filters for this Route to build. |
| 221 | func (b *RouteBuilder) Filter(filter FilterFunction) *RouteBuilder { |
| 222 | b.filters = append(b.filters, filter) |
| 223 | return b |
| 224 | } |
| 225 | |
| 226 | // If sets a condition function that controls matching the Route based on custom logic. |
| 227 | // The condition function is provided the HTTP request and should return true if the route |
| 228 | // should be considered. |
| 229 | // |
| 230 | // Efficiency note: the condition function is called before checking the method, produces, and |
| 231 | // consumes criteria, so that the correct HTTP status code can be returned. |
| 232 | // |
| 233 | // Lifecycle note: no filter functions have been called prior to calling the condition function, |
| 234 | // so the condition function should not depend on any context that might be set up by container |
| 235 | // or route filters. |
| 236 | func (b *RouteBuilder) If(condition RouteSelectionConditionFunction) *RouteBuilder { |
| 237 | b.conditions = append(b.conditions, condition) |
| 238 | return b |
| 239 | } |
| 240 | |
| 241 | // If no specific Route path then set to rootPath |
| 242 | // If no specific Produces then set to rootProduces |
| 243 | // If no specific Consumes then set to rootConsumes |
| 244 | func (b *RouteBuilder) copyDefaults(rootProduces, rootConsumes []string) { |
| 245 | if len(b.produces) == 0 { |
| 246 | b.produces = rootProduces |
| 247 | } |
| 248 | if len(b.consumes) == 0 { |
| 249 | b.consumes = rootConsumes |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | // typeNameHandler sets the function that will convert types to strings in the parameter |
| 254 | // and model definitions. |
| 255 | func (b *RouteBuilder) typeNameHandler(handler TypeNameHandleFunction) *RouteBuilder { |
| 256 | b.typeNameHandleFunc = handler |
| 257 | return b |
| 258 | } |
| 259 | |
| 260 | // Build creates a new Route using the specification details collected by the RouteBuilder |
| 261 | func (b *RouteBuilder) Build() Route { |
| 262 | pathExpr, err := newPathExpression(b.currentPath) |
| 263 | if err != nil { |
| 264 | log.Printf("[restful] Invalid path:%s because:%v", b.currentPath, err) |
| 265 | os.Exit(1) |
| 266 | } |
| 267 | if b.function == nil { |
| 268 | log.Printf("[restful] No function specified for route:" + b.currentPath) |
| 269 | os.Exit(1) |
| 270 | } |
| 271 | operationName := b.operation |
| 272 | if len(operationName) == 0 && b.function != nil { |
| 273 | // extract from definition |
| 274 | operationName = nameOfFunction(b.function) |
| 275 | } |
| 276 | route := Route{ |
| 277 | Method: b.httpMethod, |
| 278 | Path: concatPath(b.rootPath, b.currentPath), |
| 279 | Produces: b.produces, |
| 280 | Consumes: b.consumes, |
| 281 | Function: b.function, |
| 282 | Filters: b.filters, |
| 283 | If: b.conditions, |
| 284 | relativePath: b.currentPath, |
| 285 | pathExpr: pathExpr, |
| 286 | Doc: b.doc, |
| 287 | Notes: b.notes, |
| 288 | Operation: operationName, |
| 289 | ParameterDocs: b.parameters, |
| 290 | ResponseErrors: b.errorMap, |
| 291 | ReadSample: b.readSample, |
| 292 | WriteSample: b.writeSample, |
| 293 | Metadata: b.metadata, |
| 294 | Deprecated: b.deprecated} |
| 295 | route.postBuild() |
| 296 | return route |
| 297 | } |
| 298 | |
| 299 | func concatPath(path1, path2 string) string { |
| 300 | return strings.TrimRight(path1, "/") + "/" + strings.TrimLeft(path2, "/") |
| 301 | } |
| 302 | |
| 303 | var anonymousFuncCount int32 |
| 304 | |
| 305 | // nameOfFunction returns the short name of the function f for documentation. |
| 306 | // It uses a runtime feature for debugging ; its value may change for later Go versions. |
| 307 | func nameOfFunction(f interface{}) string { |
| 308 | fun := runtime.FuncForPC(reflect.ValueOf(f).Pointer()) |
| 309 | tokenized := strings.Split(fun.Name(), ".") |
| 310 | last := tokenized[len(tokenized)-1] |
| 311 | last = strings.TrimSuffix(last, ")·fm") // < Go 1.5 |
| 312 | last = strings.TrimSuffix(last, ")-fm") // Go 1.5 |
| 313 | last = strings.TrimSuffix(last, "·fm") // < Go 1.5 |
| 314 | last = strings.TrimSuffix(last, "-fm") // Go 1.5 |
| 315 | if last == "func1" { // this could mean conflicts in API docs |
| 316 | val := atomic.AddInt32(&anonymousFuncCount, 1) |
| 317 | last = "func" + fmt.Sprintf("%d", val) |
| 318 | atomic.StoreInt32(&anonymousFuncCount, val) |
| 319 | } |
| 320 | return last |
| 321 | } |