mirror of
https://github.com/IBM/fp-go.git
synced 2025-08-10 22:31:32 +02:00
fix: add support for context sensitive readers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
18
context/readerioeither/cancel.go
Normal file
18
context/readerioeither/cancel.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
CIOE "github.com/ibm/fp-go/context/ioeither"
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
)
|
||||
|
||||
// withContext wraps an existing ReaderIOEither and performs a context check for cancellation before delegating
|
||||
func WithContext[A any](ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return IOE.Left[A](err)
|
||||
}
|
||||
return CIOE.WithContext(ctx, ma(ctx))
|
||||
}
|
||||
}
|
30
context/readerioeither/eitherize.go
Normal file
30
context/readerioeither/eitherize.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
// these functions curry a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
|
||||
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
|
||||
|
||||
func Eitherize0[A any](f func(context.Context) (A, error)) ReaderIOEither[A] {
|
||||
return RE.Eitherize0[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func Eitherize1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderIOEither[A] {
|
||||
return RE.Eitherize1[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func Eitherize2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1, T2) ReaderIOEither[A] {
|
||||
return RE.Eitherize2[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func Eitherize3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderIOEither[A] {
|
||||
return RE.Eitherize3[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func Eitherize4[T1, T2, T3, T4, A any](f func(context.Context, T1, T2, T3, T4) (A, error)) func(T1, T2, T3, T4) ReaderIOEither[A] {
|
||||
return RE.Eitherize4[ReaderIOEither[A]](f)
|
||||
}
|
14
context/readerioeither/eq.go
Normal file
14
context/readerioeither/eq.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ET "github.com/ibm/fp-go/either"
|
||||
EQ "github.com/ibm/fp-go/eq"
|
||||
G "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
// Eq implements the equals predicate for values contained in the IOEither monad
|
||||
func Eq[A any](eq EQ.Eq[ET.Either[error, A]]) func(context.Context) EQ.Eq[ReaderIOEither[A]] {
|
||||
return G.Eq[ReaderIOEither[A]](eq)
|
||||
}
|
24
context/readerioeither/exec/exec.go
Normal file
24
context/readerioeither/exec/exec.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RIOE "github.com/ibm/fp-go/context/readerioeither"
|
||||
"github.com/ibm/fp-go/exec"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
GE "github.com/ibm/fp-go/internal/exec"
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
)
|
||||
|
||||
var (
|
||||
// Command executes a cancelable command
|
||||
Command = F.Curry3(command)
|
||||
)
|
||||
|
||||
func command(name string, args []string, in []byte) RIOE.ReaderIOEither[exec.CommandOutput] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, exec.CommandOutput] {
|
||||
return IOE.TryCatchError(func() (exec.CommandOutput, error) {
|
||||
return GE.Exec(ctx, name, args, in)
|
||||
})
|
||||
}
|
||||
}
|
43
context/readerioeither/file/file.go
Normal file
43
context/readerioeither/file/file.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
RIOE "github.com/ibm/fp-go/context/readerioeither"
|
||||
ET "github.com/ibm/fp-go/either"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
"github.com/ibm/fp-go/internal/file"
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
)
|
||||
|
||||
var (
|
||||
openIOE = IOE.Eitherize1(os.Open)
|
||||
// Open opens a file for reading within the given context
|
||||
Open = F.Flow3(
|
||||
openIOE,
|
||||
RIOE.FromIOEither[*os.File],
|
||||
RIOE.WithContext[*os.File],
|
||||
)
|
||||
)
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOEither[any] {
|
||||
return RIOE.FromIOEither(func() ET.Either[error, any] {
|
||||
return ET.TryCatchError(func() (any, error) {
|
||||
return c, c.Close()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ReadFile reads a file in the scope of a context
|
||||
func ReadFile(path string) RIOE.ReaderIOEither[[]byte] {
|
||||
return RIOE.WithResource[*os.File, []byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOEither[[]byte] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, []byte] {
|
||||
return IOE.MakeIO(func() ET.Either[error, []byte] {
|
||||
return file.ReadAll(ctx, r)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
30
context/readerioeither/from.go
Normal file
30
context/readerioeither/from.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
// these functions curry a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
|
||||
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
|
||||
|
||||
func From0[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
|
||||
return RE.From0[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func From1[T1, A any](f func(context.Context, T1) func() (A, error)) func(T1) ReaderIOEither[A] {
|
||||
return RE.From1[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func From2[T1, T2, A any](f func(context.Context, T1, T2) func() (A, error)) func(T1, T2) ReaderIOEither[A] {
|
||||
return RE.From2[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func From3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) func() (A, error)) func(T1, T2, T3) ReaderIOEither[A] {
|
||||
return RE.From3[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func From4[T1, T2, T3, T4, A any](f func(context.Context, T1, T2, T3, T4) func() (A, error)) func(T1, T2, T3, T4) ReaderIOEither[A] {
|
||||
return RE.From4[ReaderIOEither[A]](f)
|
||||
}
|
78
context/readerioeither/http/request.go
Normal file
78
context/readerioeither/http/request.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
B "github.com/ibm/fp-go/bytes"
|
||||
RIOE "github.com/ibm/fp-go/context/readerioeither"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
IOEF "github.com/ibm/fp-go/ioeither/file"
|
||||
J "github.com/ibm/fp-go/json"
|
||||
)
|
||||
|
||||
type (
|
||||
// Requester is a reader that constructs a request
|
||||
Requester = RIOE.ReaderIOEither[*http.Request]
|
||||
|
||||
Client interface {
|
||||
// Do can send an HTTP request considering a context
|
||||
Do(Requester) RIOE.ReaderIOEither[*http.Response]
|
||||
}
|
||||
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
NewRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
||||
)
|
||||
|
||||
func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
|
||||
return F.Pipe1(
|
||||
req,
|
||||
RIOE.ChainIOEitherK(client.doIOE),
|
||||
)
|
||||
}
|
||||
|
||||
// MakeClient creates an HTTP client proxy
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as a byte array
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return func(req Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
doReq := client.Do(req)
|
||||
return func(ctx context.Context) IOE.IOEither[error, []byte] {
|
||||
return IOEF.ReadAll(F.Pipe2(
|
||||
ctx,
|
||||
doReq,
|
||||
IOE.Map[error](func(resp *http.Response) io.ReadCloser {
|
||||
return resp.Body
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJson sends a request, reads the response and parses the response as JSON
|
||||
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
)
|
||||
}
|
31
context/readerioeither/http/request_test.go
Normal file
31
context/readerioeither/http/request_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
H "net/http"
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserId uint `json:"userId"`
|
||||
Id uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func TestSendSingleRequest(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
req1 := NewRequest("GET", "https://jsonplaceholder.typicode.com/posts/1", nil)
|
||||
|
||||
readItem := ReadJson[PostItem](client)
|
||||
|
||||
resp1 := readItem(req1)
|
||||
|
||||
resE := resp1(context.Background())()
|
||||
|
||||
fmt.Println(resE)
|
||||
}
|
219
context/readerioeither/reader.go
Normal file
219
context/readerioeither/reader.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
R "github.com/ibm/fp-go/context/reader"
|
||||
RIO "github.com/ibm/fp-go/context/readerio"
|
||||
ET "github.com/ibm/fp-go/either"
|
||||
ER "github.com/ibm/fp-go/errors"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
IO "github.com/ibm/fp-go/io"
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
RIE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
func FromEither[A any](e ET.Either[error, A]) ReaderIOEither[A] {
|
||||
return RIE.FromEither[ReaderIOEither[A]](e)
|
||||
}
|
||||
|
||||
func RightReader[A any](r R.Reader[A]) ReaderIOEither[A] {
|
||||
return RIE.RightReader[R.Reader[A], ReaderIOEither[A]](r)
|
||||
}
|
||||
|
||||
func LeftReader[A any](l R.Reader[error]) ReaderIOEither[A] {
|
||||
return RIE.LeftReader[R.Reader[error], ReaderIOEither[A]](l)
|
||||
}
|
||||
|
||||
func Left[A any](l error) ReaderIOEither[A] {
|
||||
return RIE.Left[ReaderIOEither[A]](l)
|
||||
}
|
||||
|
||||
func Right[A any](r A) ReaderIOEither[A] {
|
||||
return RIE.Right[ReaderIOEither[A]](r)
|
||||
}
|
||||
|
||||
func FromReader[A any](r R.Reader[A]) ReaderIOEither[A] {
|
||||
return RIE.FromReader[R.Reader[A], ReaderIOEither[A]](r)
|
||||
}
|
||||
|
||||
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
return RIE.MonadMap[ReaderIOEither[A], ReaderIOEither[B]](fa, f)
|
||||
}
|
||||
|
||||
func Map[A, B any](f func(A) B) func(ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.Map[ReaderIOEither[A], ReaderIOEither[B]](f)
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[B] {
|
||||
return RIE.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) ReaderIOEither[B]) func(ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.Chain[ReaderIOEither[A]](f)
|
||||
}
|
||||
|
||||
func Of[A any](a A) ReaderIOEither[A] {
|
||||
return RIE.Of[ReaderIOEither[A]](a)
|
||||
}
|
||||
|
||||
// withCancelCauseFunc wraps an IOEither such that in case of an error the cancel function is invoked
|
||||
func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOE.IOEither[error, A]) IOE.IOEither[error, A] {
|
||||
return F.Pipe3(
|
||||
ma,
|
||||
IOE.Swap[error, A],
|
||||
IOE.ChainFirstIOK[A, error, any](func(err error) IO.IO[any] {
|
||||
return IO.MakeIO(func() any {
|
||||
cancel(err)
|
||||
return nil
|
||||
})
|
||||
}),
|
||||
IOE.Swap[A, error],
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp implements the `Ap` function for a reader with context. It creates a sub-context that will
|
||||
// be canceled if any of the input operations errors out or
|
||||
func MonadAp[A, B any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
// context sensitive input
|
||||
cfab := WithContext(fab)
|
||||
cfa := WithContext(fa)
|
||||
|
||||
return func(ctx context.Context) IOE.IOEither[error, B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return IOE.Left[B](err)
|
||||
}
|
||||
|
||||
return func() ET.Either[error, B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ET.Left[B](err)
|
||||
}
|
||||
|
||||
// create sub-contexts for fa and fab, so they can cancel one other
|
||||
ctxSub, cancelSub := context.WithCancelCause(ctx)
|
||||
defer cancelSub(nil) // cancel has to be called in all paths
|
||||
|
||||
fabIOE := withCancelCauseFunc(cancelSub, cfab(ctxSub))
|
||||
faIOE := withCancelCauseFunc(cancelSub, cfa(ctxSub))
|
||||
|
||||
return IOE.MonadAp(fabIOE, faIOE)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Ap[A, B any](fa ReaderIOEither[A]) func(ReaderIOEither[func(A) B]) ReaderIOEither[B] {
|
||||
return F.Bind2nd(MonadAp[A, B], fa)
|
||||
}
|
||||
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderIOEither[A] {
|
||||
return RIE.FromPredicate[ReaderIOEither[A]](pred, onFalse)
|
||||
}
|
||||
|
||||
func Fold[A, B any](onLeft func(error) RIO.ReaderIO[B], onRight func(A) RIO.ReaderIO[B]) func(ReaderIOEither[A]) RIO.ReaderIO[B] {
|
||||
return RIE.Fold[RIO.ReaderIO[B], ReaderIOEither[A]](onLeft, onRight)
|
||||
}
|
||||
|
||||
func GetOrElse[A any](onLeft func(error) RIO.ReaderIO[A]) func(ReaderIOEither[A]) RIO.ReaderIO[A] {
|
||||
return RIE.GetOrElse[RIO.ReaderIO[A], ReaderIOEither[A]](onLeft)
|
||||
}
|
||||
|
||||
func OrElse[A any](onLeft func(error) ReaderIOEither[A]) func(ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return RIE.OrElse[ReaderIOEither[A]](onLeft)
|
||||
}
|
||||
|
||||
func OrLeft[A any](onLeft func(error) RIO.ReaderIO[error]) func(ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return RIE.OrLeft[ReaderIOEither[A], RIO.ReaderIO[error], ReaderIOEither[A]](onLeft)
|
||||
}
|
||||
|
||||
func Ask() ReaderIOEither[context.Context] {
|
||||
return RIE.Ask[ReaderIOEither[context.Context]]()
|
||||
}
|
||||
|
||||
func Asks[A any](r R.Reader[A]) ReaderIOEither[A] {
|
||||
return RIE.Asks[R.Reader[A], ReaderIOEither[A]](r)
|
||||
}
|
||||
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) ET.Either[error, B]) ReaderIOEither[B] {
|
||||
return RIE.MonadChainEitherK[ReaderIOEither[A], ReaderIOEither[B]](ma, f)
|
||||
}
|
||||
|
||||
func ChainEitherK[A, B any](f func(A) ET.Either[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.ChainEitherK[ReaderIOEither[A], ReaderIOEither[B]](f)
|
||||
}
|
||||
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) O.Option[B]) func(ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.ChainOptionK[ReaderIOEither[A], ReaderIOEither[B]](onNone)
|
||||
}
|
||||
|
||||
func FromIOEither[A any](t IOE.IOEither[error, A]) ReaderIOEither[A] {
|
||||
return RIE.FromIOEither[ReaderIOEither[A]](t)
|
||||
}
|
||||
|
||||
func FromIO[A any](t IO.IO[A]) ReaderIOEither[A] {
|
||||
return RIE.FromIO[ReaderIOEither[A]](t)
|
||||
}
|
||||
|
||||
// Never returns a 'ReaderIOEither' that never returns, except if its context gets canceled
|
||||
func Never[A any]() ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, A] {
|
||||
return IOE.MakeIO(func() ET.Either[error, A] {
|
||||
<-ctx.Done()
|
||||
return ET.Left[A](context.Cause(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO.IO[B]) ReaderIOEither[B] {
|
||||
return RIE.MonadChainIOK[ReaderIOEither[A], ReaderIOEither[B]](ma, f)
|
||||
}
|
||||
|
||||
func ChainIOK[A, B any](f func(A) IO.IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.ChainIOK[ReaderIOEither[A], ReaderIOEither[B]](f)
|
||||
}
|
||||
|
||||
func ChainIOEitherK[A, B any](f func(A) IOE.IOEither[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return RIE.ChainIOEitherK[ReaderIOEither[A], ReaderIOEither[B]](f)
|
||||
}
|
||||
|
||||
// Delay creates an operation that passes in the value after some delay
|
||||
func Delay[A any](delay time.Duration) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, A] {
|
||||
return IOE.MakeIO(func() ET.Either[error, A] {
|
||||
// manage the timeout
|
||||
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
|
||||
defer cancelTimeout()
|
||||
// whatever comes first
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
return ma(ctx)()
|
||||
case <-ctx.Done():
|
||||
return ET.Left[A](context.Cause(ctx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer will return the current time after an initial delay
|
||||
func Timer(delay time.Duration) ReaderIOEither[time.Time] {
|
||||
return F.Pipe2(
|
||||
IO.Now,
|
||||
FromIO[time.Time],
|
||||
Delay[time.Time](delay),
|
||||
)
|
||||
}
|
||||
|
||||
// Defer creates an IO by creating a brand new IO via a generator function, each time
|
||||
func Defer[A any](gen func() ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return RIE.Defer[ReaderIOEither[A]](gen)
|
||||
}
|
||||
|
||||
// TryCatch wraps a reader returning a tuple as an error into ReaderIOEither
|
||||
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
|
||||
return RIE.TryCatch[ReaderIOEither[A]](f, ER.IdentityError)
|
||||
}
|
149
context/readerioeither/reader_test.go
Normal file
149
context/readerioeither/reader_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/ibm/fp-go/either"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
"github.com/ibm/fp-go/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
|
||||
parent, parentCancel := context.WithCancel(outer)
|
||||
defer parentCancel()
|
||||
|
||||
inner, innerCancel := context.WithCancel(parent)
|
||||
defer innerCancel()
|
||||
|
||||
assert.NoError(t, parent.Err())
|
||||
assert.NoError(t, inner.Err())
|
||||
|
||||
innerCancel()
|
||||
|
||||
assert.NoError(t, parent.Err())
|
||||
assert.Error(t, inner.Err())
|
||||
|
||||
}
|
||||
|
||||
func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
|
||||
inner, innerCancel := context.WithCancel(parent)
|
||||
defer innerCancel()
|
||||
|
||||
assert.NoError(t, parent.Err())
|
||||
assert.NoError(t, inner.Err())
|
||||
|
||||
outerCancel()
|
||||
|
||||
assert.Error(t, parent.Err())
|
||||
assert.Error(t, inner.Err())
|
||||
|
||||
}
|
||||
|
||||
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
|
||||
inner, innerCancel := context.WithCancel(parent)
|
||||
defer innerCancel()
|
||||
|
||||
assert.NoError(t, parent.Err())
|
||||
assert.NoError(t, inner.Err())
|
||||
|
||||
outerCancel()
|
||||
innerCancel()
|
||||
|
||||
assert.Error(t, parent.Err())
|
||||
assert.Error(t, inner.Err())
|
||||
|
||||
outerCancel()
|
||||
innerCancel()
|
||||
|
||||
assert.Error(t, parent.Err())
|
||||
assert.Error(t, inner.Err())
|
||||
}
|
||||
|
||||
func TestCancelCauseSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
|
||||
parent, outerCancel := context.WithCancelCause(outer)
|
||||
defer outerCancel(nil)
|
||||
|
||||
inner := context.WithValue(parent, "key", "value")
|
||||
|
||||
assert.NoError(t, parent.Err())
|
||||
assert.NoError(t, inner.Err())
|
||||
|
||||
err := fmt.Errorf("test error")
|
||||
|
||||
outerCancel(err)
|
||||
|
||||
assert.Error(t, parent.Err())
|
||||
assert.Error(t, inner.Err())
|
||||
|
||||
assert.Equal(t, err, context.Cause(parent))
|
||||
assert.Equal(t, err, context.Cause(inner))
|
||||
}
|
||||
|
||||
func TestTimer(t *testing.T) {
|
||||
delta := 3 * time.Second
|
||||
timer := Timer(delta)
|
||||
ctx := context.Background()
|
||||
|
||||
t0 := time.Now()
|
||||
res := timer(ctx)()
|
||||
t1 := time.Now()
|
||||
|
||||
assert.WithinDuration(t, t0.Add(delta), t1, time.Second)
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
func TestCanceledApply(t *testing.T) {
|
||||
// our error
|
||||
err := fmt.Errorf("TestCanceledApply")
|
||||
// the actual apply value errors out after some time
|
||||
errValue := F.Pipe1(
|
||||
Left[string](err),
|
||||
Delay[string](time.Second),
|
||||
)
|
||||
// function never resolves
|
||||
fct := Never[func(string) string]()
|
||||
// apply the values, we expect an error after 1s
|
||||
|
||||
applied := F.Pipe1(
|
||||
fct,
|
||||
Ap[string, string](errValue),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
assert.Equal(t, E.Left[string](err), res)
|
||||
}
|
||||
|
||||
func TestRegularApply(t *testing.T) {
|
||||
value := Of("Carsten")
|
||||
fct := Of(utils.Upper)
|
||||
|
||||
applied := F.Pipe1(
|
||||
fct,
|
||||
Ap[string, string](value),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
assert.Equal(t, E.Of[error]("CARSTEN"), res)
|
||||
}
|
15
context/readerioeither/resource.go
Normal file
15
context/readerioeither/resource.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
F "github.com/ibm/fp-go/function"
|
||||
RIE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
|
||||
func WithResource[R, A any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[any]) func(func(R) ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
// wraps the callback functions with a context check
|
||||
return F.Flow2(
|
||||
F.Bind2nd(F.Flow2[func(R) ReaderIOEither[A], func(ReaderIOEither[A]) ReaderIOEither[A], R, ReaderIOEither[A], ReaderIOEither[A]], WithContext[A]),
|
||||
RIE.WithResource[ReaderIOEither[A]](WithContext(onCreate), onRelease),
|
||||
)
|
||||
}
|
42
context/readerioeither/sequence.go
Normal file
42
context/readerioeither/sequence.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
RE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
T "github.com/ibm/fp-go/tuple"
|
||||
)
|
||||
|
||||
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
|
||||
|
||||
func SequenceT1[A any](a ReaderIOEither[A]) ReaderIOEither[T.Tuple1[A]] {
|
||||
return RE.SequenceT1[
|
||||
ReaderIOEither[A],
|
||||
ReaderIOEither[T.Tuple1[A]],
|
||||
](a)
|
||||
}
|
||||
|
||||
func SequenceT2[A, B any](a ReaderIOEither[A], b ReaderIOEither[B]) ReaderIOEither[T.Tuple2[A, B]] {
|
||||
return RE.SequenceT2[
|
||||
ReaderIOEither[A],
|
||||
ReaderIOEither[B],
|
||||
ReaderIOEither[T.Tuple2[A, B]],
|
||||
](a, b)
|
||||
}
|
||||
|
||||
func SequenceT3[A, B, C any](a ReaderIOEither[A], b ReaderIOEither[B], c ReaderIOEither[C]) ReaderIOEither[T.Tuple3[A, B, C]] {
|
||||
return RE.SequenceT3[
|
||||
ReaderIOEither[A],
|
||||
ReaderIOEither[B],
|
||||
ReaderIOEither[C],
|
||||
ReaderIOEither[T.Tuple3[A, B, C]],
|
||||
](a, b, c)
|
||||
}
|
||||
|
||||
func SequenceT4[A, B, C, D any](a ReaderIOEither[A], b ReaderIOEither[B], c ReaderIOEither[C], d ReaderIOEither[D]) ReaderIOEither[T.Tuple4[A, B, C, D]] {
|
||||
return RE.SequenceT4[
|
||||
ReaderIOEither[A],
|
||||
ReaderIOEither[B],
|
||||
ReaderIOEither[C],
|
||||
ReaderIOEither[D],
|
||||
ReaderIOEither[T.Tuple4[A, B, C, D]],
|
||||
](a, b, c, d)
|
||||
}
|
26
context/readerioeither/traverse.go
Normal file
26
context/readerioeither/traverse.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
IOE "github.com/ibm/fp-go/ioeither"
|
||||
RE "github.com/ibm/fp-go/readerioeither/generic"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return RE.TraverseArray[ReaderIOEither[B], ReaderIOEither[[]B], IOE.IOEither[error, B], IOE.IOEither[error, []B], []A](f)
|
||||
}
|
||||
|
||||
// SequenceArray converts a homogeneous sequence of either into an either of sequence
|
||||
func SequenceArray[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
return RE.SequenceArray[ReaderIOEither[A], ReaderIOEither[[]A]](ma)
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a record
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
return RE.TraverseRecord[ReaderIOEither[B], ReaderIOEither[map[K]B], IOE.IOEither[error, B], IOE.IOEither[error, map[K]B], map[K]A](f)
|
||||
}
|
||||
|
||||
// SequenceRecord converts a homogeneous sequence of either into an either of sequence
|
||||
func SequenceRecord[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEither[map[K]A] {
|
||||
return RE.SequenceRecord[ReaderIOEither[A], ReaderIOEither[map[K]A]](ma)
|
||||
}
|
11
context/readerioeither/type.go
Normal file
11
context/readerioeither/type.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Package readerioeither implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RE "github.com/ibm/fp-go/readerioeither"
|
||||
)
|
||||
|
||||
// ReaderIOEither is a specialization of the Reader monad for the typical golang scenario
|
||||
type ReaderIOEither[A any] RE.ReaderIOEither[context.Context, error, A]
|
Reference in New Issue
Block a user