14

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)
}
matusf
  • 469
  • 1
  • 8
  • 20
  • 2
    It doesn't make much sense to first write an API implementation and then generate spec for it. Purpose of OpenAPI is exact opposite. Also such spec would be hardly complete (how such tool would know about e.g. auth headers handled by middleware or restrictions on input implemented in handler, etc.) – blami Feb 12 '21 at 16:01
  • Totally agree with above statement. I *always* write the openapi YAML first & code second. Ensures the inputs & outputs across all routes are consistent especially when using schema `$ref` references. – colm.anseo Feb 13 '21 at 01:18
  • 10
    OpenAPI as a spec does not suggest which way of creating it preferable. It all comes to particular tools and conventions. Wiring information about security definitions into API doc is definitely doable, please check https://github.com/swaggest/rest/blob/v0.1.18/_examples/task-api/internal/infra/nethttp/router.go#L72-L77 for example. And moreover, expressing spec from code is generally much less prone to human errors and outdatedness. – vearutop Feb 13 '21 at 10:05
  • 1
    I'm a vocal believer that using an OpenAPI spec generated from code as the authority on how to implement a client is generally a bad idea, akin to writing your UI/Functionality design docs after you've written your application. That said, there are absolutely reasons why you'd generate an OpenAPI spec from source code. This allows you to compare the generated spec against your designed spec to ensure it's compliant with the design. – Software2 Feb 13 '21 at 13:05
  • 4
    There are two important aspects of API spec as seen by client. First, it has to be accurate. Second, it has to be backwards compatible in a reasonable interval. Neither of these properties are exclusively granted by spec first approach. Spec that is reflected from source has highest possible accuracy. Writing a spec by hand is a tedious process prone to errors and misalignment. In contrast, writing a zero implementation in Go is guarded by compile-time type safety and is ready for further actual implementation as soon as exported API spec is approved by peers. – vearutop Feb 14 '21 at 22:04

1 Answers1

10

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)
    }
}
vearutop
  • 3,924
  • 24
  • 41