zeal

package module
v0.8.0 Latest Latest
Warning

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

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

README

     Logo

Zeal

✨ A type-safe REST API framework for Go!

About

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

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

  • 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.NewServeMux(http.NewServeMux(), "Example API")

func main() {
    addRoutes(Mux)

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

    PORT := 3975
    SWAGGER_PATTERN := "/swagger-ui/"
    fmt.Printf("Visit http://localhost:%v%v to see API definitions\n", PORT, SWAGGER_PATTERN)
    zeal.ServeSwaggerUI(Mux, spec, "GET "+SWAGGER_PATTERN)

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

Routes

A standard library http.HandlerFunc is passed to a zeal route.

Create a route definition struct and pass it to zeal.Route as a type parameter:

type PostRoot struct{}
var postRoot = zeal.Route[PostRoot](Mux)
postRoot.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, world!")
    w.WriteHeader(http.StatusOK)
})

This route uses an empty struct which means it has no:

  • Response type
  • URL parameters
  • Request body

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

Using any in place of an empty struct accomplishes the same outcome:

zeal.Route[any](Mux).HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, world!")
})

Responses

Embed zeal.RouteResponse in your route definition, passing it the response type as a type parameter.

This route responds with an int:

type GetAnswer struct{ zeal.RouteResponse[int] }
var getAnswer = zeal.Route[GetAnswer](Mux)
getAnswer.HandleFunc("GET /answer", func(w http.ResponseWriter, r *http.Request) {
    getAnswer.Route.Response(42)
})

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


A zeal.RouteResponse can use complex types.

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.RouteResponse[[]models.Menu]
}
var getMenus = zeal.Route[GetMenus](Mux)
getMenus.HandleFunc("GET /menus", func(w http.ResponseWriter, r *http.Request) {
    getMenus.Route.Response(menus, http.StatusOK)
})

The Route.Response method can be passed an optional HTTP status code (200 OK is sent by default).

URL Parameters

Embed zeal.RouteParams in your route definition, passing it the params type as a type parameter.

The params struct can be anonymous and defined in-line:

type GetMenu struct {
    zeal.RouteParams[struct {
        ID    int
        Quiet bool
    }]
    zeal.RouteResponse[models.Menu]
}
var getMenu = zeal.Route[GetMenu](Mux)
getMenu.HandleFunc("GET /menus/{ID}", func(w http.ResponseWriter, r *http.Request) {
    quiet := getMenu.Route.Params().Quiet
    if !quiet {
        fmt.Println("Getting menu")
    }

    ID := getMenu.Route.Params().ID
    for i := 0; i < len(menus); i++ {
        menu := menus[i]
        if menu.ID == ID {
            getMenu.Route.Response(menu)
            return
        }
    }

    getMenu.Error(http.StatusNotFound)
})

Params found in the URL pattern (for example, 'ID' in '/menus/{ID}') will be defined as path params - all 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

Embed zeal.RouteBody in your route definition, passing it the body type as a type parameter:

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

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

            menus[i].Items[j].Price = item.Price
            updatedItem := menus[i].Items[j]
            putItem.Route.Response(updatedItem)
            return
        }
    }

    menus[1].Items = append(menus[1].Items, item)
    updatedItem := menus[1].Items[len(menus[1].Items)-1]
    putItem.Route.Response(updatedItem, 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'.

Miscellaneous

Use the HandleErr 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.RouteParams[struct{ MenuID int }]
    zeal.RouteBody[models.Item]
}

var postItem = zeal.Route[PostItem](Mux)

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

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

    MenuID := postItem.Route.Params().MenuID
    for i := range menus {
        if menus[i].ID != MenuID {
            continue
        }

        menus[i].Items = append(menus[i].Items, item)
        return postItem.Status(http.StatusCreated)
    }

    return postItem.Error(http.StatusNotFound)
}

The Status method responds with a given HTTP status code.

The Error method responds with a given HTTP status code and an optional error message. It must be passed an error code (4xx or 5xx), or else it will respond with http.StatusInternalServerError 500 instead.

Credits

Helmet icons created by Freepik - Flaticon

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CreateOpenAPISpec added in v0.7.0

func CreateOpenAPISpec(options OpenAPISpecOptions) (*openapi3.T, error)

func ServeSwaggerUI added in v0.7.0

func ServeSwaggerUI(mux *ServeMux, spec *openapi3.T, path string) error

Types

type HandlerFunc added in v0.3.0

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

type OpenAPISpecOptions added in v0.7.0

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

type RouteBody added in v0.7.0

type RouteBody[T_Body any] struct {
	ResponseWriter http.ResponseWriter
	Request        *http.Request
}

func (RouteBody[T_Body]) Body added in v0.7.0

func (b RouteBody[T_Body]) Body() T_Body

func (RouteBody[T_Body]) Validate added in v0.7.0

func (b RouteBody[T_Body]) Validate() (T_Body, error)

type RouteMux added in v0.7.0

type RouteMux[T_Route any] struct {
	*ServeMux
	Route          T_Route
	ResponseWriter *http.ResponseWriter
}

func Route added in v0.3.0

func Route[T_Route any](mux *ServeMux) *RouteMux[T_Route]

func (*RouteMux[T_Route]) Error added in v0.7.0

func (mux *RouteMux[T_Route]) Error(status int, message ...string) error

func (*RouteMux[T_Route]) HandleErr added in v0.8.0

func (mux *RouteMux[T_Route]) HandleErr(pattern string, handlerFunc HandlerFunc)

func (*RouteMux[T_Route]) HandleFunc added in v0.7.0

func (mux *RouteMux[T_Route]) HandleFunc(pattern string, handlerFunc http.HandlerFunc)

func (*RouteMux[T_Route]) Status added in v0.7.0

func (mux *RouteMux[T_Route]) Status(status int) error

type RouteParams added in v0.8.0

type RouteParams[T_Params any] struct {
	Request *http.Request
}

func (RouteParams[T_Params]) Params added in v0.8.0

func (p RouteParams[T_Params]) Params() T_Params

func (RouteParams[T_Params]) Validate added in v0.8.0

func (p RouteParams[T_Params]) Validate() (T_Params, error)

type RouteResponse added in v0.7.0

type RouteResponse[T_Response any] struct {
	ResponseWriter http.ResponseWriter
}

func (RouteResponse[T_Response]) Response added in v0.7.0

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

type ServeMux added in v0.5.1

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

func NewServeMux added in v0.5.1

func NewServeMux(mux *http.ServeMux, apiName ...string) *ServeMux

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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