123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- package swagger
- import (
- "fmt"
- "github.com/emicklei/go-restful"
- // "github.com/emicklei/hopwatch"
- "net/http"
- "reflect"
- "sort"
- "strings"
- "github.com/emicklei/go-restful/log"
- )
- type SwaggerService struct {
- config Config
- apiDeclarationMap *ApiDeclarationList
- }
- func newSwaggerService(config Config) *SwaggerService {
- sws := &SwaggerService{
- config: config,
- apiDeclarationMap: new(ApiDeclarationList)}
- // Build all ApiDeclarations
- for _, each := range config.WebServices {
- rootPath := each.RootPath()
- // skip the api service itself
- if rootPath != config.ApiPath {
- if rootPath == "" || rootPath == "/" {
- // use routes
- for _, route := range each.Routes() {
- entry := staticPathFromRoute(route)
- _, exists := sws.apiDeclarationMap.At(entry)
- if !exists {
- sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
- }
- }
- } else { // use root path
- sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
- }
- }
- }
- // if specified then call the PostBuilderHandler
- if config.PostBuildHandler != nil {
- config.PostBuildHandler(sws.apiDeclarationMap)
- }
- return sws
- }
- // LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
- var LogInfo = func(format string, v ...interface{}) {
- // use the restful package-wide logger
- log.Printf(format, v...)
- }
- // InstallSwaggerService add the WebService that provides the API documentation of all services
- // conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
- func InstallSwaggerService(aSwaggerConfig Config) {
- RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer)
- }
- // RegisterSwaggerService add the WebService that provides the API documentation of all services
- // conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
- func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
- sws := newSwaggerService(config)
- ws := new(restful.WebService)
- ws.Path(config.ApiPath)
- ws.Produces(restful.MIME_JSON)
- if config.DisableCORS {
- ws.Filter(enableCORS)
- }
- ws.Route(ws.GET("/").To(sws.getListing))
- ws.Route(ws.GET("/{a}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations))
- ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations))
- LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
- wsContainer.Add(ws)
- // Check paths for UI serving
- if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
- swaggerPathSlash := config.SwaggerPath
- // path must end with slash /
- if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
- LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)")
- swaggerPathSlash += "/"
- }
- LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath)
- wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath))))
- //if we define a custom static handler use it
- } else if config.StaticHandler != nil && config.SwaggerPath != "" {
- swaggerPathSlash := config.SwaggerPath
- // path must end with slash /
- if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
- LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)")
- swaggerPathSlash += "/"
- }
- LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler)
- wsContainer.Handle(swaggerPathSlash, config.StaticHandler)
- } else {
- LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served")
- }
- }
- func staticPathFromRoute(r restful.Route) string {
- static := r.Path
- bracket := strings.Index(static, "{")
- if bracket <= 1 { // result cannot be empty
- return static
- }
- if bracket != -1 {
- static = r.Path[:bracket]
- }
- if strings.HasSuffix(static, "/") {
- return static[:len(static)-1]
- } else {
- return static
- }
- }
- func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
- if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" {
- // prevent duplicate header
- if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 {
- resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin)
- }
- }
- chain.ProcessFilter(req, resp)
- }
- func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
- listing := sws.produceListing()
- resp.WriteAsJson(listing)
- }
- func (sws SwaggerService) produceListing() ResourceListing {
- listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
- sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
- ref := Resource{Path: k}
- if len(v.Apis) > 0 { // use description of first (could still be empty)
- ref.Description = v.Apis[0].Description
- }
- listing.Apis = append(listing.Apis, ref)
- })
- return listing
- }
- func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
- decl, ok := sws.produceDeclarations(composeRootPath(req))
- if !ok {
- resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
- return
- }
- // unless WebServicesUrl is given
- if len(sws.config.WebServicesUrl) == 0 {
- // update base path from the actual request
- // TODO how to detect https? assume http for now
- var host string
- // X-Forwarded-Host or Host or Request.Host
- hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific?
- if !ok || len(hostvalues) == 0 {
- forwarded, ok := req.Request.Header["Host"] // without reverse-proxy
- if !ok || len(forwarded) == 0 {
- // fallback to Host field
- host = req.Request.Host
- } else {
- host = forwarded[0]
- }
- } else {
- host = hostvalues[0]
- }
- // inspect Referer for the scheme (http vs https)
- scheme := "http"
- if referer := req.Request.Header["Referer"]; len(referer) > 0 {
- if strings.HasPrefix(referer[0], "https") {
- scheme = "https"
- }
- }
- decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
- }
- resp.WriteAsJson(decl)
- }
- func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
- decls := map[string]ApiDeclaration{}
- sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
- decls[k] = v
- })
- return decls
- }
- func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
- decl, ok := sws.apiDeclarationMap.At(route)
- if !ok {
- return nil, false
- }
- decl.BasePath = sws.config.WebServicesUrl
- return &decl, true
- }
- // composeDeclaration uses all routes and parameters to create a ApiDeclaration
- func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
- decl := ApiDeclaration{
- SwaggerVersion: swaggerVersion,
- BasePath: sws.config.WebServicesUrl,
- ResourcePath: pathPrefix,
- Models: ModelList{},
- ApiVersion: ws.Version()}
- // collect any path parameters
- rootParams := []Parameter{}
- for _, param := range ws.PathParameters() {
- rootParams = append(rootParams, asSwaggerParameter(param.Data()))
- }
- // aggregate by path
- pathToRoutes := newOrderedRouteMap()
- for _, other := range ws.Routes() {
- if strings.HasPrefix(other.Path, pathPrefix) {
- pathToRoutes.Add(other.Path, other)
- }
- }
- pathToRoutes.Do(func(path string, routes []restful.Route) {
- api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
- voidString := "void"
- for _, route := range routes {
- operation := Operation{
- Method: route.Method,
- Summary: route.Doc,
- Notes: route.Notes,
- // Type gets overwritten if there is a write sample
- DataTypeFields: DataTypeFields{Type: &voidString},
- Parameters: []Parameter{},
- Nickname: route.Operation,
- ResponseMessages: composeResponseMessages(route, &decl, &sws.config)}
- operation.Consumes = route.Consumes
- operation.Produces = route.Produces
- // share root params if any
- for _, swparam := range rootParams {
- operation.Parameters = append(operation.Parameters, swparam)
- }
- // route specific params
- for _, param := range route.ParameterDocs {
- operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data()))
- }
- sws.addModelsFromRouteTo(&operation, route, &decl)
- api.Operations = append(api.Operations, operation)
- }
- decl.Apis = append(decl.Apis, api)
- })
- return decl
- }
- func withoutWildcard(path string) string {
- if strings.HasSuffix(path, ":*}") {
- return path[0:len(path)-3] + "}"
- }
- return path
- }
- // composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
- func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) {
- if route.ResponseErrors == nil {
- return messages
- }
- // sort by code
- codes := sort.IntSlice{}
- for code := range route.ResponseErrors {
- codes = append(codes, code)
- }
- codes.Sort()
- for _, code := range codes {
- each := route.ResponseErrors[code]
- message := ResponseMessage{
- Code: code,
- Message: each.Message,
- }
- if each.Model != nil {
- st := reflect.TypeOf(each.Model)
- isCollection, st := detectCollectionType(st)
- modelName := modelBuilder{}.keyFrom(st)
- if isCollection {
- modelName = "array[" + modelName + "]"
- }
- modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "")
- // reference the model
- message.ResponseModel = modelName
- }
- messages = append(messages, message)
- }
- return
- }
- // addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it.
- func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) {
- if route.ReadSample != nil {
- sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models)
- }
- if route.WriteSample != nil {
- sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models)
- }
- }
- func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
- isCollection := false
- if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
- st = st.Elem()
- isCollection = true
- } else {
- if st.Kind() == reflect.Ptr {
- if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array {
- st = st.Elem().Elem()
- isCollection = true
- }
- }
- }
- return isCollection, st
- }
- // addModelFromSample creates and adds (or overwrites) a Model from a sample resource
- func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
- if isResponse {
- type_, items := asDataType(sample, &sws.config)
- operation.Type = type_
- operation.Items = items
- }
- modelBuilder{Models: models, Config: &sws.config}.addModelFrom(sample)
- }
- func asSwaggerParameter(param restful.ParameterData) Parameter {
- return Parameter{
- DataTypeFields: DataTypeFields{
- Type: ¶m.DataType,
- Format: asFormat(param.DataType, param.DataFormat),
- DefaultValue: Special(param.DefaultValue),
- },
- Name: param.Name,
- Description: param.Description,
- ParamType: asParamType(param.Kind),
- Required: param.Required}
- }
- // Between 1..7 path parameters is supported
- func composeRootPath(req *restful.Request) string {
- path := "/" + req.PathParameter("a")
- b := req.PathParameter("b")
- if b == "" {
- return path
- }
- path = path + "/" + b
- c := req.PathParameter("c")
- if c == "" {
- return path
- }
- path = path + "/" + c
- d := req.PathParameter("d")
- if d == "" {
- return path
- }
- path = path + "/" + d
- e := req.PathParameter("e")
- if e == "" {
- return path
- }
- path = path + "/" + e
- f := req.PathParameter("f")
- if f == "" {
- return path
- }
- path = path + "/" + f
- g := req.PathParameter("g")
- if g == "" {
- return path
- }
- return path + "/" + g
- }
- func asFormat(dataType string, dataFormat string) string {
- if dataFormat != "" {
- return dataFormat
- }
- return "" // TODO
- }
- func asParamType(kind int) string {
- switch {
- case kind == restful.PathParameterKind:
- return "path"
- case kind == restful.QueryParameterKind:
- return "query"
- case kind == restful.BodyParameterKind:
- return "body"
- case kind == restful.HeaderParameterKind:
- return "header"
- case kind == restful.FormParameterKind:
- return "form"
- }
- return ""
- }
- func asDataType(any interface{}, config *Config) (*string, *Item) {
- // If it's not a collection, return the suggested model name
- st := reflect.TypeOf(any)
- isCollection, st := detectCollectionType(st)
- modelName := modelBuilder{}.keyFrom(st)
- // if it's not a collection we are done
- if !isCollection {
- return &modelName, nil
- }
- // XXX: This is not very elegant
- // We create an Item object referring to the given model
- models := ModelList{}
- mb := modelBuilder{Models: &models, Config: config}
- mb.addModelFrom(any)
- elemTypeName := mb.getElementTypeName(modelName, "", st)
- item := new(Item)
- if mb.isPrimitiveType(elemTypeName) {
- mapped := mb.jsonSchemaType(elemTypeName)
- item.Type = &mapped
- } else {
- item.Ref = &elemTypeName
- }
- tmp := "array"
- return &tmp, item
- }
|