zeal

package module
v0.9.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 14, 2024 License: MIT Imports: 13 Imported by: 0

README

     Logo

Zeal

✨ A type-safe REST API framework for Go!

About

  • Define structs to validate URL parameters, request bodies and responses.

  • Uses the standard library http.HandlerFunc for maximum compatibility.

  • URL parameters and request bodies are automatically converted to their declared type.

  • Automatically generates fully typed OpenAPI 3 spec documentation using REST and serves it with SwaggerUI.

Server

var mux = zeal.NewZealMux(http.NewServeMux(), "Example API")

func main() {
    addRoutes(mux)

    specOptions := zeal.SpecOptions{
        ZealMux:       mux,
        Version:       "v0.1.0",
        Description:   "Example API description.",
        StripPkgPaths: []string{"main", "models", "github.com/DandyCodes/zeal"},
    }
    openAPISpec, err := zeal.NewOpenAPISpec(specOptions)
    if err != nil {
        log.Fatalf("Failed to create OpenAPI spec: %v", err)
    }

    port := 3975
    swaggerPattern := "/swagger-ui/"
    fmt.Printf("Visit http://localhost:%v%v to see API definitions\n", port, swaggerPattern)
    zeal.ServeSwaggerUI(mux, openAPISpec, "GET "+swaggerPattern)

    fmt.Printf("Listening on port %v...\n", port)
    http.ListenAndServe(fmt.Sprintf(":%v", port), mux)
}

Routes

Create your route by calling zeal.NewRoute, passing it a zeal.ZealMux:

var route = zeal.NewRoute[zeal.Route](mux)

Passing the basic zeal.Route as a type parameter to zeal.NewRoute means this route has no:

  • Response type
  • URL parameters
  • Request body

Now, define your handler function using the newly created route:

route.HandleFunc("POST /hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, world!")
})

Routes handled by Zeal are automatically documented in the OpenAPI spec.

Responses

Create a route definition struct and embed zeal.Route and zeal.HasResponse.

This route will respond with an integer, so int is passed to zeal.HasResponse as a type parameter:

type GetAnswer struct {
    zeal.Route
    zeal.HasResponse[int]
}

Create your route, passing your route definition as a type parameter, and define your handler function:

var getAnswer = zeal.NewRoute[GetAnswer](mux)
getAnswer.HandleFunc("GET /answer", func(w http.ResponseWriter, r *http.Request) {
    getAnswer.Response(42)
})

The Response method will only accept data of the declared response type.


Type parameters passed to zeal.HasResponse can be more complex.

Here is some example data:

var foodMenu = models.Menu{
    ID: 1,
    Items: []models.Item{
        {Name: "Steak", Price: 13.95},
        {Name: "Potatoes", Price: 3.95},
    },
}

var drinksMenu = models.Menu{
    ID: 2,
    Items: []models.Item{
        {Name: "Juice", Price: 1.25},
        {Name: "Soda", Price: 1.75},
    },
}

This route responds with a slice of menus:

var menus = []models.Menu{foodMenu, drinksMenu}

type GetMenus struct {
    zeal.Route
    zeal.HasResponse[[]models.Menu]
}
var getMenus = zeal.NewRoute[GetMenus](mux)
getMenus.HandleFunc("GET /menus", func(w http.ResponseWriter, r *http.Request) {
    getMenus.Response(menus)
})

URL Parameters

Create a route definition struct and embed zeal.Route and zeal.HasParams.

You can pass zeal.HasParams an anonymous in-line struct definition as a type parameter.

Create your route, passing your route definition as a type parameter, and define your handler function:

