From b4bf511f035db2931388e2d6550a4317108549ca Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Mon, 27 Nov 2023 12:12:44 +0100 Subject: [PATCH] fix: http interface of IOEither (#82) Signed-off-by: Dr. Carsten Leue --- ioeither/generic/ioeither.go | 21 ++++++++ ioeither/http/request.go | 96 +++++++++++++++++++++++++----------- ioeither/http/retry_test.go | 8 ++- ioeither/ioeither.go | 8 +++ 4 files changed, 100 insertions(+), 33 deletions(-) diff --git a/ioeither/generic/ioeither.go b/ioeither/generic/ioeither.go index 47c152f..56a05e4 100644 --- a/ioeither/generic/ioeither.go +++ b/ioeither/generic/ioeither.go @@ -275,6 +275,27 @@ func ChainFirstIOK[GA ~func() ET.Either[E, A], GIOB ~func() B, E, A, B any](f fu ) } +// MonadChainFirstEitherK runs the monad returned by the function but returns the result of the original monad +func MonadChainFirstEitherK[GA ~func() ET.Either[E, A], E, A, B any](first GA, f func(A) ET.Either[E, B]) GA { + return FE.MonadChainFirstEitherK( + MonadChain[GA, GA, E, A, A], + MonadMap[func() ET.Either[E, B], GA, E, B, A], + FromEither[func() ET.Either[E, B], E, B], + first, + f, + ) +} + +// ChainFirstEitherK runs the monad returned by the function but returns the result of the original monad +func ChainFirstEitherK[GA ~func() ET.Either[E, A], E, A, B any](f func(A) ET.Either[E, B]) func(GA) GA { + return FE.ChainFirstEitherK( + MonadChain[GA, GA, E, A, A], + MonadMap[func() ET.Either[E, B], GA, E, B, A], + FromEither[func() ET.Either[E, B], E, B], + f, + ) +} + // Swap changes the order of type parameters func Swap[GEA ~func() ET.Either[E, A], GAE ~func() ET.Either[A, E], E, A any](val GEA) GAE { return MonadFold(val, Right[GAE], Left[GAE]) diff --git a/ioeither/http/request.go b/ioeither/http/request.go index c54170c..daa626b 100644 --- a/ioeither/http/request.go +++ b/ioeither/http/request.go @@ -20,54 +20,94 @@ import ( "net/http" B "github.com/IBM/fp-go/bytes" - ER "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" + H "github.com/IBM/fp-go/http" IOE "github.com/IBM/fp-go/ioeither" IOEF "github.com/IBM/fp-go/ioeither/file" J "github.com/IBM/fp-go/json" + T "github.com/IBM/fp-go/tuple" ) -type Client interface { - Do(req *http.Request) IOE.IOEither[error, *http.Response] -} +type ( + // Requester is a reader that constructs a request + Requester = IOE.IOEither[error, *http.Request] -type client struct { - delegate *http.Client -} + Client interface { + Do(Requester) IOE.IOEither[error, *http.Response] + } -func (client client) Do(req *http.Request) IOE.IOEither[error, *http.Response] { - return IOE.TryCatch(func() (*http.Response, error) { - return client.delegate.Do(req) - }, ER.IdentityError) + client struct { + delegate *http.Client + doIOE func(*http.Request) IOE.IOEither[error, *http.Response] + } +) + +var ( + // MakeRequest is an eitherized version of [http.NewRequest] + MakeRequest = IOE.Eitherize3(http.NewRequest) + makeRequest = F.Bind13of3(MakeRequest) + + // specialize + MakeGetRequest = makeRequest("GET", nil) +) + +func (client client) Do(req Requester) IOE.IOEither[error, *http.Response] { + return F.Pipe1( + req, + IOE.Chain(client.doIOE), + ) } func MakeClient(httpClient *http.Client) Client { - return client{delegate: httpClient} + return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)} } -func ReadAll(client Client) func(*http.Request) IOE.IOEither[error, []byte] { - return func(req *http.Request) IOE.IOEither[error, []byte] { - return IOEF.ReadAll(F.Pipe2( - req, - client.Do, - IOE.Map[error](func(resp *http.Response) io.ReadCloser { - return resp.Body - }), - ), - ) - } +// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple +func ReadFullResponse(client Client) func(Requester) IOE.IOEither[error, H.FullResponse] { + return F.Flow3( + client.Do, + IOE.ChainEitherK(H.ValidateResponse), + IOE.Chain(func(resp *http.Response) IOE.IOEither[error, H.FullResponse] { + return F.Pipe1( + F.Pipe3( + resp, + H.GetBody, + IOE.Of[error, io.ReadCloser], + IOEF.ReadAll[io.ReadCloser], + ), + IOE.Map[error](F.Bind1st(T.MakeTuple2[*http.Response, []byte], resp)), + ) + }), + ) } -func ReadText(client Client) func(*http.Request) IOE.IOEither[error, string] { +// ReadAll sends a request and reads the response as bytes +func ReadAll(client Client) func(Requester) IOE.IOEither[error, []byte] { + return F.Flow2( + ReadFullResponse(client), + IOE.Map[error](H.Body), + ) +} + +// ReadText sends a request, reads the response and represents the response as a text string +func ReadText(client Client) func(Requester) IOE.IOEither[error, string] { return F.Flow2( ReadAll(client), IOE.Map[error](B.ToString), ) } -func ReadJson[A any](client Client) func(*http.Request) IOE.IOEither[error, A] { - return F.Flow2( - ReadAll(client), - IOE.ChainEitherK(J.Unmarshal[A]), +// ReadJson sends a request, reads the response and parses the response as JSON +func ReadJson[A any](client Client) func(Requester) IOE.IOEither[error, A] { + return F.Flow3( + ReadFullResponse(client), + IOE.ChainFirstEitherK(F.Flow2( + H.Response, + H.ValidateJsonResponse, + )), + IOE.ChainEitherK(F.Flow2( + H.Body, + J.Unmarshal[A], + )), ) } diff --git a/ioeither/http/retry_test.go b/ioeither/http/retry_test.go index 80aca6a..b199b40 100644 --- a/ioeither/http/retry_test.go +++ b/ioeither/http/retry_test.go @@ -23,7 +23,6 @@ import ( AR "github.com/IBM/fp-go/array" E "github.com/IBM/fp-go/either" - HE "github.com/IBM/fp-go/either/http" "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" @@ -53,10 +52,9 @@ func TestRetryHttp(t *testing.T) { client := MakeClient(&http.Client{}) action := func(status R.RetryStatus) IOE.IOEither[error, *PostItem] { - return F.Pipe2( - HE.GetRequest(urls[status.IterNumber]), - IOE.FromEither[error, *http.Request], - IOE.Chain(ReadJson[*PostItem](client)), + return F.Pipe1( + MakeGetRequest(urls[status.IterNumber]), + ReadJson[*PostItem](client), ) } diff --git a/ioeither/ioeither.go b/ioeither/ioeither.go index b629895..a86ed9a 100644 --- a/ioeither/ioeither.go +++ b/ioeither/ioeither.go @@ -211,6 +211,14 @@ func ChainFirst[E, A, B any](f func(A) IOEither[E, B]) func(IOEither[E, A]) IOEi return G.ChainFirst[IOEither[E, A]](f) } +func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f func(A) ET.Either[E, B]) IOEither[E, A] { + return G.MonadChainFirstEitherK[IOEither[E, A]](ma, f) +} + +func ChainFirstEitherK[A, E, B any](f func(A) ET.Either[E, B]) func(ma IOEither[E, A]) IOEither[E, A] { + return G.ChainFirstEitherK[IOEither[E, A]](f) +} + // MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f func(A) I.IO[B]) IOEither[E, A] { return G.MonadChainFirstIOK(ma, f)