zeal

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Apr 2, 2024 License: MIT Imports: 11 Imported by: 0

README

Zeal

A type-safe REST API framework for Go!

Structs can be used to define and validate URL parameters, request bodies and response types.

Params and request bodies are automatically converted to their declared types for easy use within your routes.

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

Usage

func main() {
    mux := zeal.NewServeMux("Example API")
    addRoutes(mux)

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

    serveOptions := zeal.ServeOptions{
        Port:           3975,
        Spec:           spec,
        ServeSwaggerUI: true,
        SwaggerPattern: "/swagger-ui/",
    }
    mux.ListenAndServe(serveOptions)
}

Routes handled by Zeal are automatically documented in the OpenAPI schema

This route has no response type, no URL params and no request body

zeal.Handle(mux, "POST /",
    func(response zeal.Response[any], params any, body any) error {
        fmt.Println("Hello, world!")
        return response.Status(http.StatusOK)
    })

The response type is passed as a type parameter

This route responds with an int - zeal.Response[int]

zeal.Handle(mux, "GET /the_answer",
    func(r zeal.Response[int], p any, b any) error {
        return r.JSON(42)
    })

The JSON method will only accept data of the declared response type


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 - zeal.Response[[]models.Menu]

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

zeal.Handle(mux, "GET /menus",
    func(r zeal.Response[[]models.Menu], p any, b any) error {
        return r.JSON(menus, http.StatusOK)
    })

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


Params can be query or path params

Struct representing URL params can be defined in-line

zeal.Handle(mux, "GET /menus/{ID}",
    func(r zeal.Response[models.Menu], p struct{ ID int }, b any) error {
        for _, menu := range menus {
            if menu.ID == p.ID {
                return r.JSON(menu)

            }
        }

        return r.Error(http.StatusNotFound)
    })

Params and request bodies are converted to their declared type

If this fails, http.StatusUnprocessableEntity 422 is sent immediately

type PutItemParams struct {
    Quiet bool
}
zeal.Handle(mux, "PUT /items",
    func(r zeal.Response[models.Item], p PutItemParams, item models.Item) error {
        if item.Price < 0 {
            return r.Error(http.StatusBadRequest, "Price cannot be negative")

        }

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

                if !p.Quiet {
                    fmt.Println("Updating item:", item)
                }
                menus[i].Items[j].Price = item.Price
                updatedItem := menus[i].Items[j]
                return r.JSON(updatedItem)

            }
        }

        if !p.Quiet {
            fmt.Println("Creating new item:", item)
        }
        menus[1].Items = append(menus[1].Items, item)
        updatedItem := menus[1].Items[len(menus[1].Items)-1]
        return r.JSON(updatedItem, http.StatusCreated)
    })

Struct fields must be capitalized (i.e. 'Quiet')

zeal.Handle(mux, "POST /items", HandlePostItem)

func HandlePostItem(r zeal.Response[any], p struct{ MenuID int }, item models.Item) error {
    if item.Price < 0 {
        return r.Error(http.StatusBadRequest, "Price cannot be negative")
    }

    for i := range menus {
        if menus[i].ID != p.MenuID {
            continue
        }

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

    return nil
}

The Error method takes an HTTP status code and an optional error message

Standard Library Integration

The standard library *http.Request and http.ResponseWriter can still be accessed in a zeal route

zeal.Handle(mux, "GET /",
    func(r zeal.Response[any], p any, b any) error {
        fmt.Println(r.Request)        // *http.Request
        fmt.Println(r.ResponseWriter) // http.ResponseWriter
        return nil
    })

And you can use *zeal.ServeMux to define a regular http.HandlerFunc

func DefineStandardRoute(mux *zeal.ServeMux) {
    mux.HandleFunc("GET /std", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    })
}

However, routes defined this way will not be documented in the OpenAPI spec

Middleware

To add middleware such as logging, you can create your own wrapper around zeal.Handle