type DeleteMenu struct {
    zeal.Route
    zeal.HasParams[struct {
        ID    int
        Quiet bool
    }]
}
var deleteMenu = zeal.NewRoute[DeleteMenu](mux)
deleteMenu.HandleFunc("DELETE /menus/{ID}", func(w http.ResponseWriter, r *http.Request) {
    if !deleteMenu.Params().Quiet {
        fmt.Println("Deleting menu")
    }

    for i := 0; i < len(menus); i++ {
        if menus[i].ID == deleteMenu.Params().ID {
            menus = append(menus[:i], menus[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }

    w.WriteHeader(http.StatusNotFound)
})

Params found in the URL pattern (for example, 'ID' in '/menus/{ID}') will be defined as path params whilst others will be query params.

Params are converted to their declared type. If this fails, http.StatusUnprocessableEntity 422 is sent immediately.

Struct fields must be capitalized to be accessed in the route - for example, 'Quiet'.

Request Bodies

Create a route definition struct and embed zeal.Route and zeal.HasBody.

Pass the body type to zeal.HasBody as a type parameter.

Create your route, passing your route definition as a type parameter, and define your handler function:

type PutItem struct {
    zeal.Route
    zeal.HasBody[models.Item]
}
var putItem = zeal.NewRoute[PutItem](mux)
putItem.HandleFunc("PUT /items", func(w http.ResponseWriter, r *http.Request) {
    item := putItem.Body()
    if item.Price < 0 {
        http.Error(w, "Price cannot be negative", http.StatusBadRequest)
        return
    }

    for i := range menus {
        for j := range menus[i].Items {
            if menus[i].Items[j].Name == item.Name {
                menus[i].Items[j].Price = item.Price
                return
            }
        }
    }

    menus[0].Items = append(menus[0].Items, item)
    w.WriteHeader(http.StatusCreated)
})

The body is converted to its declared type. If this fails, http.StatusUnprocessableEntity 422 is sent immediately.

Struct fields must be capitalized to be accessed in the route - for example, 'Price'.

Error Handling

Use the HandleFuncErr method to create a handler function which returns an error.

Route handler functions can be defined in an outer scope:

var mux = zeal.NewServeMux(http.NewServeMux(), "Example API")

type PostItem struct {
    zeal.Route
    zeal.HasParams[struct{ MenuID int }]
    zeal.HasBody[models.Item]
    zeal.HasResponse[models.Item]
}

var postItem = zeal.NewRoute[PostItem](mux)

func addOuterScopeRoute() {
    postItem.HandleFuncErr("POST /items/{MenuID}", HandlePostItem)
}

func HandlePostItem(w http.ResponseWriter, r *http.Request) error {
    item := postItem.Body()
    if item.Price < 0 {
        return zeal.Error(w, "Price cannot be negative", http.StatusBadRequest)
    }

    for i := range menus {
        if menus[i].ID == postItem.Params().MenuID {
            menus[i].Items = append(menus[i].Items, item)
            return postItem.Response(item, http.StatusCreated)
        }
    }

    return zeal.WriteHeader(w, http.StatusNotFound)
}

The zeal.Error function returns a nil error after calling http.Error with a given error message and HTTP status code.

The Response method can be passed an optional HTTP status code (200 OK is sent by default). It returns a nil error if successful. Otherwise, it returns the JSON serialization error after calling http.Error with http.StatusInternalServerError.

The zeal.WriteHeader function returns a nil error after calling http.ResponseWriter.WriteHeader with a given HTTP status code.

Nested Handlers

Use zeal.ZealMux.Handle to preserve route documentation of sub handlers, using zeal.StripPrefix if necessary:

topMux := zeal.NewZealMux(http.NewServeMux(), "Example API")
subMux := zeal.NewZealMux(http.NewServeMux())
addRoutes(subMux)
topMux.Handle("/sub_api/", zeal.StripPrefix("/sub_api", subMux))

And use the top zeal.ZealMux to create the OpenAPI spec and listen and serve:

specOptions := zeal.SpecOptions{
    ZealMux:       topMux,
    Version:       "v0.1.0",
    Description:   "Example API description.",
    StripPkgPaths: []string{"main", "models", "github.com/DandyCodes/zeal"},
}
openAPISpec, err := zeal.NewOpenAPISpec(specOptions)
if err != nil {
    log.Fatalf("Failed to create OpenAPI spec: %v", err)
}

port := 3975
swaggerPattern := "/swagger-ui/"
fmt.Printf("Visit http://localhost:%v%v to see API definitions\n", port, swaggerPattern)
zeal.ServeSwaggerUI(topMux, openAPISpec, "GET "+swaggerPattern)

fmt.Printf("Listening on port %v...\n", port)
http.ListenAndServe(fmt.Sprintf(":%v", port), topMux)

Credits

Helmet icons created by Freepik - Flaticon

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Error added in v0.4.0

func Error(w http.ResponseWriter, error string, code int) error

func NewOpenAPISpec added in v0.9.0

func NewOpenAPISpec(options SpecOptions) (*openapi3.T, error)

func NewRoute added in v0.9.0

func NewRoute[T_Route http.Handler](mux *ZealMux) *T_Route

func ServeSwaggerUI added in v0.7.0

func ServeSwaggerUI(mux *ZealMux, openAPISpec *openapi3.T, path string) error

func WriteHeader added in v0.9.0

func WriteHeader(w http.ResponseWriter, statusCode int) error

Types

type HandlerFuncErr added in v0.9.0

type HandlerFuncErr func(http.ResponseWriter, *http.Request) error

type HasBody added in v0.9.0

type HasBody[T_Body any] struct {
	// contains filtered or unexported fields
}

func (*HasBody[T_Body]) Body added in v0.9.0

func (b *HasBody[T_Body]) Body() T_Body

func (*HasBody[T_Body]) Validate added in v0.9.0

func (b *HasBody[T_Body]) Validate(request *http.Request) (T_Body, error)

type HasParams added in v0.9.0

type HasParams[T_Params any] struct {
	// contains filtered or unexported fields
}

func (*HasParams[T_Params]) Params added in v0.9.0

func (p *HasParams[T_Params]) Params() T_Params

func (*HasParams[T_Params]) Validate added in v0.9.0

func (p *HasParams[T_Params]) Validate(request *http.Request) (T_Params, error)

type HasResponse added in v0.9.0

type HasResponse[T_Response any] struct {
	// contains filtered or unexported fields
}

func (*HasResponse[T_Response]) Response added in v0.9.0

func (r *HasResponse[T_Response]) Response(data T_Response, status ...int) error

func (*HasResponse[T_Response]) Validate added in v0.9.2

func (r *HasResponse[T_Response]) Validate(responseWriter *http.ResponseWriter)

type Route added in v0.3.0

type Route struct {
	*ZealMux
	// contains filtered or unexported fields
}

func (*Route) HandleFunc added in v0.9.0

func (mux *Route) HandleFunc(pattern string, handlerFunc http.HandlerFunc)

func (*Route) HandleFuncErr added in v0.9.0

func (mux *Route) HandleFuncErr(pattern string, handlerFunc HandlerFuncErr)

func (*Route) Validate added in v0.9.2

func (r *Route) Validate(routeDefinition ...*any) *any

type SpecOptions added in v0.5.2

type SpecOptions struct {
	ZealMux       *ZealMux
	Version       string
	Description   string
	StripPkgPaths []string
}

type ZealMux added in v0.9.2

type ZealMux struct {
	*http.ServeMux
	Api *rest.API
}

func NewZealMux added in v0.9.2

func NewZealMux(mux *http.ServeMux, apiName ...string) *ZealMux

func StripPrefix added in v0.9.3

func StripPrefix(prefix string, h *ZealMux) *ZealMux

func (*ZealMux) Handle added in v0.9.3

func (m *ZealMux) Handle(pattern string, handler http.Handler)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL