Chai - type safe http handlers with automatic swagger generation

chai

Description

chai is an extension for a few popular http routers that adds support for type safe http handlers via Go 1.18's generics. This allows it to also generate a swagger spec by automatically detecting the request/response types, http methods, route paths and path params.

chai uses swaggo/swag annotations for the parts of the swagger spec that cannot be automatically inferred.

Supported http routers

Project status

chai is still a work in progress

Gotchas

  • YAML marshalling

    Currently only https://github.com/ghodss/yaml is supported as a yaml marshaller for the generated swagger spec, which is also provided via openapi2.MarshalYAML() as an alias

Examples

Usage

This swagger.yaml is generated by the program below. Notice that the spec for the PostHandler handler was generated without any annotations. The request/response types and the route were detected automatically from the router itself.

image

package main

import (
	"fmt"
	"net/http"
	"strconv"

	chai "github.com/go-chai/chai/chi"
	_ "github.com/go-chai/chai/examples/docs/basic" // This is required to be able to serve the stored swagger spec in prod
	"github.com/go-chai/chai/examples/shared/model"
	"github.com/go-chai/chai/openapi2"
	"github.com/go-chi/chi/v5"
	"github.com/go-openapi/spec"
	httpSwagger "github.com/swaggo/http-swagger"
)

func main() {
	r := chi.NewRouter()

	r.Route("/api/v1", func(r chi.Router) {
		r.Route("/examples", func(r chi.Router) {
			chai.Post(r, "/post", PostHandler)
			chai.Get(r, "/calc", CalcHandler)
			chai.Get(r, "/ping", PingHandler)
			chai.Get(r, "/groups/{group_id}/accounts/{account_id}", PathParamsHandler)
			chai.Get(r, "/header", HeaderHandler)
			chai.Get(r, "/securities", SecuritiesHandler)
			chai.Get(r, "/attribute", AttributeHandler)
		})
	})

	// This must be used only during development to generate the swagger spec
	docs, err := chai.OpenAPI2(r)
	if err != nil {
		panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err))
	}

	// This should be used in prod to serve the swagger spec
	r.Get("/swagger/*", httpSwagger.Handler(
		httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition
	))

	addCustomDocs(docs)

	openapi2.LogYAML(docs)

	// This must be used only during development to store the swagger spec
	err = openapi2.WriteDocs(docs, &openapi2.GenConfig{
		OutputDir: "examples/docs/basic",
	})
	if err != nil {
		panic(fmt.Sprintf("failed to write the swagger spec: %+v", err))
	}

	fmt.Println("The swagger spec is available at http://localhost:8080/swagger/")

	http.ListenAndServe(":8080", r)
}

type Error struct {
	Message          string `json:"error"`
	ErrorDebug       string `json:"error_debug,omitempty"`
	ErrorDescription string `json:"error_description,omitempty"`
	StatusCode       int    `json:"status_code,omitempty"`
}

func (e *Error) Error() string {
	return e.Message
}

func PostHandler(account *model.Account, w http.ResponseWriter, r *http.Request) (*model.Account, int, *Error) {
	return account, http.StatusOK, nil
}

// @Param        val1  query      int     true  "used for calc"
// @Param        val2  query      int     true  "used for calc"
// @Success      203
// @Failure      400,404
func CalcHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	val1, err := strconv.Atoi(r.URL.Query().Get("val1"))
	if err != nil {
		return "", http.StatusBadRequest, err
	}
	val2, err := strconv.Atoi(r.URL.Query().Get("val2"))
	if err != nil {
		return "", http.StatusBadRequest, err
	}
	return fmt.Sprintf("%d", val1*val2), http.StatusOK, nil
}

// PingExample godoc
// @Summary      ping example
// @Description  do ping
// @Tags         example
func PingHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	return "pong", http.StatusOK, nil
}

// PathParamsHandler godoc
// @Summary      path params example
// @Description  path params
// @Tags         example
// @Param        group_id    path      int     true  "Group ID"
// @Param        account_id  path      int     true  "Account ID"
// @Failure      400,404
func PathParamsHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	groupID, err := strconv.Atoi(chi.URLParam(r, "group_id"))
	if err != nil {
		return "", http.StatusBadRequest, err
	}
	accountID, err := strconv.Atoi(chi.URLParam(r, "account_id"))
	if err != nil {
		return "", http.StatusBadRequest, err
	}

	return fmt.Sprintf("group_id=%d account_id=%d", groupID, accountID), http.StatusOK, nil
}

