blob: 4ebecbd8c41b3f4732908ce80a4940a6756c73e1 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001package 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
7import (
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.
19type 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// }
50func (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.
59func (b *RouteBuilder) To(function RouteFunction) *RouteBuilder {
60 b.function = function
61 return b
62}
63
64// Method specifies what HTTP method to match. Required.
65func (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.
71func (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
77func (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 "/".
83func (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.
89func (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.
95func (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.
102func (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()).
123func (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.
133func (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).
139func (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(..).
149func (b *RouteBuilder) Operation(name string) *RouteBuilder {
150 b.operation = name
151 return b
152}
153
154// ReturnsError is deprecated, use Returns instead.
155func (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.
162func (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.
178func (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.
193func (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
202func (b *RouteBuilder) Deprecate() *RouteBuilder {
203 b.deprecated = true
204 return b
205}
206
207// ResponseError represents a response; not necessarily an error.
208type ResponseError struct {
209 Code int
210 Message string
211 Model interface{}
212 IsDefault bool
213}
214
215func (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.
221func (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.
236func (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
244func (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.
255func (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
261func (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
299func concatPath(path1, path2 string) string {
300 return strings.TrimRight(path1, "/") + "/" + strings.TrimLeft(path2, "/")
301}
302
303var 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.
307func 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}