Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame^] | 1 | package swagger |
| 2 | |
| 3 | import ( |
| 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 | |
| 16 | type SwaggerService struct { |
| 17 | config Config |
| 18 | apiDeclarationMap *ApiDeclarationList |
| 19 | } |
| 20 | |
| 21 | func 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 |
| 54 | var 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). |
| 61 | func 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). |
| 67 | func 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 | |
| 115 | func 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 | |
| 131 | func 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 | |
| 141 | func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) { |
| 142 | listing := sws.produceListing() |
| 143 | resp.WriteAsJson(listing) |
| 144 | } |
| 145 | |
| 146 | func (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 | |
| 158 | func (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 | |
| 194 | func (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 | |
| 202 | func (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 |
| 212 | func (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 | |
| 269 | func 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. |
| 277 | func 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. |
| 309 | func (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 | |
| 318 | func 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 |
| 335 | func (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 | |
| 345 | func asSwaggerParameter(param restful.ParameterData) Parameter { |
| 346 | return Parameter{ |
| 347 | DataTypeFields: DataTypeFields{ |
| 348 | Type: ¶m.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 |
| 360 | func 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 | |
| 394 | func asFormat(dataType string, dataFormat string) string { |
| 395 | if dataFormat != "" { |
| 396 | return dataFormat |
| 397 | } |
| 398 | return "" // TODO |
| 399 | } |
| 400 | |
| 401 | func 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 | |
| 417 | func 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 | } |