// HeaderHandler godoc
// @Summary      custome header example
// @Description  custome header
// @Tags         example
// @Param        Authorization  header    string  true  "Authentication header"
// @Failure      400,404
func HeaderHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	return r.Header.Get("Authorization"), http.StatusOK, nil
}

// SecuritiesHandler godoc
// @Summary      custome header example
// @Description  custome header
// @Tags         example
// @Param        Authorization  header    string  true  "Authentication header"
// @Failure      400,404
// @Security     ApiKeyAuth
func SecuritiesHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	return "ok", http.StatusOK, nil
}

// AttributeHandler godoc
// @Summary      attribute example
// @Description  attribute
// @Tags         example
// @Param        enumstring  query     string  false  "string enums"    Enums(A, B, C)
// @Param        enumint     query     int     false  "int enums"       Enums(1, 2, 3)
// @Param        enumnumber  query     number  false  "int enums"       Enums(1.1, 1.2, 1.3)
// @Param        string      query     string  false  "string valid"    minlength(5)  maxlength(10)
// @Param        int         query     int     false  "int valid"       minimum(1)    maximum(10)
// @Param        default     query     string  false  "string default"  default(A)
// @Success      200 "answer"
// @Failure      400,404 "ok"
func AttributeHandler(w http.ResponseWriter, r *http.Request) (string, int, error) {
	return fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s",
		r.URL.Query().Get("enumstring"),
		r.URL.Query().Get("enumint"),
		r.URL.Query().Get("enumnumber"),
		r.URL.Query().Get("string"),
		r.URL.Query().Get("int"),
		r.URL.Query().Get("default"),
	), http.StatusOK, nil
}

func addCustomDocs(docs *spec.Swagger) {
	docs.Swagger = "2.0"
	docs.Host = "localhost:8080"
	docs.Info = &spec.Info{
		InfoProps: spec.InfoProps{
			Description:    "This is a sample celler server.",
			Title:          "Swagger Example API",
			TermsOfService: "http://swagger.io/terms/",
			Contact: &spec.ContactInfo{
				ContactInfoProps: spec.ContactInfoProps{
					Name:  "API Support",
					URL:   "http://www.swagger.io/support",
					Email: "[email protected]",
				},
			},
			License: &spec.License{
				LicenseProps: spec.LicenseProps{
					Name: "Apache 2.0",
					URL:  "http://www.apache.org/licenses/LICENSE-2.0.html",
				},
			},
			Version: "1.0",
		},
	}
	docs.SecurityDefinitions = map[string]*spec.SecurityScheme{
		"ApiKeyAuth": {
			SecuritySchemeProps: spec.SecuritySchemeProps{
				Type: "apiKey",
				In:   "header",
				Name: "Authorization",
			},
		},
	}
}
Similar Resources

Automatic manual tracing :)

