gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
76.76k stars 7.92k forks source link

Automatically generate RESTful API documentation with Swagger #155

Open ndbroadbent opened 9 years ago

ndbroadbent commented 9 years ago

One thing I really like about go-restful is their built-in Swagger integration. Check out the example project: https://github.com/emicklei/mora#swagger

Some things I really love about Gin is the focus on performance, and overall, it just seems like a much better designed framework. However, I think that auto-generated documentation with Swagger is pretty awesome. It makes it easy for new engineers to get up-to-speed quickly by reading through the docs, and I especially love the way you can auto-generate API clients for lots of different languages.

I'm not sure how involved this change would be, I'm not sure if this could be done as an extension. But we could probably take lots of inspiration from (or just copy) the swagger code from go-restful (MIT licensed).

Anyway, this is not a huge priority for me, just a nice to have.

rogeriomarques commented 9 years ago

+1

phalt commented 9 years ago

+1

redstrike commented 9 years ago

+1, swagger is very useful for API development

matejkramny commented 9 years ago

:+1:

javierprovecho commented 9 years ago

@phalt @redstrike @matejkramny @rogeriomarques @ndbroadbent I'll add support for it with v0.6 in mind. Thank you for submitting this request.

salimane commented 9 years ago

+1

colthreepv commented 9 years ago

I'm gonna raise the biddings on this issue, Ok for Swagger, but why not RAML? In my personal experience using both swagger and raml barebone, the latter has a better explained specification and has a more pragmatic approach.

I'll explore what can be done with my limited golang skills, and keep you posted

colthreepv commented 9 years ago

One of the main concerns in thinking this feature out, is design this swagger/raml feature producing a base json/raml document that can be extended to let the user exploit the full-feature set of their API specifications.

I hardly seen that in other frameworks, but I'm hoping someone could propose some input regarding this

njuettner commented 9 years ago

+1 for swagger support

itsjamie commented 9 years ago

I'm surprised there isn't someone who has created a parser that looks at an AST + attached routes for net/http to generate Swagger/RAML style documentation.

If it isn't a project, perhaps that would be the better method of implementation for this? Rather than having it part of Gin core, if it was a separate binary that scanned a package?

colthreepv commented 9 years ago

I think to externalize API documentation, gin.router should be exported (so Router). Moreso, I think model validation should be expanded before implementing API documentation, since there is a big difference between the vastity of API validation options and gin ones.

DustinHigginbotham commented 9 years ago

Noticed Swagger support was on the README checklist for V1. That seems to be gone now. Should we not have our hopes up for this being released?

manucorporat commented 9 years ago

@dustinwtf We do not need swagger support in 1.0. 1.0 is about having a stable API, production-ready and performance.

Once 1.0 final is reached, we can focus in features. Also, I do not think the swagger support should be part of Gin core. Probably a different package.

DustinHigginbotham commented 9 years ago

Agreed! That's definitely a good focus. Just wondered considering it was there and disappeared.

nazwa commented 9 years ago

Is there an easy way to get all the registered routes now? Like a map of url -> handlers or something.

manucorporat commented 9 years ago

@nazwa no, we need to implement it. Two solutions:

  1. Iterate over the trees and collect the routes.
  2. Save the routes in a separated slice (only for documentation purposes)

While the second way is the easiest to implement it adds a unneeded overhead and complexity all over the place. So I would try to iterate the trees, it can not be that hard.

*Engine should have a method called Routes() []RouteInfo or something like that. We do not want to expose the trees directly.

manucorporat commented 9 years ago

Also, since the router is not longer dependent of the HttpRouter package, we have access to all the info.

manucorporat commented 9 years ago

something like this, I do not have time to do it just now. If anybody is interested, please create a PR!!


func (engine *Engine) Routes() (routes []RouteInfo) {
    for _, root := range engine.trees {
        routes = iterate(routes, root)
    }
    return routes
}

func iterate(routes []RouteInfo, root *node) []RouteInfo {
    for _, node := range root.children {
        routes = iterate(routes, node)
    }
    if root.handlers != nil {
        routes = append(routes,root.path)
    }
    return routes
}
nazwa commented 9 years ago

Yea, something like this would be perfect. I'll try to have a proper play with it in the evening, as it definitely would be useful in the long run.

manucorporat commented 9 years ago

@nazwa @dustinwtf @ndbroadbent https://github.com/gin-gonic/gin/commit/45dd7776938fddc914e8a2fe60367a3eb99e1e53

manucorporat commented 9 years ago
router := gin.New()
router.GET("/favicon.ico", handler)
router.GET("/", handler)
group := router.Group("/users")
group.GET("/", handler)
group.GET("/:id", handler)
group.POST("/:id", handler)
router.Static("/static", ".")