func MyHandle[T_Response, T_Params, T_Body any](
    mux *zeal.ServeMux, urlPattern string, handlerFunc zeal.HandlerFunc[T_Response, T_Params, T_Body],
) {
    myHanlderFunc := func(r zeal.Response[T_Response], p T_Params, b T_Body) error {
        err := handlerFunc(r, p, b)
        if err != nil {
            log.Println(err)
        }
        return err
    }
    zeal.Handle(mux, urlPattern, myHanlderFunc)
}

And define your route using your wrapper

MyHandle(mux, "GET /errors_logged",
    func(r zeal.Response[[]models.Menu], p any, b any) error {
        if rand.Float64() > 0.33 {
            return fmt.Errorf("an error occurred")
        } else {
            return r.JSON(menus)
        }
    })

Or create a stack of middleware

func LogErrorHandle[T_Response, T_Params, T_Body any](
    handlerFunc zeal.HandlerFunc[T_Response, T_Params, T_Body],
) zeal.HandlerFunc[T_Response, T_Params, T_Body] {
    return func(r zeal.Response[T_Response], p T_Params, b T_Body) error {
        err := handlerFunc(r, p, b)
        if err != nil {
            log.Println(err)
        }
        return err
    }
}

func LogRequestHandle[T_Response, T_Params, T_Body any](
    handlerFunc zeal.HandlerFunc[T_Response, T_Params, T_Body],
) zeal.HandlerFunc[T_Response, T_Params, T_Body] {
    return func(r zeal.Response[T_Response], p T_Params, b T_Body) error {
        log.Println(r.Request)
        return handlerFunc(r, p, b)
    }
}

func ProtectDdosHandle[T_Response, T_Params, T_Body any](
    handlerFunc zeal.HandlerFunc[T_Response, T_Params, T_Body],
) zeal.HandlerFunc[T_Response, T_Params, T_Body] {
    return func(r zeal.Response[T_Response], p T_Params, b T_Body) error {
        if rand.Float64() > 0.33 {
            return fmt.Errorf("computer says no")
        }
        return handlerFunc(r, p, b)
    }
}

func MyStackHandle[T_Response, T_Params, T_Body any](
    mux *zeal.ServeMux, urlPattern string, handlerFunc zeal.HandlerFunc[T_Response, T_Params, T_Body],
) {
    logErrorHandlerFunc := LogErrorHandle(handlerFunc)
    logRequestHandlerFunc := LogRequestHandle(logErrorHandlerFunc)
    protectDdosHandlerFunc := ProtectDdosHandle(logRequestHandlerFunc)
    zeal.Handle(mux, urlPattern, protectDdosHandlerFunc)
}

And define your route using this wrapper

MyStackHandle(mux, "GET /stack",
    func(r zeal.Response[[]models.Menu], p any, b any) error {
        if rand.Float64() > 0.33 {
            return fmt.Errorf("an error occurred")
        } else {
            return r.JSON(menus)
        }
    })

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Handle added in v0.5.0

func Handle[T_Response, T_Params, T_Body any](mux *ServeMux, pattern string, handlerFunc HandlerFunc[T_Response, T_Params, T_Body])

Types

type HandlerFunc added in v0.3.0

type HandlerFunc[T_Response, T_Params, T_Body any] func(Response[T_Response], T_Params, T_Body) error

type Response added in v0.5.1

type Response[T_Response any] struct {
	ResponseWriter http.ResponseWriter
	Request        *http.Request
}

func (Response[T_Response]) Error added in v0.5.1

func (r Response[T_Response]) Error(status int, errorMsg ...string) error

func (*Response[T_Response]) JSON added in v0.5.1

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

func (Response[T_Response]) Status added in v0.5.1

func (r Response[T_Response]) Status(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(apiName ...string) *ServeMux

func (*ServeMux) CreateSpec added in v0.5.2

func (mux *ServeMux) CreateSpec(options SpecOptions) (*openapi3.T, error)

func (*ServeMux) ListenAndServe added in v0.5.1

func (mux *ServeMux) ListenAndServe(options ServeOptions) error

func (*ServeMux) ServeSwaggerUI added in v0.5.1

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

type ServeOptions added in v0.5.1

type ServeOptions struct {
	Port           int
	Spec           *openapi3.T
	ServeSwaggerUI bool
	SwaggerPattern string
}

type SpecOptions added in v0.5.2

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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