zeal

package module
v0.1.2 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(42, http.StatusOK)
  })

  // 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]) {
    // http status is optional and can be omitted (will send http.StatusOK 200 by default)
    w.JSON(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(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(newItem, http.StatusCreated)
      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(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(data ResponseType, statuses ...int)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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