diff --git a/examples/full-app-gourmet/views/recipe.go b/examples/full-app-gourmet/views/recipe.go index c4370b13..97d29665 100644 --- a/examples/full-app-gourmet/views/recipe.go +++ b/examples/full-app-gourmet/views/recipe.go @@ -110,12 +110,12 @@ func (rs Ressource) showRecipes(c fuego.ContextNoBody) (*fuego.DataOrTemplate[[] return nil, err } - return &fuego.DataOrTemplate[[]store.Recipe]{ - Data: recipes, - Template: 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) { diff --git a/multi_return.go b/multi_return.go index afc9259d..38def6d1 100644 --- a/multi_return.go +++ b/multi_return.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "encoding/xml" + "fmt" "io" "gopkg.in/yaml.v3" @@ -17,10 +18,11 @@ type DataOrTemplate[T any] struct { } var ( - _ CtxRenderer = DataOrTemplate[any]{} // Can render HTML - _ json.Marshaler = DataOrTemplate[any]{} // Can render JSON - _ xml.Marshaler = DataOrTemplate[any]{} // Can render XML - _ yaml.Marshaler = DataOrTemplate[any]{} // Can render YAML + _ CtxRenderer = DataOrTemplate[any]{} // Can render HTML (template) + _ json.Marshaler = DataOrTemplate[any]{} // Can render JSON (data) + _ xml.Marshaler = DataOrTemplate[any]{} // Can render XML (data) + _ yaml.Marshaler = DataOrTemplate[any]{} // Can render YAML (data) + _ fmt.Stringer = DataOrTemplate[any]{} // Can render string (data) ) func (m DataOrTemplate[T]) MarshalJSON() ([]byte, error) { @@ -32,9 +34,11 @@ func (m DataOrTemplate[T]) MarshalXML(e *xml.Encoder, _ xml.StartElement) error } func (m DataOrTemplate[T]) MarshalYAML() (interface{}, error) { - encoder := yaml.NewEncoder(io.Discard) - err := encoder.Encode(m.Data) - return nil, err + return m.Data, nil +} + +func (m DataOrTemplate[T]) String() string { + return fmt.Sprintf("%v", m.Data) } func (m DataOrTemplate[T]) Render(c context.Context, w io.Writer) error { @@ -48,8 +52,9 @@ func (m DataOrTemplate[T]) Render(c context.Context, w io.Writer) error { } } -func DataOrHTML[T any](data T, template any) DataOrTemplate[T] { - return DataOrTemplate[T]{ +// Helper function to create a DataOrTemplate return item without specifying the type. +func DataOrHTML[T any](data T, template any) *DataOrTemplate[T] { + return &DataOrTemplate[T]{ Data: data, Template: template, } diff --git a/multi_return_test.go b/multi_return_test.go index 157e1f7f..83240b43 100644 --- a/multi_return_test.go +++ b/multi_return_test.go @@ -14,6 +14,15 @@ type MockCtxRenderer struct { RenderFunc func(context.Context, io.Writer) error } +func RenderString(s string) MockCtxRenderer { + return MockCtxRenderer{ + RenderFunc: func(c context.Context, w io.Writer) error { + _, err := w.Write([]byte(s)) + return err + }, + } +} + var _ CtxRenderer = MockCtxRenderer{} func (m MockCtxRenderer) Render(c context.Context, w io.Writer) error { @@ -34,27 +43,18 @@ func TestMultiReturn(t *testing.T) { entity := MyType{Name: "Ewen"} return DataOrTemplate[MyType]{ - Data: entity, - Template: MockCtxRenderer{ - RenderFunc: func(c context.Context, w io.Writer) error { - _, err := w.Write([]byte(`
` + entity.Name + `
`)) - return err - }, - }, + Data: entity, + Template: RenderString(`
` + entity.Name + `
`), }, nil }) - Get(s, "/other", func(c ContextNoBody) (DataOrTemplate[MyType], error) { + Get(s, "/other", func(c ContextNoBody) (*DataOrTemplate[MyType], error) { entity := MyType{Name: "Ewen"} return DataOrHTML( entity, - MockCtxRenderer{ - RenderFunc: func(c context.Context, w io.Writer) error { - _, err := w.Write([]byte(`
` + entity.Name + `
`)) - return err - }, - }), nil + RenderString(`
`+entity.Name+`
`), + ), nil }) t.Run("requests HTML by default", func(t *testing.T) { diff --git a/serialization.go b/serialization.go index dacc8940..1880840b 100644 --- a/serialization.go +++ b/serialization.go @@ -80,7 +80,8 @@ func Send(w http.ResponseWriter, text string) { } // SendYAML sends a YAML response. -func SendYAML(w http.ResponseWriter, ans any) { +// Declared as a variable to be able to override it for clients that need to customize serialization. +var SendYAML = func(w http.ResponseWriter, ans any) { w.Header().Set("Content-Type", "application/x-yaml") err := yaml.NewEncoder(w).Encode(ans) if err != nil { @@ -92,7 +93,8 @@ func SendYAML(w http.ResponseWriter, ans any) { } // SendJSON sends a JSON response. -func SendJSON(w http.ResponseWriter, ans any) { +// Declared as a variable to be able to override it for clients that need to customize serialization. +var SendJSON = func(w http.ResponseWriter, ans any) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(ans) if err != nil { @@ -103,7 +105,9 @@ func SendJSON(w http.ResponseWriter, ans any) { } } -func SendError(w http.ResponseWriter, r *http.Request, err error) { +// SendError sends an error. +// Declared as a variable to be able to override it for clients that need to customize serialization. +var SendError = func(w http.ResponseWriter, r *http.Request, err error) { accept := parseAcceptHeader(r.Header.Get("Accept"), nil) if accept == "" { accept = "application/json" @@ -144,7 +148,8 @@ func SendJSONError(w http.ResponseWriter, err error) { } // SendXML sends a XML response. -func SendXML(w http.ResponseWriter, ans any) { +// Declared as a variable to be able to override it for clients that need to customize serialization. +var SendXML = func(w http.ResponseWriter, ans any) { w.Header().Set("Content-Type", "application/xml") err := xml.NewEncoder(w).Encode(ans) if err != nil { @@ -181,7 +186,8 @@ func SendHTMLError(ctx context.Context, w http.ResponseWriter, err error) error } // SendHTML sends a HTML response. -func SendHTML(ctx context.Context, w http.ResponseWriter, ans any) error { +// Declared as a variable to be able to override it for clients that need to customize serialization. +var SendHTML = func(ctx context.Context, w http.ResponseWriter, ans any) error { w.Header().Set("Content-Type", "text/html; charset=utf-8") ctxRenderer, ok := any(ans).(CtxRenderer) @@ -215,7 +221,12 @@ func SendText(w http.ResponseWriter, ans any) error { w.Header().Set("Content-Type", "text/plain; charset=utf-8") stringToWrite, ok := any(ans).(string) if !ok { - stringToWrite = *any(ans).(*string) + stringToWritePtr, okPtr := any(ans).(*string) + if okPtr { + stringToWrite = *stringToWritePtr + } else { + stringToWrite = fmt.Sprintf("%v", ans) + } } _, err = w.Write([]byte(stringToWrite)) @@ -228,6 +239,11 @@ func InferAcceptHeaderFromType(ans any) string { return "text/plain" } + _, ok = any(ans).(*string) + if ok { + return "text/plain" + } + _, ok = any(ans).(HTML) if ok { return "text/html" @@ -238,10 +254,20 @@ func InferAcceptHeaderFromType(ans any) string { return "text/html" } + _, ok = any(&ans).(*CtxRenderer) + if ok { + return "text/html" + } + _, ok = any(ans).(Renderer) if ok { return "text/html" } + _, ok = any(&ans).(*Renderer) + if ok { + return "text/html" + } + return "application/json" } diff --git a/serialization_test.go b/serialization_test.go index be9f21d7..e894d25c 100644 --- a/serialization_test.go +++ b/serialization_test.go @@ -2,6 +2,7 @@ package fuego import ( "context" + "io" "net/http/httptest" "testing" @@ -239,3 +240,29 @@ func TestSend(t *testing.T) { require.Equal(t, "Hello World", w.Body.String()) } + +type templateMock struct{} + +func (t templateMock) Render(w io.Writer) error { + return nil +} + +var _ Renderer = templateMock{} + +func TestInferAcceptHeaderFromType(t *testing.T) { + t.Run("can infer json", func(t *testing.T) { + accept := InferAcceptHeaderFromType(response{}) + require.Equal(t, "application/json", accept) + }) + + t.Run("can infer that type is a template (implements Renderer)", func(t *testing.T) { + accept := InferAcceptHeaderFromType(templateMock{}) + require.Equal(t, "text/html", accept) + }) + + t.Run("can infer that type is a template (implements CtxRenderer)", func(t *testing.T) { + + accept := InferAcceptHeaderFromType(MockCtxRenderer{}) + require.Equal(t, "text/html", accept) + }) +} diff --git a/serve.go b/serve.go index 9837f087..55dfac90 100644 --- a/serve.go +++ b/serve.go @@ -78,6 +78,7 @@ func initContext[Contextable ctx[Body], Body any](baseContext ContextNoBody) Con // HTTPHandler converts a Fuego controller into a http.HandlerFunc. func HTTPHandler[ReturnType, Body any, Contextable ctx[Body]](s *Server, controller func(c Contextable) (ReturnType, error)) http.HandlerFunc { + // Just a check, not used at request time baseContext := *new(Contextable) if reflect.TypeOf(baseContext) == nil { slog.Info(fmt.Sprintf("context is nil: %v %T", baseContext, baseContext))