fmt.Println(router.Routes())
[{GET /} {GET /users/} {GET /users/:id} {GET /favicon.ico} {GET /static/*filepath} {POST /users/:id} {HEAD /static/*filepath}]
manucorporat commented 9 years ago

Here's a proposal for auto API documentation:

The swagger generator will use the information from the comments + engine.Routes() to generate the documentation.

DustinHigginbotham commented 9 years ago

This is perfect, @manucorporat! When do you see this being merged into master?

manucorporat commented 9 years ago

@dustinwtf done! https://github.com/gin-gonic/gin/commit/451f3b988b647f9565ce2c277ab7775e3b65e363

zserge commented 9 years ago

@manucorporat Looks great, thanks!

Is there any reason to return the handler function name as a string, not as a Func pointer? If I understand it correctly - the final goal is to find the comments before the functions and parse them to generate Swagger/RAML docs? Probably the file name and line number would be more helpful (like returned by the Func.FileLine). Then the parser could just read the lines above it.

manucorporat commented 9 years ago

@zserge A pointer to a function is too low level, in my opinion, a high level API (like Gin) should hide low level details, it makes everybody's code safer. For example: if the middleware API would allow to jump between handlers and do crazy things, the whole community of middleware would be more error-prone and unsecure.

I like the idea of providing the filename and line, that's why I created the RouteInfo struct, we can add new fields in a completely backcompatible way! what about FileName string and FileLine uint? https://github.com/gin-gonic/gin/blob/master/gin.go#L37-L41

MattDavisRV commented 8 years ago

Is there a working example of how to use this with one of the swagger generators?

lluvio commented 8 years ago

@MattDavisRV +1

otraore commented 8 years ago

+1

daemonza commented 8 years ago

+1 on a example of how to use this

verdverm commented 8 years ago

Ok, here is an example, but it requires modifying the Gin codes slightly

Outline

I may have missed something, let me know and I will edit this.

You may only have to add the HandleHTTP field to Engine. I wanted the same JSON responses on OPTIONS requests.

Cheers!

~ Tony

Steps in detail

Step 1. go get the swagger tool

go get github.com/yvasiyarov/swagger

Step 2. build swagger-ui

git clone https://github.com/swagger-api/swagger-ui
cd swagger-ui
npm install
gulp

If you make any modifications to swagger-ui, be sure to rerun gulp

Step 3. modify github.com/gin-gonic/gin/gin.go

Add the following fields to the Engine definition [~ line 32]

HandleOPTIONS       bool
ApiDescriptionsJson map[string]string

Add fields to the engine.New() call [~ line 107 now]

HandleOPTIONS:          true,` 

The remainder of step 3 may be unnecessary, however you will be a better OPTIONS netizen if you do.

Change handleHTTPRequest to the following [~ line 274 now]

func (engine *Engine) handleHTTPRequest(context *Context) {
    httpMethod := context.Request.Method
    path := context.Request.URL.Path

    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method == httpMethod {
            root := t[i].root
            // Find route in tree
            handlers, params, tsr := root.getValue(path, context.Params)
            if handlers != nil {
                context.handlers = handlers
                context.Params = params
                context.Next()
                context.writermem.WriteHeaderNow()
                return

            } else if httpMethod != "CONNECT" && path != "/" {
                if tsr && engine.RedirectTrailingSlash {
                    redirectTrailingSlash(context)
                    return
                }
                if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) {
                    return
                }
            }
            break
        }
    }

    if httpMethod == "OPTIONS" {
        // Handle OPTIONS requests
        if engine.HandleOPTIONS {
            if allow := engine.allowed(path, httpMethod, context.Params); len(allow) > 0 {

                apiKey := strings.Trim(context.Request.RequestURI, "/")
                if json, ok := engine.ApiDescriptionsJson[apiKey]; ok {
                    t, e := template.New("desc").Parse(json)
                    if e != nil {
                        serveError(context, http.StatusInternalServerError, []byte("Internal Server Error 500\n"))
                        return
                    }
                    t.Execute(context.Writer, context.Request.Host)
                    context.writermem.Header()["Allow"] = []string{allow}
                }

                context.writermem.WriteHeaderNow()
                return
            }
        }
    } else {
        // TODO: unit test
        if engine.HandleMethodNotAllowed {
            for _, tree := range engine.trees {
                if tree.method != httpMethod {
                    if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
                        context.handlers = engine.allNoMethod
                        serveError(context, 405, default405Body)
                        return
                    }
                }
            }
        }
    }
    context.handlers = engine.allNoRoute
    serveError(context, 404, default404Body)
}

Add the following just above the handleHTTPRequest

func (engine *Engine) allowed(path, reqMethod string, po Params) (allow string) {
    if path == "*" { // server-wide
        for _, T := range engine.trees {
            if T.method == "OPTIONS" {
                continue
            }

            // add request method to list of allowed methods
            if len(allow) == 0 {
                allow = T.method
            } else {
                allow += ", " + T.method
            }
        }
    } else { // specific path
        for _, T := range engine.trees {
            // Skip the requested method - we already tried this one
            if T.method == reqMethod || T.method == "OPTIONS" {
                continue
            }

            handle, _, _ := engine.trees.get(T.method).getValue(path, po)
            if handle != nil {
                // add request method to list of allowed methods
                if len(allow) == 0 {
                    allow = T.method
                } else {
                    allow += ", " + T.method
                }
            }
        }
    }
    if len(allow) > 0 {
        allow += ", OPTIONS"
    }
    return
}

Step 4. add a swagger.go file at the root of your project

Be sure to update SWAGDIR to the location of the swagger-ui repository

package main

import (
    "flag"
    "net/http"
    "strings"
    "text/template"

    "github.com/gin-gonic/gin"
)

var SWAGDIR = "../swagger-ui/dist"
var staticContent = flag.String("staticPath", SWAGDIR, "Path to folder with Swagger UI")
var apiurl = flag.String("api", "http://localhost:8080", "The base path URI of the API service")

func swaggify(router *gin.Engine) {

    // Swagger Routes
    router.GET("/", IndexHandler)
    router.Static("/swagger-ui", *staticContent)
    for apiKey := range apiDescriptionsJson {
        router.GET("/"+apiKey+"/", ApiDescriptionHandler)
    }

    // API json data
    router.ApiDescriptionsJson = apiDescriptionsJson
}

func IndexHandler(c *gin.Context) {
    w := c.Writer
    r := c.Request

    isJsonRequest := false

    if acceptHeaders, ok := r.Header["Accept"]; ok {
        for _, acceptHeader := range acceptHeaders {
            if strings.Contains(acceptHeader, "json") {
                isJsonRequest = true
                break
            }
        }
    }

    if isJsonRequest {
        t, e := template.New("desc").Parse(resourceListingJson)
        if e != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        t.Execute(w, *apiurl)
    } else {
        http.Redirect(w, r, "/swagger-ui/", http.StatusFound)
    }
}

func ApiDescriptionHandler(c *gin.Context) {
    w := c.Writer
    r := c.Request

    apiKey := strings.Trim(r.RequestURI, "/")

    if json, ok := apiDescriptionsJson[apiKey]; ok {
        t, e := template.New("desc").Parse(json)
        if e != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        t.Execute(w, *apiurl)
    } else {
        w.WriteHeader(http.StatusNotFound)
    }
}

Step 5. pass your Router to be swaggified

by calling

swaggify(router)

Step 6. Annotate your code with comments (see the swagger tool docs)

add the following to your main.go

// @APIVersion 0.0.0
// @APITitle title
// @APIDescription description
// @Contact user@domain.com
// @TermsOfServiceUrl http://...
// @License MIT
// @LicenseUrl http://osensource.org/licenses/MIT

Step 7. run the swagger tool

swagger -apiPackage="path/to/your/api" -ignore "$vendor"

note path/to/your/api is a `go getable path

Step 8. run your API!!!

marioluan commented 7 years ago

Any progress with this feature?

landru29 commented 7 years ago

I'm beginner in go; I began to rewrite a comment parser to generate swagger.json. The project is still on-going. I decided to write my own parser as I could not manage to use https://github.com/yvasiyarov/swagger (missing deps with context). But I use the same wording.

Some data are not in comments, such host, scheme and basepath, as it depends on the environment where the API is deployed.

My project is only based on comments and does not parse the code. So it can be used with other frameworks https://github.com/landru29/swaggo

Some features are still missing. For today, only basics work.

dz0ny commented 7 years ago

@landru29 you need to run yvasiyarov/swagger with -ignore

landru29 commented 7 years ago
  1. Did not work.
  2. Need to pass some variable in the CLI (like 'host', 'basepath', ...)
  3. Want a tools not linked to a framework (no code parsing, only comments)
  4. Want cascading sub-routes (to follow the cascading of Groups)
jsloyer commented 7 years ago

+1

masato25 commented 7 years ago

++1

cch123 commented 7 years ago

+1

tboerger commented 7 years ago

Please use the reactions or the subscribe button.

harshadptl commented 7 years ago

👍

samtech09 commented 7 years ago

+1

savaki commented 7 years ago

I've been using swagger with go using code to generate the swagger file. The project is

github.com/savaki/swag

and I've included an example using gin, https://github.com/savaki/swag/blob/master/examples/gin/main.go

Feedback would be greatly appreciated.

mattfarina commented 7 years ago

@savaki There's no license in github.com/savaki/swag so I can't use it. Any chance of it getting an open source license?

alwqx commented 7 years ago

+1

NirmalVatsyayan commented 6 years ago

+1

jmp0xf commented 6 years ago

+65535

MTRNord commented 6 years ago

Just wondering will there be a PR to this repo about it? I would love to use it a official way :(

puma007 commented 6 years ago

@javierprovecho when will gin official support swagger?Thanks!

tonivj5 commented 6 years ago

+1

michelia commented 6 years ago

+1 👍