autotel Automatic manual tracing :) The aim of this project is to show how golang can be used to automatically inject open telemetry tracing (https://

Nov 7, 2022

The open source public cloud platform. An AWS alternative for the next generation of developers.

The open source public cloud platform. An AWS alternative for the next generation of developers.

M3O M3O is an open source public cloud platform. We are building an AWS alternative for the next generation of developers. Overview AWS was a first ge

Jan 2, 2023

Example goreleaser + github actions config with keyless signing and SBOM generation

supply-chain-example GoReleaser + Go Mod proxying + Cosign keyless signing + Syft SBOM generation example. How it works GoReleaser manages the entire

Nov 15, 2022

Next generation recitation assignment tool for 6.033. Modular, scalable, fast

Next generation recitation assignment tool for 6.033. Modular, scalable, fast

Feb 3, 2022

sget is a keyless safe script retrieval and execution tool

sget ⚠️ Not ready for use yet! sget is a keyless safe script retrieval and execution tool Security Should you discover any security issues, please ref

Dec 18, 2022

Image clone controller is a kubernetes controller to safe guard against the risk of container images disappearing

Image clone controller image clone controller is a kubernetes controller to safe guard against the risk of container images disappearing from public r

Oct 10, 2021

Generic sharded thread safe LRU cache in Go.

cache Cache is a thread safe, generic, and sharded in memory LRU cache object. This is achieved by partitioning values across many smaller LRU (least

Sep 12, 2022

Traefik-redirect-operator is created to substitute manual effort of creating an ingress and service type External.

Traefik-redirect-operator is created to substitute manual effort of creating an ingress and service type External.

Overview Traefik Redirect Operator is used to help creating a combination of Ingress of Traefik controller along with Service's ExternalName type. The

Sep 22, 2021

The fantastic Any type implementation for Go

Any The fantastic Any type implementation for Go. Overview New type which might be used as any type in Go. Getting Started package main import ( "fm

Dec 15, 2021
Comments
  • Weird error issue makes handlers return null even if error is nil

    Weird error issue makes handlers return null even if error is nil

    Hey, awesome project!!!

    I was just playing with it a bit and I could not make my get handler return data, I always got null. after digging a little around I found that errors.Is(err, nil) seems to always return false, and therefore always handles the response as if it would have been an error.... did you see this before ? I have tried with beta 1 and 2... the code I use matches the chi example very closely...

    After playing with it for a bit more time I found that I could make it work by creating a new variable using the errortype first like so:

    func (h *ResHandler[Res, Err]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    	res, code, err := h.f(w, r)
    	var nilErr Err
    	if !errors.Is(err, nilErr) {
    

    It is generally still a little strange to me, as I do not understand why nil suddenly gets somehow linked to the type ?! Anyways, maybe you have some thoughts to this...

    Greetings, Lukas

  • Swagger spec generation doesn't work with go1.18 due to changes in runtime.FuncForPC

    Swagger spec generation doesn't work with go1.18 due to changes in runtime.FuncForPC

    Something changed between go1.18beta2 and go1.18 resulting in runtime.FuncForPC(ptr).FileLine() returning <autogenerated> instead of a file name

    https://github.com/golang/go/issues/51774

Related tags
moreHandlers is a library which makes possible the use of multiple handlers for the MCBE server software

moreHandlers moreHandlers is a library which makes possible the use of multiple handlers for the MCBE server software https://github.com/df-mc/dragonf

Aug 4, 2022
OpenAPI Terraform Provider that configures itself at runtime with the resources exposed by the service provider (defined in a swagger file)
OpenAPI Terraform Provider that configures itself at runtime with the resources exposed by the service provider (defined in a swagger file)

Terraform Provider OpenAPI This terraform provider aims to minimise as much as possible the efforts needed from service providers to create and mainta

Dec 26, 2022
Prevent Kubernetes misconfigurations from ever making it (again 😤) to production! The CLI integration provides policy enforcement solution to run automatic checks for rule violations. Docs: https://hub.datree.io
Prevent Kubernetes misconfigurations from ever making it  (again 😤) to production! The CLI integration provides policy enforcement solution to run automatic checks for rule violations.  Docs: https://hub.datree.io

What is Datree? Datree helps to prevent Kubernetes misconfigurations from ever making it to production. The CLI integration can be used locally or in

Jan 1, 2023
Testcontainers is a Golang library that providing a friendly API to run Docker container. It is designed to create runtime environment to use during your automatic tests.

When I was working on a Zipkin PR I discovered a nice Java library called Testcontainers. It provides an easy and clean API over the go docker sdk to

Jan 7, 2023
A Grafana backend plugin for automatic synchronization of dashboard between multiple Grafana instances.

Grafana Dashboard Synchronization Backend Plugin A Grafana backend plugin for automatic synchronization of dashboard between multiple Grafana instance

Dec 23, 2022
Docker Swarm Ingress service based on OpenResty with automatic Let's Encrypt SSL provisioning

Ingress Service for Docker Swarm Swarm Ingress OpenResty is a ingress service for Docker in Swarm mode that makes deploying microservices easy. It con

Jun 23, 2022
Kubernetes-native automatic dashboard for Ingress
Kubernetes-native automatic dashboard for Ingress

ingress-dashboard Automatic dashboard generation for Ingress objects. Features: No JS Supports OIDC (Keycloak, Google, Okta, ...) and Basic authorizat

Oct 20, 2022
A Kubernetes operator that allows for automatic provisioning and distribution of cert-manager certs across namespaces

cached-certificate-operator CachedCertificate Workflow When a CachedCertificate is created or updated the operator does the following: Check for a val

Sep 6, 2022
Automatic-Update-Launcher - A general purpose updater for updating program binaries when update folder exists
Automatic-Update-Launcher - A general purpose updater for updating program binaries when update folder exists

Automatic Update Launcher A general purpose updater for updating (web) applicati

Jun 27, 2022
Automatic sync from IMDb to Trakt (watchlist, lists, ratings and history) using GitHub actions

imdb-trakt-sync GoLang app that can sync IMDb and Trakt user data - watchlist, ratings and lists. For its data needs, the app is communicating with th

Jan 2, 2023