Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi return #137

Merged
merged 6 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- markdownlint-disable MD041 -->
<p align="center">
<img src="./data/fuego.svg" height="200" alt="Fuego Logo" />
<img src="./static/fuego.svg" height="200" alt="Fuego Logo" />
</p>

# Fuego 🔥
Expand Down
57 changes: 10 additions & 47 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type ctx[B any] interface {
// and you want to override one above the other, you can do:
// c.Render("admin.page.html", recipes, "partials/aaa/nav.partial.html")
// By default, [templateToExecute] is added to the list of templates to override.
Render(templateToExecute string, data any, templateGlobsToOverride ...string) (HTML, error)
Render(templateToExecute string, data any, templateGlobsToOverride ...string) (CtxRenderer, error)

Cookie(name string) (*http.Cookie, error) // Get request cookie
SetCookie(cookie http.Cookie) // Sets response cookie
Expand Down Expand Up @@ -222,51 +222,14 @@ 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) (HTML, error) {
if strings.Contains(templateToExecute, "/") || strings.Contains(templateToExecute, "*") {

layoutsGlobs = append(layoutsGlobs, templateToExecute) // To override all blocks defined in the main template
cloned := template.Must(c.templates.Clone())
tmpl, err := cloned.ParseFS(c.fs, layoutsGlobs...)
if err != nil {
return "", HTTPError{
Err: err,
Status: http.StatusInternalServerError,
Title: "Error parsing template",
Detail: fmt.Errorf("error parsing template '%s': %w", layoutsGlobs, err).Error(),
Errors: []ErrorItem{
{
Name: "templates",
Reason: "Check that the template exists and have the correct extension. Globs: " + strings.Join(layoutsGlobs, ", "),
},
},
}
}
c.templates = template.Must(tmpl.Clone())
}

// Get only last template name (for example, with partials/nav/main/nav.partial.html, get nav.partial.html)
myTemplate := strings.Split(templateToExecute, "/")
templateToExecute = myTemplate[len(myTemplate)-1]

c.Res.Header().Set("Content-Type", "text/html; charset=utf-8")
err := c.templates.ExecuteTemplate(c.Res, templateToExecute, data)
if err != nil {
return "", HTTPError{
Err: err,
Status: http.StatusInternalServerError,
Title: "Error rendering template",
Detail: fmt.Errorf("error executing template '%s': %w", templateToExecute, err).Error(),
Errors: []ErrorItem{
{
Name: "templates",
Reason: "Check that the template exists and have the correct extension. Template: " + templateToExecute,
},
},
}
}

return "", err
func (c ContextNoBody) Render(templateToExecute string, data any, layoutsGlobs ...string) (CtxRenderer, error) {
return &StdRenderer{
templateToExecute: templateToExecute,
templates: c.templates,
layoutsGlobs: layoutsGlobs,
fs: c.fs,
data: data,
}, nil
}

// PathParams returns the path parameters of the request.
Expand Down Expand Up @@ -427,7 +390,7 @@ func body[B any](c ContextNoBody) (B, error) {
body, err = readURLEncoded[B](c.Req, c.readOptions)
case "application/xml":
body, err = readXML[B](c.Req.Context(), c.Req.Body, c.readOptions)
case "application/x-yaml":
case "application/x-yaml", "text/yaml; charset=utf-8", "application/yaml": // https://www.rfc-editor.org/rfc/rfc9512.html
body, err = readYAML[B](c.Req.Context(), c.Req.Body, c.readOptions)
case "application/octet-stream":
// Read c.Req Body to bytes
Expand Down
4 changes: 2 additions & 2 deletions examples/custom-serializer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ var json = jsoniter.ConfigCompatibleWithStandardLibrary
func main() {
s := fuego.NewServer()

s.Serialize = func(w http.ResponseWriter, ans any) {
s.Serialize = func(w http.ResponseWriter, _ *http.Request, ans any) error {
w.Header().Set("Content-Type", "text/plain")
json.NewEncoder(w).Encode(ans)
return json.NewEncoder(w).Encode(ans)
}

fuego.Get(s, "/", helloWorld)
Expand Down
4 changes: 2 additions & 2 deletions examples/full-app-gourmet/views/partials.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import (
"github.com/go-fuego/fuego/examples/full-app-gourmet/store/types"
)

func (rs Ressource) unitPreselected(c fuego.ContextNoBody) (fuego.HTML, error) {
func (rs Ressource) unitPreselected(c fuego.ContextNoBody) (fuego.CtxRenderer, error) {
id := c.QueryParam("IngredientID")

ingredient, err := rs.IngredientsQueries.GetIngredient(c.Context(), id)
if err != nil {
return "", err
return nil, err
}

return c.Render("preselected-unit.partial.html", fuego.H{
Expand Down
22 changes: 11 additions & 11 deletions examples/full-app-gourmet/views/recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ func (rs Ressource) showIndex(c fuego.ContextNoBody) (fuego.Templ, error) {
}), nil
}

func (rs Ressource) showRecipes(c fuego.ContextNoBody) (fuego.Templ, error) {
func (rs Ressource) showRecipes(c fuego.ContextNoBody) (*fuego.DataOrTemplate[[]store.Recipe], error) {
recipes, err := rs.RecipesQueries.GetRecipes(c.Context())
if err != nil {
return nil, err
}

return templa.SearchPage(templa.SearchProps{
return fuego.DataOrHTML(recipes, templa.SearchPage(templa.SearchProps{
Recipes: recipes,
}), nil
})), nil
}

func (rs Ressource) relatedRecipes(c fuego.ContextNoBody) (fuego.Templ, error) {
Expand Down Expand Up @@ -221,7 +221,7 @@ func (rs Ressource) healthyRecipes(c fuego.ContextNoBody) (fuego.Templ, error) {
}), nil
}

func (rs Ressource) showRecipesList(c fuego.ContextNoBody) (fuego.HTML, error) {
func (rs Ressource) showRecipesList(c fuego.ContextNoBody) (fuego.CtxRenderer, error) {
search := c.QueryParam("search")
recipes, err := rs.RecipesQueries.SearchRecipes(c.Context(), store.SearchRecipesParams{
Search: sql.NullString{
Expand All @@ -230,41 +230,41 @@ func (rs Ressource) showRecipesList(c fuego.ContextNoBody) (fuego.HTML, error) {
},
})
if err != nil {
return "", err
return nil, err
}

return c.Render("partials/recipes-list.partial.html", recipes)
}

func (rs Ressource) addRecipe(c *fuego.ContextWithBody[store.CreateRecipeParams]) (fuego.HTML, error) {
func (rs Ressource) addRecipe(c *fuego.ContextWithBody[store.CreateRecipeParams]) (fuego.CtxRenderer, error) {
body, err := c.Body()
if err != nil {
return "", err
return nil, err
}

body.ID = uuid.NewString()

_, err = rs.RecipesQueries.CreateRecipe(c.Context(), body)
if err != nil {
return "", err
return nil, err
}

recipes, err := rs.RecipesQueries.GetRecipes(c.Context())
if err != nil {
return "", err
return nil, err
}

return c.Render("pages/admin.page.html", fuego.H{
"Recipes": recipes,
})
}

func (rs Ressource) RecipePage(c fuego.ContextNoBody) (fuego.HTML, error) {
func (rs Ressource) RecipePage(c fuego.ContextNoBody) (fuego.CtxRenderer, error) {
id := c.PathParam("id")

recipe, err := rs.RecipesQueries.GetRecipe(c.Context(), id)
if err != nil {
return "", fmt.Errorf("error getting recipe %s: %w", id, err)
return nil, fmt.Errorf("error getting recipe %s: %w", id, err)
}

ingredients, err := rs.IngredientsQueries.GetIngredientsOfRecipe(c.Context(), id)
Expand Down
21 changes: 21 additions & 0 deletions examples/full-app-gourmet/views/recipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/go-fuego/fuego"
"github.com/go-fuego/fuego/examples/full-app-gourmet/server"
"github.com/go-fuego/fuego/examples/full-app-gourmet/store"
"github.com/go-fuego/fuego/examples/full-app-gourmet/views"
Expand Down Expand Up @@ -100,3 +101,23 @@ func BenchmarkShowIndexExt(b *testing.B) {
}
}
}

func TestShowRecipesOpenAPITypes(t *testing.T) {
s := fuego.NewServer()

type MyStruct struct {
A string
B string
}

route := fuego.Get(s, "/data", func(*fuego.ContextNoBody) (*fuego.DataOrTemplate[MyStruct], error) {
entity := MyStruct{}

return &fuego.DataOrTemplate[MyStruct]{
Data: entity,
Template: nil,
}, nil
})

require.Equal(t, "#/components/schemas/MyStruct", route.Operation.Responses.Value("200").Value.Content["application/json"].Schema.Ref, "should have MyStruct schema instead of DataOrTemplate[MyStruct] schema")
}
64 changes: 63 additions & 1 deletion html.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import (
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"strings"
)

// CtxRenderer can be used with [github.com/a-h/templ]
// CtxRenderer is an interface that can be used to render a response.
// It is used with standard library templating engine, by using fuego.ContextXXX.Render
// It is compatible with [github.com/a-h/templ] out of the box.
// Example:
//
// func getRecipes(ctx fuego.ContextNoBody) (fuego.CtxRenderer, error) {
Expand Down Expand Up @@ -50,6 +55,63 @@ type HTML string
// H is a shortcut for map[string]any
type H map[string]any

// StdRenderer renders a template using the standard library templating engine.
type StdRenderer struct {
templateToExecute string
templates *template.Template
layoutsGlobs []string
fs fs.FS
data any
}

var _ CtxRenderer = StdRenderer{}

func (s StdRenderer) Render(ctx context.Context, w io.Writer) error {
if strings.Contains(s.templateToExecute, "/") || strings.Contains(s.templateToExecute, "*") {

s.layoutsGlobs = append(s.layoutsGlobs, s.templateToExecute) // To override all blocks defined in the main template
cloned := template.Must(s.templates.Clone())
tmpl, err := cloned.ParseFS(s.fs, s.layoutsGlobs...)
if err != nil {
return HTTPError{
Err: err,
Status: http.StatusInternalServerError,
Title: "Error parsing template",
Detail: fmt.Errorf("error parsing template '%s': %w", s.layoutsGlobs, err).Error(),
Errors: []ErrorItem{
{
Name: "templates",
Reason: "Check that the template exists and have the correct extension. Globs: " + strings.Join(s.layoutsGlobs, ", "),
},
},
}
}
s.templates = template.Must(tmpl.Clone())
}

// Get only last template name (for example, with partials/nav/main/nav.partial.html, get nav.partial.html)
myTemplate := strings.Split(s.templateToExecute, "/")
s.templateToExecute = myTemplate[len(myTemplate)-1]

err := s.templates.ExecuteTemplate(w, s.templateToExecute, s.data)
if err != nil {
return HTTPError{
Err: err,
Status: http.StatusInternalServerError,
Title: "Error rendering template",
Detail: fmt.Errorf("error executing template '%s': %w", s.templateToExecute, err).Error(),
Errors: []ErrorItem{
{
Name: "templates",
Reason: "Check that the template exists and have the correct extension. Template: " + s.templateToExecute,
},
},
}
}

return err
}

// loadTemplates
func (s *Server) loadTemplates(patterns ...string) error {
tmpl, err := template.ParseFS(s.fs, patterns...)
Expand Down
10 changes: 5 additions & 5 deletions html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestRender(t *testing.T) {
WithTemplateGlobs("testdata/*.html"),
)

Get(s, "/test", func(ctx *ContextNoBody) (HTML, error) {
Get(s, "/test", func(ctx *ContextNoBody) (CtxRenderer, error) {
return ctx.Render("testdata/test.html", H{"Name": "test"})
})

Expand All @@ -43,7 +43,7 @@ func TestRender(t *testing.T) {
})

t.Run("cannot parse unexisting file", func(t *testing.T) {
Get(s, "/file-not-found", func(ctx ContextNoBody) (HTML, error) {
Get(s, "/file-not-found", func(ctx ContextNoBody) (CtxRenderer, error) {
return ctx.Render("testdata/not-found.html", H{"Name": "test"})
})

Expand All @@ -56,7 +56,7 @@ func TestRender(t *testing.T) {
})

t.Run("can execute template with missing variable in map", func(t *testing.T) {
Get(s, "/impossible", func(ctx ContextNoBody) (HTML, error) {
Get(s, "/impossible", func(ctx ContextNoBody) (CtxRenderer, error) {
return ctx.Render("testdata/test.html", H{"NotName": "test"})
})

Expand All @@ -71,7 +71,7 @@ func TestRender(t *testing.T) {
})

t.Run("cannot execute template with missing variable in struct", func(t *testing.T) {
Get(s, "/impossible-struct", func(ctx ContextNoBody) (HTML, error) {
Get(s, "/impossible-struct", func(ctx ContextNoBody) (CtxRenderer, error) {
return ctx.Render("testdata/test.html", struct{}{})
})

Expand All @@ -93,7 +93,7 @@ func BenchmarkRender(b *testing.B) {
WithTemplateGlobs("testdata/*.html"),
)

Get(s, "/test", func(ctx ContextNoBody) (HTML, error) {
Get(s, "/test", func(ctx ContextNoBody) (CtxRenderer, error) {
return ctx.Render("testdata/test.html", H{"Name": "test"})
})

Expand Down
Loading