zeal

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 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 for routing and 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]) {
      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 {
      SP   string
      IP   int
      BQ   bool
      F32Q float32
   }
   // The parameters type is passed to the request - *zeal.Rqr[GetPrintParams]
   // This route has no response type so any is passed to the writer - zeal.Writer[any]
   zeal.Get(rt, "/print/{IP}/{SP}", func(w zeal.Writer[any], r *zeal.Rqr[GetPrintParams]) {
      // IP and SP are path params because they appear in the URL path "/print/{IP}/{SP}"
      // BQ and F32Q are, therefore, automatically query params
      // Both kinds of validated params are found in the Params field of the request

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

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

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

      aFloat32QueryParameter := r.Params.F32Q
      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, "/menu/{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 Menu struct {
      MenuID int
   }
   // Params and body types are passed to the write request - *zeal.Rqw[Menu, models.Item]
   // Request bodies are automatically validated
   // If validation fails, zeal responds with http.StatusUnprocessableEntity 422
   zeal.Post(rt, "/item", func(w zeal.Writer[models.Item], r *zeal.Rqw[Menu, models.Item]) {
      // The validated body is found in the Body field of the request
      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, "/item", func(w zeal.Writer[models.Item], r *zeal.Rqw[any, models.Item]) {
      updatedItem := r.Body
      for _, menu := range menus {
         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, "/item", handleDeleteItem)
}

type DeleteItemParams struct {
   ItemName string
}

func handleDeleteItem(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