From 6a43aa72462fd0f1a194ba63e944bd2a13aa3c3f Mon Sep 17 00:00:00 2001 From: fogfish Date: Tue, 7 Nov 2023 00:16:57 +0200 Subject: [PATCH] unify hinted content codec for Arrows & Combinators (#58) * unify hinted content codec for Arrows & Combinators * improve test coverage for http.IO --- examples/http-response-image/main.go | 39 ++++++++++++---------------- http/recv/arrows.go | 30 +++------------------ http/recv/arrows_test.go | 1 - http/types.go | 13 +++++++--- http/types_test.go | 26 +++++++++++++++++++ 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/examples/http-response-image/main.go b/examples/http-response-image/main.go index 9248a0a..e22f793 100644 --- a/examples/http-response-image/main.go +++ b/examples/http-response-image/main.go @@ -19,38 +19,31 @@ import ( ø "github.com/fogfish/gurl/v2/http/send" ) -type Heap struct { - image image.Image +type api struct { + http.Stack } -// declares http I/O -func (h *Heap) request() http.Arrow { - return http.GET( - // specify specify the request - ø.URI("https://avatars.githubusercontent.com/u/716093"), - ø.Accept.Set("image/*"), - - // specify requirements to the response - ƒ.Status.OK, - ƒ.ContentType.Is("image/jpeg"), - ƒ.Body(&h.image), +func (api api) request(ctx context.Context) (*image.Image, error) { + return http.IO[image.Image](api.WithContext(ctx), + http.GET( + ø.URI("https://avatars.githubusercontent.com/u/716093"), + ø.Accept.Set("image/*"), + + ƒ.Status.OK, + ƒ.ContentType.Is("image/jpeg"), + ), ) } func main() { - // instance of http stack - stack := http.New(http.WithDebugPayload()) - - // declares http i/o - heap := &Heap{} - lazy := heap.request() + api := api{ + Stack: http.New(http.WithDebugPayload()), + } - // executes http I/O - err := stack.IO(context.Background(), lazy) + img, err := api.request(context.Background()) if err != nil { panic(err) } - // process image - jpeg.Encode(os.Stdout, heap.image, &jpeg.Options{Quality: 93}) + jpeg.Encode(os.Stdout, *img, &jpeg.Options{Quality: 93}) } diff --git a/http/recv/arrows.go b/http/recv/arrows.go index ae72553..6e8e2e5 100644 --- a/http/recv/arrows.go +++ b/http/recv/arrows.go @@ -12,13 +12,11 @@ package recv import ( "encoding/json" "fmt" - "image" "io" "strconv" "strings" "time" - "github.com/ajg/form" "github.com/fogfish/gurl/v2" "github.com/fogfish/gurl/v2/http" "github.com/google/go-cmp/cmp" @@ -635,7 +633,7 @@ const ( // Supply the pointer to data target data structure. func Body[T any](out *T) http.Arrow { return func(cat *http.Context) error { - err := decode( + err := http.HintedContentCodec( cat.Response.Header.Get("Content-Type"), cat.Response.Body, out, @@ -655,7 +653,7 @@ func Recv[T any](out *T) http.Arrow { func Expect[T any](expect T) http.Arrow { return func(cat *http.Context) error { var actual T - err := decode( + err := http.HintedContentCodec( cat.Response.Header.Get("Content-Type"), cat.Response.Body, &actual, @@ -678,28 +676,6 @@ func Expect[T any](expect T) http.Arrow { } } -func decode[T any](content string, stream io.ReadCloser, data *T) error { - switch { - case strings.Contains(content, "json"): - return json.NewDecoder(stream).Decode(data) - case strings.Contains(content, "www-form"): - return form.NewDecoder(stream).Decode(data) - case strings.HasPrefix(content, "image/"): - img, _, err := image.Decode(stream) - if err == nil { - *data = img.(T) - } - return err - default: - return &gurl.NoMatch{ - ID: "http.Recv", - Diff: fmt.Sprintf("- Content-Type: application/{json | www-form}\n+ Content-Type: %s", content), - Protocol: "codec", - Actual: content, - } - } -} - // Bytes receive raw binary from HTTP response func Bytes(val *[]byte) http.Arrow { return func(cat *http.Context) (err error) { @@ -720,7 +696,7 @@ func Match(val string) http.Arrow { return func(cat *http.Context) (err error) { var val any - err = decode( + err = http.HintedContentCodec( cat.Response.Header.Get("Content-Type"), cat.Response.Body, &val, diff --git a/http/recv/arrows_test.go b/http/recv/arrows_test.go index 58f5fcd..d0952e7 100644 --- a/http/recv/arrows_test.go +++ b/http/recv/arrows_test.go @@ -329,7 +329,6 @@ func TestBodyImage(t *testing.T) { it.Then(t).Should( it.Nil(err), - // it.Equal(site.Site, "example.com"), ) } diff --git a/http/types.go b/http/types.go index 28cdb17..f058367 100644 --- a/http/types.go +++ b/http/types.go @@ -11,6 +11,7 @@ package http import ( "encoding/json" "fmt" + "image" "io" "net/http" "strings" @@ -116,7 +117,7 @@ func IO[T any](ctx *Context, arrows ...Arrow) (*T, error) { defer ctx.Response.Body.Close() var val T - err := decode( + err := HintedContentCodec( ctx.Response.Header.Get("Content-Type"), ctx.Response.Body, &val, @@ -128,16 +129,22 @@ func IO[T any](ctx *Context, arrows ...Arrow) (*T, error) { return &val, nil } -func decode[T any](content string, stream io.ReadCloser, data *T) error { +func HintedContentCodec[T any](content string, stream io.ReadCloser, data *T) error { switch { case strings.Contains(content, "json"): return json.NewDecoder(stream).Decode(data) case strings.Contains(content, "www-form"): return form.NewDecoder(stream).Decode(data) + case strings.HasPrefix(content, "image/"): + img, _, err := image.Decode(stream) + if err == nil { + *data = img.(T) + } + return err default: return &gurl.NoMatch{ ID: "http.Recv", - Diff: fmt.Sprintf("- Content-Type: application/{json | www-form}\n+ Content-Type: %s", content), + Diff: fmt.Sprintf("- Content-Type: {json | www-form | image}\n+ Content-Type: %s", content), Protocol: "codec", Actual: content, } diff --git a/http/types_test.go b/http/types_test.go index a865c6b..cc4ebfc 100644 --- a/http/types_test.go +++ b/http/types_test.go @@ -10,9 +10,13 @@ package http_test import ( "context" + "encoding/base64" "fmt" + "image" + _ "image/png" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -231,6 +235,21 @@ func TestIO(t *testing.T) { ) }) + t.Run("Image", func(t *testing.T) { + val, err := µ.IO[image.Image](cat.WithContext(context.Background()), + µ.GET( + ø.URI("%s/image", ø.Authority(ts.URL)), + ƒ.Status.OK, + ƒ.ContentType.Is("image/png"), + ), + ) + + it.Then(t).Should( + it.Nil(err), + ).ShouldNot( + it.Nil(val), + ) + }) } func mock() *httptest.Server { @@ -243,6 +262,13 @@ func mock() *httptest.Server { case r.URL.Path == "/form": w.Header().Add("Content-Type", "application/x-www-form-urlencoded") w.Write([]byte("site=example.com")) + case strings.HasPrefix(r.URL.Path, "/image"): + w.Header().Add("Content-Type", "image/png") + dst, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=") + if err != nil { + panic(err) + } + w.Write(dst) case r.URL.Path == "/ok": w.WriteHeader(http.StatusOK) case r.URL.Path == "/opts":