zeal

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2023 License: MIT Imports: 14 Imported by: 0

README

Zeal

A type-safe REST API framework for Go!

Inspired by FastAPI, types are used to define and validate path parameters, request bodies and responses.

Automatically generates OpenAPI 3 schema documentation and serves it using Swagger.

It builds upon chi (github.com/go-chi/chi) for routing and rest (github.com/a-h/rest) for generating the OpenAPI spec.


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}},
}

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

func main() {
	rt := zeal.NewRouter("API")

	addRoutes(rt)

	rt.Api.StripPkgPaths = []string{"main", "models", "github.com/DandyCodes/zeal"}
	spec := rt.CreateSpec("v1.0.0", "Spec")
	rt.ServeSwaggerUI(spec, "/swagger-ui*")

	fmt.Println("Listening on port 3000...")
	fmt.Println("Visit http://localhost:3000/swagger-ui to see API definitions")
	http.ListenAndServe(":3000", rt)
}

func addRoutes(rt *zeal.Router) {
	// A zeal route
	zeal.Get(rt, "/", func(w zeal.Writer[any], r *zeal.Rqr[any]) {
		w.Write([]byte("Hello, world!"))
	})

	// This route responds with an integer. Instead of no type (any),
	// the response type (int) is passed to the writer - zeal.Writer[int]
	zeal.Get(rt, "/the_answer", func(w zeal.Writer[int], r *zeal.Rqr[any]) {
		// This JSON convenience method will only accept data of the declared response type
		w.JSON(http.StatusOK, 42)
	})

	// This route responds with a slice of menus - []models.Menu
	// The response type is passed to the writer - zeal.Writer[[]models.Menu]
	zeal.Get(rt, "/menus", func(w zeal.Writer[[]models.Menu], r *zeal.Rqr[any]) {
		menus := menus
		w.JSON(http.StatusOK, menus)
	})

	// Struct type definition representing both path and query URL parameters
	// Fields must be capitalized so that Go exports them
	// Parameters are automatically validated and converted to their declared type
	// If validation fails, zeal responds with http.StatusUnprocessableEntity 422
	type GetPrintParams struct {
		StringP  string
		IntP     uint8
		BoolQ    bool
		Float32Q float32
	}
	// The parameters type is passed to the request - *zeal.Rqr[GetPrintParams]
	// Since this route has no response type, no type (any) is passed to the writer - zeal.Writer[any]
	zeal.Get(rt, "/print/{IntP}/{StringP}", func(w zeal.Writer[any], r *zeal.Rqr[GetPrintParams]) {
		// IntP and StringP are path params because they appear in the URL path "/print/{IntP}/{StringP}"
		// BoolQ and Float32Q are, therefore, automatically query params
		// Both types of URL parameters are found in the Params field of the request

		aStringPathParameter := r.Params.StringP
		fmt.Println(aStringPathParameter, reflect.TypeOf(aStringPathParameter)) // string type

		anIntPathParameter := r.Params.IntP
		fmt.Println(anIntPathParameter, reflect.TypeOf(anIntPathParameter)) // int type

		aBooleanQueryParameter := r.Params.BoolQ
		fmt.Println(aBooleanQueryParameter, reflect.TypeOf(aBooleanQueryParameter)) // bool type

		aFloat32QueryParameter := r.Params.Float32Q
		fmt.Println(aFloat32QueryParameter, reflect.TypeOf(aFloat32QueryParameter)) // float32 type

		// If a query param and path param sharing the same name are sent in a single request,
		// the path param takes precedence - the value will be that of the path param
	})

	// Parameter type passed to the request - *zeal.Rqr[GetMenuParams]
	type GetMenuParams struct {
		MenuID int
	}
	// Response type passed to the writer - zeal.Writer[models.Menu]
	zeal.Get(rt, "/menus/{MenuID}", func(w zeal.Writer[models.Menu], r *zeal.Rqr[GetMenuParams]) {
		for _, menu := range menus {
			if menu.ID == r.Params.MenuID {
				w.JSON(http.StatusOK, menu)
				return
			}
		}

		w.WriteHeader(http.StatusNotFound)
	})

	// Read requests such as GET can contain parameters but never a request body
	// Write requests such as POST can contain both parameters and a request body
	type PostItemParams struct {
		MenuID int
	}
	// Params and body types are passed to the write request - *zeal.Rqw[PostItemParams, models.Item]
	zeal.Post(rt, "/items", func(w zeal.Writer[models.Item], r *zeal.Rqw[PostItemParams, models.Item]) {
		newItem := r.Body
		if newItem.Price < 10 {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		for _, menu := range menus {
			if menu.ID != r.Params.MenuID {
				continue
			}

			menu.Items = append(menu.Items, newItem)
			w.JSON(http.StatusCreated, newItem)
			return
		}

		w.WriteHeader(http.StatusNotFound)
	})

	// PUT is also a write request
	zeal.Put(rt, "/items", func(w zeal.Writer[models.Item], r *zeal.Rqw[any, models.Item]) {
		for _, menu := range menus {
			updatedItem := r.Body
			for _, item := range menu.Items {
				if item.Name == updatedItem.Name {
					item.Price = updatedItem.Price
					w.JSON(http.StatusOK, item)
					return
				}
			}
		}

		w.WriteHeader(http.StatusNotFound)
	})

	// DELETE is also a write request
	// Params type and handler function declared in outer scope
	zeal.Delete(rt, "/items", handleDeleteItemParams)
}

type DeleteItemParams struct {
	ItemName string
}

func handleDeleteItemParams(w zeal.Writer[any], r *zeal.Rqw[DeleteItemParams, any]) {
	for _, menu := range menus {
		for i, item := range menu.Items {
			if item.Name == r.Params.ItemName {
				menu.Items = append(menu.Items[:i], menu.Items[i+1:]...)
				w.WriteHeader(http.StatusNoContent)
				return
			}
		}
	}

	w.WriteHeader(http.StatusNotFound)
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Delete

func Delete[ResponseType, ParamsType, BodyType any](router *Router, pattern string, handlerFunc HandlerFuncWrite[ResponseType, ParamsType, BodyType])

func Get

func Get[ResponseType, ParamsType any](router *Router, pattern string, handlerFunc HandlerFuncRead[ResponseType, ParamsType])

func Post

func Post[ResponseType, ParamsType, BodyType any](router *Router, pattern string, handlerFunc HandlerFuncWrite[ResponseType, ParamsType, BodyType])

func Put

func Put[ResponseType, ParamsType, BodyType any](router *Router, pattern string, handlerFunc HandlerFuncWrite[ResponseType, ParamsType, BodyType])

Types

type HandlerFuncRead

type HandlerFuncRead[ResponseType, ParamsType any] func(Writer[ResponseType], *Rqr[ParamsType])

type HandlerFuncWrite

type HandlerFuncWrite[ResponseType, ParamsType, BodyType any] func(Writer[ResponseType], *Rqw[ParamsType, BodyType])

type QueryParam

type QueryParam struct {
	// contains filtered or unexported fields
}

type RouteSchema

type RouteSchema struct {
	// contains filtered or unexported fields
}

type Router

type Router struct {
	chi.Mux
	Api *rest.API
}

func NewRouter

func NewRouter(name string) *Router

func (*Router) CreateSpec

func (r *Router) CreateSpec(version string, description string) *openapi3.T

func (*Router) ServeSwaggerUI

func (r *Router) ServeSwaggerUI(spec *openapi3.T, path string)

type Rqr

type Rqr[ParamsType any] struct {
	Request *http.Request
	Params  ParamsType
}

type Rqw

type Rqw[ParamsType, BodyType any] struct {
	Rqr[ParamsType]
	Body BodyType
}

type Writer

type Writer[ResponseType any] struct {
	http.ResponseWriter
}

func (*Writer[ResponseType]) JSON

func (w *Writer[ResponseType]) JSON(status int, data ResponseType)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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