Skip to content

Commit

Permalink
BREAKING CHANGE: Remove pointer from ContextWithBody and ContextWithBody
Browse files Browse the repository at this point in the history
We're changing Fuego's signature from generic type constrained by the interface
  • Loading branch information
EwenQuim committed Dec 19, 2024
1 parent 06c6de0 commit 16158a8
Show file tree
Hide file tree
Showing 33 changed files with 238 additions and 289 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func main() {
s.Run()
}

func myController(c *fuego.ContextWithBody[MyInput]) (MyOutput, error) {
func myController(c fuego.ContextWithBody[MyInput]) (MyOutput, error) {
body, err := c.Body()
if err != nil {
return MyOutput{}, err
Expand Down Expand Up @@ -257,7 +257,7 @@ func main() {
fuego.Use(s, chiMiddleware.Compress(5, "text/html", "text/css"))

// Fuego 🔥 handler with automatic OpenAPI generation, validation, (de)serialization and error handling
fuego.Post(s, "/", func(c *fuego.ContextWithBody[Received]) (MyResponse, error) {
fuego.Post(s, "/", func(c fuego.ContextWithBody[Received]) (MyResponse, error) {
data, err := c.Body()
if err != nil {
return MyResponse{}, err
Expand Down
2 changes: 1 addition & 1 deletion cmd/fuego/commands/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestCreateController(t *testing.T) {
require.NoError(t, err)
require.Contains(t, res, "package controller")
require.Contains(t, res, `fuego.Get(booksGroup, "/{id}", rs.getBooks)`)
require.Contains(t, res, `func (rs BooksResources) postBooks(c *fuego.ContextWithBody[BooksCreate]) (Books, error)`)
require.Contains(t, res, `func (rs BooksResources) postBooks(c fuego.ContextWithBody[BooksCreate]) (Books, error)`)
require.FileExists(t, "./controller/books.go")
os.Remove("./controller/books.go")
}
6 changes: 3 additions & 3 deletions cmd/fuego/templates/newEntity/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (rs NewEntityResources) getAllNewEntity(c fuego.ContextNoBody) ([]NewEntity
return rs.NewEntityService.GetAllNewEntity()
}

func (rs NewEntityResources) postNewEntity(c *fuego.ContextWithBody[NewEntityCreate]) (NewEntity, error) {
func (rs NewEntityResources) postNewEntity(c fuego.ContextWithBody[NewEntityCreate]) (NewEntity, error) {
body, err := c.Body()
if err != nil {
return NewEntity{}, err
Expand All @@ -39,7 +39,7 @@ func (rs NewEntityResources) getNewEntity(c fuego.ContextNoBody) (NewEntity, err
return rs.NewEntityService.GetNewEntity(id)
}

func (rs NewEntityResources) putNewEntity(c *fuego.ContextWithBody[NewEntityUpdate]) (NewEntity, error) {
func (rs NewEntityResources) putNewEntity(c fuego.ContextWithBody[NewEntityUpdate]) (NewEntity, error) {
id := c.PathParam("id")

body, err := c.Body()
Expand All @@ -50,6 +50,6 @@ func (rs NewEntityResources) putNewEntity(c *fuego.ContextWithBody[NewEntityUpda
return rs.NewEntityService.UpdateNewEntity(id, body)
}

func (rs NewEntityResources) deleteNewEntity(c *fuego.ContextNoBody) (any, error) {
func (rs NewEntityResources) deleteNewEntity(c fuego.ContextNoBody) (any, error) {
return rs.NewEntityService.DeleteNewEntity(c.PathParam("id"))
}
120 changes: 63 additions & 57 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ const (
maxBodySize = 1048576
)

type ContextNoBody = ContextWithBody[any]

// ctx is the context of the request.
// It contains the request body, the path parameters, the query parameters, and the HTTP request.
// Please do not use a pointer type as parameter.
type ctx[B any] interface {
type ContextWithBody[B any] interface {
context.Context
// Body returns the body of the request.
// If (*B) implements [InTransformer], it will be transformed after deserialization.
// It caches the result, so it can be called multiple times.
Expand Down Expand Up @@ -91,34 +94,44 @@ type ctx[B any] interface {
Redirect(code int, url string) (any, error)
}

// NewContext returns a new context. It is used internally by Fuego. You probably want to use Ctx[B] instead.
func NewContext[B any](w http.ResponseWriter, r *http.Request, options readOptions) *ContextWithBody[B] {
c := &ContextWithBody[B]{
ContextNoBody: ContextNoBody{
Res: w,
Req: r,
readOptions: readOptions{
DisallowUnknownFields: options.DisallowUnknownFields,
MaxBodySize: options.MaxBodySize,
},
urlValues: r.URL.Query(),
},
// NewContextWithBody returns a new context. It is used internally by Fuego. You probably want to use Ctx[B] instead.
func NewContextWithBody[B any](w http.ResponseWriter, r *http.Request, options readOptions) ContextWithBody[B] {
c := &contextWithBodyImpl[B]{
contextNoBodyImpl: NewContextNoBody(w, r, options),
}

return c
}

func NewContextNoBody(w http.ResponseWriter, r *http.Request, options readOptions) contextNoBodyImpl {
c := contextNoBodyImpl{
Res: w,
Req: r,
readOptions: readOptions{
DisallowUnknownFields: options.DisallowUnknownFields,
MaxBodySize: options.MaxBodySize,
},
urlValues: r.URL.Query(),
}
return c
}

// ContextWithBody is the same as fuego.ContextNoBody, but
// has a Body. The Body type parameter represents the expected data type
// from http.Request.Body. Please do not use a pointer as a type parameter.
type ContextWithBody[Body any] struct {
type contextWithBodyImpl[Body any] struct {
body *Body // Cache the body in request context, because it is not possible to read an HTTP request body multiple times.
ContextNoBody
contextNoBodyImpl
}

var (
_ ContextWithBody[any] = &contextWithBodyImpl[any]{} // Check that ContextWithBody implements Ctx.
_ ContextWithBody[string] = &contextWithBodyImpl[string]{} // Check that ContextWithBody implements Ctx.
)

// ContextNoBody is used when the controller does not have a body.
// It is used as a base context for other Context types.
type ContextNoBody struct {
type contextNoBodyImpl struct {
Req *http.Request
Res http.ResponseWriter

Expand All @@ -132,16 +145,16 @@ type ContextNoBody struct {
}

var (
_ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx.
_ context.Context = ContextNoBody{} // Check that ContextNoBody implements context.Context.
_ ContextWithBody[any] = contextNoBodyImpl{} // Check that ContextNoBody implements Ctx.
_ context.Context = contextNoBodyImpl{} // Check that ContextNoBody implements context.Context.
)

func (c ContextNoBody) Body() (any, error) {
func (c contextNoBodyImpl) Body() (any, error) {
slog.Warn("this method should not be called. It probably happened because you passed the context to another controller.")
return body[map[string]any](c)
}

func (c ContextNoBody) MustBody() any {
func (c contextNoBodyImpl) MustBody() any {
b, err := c.Body()
if err != nil {
panic(err)
Expand All @@ -151,7 +164,7 @@ func (c ContextNoBody) MustBody() any {

// SetStatus sets the status code of the response.
// Alias to http.ResponseWriter.WriteHeader.
func (c ContextNoBody) SetStatus(code int) {
func (c contextNoBodyImpl) SetStatus(code int) {
c.Res.WriteHeader(code)
}

Expand All @@ -162,72 +175,65 @@ type readOptions struct {
LogBody bool
}

var (
_ ctx[any] = &ContextWithBody[any]{} // Check that ContextWithBody[any] implements Ctx.
_ ctx[string] = &ContextWithBody[string]{} // Check that ContextWithBody[string] implements Ctx.
_ ctx[any] = &ContextNoBody{} // Check that ContextNoBody implements Ctx.
_ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx.
)

func (c ContextNoBody) Redirect(code int, url string) (any, error) {
func (c contextNoBodyImpl) Redirect(code int, url string) (any, error) {
http.Redirect(c.Res, c.Req, url, code)

return nil, nil
}

// ContextNoBody implements the context interface via [net/http.Request.Context]
func (c ContextNoBody) Deadline() (deadline time.Time, ok bool) {
func (c contextNoBodyImpl) Deadline() (deadline time.Time, ok bool) {
return c.Req.Context().Deadline()
}

// ContextNoBody implements the context interface via [net/http.Request.Context]
func (c ContextNoBody) Done() <-chan struct{} {
func (c contextNoBodyImpl) Done() <-chan struct{} {
return c.Req.Context().Done()
}

// ContextNoBody implements the context interface via [net/http.Request.Context]
func (c ContextNoBody) Err() error {
func (c contextNoBodyImpl) Err() error {
return c.Req.Context().Err()
}

// ContextNoBody implements the context interface via [net/http.Request.Context]
func (c ContextNoBody) Value(key any) any {
func (c contextNoBodyImpl) Value(key any) any {
return c.Req.Context().Value(key)
}

// ContextNoBody implements the context interface via [net/http.Request.Context]
func (c ContextNoBody) Context() context.Context {
func (c contextNoBodyImpl) Context() context.Context {
return c.Req.Context()
}

// Get request header
func (c ContextNoBody) Header(key string) string {
func (c contextNoBodyImpl) Header(key string) string {
return c.Request().Header.Get(key)
}

// Has request header
func (c ContextNoBody) HasHeader(key string) bool {
func (c contextNoBodyImpl) HasHeader(key string) bool {
return c.Header(key) != ""
}

// Sets response header
func (c ContextNoBody) SetHeader(key, value string) {
func (c contextNoBodyImpl) SetHeader(key, value string) {
c.Response().Header().Set(key, value)
}

// Get request cookie
func (c ContextNoBody) Cookie(name string) (*http.Cookie, error) {
func (c contextNoBodyImpl) Cookie(name string) (*http.Cookie, error) {
return c.Request().Cookie(name)
}

// Has request cookie
func (c ContextNoBody) HasCookie(name string) bool {
func (c contextNoBodyImpl) HasCookie(name string) bool {
_, err := c.Cookie(name)
return err == nil
}

// Sets response cookie
func (c ContextNoBody) SetCookie(cookie http.Cookie) {
func (c contextNoBodyImpl) SetCookie(cookie http.Cookie) {
http.SetCookie(c.Response(), &cookie)
}

Expand All @@ -239,7 +245,7 @@ func (c ContextNoBody) SetCookie(cookie http.Cookie) {
// that the templates will be parsed only once, removing
// the need to parse the templates on each request but also preventing
// to dynamically use new templates.
func (c ContextNoBody) Render(templateToExecute string, data any, layoutsGlobs ...string) (CtxRenderer, error) {
func (c contextNoBodyImpl) Render(templateToExecute string, data any, layoutsGlobs ...string) (CtxRenderer, error) {
return &StdRenderer{
templateToExecute: templateToExecute,
templates: c.templates,
Expand All @@ -250,7 +256,7 @@ func (c ContextNoBody) Render(templateToExecute string, data any, layoutsGlobs .
}

// PathParams returns the path parameters of the request.
func (c ContextNoBody) PathParam(name string) string {
func (c contextNoBodyImpl) PathParam(name string) string {
return c.Req.PathValue(name)
}

Expand All @@ -274,12 +280,12 @@ func (e QueryParamInvalidTypeError) Error() string {
}

// QueryParams returns the query parameters of the request. It is a shortcut for c.Req.URL.Query().
func (c ContextNoBody) QueryParams() url.Values {
func (c contextNoBodyImpl) QueryParams() url.Values {
return c.urlValues
}

// QueryParamsArr returns an slice of string from the given query parameter.
func (c ContextNoBody) QueryParamArr(name string) []string {
func (c contextNoBodyImpl) QueryParamArr(name string) []string {
_, ok := c.params[name]
if !ok {
slog.Warn("query parameter not expected in OpenAPI spec", "param", name)
Expand All @@ -295,7 +301,7 @@ func (c ContextNoBody) QueryParamArr(name string) []string {
// fuego.Get(s, "/test", myController,
// option.Query("name", "Name", param.Default("hey"))
// )
func (c ContextNoBody) QueryParam(name string) string {
func (c contextNoBodyImpl) QueryParam(name string) string {
_, ok := c.params[name]
if !ok {
slog.Warn("query parameter not expected in OpenAPI spec", "param", name, "expected_one_of", c.params)
Expand All @@ -308,7 +314,7 @@ func (c ContextNoBody) QueryParam(name string) string {
return c.urlValues.Get(name)
}

func (c ContextNoBody) QueryParamIntErr(name string) (int, error) {
func (c contextNoBodyImpl) QueryParamIntErr(name string) (int, error) {
param := c.QueryParam(name)
if param == "" {
defaultValue, ok := c.params[name].Default.(int)
Expand Down Expand Up @@ -342,7 +348,7 @@ func (c ContextNoBody) QueryParamIntErr(name string) (int, error) {
//
// and the query parameter does not exist, it will return 1.
// If the query parameter does not exist and there is no default value, or if it is not an int, it returns 0.
func (c ContextNoBody) QueryParamInt(name string) int {
func (c contextNoBodyImpl) QueryParamInt(name string) int {
param, err := c.QueryParamIntErr(name)
if err != nil {
return 0
Expand All @@ -361,7 +367,7 @@ func (c ContextNoBody) QueryParamInt(name string) int {
//
// and the query parameter does not exist in the HTTP request, it will return true.
// Accepted values are defined as [strconv.ParseBool]
func (c ContextNoBody) QueryParamBoolErr(name string) (bool, error) {
func (c contextNoBodyImpl) QueryParamBoolErr(name string) (bool, error) {
param := c.QueryParam(name)
if param == "" {
defaultValue, ok := c.params[name].Default.(bool)
Expand Down Expand Up @@ -394,7 +400,7 @@ func (c ContextNoBody) QueryParamBoolErr(name string) (bool, error) {
// )
//
// and the query parameter does not exist in the HTTP request, it will return true.
func (c ContextNoBody) QueryParamBool(name string) bool {
func (c contextNoBodyImpl) QueryParamBool(name string) bool {
param, err := c.QueryParamBoolErr(name)
if err != nil {
return false
Expand All @@ -403,26 +409,26 @@ func (c ContextNoBody) QueryParamBool(name string) bool {
return param
}

func (c ContextNoBody) MainLang() string {
func (c contextNoBodyImpl) MainLang() string {
return strings.Split(c.MainLocale(), "-")[0]
}

func (c ContextNoBody) MainLocale() string {
func (c contextNoBodyImpl) MainLocale() string {
return strings.Split(c.Req.Header.Get("Accept-Language"), ",")[0]
}

// Request returns the HTTP request.
func (c ContextNoBody) Request() *http.Request {
func (c contextNoBodyImpl) Request() *http.Request {
return c.Req
}

// Response returns the HTTP response writer.
func (c ContextNoBody) Response() http.ResponseWriter {
func (c contextNoBodyImpl) Response() http.ResponseWriter {
return c.Res
}

// MustBody works like Body, but panics if there is an error.
func (c *ContextWithBody[B]) MustBody() B {
func (c *contextWithBodyImpl[B]) MustBody() B {
b, err := c.Body()
if err != nil {
panic(err)
Expand All @@ -435,17 +441,17 @@ func (c *ContextWithBody[B]) MustBody() B {
// It caches the result, so it can be called multiple times.
// The reason the body is cached is that it is impossible to read an HTTP request body multiple times, not because of performance.
// For decoding, it uses the Content-Type header. If it is not set, defaults to application/json.
func (c *ContextWithBody[B]) Body() (B, error) {
func (c *contextWithBodyImpl[B]) Body() (B, error) {
if c.body != nil {
return *c.body, nil
}

body, err := body[B](c.ContextNoBody)
body, err := body[B](c.contextNoBodyImpl)
c.body = &body
return body, err
}

func body[B any](c ContextNoBody) (B, error) {
func body[B any](c contextNoBodyImpl) (B, error) {
// Limit the size of the request body.
if c.readOptions.MaxBodySize != 0 {
c.Req.Body = http.MaxBytesReader(nil, c.Req.Body, c.readOptions.MaxBodySize)
Expand Down
Loading

0 comments on commit 16158a8

Please sign in to comment.