blob: d90623120a6962a77f2fc6100f2dd3d1005d21be [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001package swagger
2
3import (
4 "fmt"
5
6 "github.com/emicklei/go-restful"
7 // "github.com/emicklei/hopwatch"
8 "net/http"
9 "reflect"
10 "sort"
11 "strings"
12
13 "github.com/emicklei/go-restful/log"
14)
15
16type SwaggerService struct {
17 config Config
18 apiDeclarationMap *ApiDeclarationList
19}
20
21func newSwaggerService(config Config) *SwaggerService {
22 sws := &SwaggerService{
23 config: config,
24 apiDeclarationMap: new(ApiDeclarationList)}
25
26 // Build all ApiDeclarations
27 for _, each := range config.WebServices {
28 rootPath := each.RootPath()
29 // skip the api service itself
30 if rootPath != config.ApiPath {
31 if rootPath == "" || rootPath == "/" {
32 // use routes
33 for _, route := range each.Routes() {
34 entry := staticPathFromRoute(route)
35 _, exists := sws.apiDeclarationMap.At(entry)
36 if !exists {
37 sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
38 }
39 }
40 } else { // use root path
41 sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
42 }
43 }
44 }
45
46 // if specified then call the PostBuilderHandler
47 if config.PostBuildHandler != nil {
48 config.PostBuildHandler(sws.apiDeclarationMap)
49 }
50 return sws
51}
52
53// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
54var LogInfo = func(format string, v ...interface{}) {
55 // use the restful package-wide logger
56 log.Printf(format, v...)
57}
58
59// InstallSwaggerService add the WebService that provides the API documentation of all services
60// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
61func InstallSwaggerService(aSwaggerConfig Config) {
62 RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer)
63}
64
65// RegisterSwaggerService add the WebService that provides the API documentation of all services
66// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
67func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
68 sws := newSwaggerService(config)
69 ws := new(restful.WebService)
70 ws.Path(config.ApiPath)
71 ws.Produces(restful.MIME_JSON)
72 if config.DisableCORS {
73 ws.Filter(enableCORS)
74 }
75 ws.Route(ws.GET("/").To(sws.getListing))
76 ws.Route(ws.GET("/{a}").To(sws.getDeclarations))
77 ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations))
78 ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations))
79 ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations))
80 ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations))
81 ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations))
82 ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations))
83 LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
84 wsContainer.Add(ws)
85
86 // Check paths for UI serving
87 if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
88 swaggerPathSlash := config.SwaggerPath
89 // path must end with slash /
90 if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
91 LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)")
92 swaggerPathSlash += "/"
93 }
94
95 LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath)
96 wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath))))
97
98 //if we define a custom static handler use it
99 } else if config.StaticHandler != nil && config.SwaggerPath != "" {
100 swaggerPathSlash := config.SwaggerPath
101 // path must end with slash /
102 if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
103 LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)")
104 swaggerPathSlash += "/"
105
106 }
107 LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler)
108 wsContainer.Handle(swaggerPathSlash, config.StaticHandler)
109
110 } else {
111 LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served")
112 }
113}
114
115func staticPathFromRoute(r restful.Route) string {
116 static := r.Path
117 bracket := strings.Index(static, "{")
118 if bracket <= 1 { // result cannot be empty
119 return static
120 }
121 if bracket != -1 {
122 static = r.Path[:bracket]
123 }
124 if strings.HasSuffix(static, "/") {
125 return static[:len(static)-1]
126 } else {
127 return static
128 }
129}
130
131func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
132 if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" {
133 // prevent duplicate header
134 if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 {
135 resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin)
136 }
137 }
138 chain.ProcessFilter(req, resp)
139}
140
141func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
142 listing := sws.produceListing()
143 resp.WriteAsJson(listing)
144}
145
146func (sws SwaggerService) produceListing() ResourceListing {
147 listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
148 sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
149 ref := Resource{Path: k}
150 if len(v.Apis) > 0 { // use description of first (could still be empty)
151 ref.Description = v.Apis[0].Description
152 }
153 listing.Apis = append(listing.Apis, ref)
154 })
155 return listing
156}
157
158func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
159 decl, ok := sws.produceDeclarations(composeRootPath(req))
160 if !ok {
161 resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
162 return
163 }
164 // unless WebServicesUrl is given
165 if len(sws.config.WebServicesUrl) == 0 {
166 // update base path from the actual request
167 // TODO how to detect https? assume http for now
168 var host string
169 // X-Forwarded-Host or Host or Request.Host
170 hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific?
171 if !ok || len(hostvalues) == 0 {
172 forwarded, ok := req.Request.Header["Host"] // without reverse-proxy
173 if !ok || len(forwarded) == 0 {
174 // fallback to Host field
175 host = req.Request.Host
176 } else {
177 host = forwarded[0]
178 }
179 } else {
180 host = hostvalues[0]
181 }
182 // inspect Referer for the scheme (http vs https)
183 scheme := "http"
184 if referer := req.Request.Header["Referer"]; len(referer) > 0 {
185 if strings.HasPrefix(referer[0], "https") {
186 scheme = "https"
187 }
188 }
189 decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
190 }
191 resp.WriteAsJson(decl)
192}
193
194func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
195 decls := map[string]ApiDeclaration{}
196 sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
197 decls[k] = v
198 })
199 return decls
200}
201
202func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
203 decl, ok := sws.apiDeclarationMap.At(route)
204 if !ok {
205 return nil, false
206 }
207 decl.BasePath = sws.config.WebServicesUrl
208 return &decl, true
209}
210
211// composeDeclaration uses all routes and parameters to create a ApiDeclaration
212func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
213 decl := ApiDeclaration{
214 SwaggerVersion: swaggerVersion,
215 BasePath: sws.config.WebServicesUrl,
216 ResourcePath: pathPrefix,
217 Models: ModelList{},
218 ApiVersion: ws.Version()}
219
220 // collect any path parameters
221 rootParams := []Parameter{}
222 for _, param := range ws.PathParameters() {
223 rootParams = append(rootParams, asSwaggerParameter(param.Data()))
224 }
225 // aggregate by path
226 pathToRoutes := newOrderedRouteMap()
227 for _, other := range ws.Routes() {
228 if strings.HasPrefix(other.Path, pathPrefix) {
229 if len(pathPrefix) > 1 && len(other.Path) > len(pathPrefix) && other.Path[len(pathPrefix)] != '/' {
230 continue
231 }
232 pathToRoutes.Add(other.Path, other)
233 }
234 }
235 pathToRoutes.Do(func(path string, routes []restful.Route) {
236 api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
237 voidString := "void"
238 for _, route := range routes {
239 operation := Operation{
240 Method: route.Method,
241 Summary: route.Doc,
242 Notes: route.Notes,
243 // Type gets overwritten if there is a write sample
244 DataTypeFields: DataTypeFields{Type: &voidString},
245 Parameters: []Parameter{},
246 Nickname: route.Operation,
247 ResponseMessages: composeResponseMessages(route, &decl, &sws.config)}
248
249 operation.Consumes = route.Consumes
250 operation.Produces = route.Produces
251
252 // share root params if any
253 for _, swparam := range rootParams {
254 operation.Parameters = append(operation.Parameters, swparam)
255 }
256 // route specific params
257 for _, param := range route.ParameterDocs {
258 operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data()))
259 }
260
261 sws.addModelsFromRouteTo(&operation, route, &decl)
262 api.Operations = append(api.Operations, operation)
263 }
264 decl.Apis = append(decl.Apis, api)
265 })
266 return decl
267}
268
269func withoutWildcard(path string) string {
270 if strings.HasSuffix(path, ":*}") {
271 return path[0:len(path)-3] + "}"
272 }
273 return path
274}
275
276// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
277func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) {
278 if route.ResponseErrors == nil {
279 return messages
280 }
281 // sort by code
282 codes := sort.IntSlice{}
283 for code := range route.ResponseErrors {
284 codes = append(codes, code)
285 }
286 codes.Sort()
287 for _, code := range codes {
288 each := route.ResponseErrors[code]
289 message := ResponseMessage{
290 Code: code,
291 Message: each.Message,
292 }
293 if each.Model != nil {
294 st := reflect.TypeOf(each.Model)
295 isCollection, st := detectCollectionType(st)
296 // collection cannot be in responsemodel
297 if !isCollection {
298 modelName := modelBuilder{}.keyFrom(st)
299 modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "")
300 message.ResponseModel = modelName
301 }
302 }
303 messages = append(messages, message)
304 }
305 return
306}
307
308// addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it.
309func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) {
310 if route.ReadSample != nil {
311 sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models)
312 }
313 if route.WriteSample != nil {
314 sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models)
315 }
316}
317
318func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
319 isCollection := false
320 if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
321 st = st.Elem()
322 isCollection = true
323 } else {
324 if st.Kind() == reflect.Ptr {
325 if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array {
326 st = st.Elem().Elem()
327 isCollection = true
328 }
329 }
330 }
331 return isCollection, st
332}
333
334// addModelFromSample creates and adds (or overwrites) a Model from a sample resource
335func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
336 mb := modelBuilder{Models: models, Config: &sws.config}
337 if isResponse {
338 sampleType, items := asDataType(sample, &sws.config)
339 operation.Type = sampleType
340 operation.Items = items
341 }
342 mb.addModelFrom(sample)
343}
344
345func asSwaggerParameter(param restful.ParameterData) Parameter {
346 return Parameter{
347 DataTypeFields: DataTypeFields{
348 Type: &param.DataType,
349 Format: asFormat(param.DataType, param.DataFormat),
350 DefaultValue: Special(param.DefaultValue),
351 },
352 Name: param.Name,
353 Description: param.Description,
354 ParamType: asParamType(param.Kind),
355
356 Required: param.Required}
357}
358
359// Between 1..7 path parameters is supported
360func composeRootPath(req *restful.Request) string {
361 path := "/" + req.PathParameter("a")
362 b := req.PathParameter("b")
363 if b == "" {
364 return path
365 }
366 path = path + "/" + b
367 c := req.PathParameter("c")
368 if c == "" {
369 return path
370 }
371 path = path + "/" + c
372 d := req.PathParameter("d")
373 if d == "" {
374 return path
375 }
376 path = path + "/" + d
377 e := req.PathParameter("e")
378 if e == "" {
379 return path
380 }
381 path = path + "/" + e
382 f := req.PathParameter("f")
383 if f == "" {
384 return path
385 }
386 path = path + "/" + f
387 g := req.PathParameter("g")
388 if g == "" {
389 return path
390 }
391 return path + "/" + g
392}
393
394func asFormat(dataType string, dataFormat string) string {
395 if dataFormat != "" {
396 return dataFormat
397 }
398 return "" // TODO
399}
400
401func asParamType(kind int) string {
402 switch {
403 case kind == restful.PathParameterKind:
404 return "path"
405 case kind == restful.QueryParameterKind:
406 return "query"
407 case kind == restful.BodyParameterKind:
408 return "body"
409 case kind == restful.HeaderParameterKind:
410 return "header"
411 case kind == restful.FormParameterKind:
412 return "form"
413 }
414 return ""
415}
416
417func asDataType(any interface{}, config *Config) (*string, *Item) {
418 // If it's not a collection, return the suggested model name
419 st := reflect.TypeOf(any)
420 isCollection, st := detectCollectionType(st)
421 modelName := modelBuilder{}.keyFrom(st)
422 // if it's not a collection we are done
423 if !isCollection {
424 return &modelName, nil
425 }
426
427 // XXX: This is not very elegant
428 // We create an Item object referring to the given model
429 models := ModelList{}
430 mb := modelBuilder{Models: &models, Config: config}
431 mb.addModelFrom(any)
432
433 elemTypeName := mb.getElementTypeName(modelName, "", st)
434 item := new(Item)
435 if mb.isPrimitiveType(elemTypeName) {
436 mapped := mb.jsonSchemaType(elemTypeName)
437 item.Type = &mapped
438 } else {
439 item.Ref = &elemTypeName
440 }
441 tmp := "array"
442 return &tmp, item
443}