Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generate OpenAPI v3 specification from Go source code?

Tags:

go

openapi

Is there a way to generate OpenAPI v3 specification from go source code? Let's say I have a go API like the one below and I'd like to generate the OpenAPI specification (yaml file) from it. Something similar to Python's Flask RESTX. I know there are tools that generate go source code from the specs, however, I'd like to do it the other way around.

package main

import "net/http"

func main() {
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("world\n"))
    })
    http.ListenAndServe(":5050", nil)
}
like image 775
matusf Avatar asked Nov 06 '22 02:11

matusf


1 Answers

You can employ github.com/swaggest/rest to build a self-documenting HTTP REST API. This library establishes a convention to declare handlers in a way that can be used to reflect documentation and schema and maintain a single source of truth about it.

In my personal opinion code first approach has advantages comparing to spec first approach. It can lower the entry bar by not requiring to be an expert in spec language syntax. And it may help to come up with a spec that is well balanced with implementation details.

With code first approach it is not necessary to implement a full service to get the spec. You only need to define the structures and interfaces and may postpone actual logic implementation.

Please check a brief usage example.

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    "github.com/swaggest/rest"
    "github.com/swaggest/rest/chirouter"
    "github.com/swaggest/rest/jsonschema"
    "github.com/swaggest/rest/nethttp"
    "github.com/swaggest/rest/openapi"
    "github.com/swaggest/rest/request"
    "github.com/swaggest/rest/response"
    "github.com/swaggest/rest/response/gzip"
    "github.com/swaggest/swgui/v3cdn"
    "github.com/swaggest/usecase"
    "github.com/swaggest/usecase/status"
)

func main() {
    // Init API documentation schema.
    apiSchema := &openapi.Collector{}
    apiSchema.Reflector().SpecEns().Info.Title = "Basic Example"
    apiSchema.Reflector().SpecEns().Info.WithDescription("This app showcases a trivial REST API.")
    apiSchema.Reflector().SpecEns().Info.Version = "v1.2.3"

    // Setup request decoder and validator.
    validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
    decoderFactory := request.NewDecoderFactory()
    decoderFactory.ApplyDefaults = true
    decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)

    // Create router.
    r := chirouter.NewWrapper(chi.NewRouter())

    // Setup middlewares.
    r.Use(
        middleware.Recoverer,                          // Panic recovery.
        nethttp.OpenAPIMiddleware(apiSchema),          // Documentation collector.
        request.DecoderMiddleware(decoderFactory),     // Request decoder setup.
        request.ValidatorMiddleware(validatorFactory), // Request validator setup.
        response.EncoderMiddleware,                    // Response encoder setup.
        gzip.Middleware,                               // Response compression with support for direct gzip pass through.
    )

    // Create use case interactor.
    u := usecase.IOInteractor{}

    // Describe use case interactor.
    u.SetTitle("Greeter")
    u.SetDescription("Greeter greets you.")

    // Declare input port type.
    type helloInput struct {
        Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
        Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
    }
    u.Input = new(helloInput)

    // Declare output port type.
    type helloOutput struct {
        Now     time.Time `header:"X-Now" json:"-"`
        Message string    `json:"message"`
    }
    u.Output = new(helloOutput)

    u.SetExpectedErrors(status.InvalidArgument)
    messages := map[string]string{
        "en-US": "Hello, %s!",
        "ru-RU": "Привет, %s!",
    }
    u.Interactor = usecase.Interact(func(ctx context.Context, input, output interface{}) error {
        var (
            in  = input.(*helloInput)
            out = output.(*helloOutput)
        )

        msg, available := messages[in.Locale]
        if !available {
            return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
        }

        out.Message = fmt.Sprintf(msg, in.Name)
        out.Now = time.Now()

        return nil
    })

    // Add use case handler to router.
    r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u))

    // Swagger UI endpoint at /docs.
    r.Method(http.MethodGet, "/docs/openapi.json", apiSchema)
    r.Mount("/docs", v3cdn.NewHandler(apiSchema.Reflector().Spec.Info.Title,
        "/docs/openapi.json", "/docs"))

    // Start server.
    log.Println("http://localhost:8011/docs")
    if err := http.ListenAndServe(":8011", r); err != nil {
        log.Fatal(err)
    }
}
like image 95
vearutop Avatar answered Nov 11 '22 05:11

vearutop