mirror of
https://github.com/IBM/fp-go.git
synced 2025-08-10 22:31:32 +02:00
fix: rework http support
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
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"
|
||||
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 (
|
||||
@@ -44,23 +45,35 @@ 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
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
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)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.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(
|
||||
@@ -71,8 +84,15 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
|
||||
// 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]),
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
H.Response,
|
||||
H.ValidateJsonResponse,
|
||||
)),
|
||||
RIOE.ChainEitherK(F.Flow2(
|
||||
H.Body,
|
||||
J.Unmarshal[A],
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
17
http/types.go
Normal file
17
http/types.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
H "net/http"
|
||||
|
||||
T "github.com/ibm/fp-go/tuple"
|
||||
)
|
||||
|
||||
type (
|
||||
// FullResponse represents a full http response, including headers and body
|
||||
FullResponse = T.Tuple2[*H.Response, []byte]
|
||||
)
|
||||
|
||||
var (
|
||||
Response = T.First[*H.Response, []byte]
|
||||
Body = T.Second[*H.Response, []byte]
|
||||
)
|
79
http/utils.go
Normal file
79
http/utils.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
H "net/http"
|
||||
"regexp"
|
||||
|
||||
A "github.com/ibm/fp-go/array"
|
||||
E "github.com/ibm/fp-go/either"
|
||||
"github.com/ibm/fp-go/errors"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
R "github.com/ibm/fp-go/record/generic"
|
||||
T "github.com/ibm/fp-go/tuple"
|
||||
)
|
||||
|
||||
type (
|
||||
ParsedMediaType = T.Tuple2[string, map[string]string]
|
||||
)
|
||||
|
||||
var (
|
||||
// mime type to check if a media type matches
|
||||
reJsonMimeType = regexp.MustCompile(`application/(?:\w+\+)?json`)
|
||||
// ValidateResponse validates an HTTP response and returns an [E.Either] if the response is not a success
|
||||
ValidateResponse = E.FromPredicate(isValidStatus, StatusCodeError)
|
||||
// alidateJsonContentTypeString parses a content type a validates that it is valid JSON
|
||||
validateJsonContentTypeString = F.Flow2(
|
||||
ParseMediaType,
|
||||
E.ChainFirst(F.Flow2(
|
||||
T.First[string, map[string]string],
|
||||
E.FromPredicate(reJsonMimeType.MatchString, func(mimeType string) error {
|
||||
return fmt.Errorf("mimetype [%s] is not a valid JSON content type", mimeType)
|
||||
}),
|
||||
)),
|
||||
)
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderContentType = "Content-Type"
|
||||
)
|
||||
|
||||
// ParseMediaType parses a media type into a tuple
|
||||
func ParseMediaType(mediaType string) E.Either[error, ParsedMediaType] {
|
||||
return E.TryCatchError(func() (ParsedMediaType, error) {
|
||||
m, p, err := mime.ParseMediaType(mediaType)
|
||||
return T.MakeTuple2(m, p), err
|
||||
})
|
||||
}
|
||||
|
||||
func GetHeader(resp *H.Response) H.Header {
|
||||
return resp.Header
|
||||
}
|
||||
|
||||
func GetBody(resp *H.Response) io.ReadCloser {
|
||||
return resp.Body
|
||||
}
|
||||
|
||||
func isValidStatus(resp *H.Response) bool {
|
||||
return resp.StatusCode >= H.StatusOK && resp.StatusCode < H.StatusMultipleChoices
|
||||
}
|
||||
|
||||
func StatusCodeError(resp *H.Response) error {
|
||||
return fmt.Errorf("invalid status code [%d] when accessing URL [%s]", resp.StatusCode, resp.Request.URL)
|
||||
}
|
||||
|
||||
// ValidateJsonResponse checks if an HTTP response is a valid JSON response
|
||||
func ValidateJsonResponse(resp *H.Response) E.Either[error, *H.Response] {
|
||||
return F.Pipe6(
|
||||
resp,
|
||||
GetHeader,
|
||||
R.Lookup[H.Header](HeaderContentType),
|
||||
O.Chain(A.First[string]),
|
||||
E.FromOption[error, string](errors.OnNone("unable to access the [%s] header", HeaderContentType)),
|
||||
E.ChainFirst(validateJsonContentTypeString),
|
||||
E.MapTo[error, string](resp),
|
||||
)
|
||||
}
|
41
http/utils_test.go
Normal file
41
http/utils_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/ibm/fp-go/either"
|
||||
F "github.com/ibm/fp-go/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NoError[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
return E.Fold(func(err error) bool {
|
||||
return assert.NoError(t, err)
|
||||
}, F.Constant1[A](true))
|
||||
}
|
||||
|
||||
func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
return E.Fold(F.Constant1[error](true), func(A) bool {
|
||||
return assert.Error(t, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJsonContentTypeString("application/json"),
|
||||
NoError[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
func TestValidateInvalidJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJsonContentTypeString("application/xml"),
|
||||
Error[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
@@ -21,11 +21,11 @@ func RightReader[E, L, A any](r R.Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return G.RightReader[R.Reader[E, A], ReaderEither[E, L, A]](r)
|
||||
}
|
||||
|
||||
func LeftReader[E, L, A any](l R.Reader[E, L]) ReaderEither[E, L, A] {
|
||||
func LeftReader[A, E, L any](l R.Reader[E, L]) ReaderEither[E, L, A] {
|
||||
return G.LeftReader[R.Reader[E, L], ReaderEither[E, L, A]](l)
|
||||
}
|
||||
|
||||
func Left[E, L, A any](l L) ReaderEither[E, L, A] {
|
||||
func Left[E, A, L any](l L) ReaderEither[E, L, A] {
|
||||
return G.Left[ReaderEither[E, L, A]](l)
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ var (
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext, error]("s1")
|
||||
e1 := Left[MyContext, error, string](testError)
|
||||
e1 := Left[MyContext, string](testError)
|
||||
|
||||
res1 := SequenceT1(t1)
|
||||
assert.Equal(t, E.Of[error](T.MakeTuple1("s1")), res1(defaultContext))
|
||||
@@ -28,9 +28,9 @@ func TestSequenceT1(t *testing.T) {
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext, error]("s1")
|
||||
e1 := Left[MyContext, error, string](testError)
|
||||
e1 := Left[MyContext, string](testError)
|
||||
t2 := Of[MyContext, error](2)
|
||||
e2 := Left[MyContext, error, int](testError)
|
||||
e2 := Left[MyContext, int](testError)
|
||||
|
||||
res1 := SequenceT2(t1, t2)
|
||||
assert.Equal(t, E.Of[error](T.MakeTuple2("s1", 2)), res1(defaultContext))
|
||||
@@ -45,11 +45,11 @@ func TestSequenceT2(t *testing.T) {
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext, error]("s1")
|
||||
e1 := Left[MyContext, error, string](testError)
|
||||
e1 := Left[MyContext, string](testError)
|
||||
t2 := Of[MyContext, error](2)
|
||||
e2 := Left[MyContext, error, int](testError)
|
||||
e2 := Left[MyContext, int](testError)
|
||||
t3 := Of[MyContext, error](true)
|
||||
e3 := Left[MyContext, error, bool](testError)
|
||||
e3 := Left[MyContext, bool](testError)
|
||||
|
||||
res1 := SequenceT3(t1, t2, t3)
|
||||
assert.Equal(t, E.Of[error](T.MakeTuple3("s1", 2, true)), res1(defaultContext))
|
||||
|
@@ -30,7 +30,7 @@ func RightReaderIO[R, E, A any](ma RIO.ReaderIO[R, A]) ReaderIOEither[R, E, A] {
|
||||
return G.RightReaderIO[ReaderIOEither[R, E, A]](ma)
|
||||
}
|
||||
|
||||
func LeftReaderIO[R, E, A any](me RIO.ReaderIO[R, E]) ReaderIOEither[R, E, A] {
|
||||
func LeftReaderIO[A, R, E any](me RIO.ReaderIO[R, E]) ReaderIOEither[R, E, A] {
|
||||
return G.LeftReaderIO[ReaderIOEither[R, E, A]](me)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func Right[R, E, A any](a A) ReaderIOEither[R, E, A] {
|
||||
return G.Right[ReaderIOEither[R, E, A]](a)
|
||||
}
|
||||
|
||||
func Left[R, E, A any](e E) ReaderIOEither[R, E, A] {
|
||||
func Left[R, A, E any](e E) ReaderIOEither[R, E, A] {
|
||||
return G.Left[ReaderIOEither[R, E, A]](e)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func RightReader[R, E, A any](ma RD.Reader[R, A]) ReaderIOEither[R, E, A] {
|
||||
return G.RightReader[RD.Reader[R, A], ReaderIOEither[R, E, A]](ma)
|
||||
}
|
||||
|
||||
func LeftReader[R, E, A any](ma RD.Reader[R, E]) ReaderIOEither[R, E, A] {
|
||||
func LeftReader[A, R, E any](ma RD.Reader[R, E]) ReaderIOEither[R, E, A] {
|
||||
return G.LeftReader[RD.Reader[R, E], ReaderIOEither[R, E, A]](ma)
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func RightIO[R, E, A any](ma io.IO[A]) ReaderIOEither[R, E, A] {
|
||||
return G.RightIO[ReaderIOEither[R, E, A]](ma)
|
||||
}
|
||||
|
||||
func LeftIO[R, E, A any](ma io.IO[E]) ReaderIOEither[R, E, A] {
|
||||
func LeftIO[R, A, E any](ma io.IO[E]) ReaderIOEither[R, E, A] {
|
||||
return G.LeftIO[ReaderIOEither[R, E, A]](ma)
|
||||
}
|
||||
|
||||
|
@@ -34,7 +34,7 @@ func TestOrLeft(t *testing.T) {
|
||||
)
|
||||
|
||||
g2 := F.Pipe1(
|
||||
Left[context.Context, string, int]("a"),
|
||||
Left[context.Context, int]("a"),
|
||||
f,
|
||||
)
|
||||
|
||||
|
10
record/eq.go
Normal file
10
record/eq.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
E "github.com/ibm/fp-go/eq"
|
||||
G "github.com/ibm/fp-go/record/generic"
|
||||
)
|
||||
|
||||
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[map[K]V] {
|
||||
return G.Eq[map[K]V, K, V](e)
|
||||
}
|
24
record/generic/eq.go
Normal file
24
record/generic/eq.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
E "github.com/ibm/fp-go/eq"
|
||||
)
|
||||
|
||||
func equals[M ~map[K]V, K comparable, V any](left, right M, eq func(V, V) bool) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for k, v1 := range left {
|
||||
if v2, ok := right[k]; !ok || !eq(v1, v2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Eq[M ~map[K]V, K comparable, V any](e E.Eq[V]) E.Eq[M] {
|
||||
eq := e.Equals
|
||||
return E.FromEquals(func(left, right M) bool {
|
||||
return equals(left, right, eq)
|
||||
})
|
||||
}
|
13
record/generic/monoid.go
Normal file
13
record/generic/monoid.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
M "github.com/ibm/fp-go/monoid"
|
||||
S "github.com/ibm/fp-go/semigroup"
|
||||
)
|
||||
|
||||
func UnionMonoid[N ~map[K]V, K comparable, V any](s S.Semigroup[V]) M.Monoid[N] {
|
||||
return M.MakeMonoid(
|
||||
UnionSemigroup[N](s).Concat,
|
||||
Empty[N](),
|
||||
)
|
||||
}
|
270
record/generic/record.go
Normal file
270
record/generic/record.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/ibm/fp-go/function"
|
||||
G "github.com/ibm/fp-go/internal/record"
|
||||
Mg "github.com/ibm/fp-go/magma"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
T "github.com/ibm/fp-go/tuple"
|
||||
)
|
||||
|
||||
func IsEmpty[M ~map[K]V, K comparable, V any](r M) bool {
|
||||
return len(r) == 0
|
||||
}
|
||||
|
||||
func IsNonEmpty[M ~map[K]V, K comparable, V any](r M) bool {
|
||||
return len(r) > 0
|
||||
}
|
||||
|
||||
func Keys[M ~map[K]V, GK ~[]K, K comparable, V any](r M) GK {
|
||||
return collect[M, GK](r, F.First[K, V])
|
||||
}
|
||||
|
||||
func Values[M ~map[K]V, GV ~[]V, K comparable, V any](r M) GV {
|
||||
return collect[M, GV](r, F.Second[K, V])
|
||||
}
|
||||
|
||||
func collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](r M, f func(K, V) R) GR {
|
||||
count := len(r)
|
||||
result := make(GR, count)
|
||||
idx := 0
|
||||
for k, v := range r {
|
||||
result[idx] = f(k, v)
|
||||
idx++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](f func(K, V) R) func(M) GR {
|
||||
return F.Bind2nd(collect[M, GR, K, V, R], f)
|
||||
}
|
||||
|
||||
func Reduce[M ~map[K]V, K comparable, V, R any](f func(R, V) R, initial R) func(M) R {
|
||||
return func(r M) R {
|
||||
return G.Reduce(r, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceWithIndex[M ~map[K]V, K comparable, V, R any](f func(K, R, V) R, initial R) func(M) R {
|
||||
return func(r M) R {
|
||||
return G.ReduceWithIndex(r, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceRef[M ~map[K]V, K comparable, V, R any](f func(R, *V) R, initial R) func(M) R {
|
||||
return func(r M) R {
|
||||
return G.ReduceRef(r, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceRefWithIndex[M ~map[K]V, K comparable, V, R any](f func(K, R, *V) R, initial R) func(M) R {
|
||||
return func(r M) R {
|
||||
return G.ReduceRefWithIndex(r, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func MonadMap[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(V) R) N {
|
||||
return MonadMapWithIndex[M, N](r, F.Ignore1of2[K](f))
|
||||
}
|
||||
|
||||
func MonadMapWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(K, V) R) N {
|
||||
return G.ReduceWithIndex(r, func(k K, dst N, v V) N {
|
||||
return upsertAtReadWrite(dst, k, f(k, v))
|
||||
}, make(N, len(r)))
|
||||
}
|
||||
|
||||
func MonadMapRefWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(K, *V) R) N {
|
||||
return G.ReduceRefWithIndex(r, func(k K, dst N, v *V) N {
|
||||
return upsertAtReadWrite(dst, k, f(k, v))
|
||||
}, make(N, len(r)))
|
||||
}
|
||||
|
||||
func MonadMapRef[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(*V) R) N {
|
||||
return MonadMapRefWithIndex[M, N](r, F.Ignore1of2[K](f))
|
||||
}
|
||||
|
||||
func Map[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(V) R) func(M) N {
|
||||
return F.Bind2nd(MonadMap[M, N, K, V, R], f)
|
||||
}
|
||||
|
||||
func MapRef[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(*V) R) func(M) N {
|
||||
return F.Bind2nd(MonadMapRef[M, N, K, V, R], f)
|
||||
}
|
||||
|
||||
func MapWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(K, V) R) func(M) N {
|
||||
return F.Bind2nd(MonadMapWithIndex[M, N, K, V, R], f)
|
||||
}
|
||||
|
||||
func MapRefWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(K, *V) R) func(M) N {
|
||||
return F.Bind2nd(MonadMapRefWithIndex[M, N, K, V, R], f)
|
||||
}
|
||||
|
||||
func lookup[M ~map[K]V, K comparable, V any](r M, k K) O.Option[V] {
|
||||
if val, ok := r[k]; ok {
|
||||
return O.Some(val)
|
||||
}
|
||||
return O.None[V]()
|
||||
}
|
||||
|
||||
func Lookup[M ~map[K]V, K comparable, V any](k K) func(M) O.Option[V] {
|
||||
return F.Bind2nd(lookup[M, K, V], k)
|
||||
}
|
||||
|
||||
func Has[M ~map[K]V, K comparable, V any](k K, r M) bool {
|
||||
_, ok := r[k]
|
||||
return ok
|
||||
}
|
||||
|
||||
func union[M ~map[K]V, K comparable, V any](m Mg.Magma[V], left M, right M) M {
|
||||
lenLeft := len(left)
|
||||
|
||||
if lenLeft == 0 {
|
||||
return right
|
||||
}
|
||||
|
||||
lenRight := len(right)
|
||||
if lenRight == 0 {
|
||||
return left
|
||||
}
|
||||
|
||||
result := make(M, lenLeft+lenRight)
|
||||
|
||||
for k, v := range left {
|
||||
if val, ok := right[k]; ok {
|
||||
result[k] = m.Concat(v, val)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range right {
|
||||
if _, ok := left[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func Union[M ~map[K]V, K comparable, V any](m Mg.Magma[V]) func(M) func(M) M {
|
||||
return func(right M) func(M) M {
|
||||
return func(left M) M {
|
||||
return union(m, left, right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Empty[M ~map[K]V, K comparable, V any]() M {
|
||||
return make(M)
|
||||
}
|
||||
|
||||
func Size[M ~map[K]V, K comparable, V any](r M) int {
|
||||
return len(r)
|
||||
}
|
||||
|
||||
func ToArray[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
|
||||
return collect[M, GT](r, T.MakeTuple2[K, V])
|
||||
}
|
||||
|
||||
func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
|
||||
return ToArray[M, GT](r)
|
||||
}
|
||||
|
||||
func FromEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](fa GT) M {
|
||||
m := make(M)
|
||||
for _, t := range fa {
|
||||
upsertAtReadWrite(m, t.F1, t.F2)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func duplicate[M ~map[K]V, K comparable, V any](r M) M {
|
||||
return MonadMap[M, M](r, F.Identity[V])
|
||||
}
|
||||
|
||||
func upsertAt[M ~map[K]V, K comparable, V any](r M, k K, v V) M {
|
||||
dup := duplicate(r)
|
||||
dup[k] = v
|
||||
return dup
|
||||
}
|
||||
|
||||
func deleteAt[M ~map[K]V, K comparable, V any](r M, k K) M {
|
||||
dup := duplicate(r)
|
||||
delete(dup, k)
|
||||
return dup
|
||||
}
|
||||
|
||||
func upsertAtReadWrite[M ~map[K]V, K comparable, V any](r M, k K, v V) M {
|
||||
r[k] = v
|
||||
return r
|
||||
}
|
||||
|
||||
func UpsertAt[M ~map[K]V, K comparable, V any](k K, v V) func(M) M {
|
||||
return func(ma M) M {
|
||||
return upsertAt(ma, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteAt[M ~map[K]V, K comparable, V any](k K) func(M) M {
|
||||
return F.Bind2nd(deleteAt[M, K, V], k)
|
||||
}
|
||||
|
||||
func Singleton[M ~map[K]V, K comparable, V any](k K, v V) M {
|
||||
return M{k: v}
|
||||
}
|
||||
|
||||
func filterMapWithIndex[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](fa M, f func(K, V1) O.Option[V2]) N {
|
||||
return G.ReduceWithIndex(fa, func(key K, n N, value V1) N {
|
||||
return O.MonadFold(f(key, value), F.Constant(n), func(v V2) N {
|
||||
return upsertAtReadWrite(n, key, v)
|
||||
})
|
||||
}, make(N))
|
||||
}
|
||||
|
||||
func filterWithIndex[M ~map[K]V, K comparable, V any](fa M, f func(K, V) bool) M {
|
||||
return filterMapWithIndex[M, M](fa, func(k K, v V) O.Option[V] {
|
||||
if f(k, v) {
|
||||
return O.Of(v)
|
||||
}
|
||||
return O.None[V]()
|
||||
})
|
||||
}
|
||||
|
||||
func filter[M ~map[K]V, K comparable, V any](fa M, f func(K) bool) M {
|
||||
return filterWithIndex(fa, F.Ignore2of2[V](f))
|
||||
}
|
||||
|
||||
// Filter creates a new map with only the elements that match the predicate
|
||||
func Filter[M ~map[K]V, K comparable, V any](f func(K) bool) func(M) M {
|
||||
return F.Bind2nd(filter[M, K, V], f)
|
||||
}
|
||||
|
||||
// FilterWithIndex creates a new map with only the elements that match the predicate
|
||||
func FilterWithIndex[M ~map[K]V, K comparable, V any](f func(K, V) bool) func(M) M {
|
||||
return F.Bind2nd(filterWithIndex[M, K, V], f)
|
||||
}
|
||||
|
||||
// FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMapWithIndex[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) func(M) N {
|
||||
return F.Bind2nd(filterMapWithIndex[M, N, K, V1, V2], f)
|
||||
}
|
||||
|
||||
// FilterMap creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMap[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](f func(V1) O.Option[V2]) func(M) N {
|
||||
return F.Bind2nd(filterMapWithIndex[M, N, K, V1, V2], F.Ignore1of2[K](f))
|
||||
}
|
||||
|
||||
// IsNil checks if the map is set to nil
|
||||
func IsNil[M ~map[K]V, K comparable, V any](m M) bool {
|
||||
return m == nil
|
||||
}
|
||||
|
||||
// IsNonNil checks if the map is set to nil
|
||||
func IsNonNil[M ~map[K]V, K comparable, V any](m M) bool {
|
||||
return m != nil
|
||||
}
|
||||
|
||||
// ConstNil return a nil map
|
||||
func ConstNil[M ~map[K]V, K comparable, V any]() M {
|
||||
return (M)(nil)
|
||||
}
|
12
record/generic/semigroup.go
Normal file
12
record/generic/semigroup.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
S "github.com/ibm/fp-go/semigroup"
|
||||
)
|
||||
|
||||
func UnionSemigroup[N ~map[K]V, K comparable, V any](s S.Semigroup[V]) S.Semigroup[N] {
|
||||
union := Union[N, K, V](s)
|
||||
return S.MakeSemigroup(func(first N, second N) N {
|
||||
return union(second)(first)
|
||||
})
|
||||
}
|
11
record/monoid.go
Normal file
11
record/monoid.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
M "github.com/ibm/fp-go/monoid"
|
||||
G "github.com/ibm/fp-go/record/generic"
|
||||
S "github.com/ibm/fp-go/semigroup"
|
||||
)
|
||||
|
||||
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) M.Monoid[map[K]V] {
|
||||
return G.UnionMonoid[map[K]V](s)
|
||||
}
|
41
record/monoid_test.go
Normal file
41
record/monoid_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
S "github.com/ibm/fp-go/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnionMonoid(t *testing.T) {
|
||||
m := UnionMonoid[string](S.Semigroup())
|
||||
|
||||
e := Empty[string, string]()
|
||||
|
||||
x := map[string]string{
|
||||
"a": "a1",
|
||||
"b": "b1",
|
||||
"c": "c1",
|
||||
}
|
||||
|
||||
y := map[string]string{
|
||||
"b": "b2",
|
||||
"c": "c2",
|
||||
"d": "d2",
|
||||
}
|
||||
|
||||
res := map[string]string{
|
||||
"a": "a1",
|
||||
"b": "b1b2",
|
||||
"c": "c1c2",
|
||||
"d": "d2",
|
||||
}
|
||||
|
||||
assert.Equal(t, x, m.Concat(x, m.Empty()))
|
||||
assert.Equal(t, x, m.Concat(m.Empty(), x))
|
||||
|
||||
assert.Equal(t, x, m.Concat(x, e))
|
||||
assert.Equal(t, x, m.Concat(e, x))
|
||||
|
||||
assert.Equal(t, res, m.Concat(x, y))
|
||||
}
|
155
record/record.go
Normal file
155
record/record.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
Mg "github.com/ibm/fp-go/magma"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
G "github.com/ibm/fp-go/record/generic"
|
||||
T "github.com/ibm/fp-go/tuple"
|
||||
)
|
||||
|
||||
func IsEmpty[K comparable, V any](r map[K]V) bool {
|
||||
return G.IsEmpty(r)
|
||||
}
|
||||
|
||||
func IsNonEmpty[K comparable, V any](r map[K]V) bool {
|
||||
return G.IsNonEmpty(r)
|
||||
}
|
||||
|
||||
func Keys[K comparable, V any](r map[K]V) []K {
|
||||
return G.Keys[map[K]V, []K](r)
|
||||
}
|
||||
|
||||
func Values[K comparable, V any](r map[K]V) []V {
|
||||
return G.Values[map[K]V, []V](r)
|
||||
}
|
||||
|
||||
func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R {
|
||||
return G.Collect[map[K]V, []R](f)
|
||||
}
|
||||
|
||||
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R {
|
||||
return G.Reduce[map[K]V](f, initial)
|
||||
}
|
||||
|
||||
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceWithIndex[map[K]V](f, initial)
|
||||
}
|
||||
|
||||
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceRef[map[K]V](f, initial)
|
||||
}
|
||||
|
||||
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceRefWithIndex[map[K]V](f, initial)
|
||||
}
|
||||
|
||||
func MonadMap[K comparable, V, R any](r map[K]V, f func(V) R) map[K]R {
|
||||
return G.MonadMap[map[K]V, map[K]R](r, f)
|
||||
}
|
||||
|
||||
func MonadMapWithIndex[K comparable, V, R any](r map[K]V, f func(K, V) R) map[K]R {
|
||||
return G.MonadMapWithIndex[map[K]V, map[K]R](r, f)
|
||||
}
|
||||
|
||||
func MonadMapRefWithIndex[K comparable, V, R any](r map[K]V, f func(K, *V) R) map[K]R {
|
||||
return G.MonadMapRefWithIndex[map[K]V, map[K]R](r, f)
|
||||
}
|
||||
|
||||
func MonadMapRef[K comparable, V, R any](r map[K]V, f func(*V) R) map[K]R {
|
||||
return G.MonadMapRef[map[K]V, map[K]R](r, f)
|
||||
}
|
||||
|
||||
func Map[K comparable, V, R any](f func(V) R) func(map[K]V) map[K]R {
|
||||
return G.Map[map[K]V, map[K]R](f)
|
||||
}
|
||||
|
||||
func MapRef[K comparable, V, R any](f func(*V) R) func(map[K]V) map[K]R {
|
||||
return G.MapRef[map[K]V, map[K]R](f)
|
||||
}
|
||||
|
||||
func MapWithIndex[K comparable, V, R any](f func(K, V) R) func(map[K]V) map[K]R {
|
||||
return G.MapWithIndex[map[K]V, map[K]R](f)
|
||||
}
|
||||
|
||||
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[K]R {
|
||||
return G.MapRefWithIndex[map[K]V, map[K]R](f)
|
||||
}
|
||||
|
||||
func Lookup[K comparable, V any](k K) func(map[K]V) O.Option[V] {
|
||||
return G.Lookup[map[K]V](k)
|
||||
}
|
||||
|
||||
func Has[K comparable, V any](k K, r map[K]V) bool {
|
||||
return G.Has(k, r)
|
||||
}
|
||||
|
||||
func Union[K comparable, V any](m Mg.Magma[V]) func(map[K]V) func(map[K]V) map[K]V {
|
||||
return G.Union[map[K]V](m)
|
||||
}
|
||||
|
||||
func Empty[K comparable, V any]() map[K]V {
|
||||
return G.Empty[map[K]V]()
|
||||
}
|
||||
|
||||
func Size[K comparable, V any](r map[K]V) int {
|
||||
return G.Size(r)
|
||||
}
|
||||
|
||||
func ToArray[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
|
||||
return G.ToArray[map[K]V, []T.Tuple2[K, V]](r)
|
||||
}
|
||||
|
||||
func ToEntries[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
|
||||
return G.ToEntries[map[K]V, []T.Tuple2[K, V]](r)
|
||||
}
|
||||
|
||||
func FromEntries[K comparable, V any](fa []T.Tuple2[K, V]) map[K]V {
|
||||
return G.FromEntries[map[K]V](fa)
|
||||
}
|
||||
|
||||
func UpsertAt[K comparable, V any](k K, v V) func(map[K]V) map[K]V {
|
||||
return G.UpsertAt[map[K]V](k, v)
|
||||
}
|
||||
|
||||
func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V {
|
||||
return G.DeleteAt[map[K]V](k)
|
||||
}
|
||||
|
||||
func Singleton[K comparable, V any](k K, v V) map[K]V {
|
||||
return G.Singleton[map[K]V](k, v)
|
||||
}
|
||||
|
||||
// FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterMapWithIndex[map[K]V1, map[K]V2](f)
|
||||
}
|
||||
|
||||
// FilterMap creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMap[K comparable, V1, V2 any](f func(V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterMap[map[K]V1, map[K]V2](f)
|
||||
}
|
||||
|
||||
// Filter creates a new map with only the elements that match the predicate
|
||||
func Filter[K comparable, V any](f func(K) bool) func(map[K]V) map[K]V {
|
||||
return G.Filter[map[K]V](f)
|
||||
}
|
||||
|
||||
// FilterWithIndex creates a new map with only the elements that match the predicate
|
||||
func FilterWithIndex[K comparable, V any](f func(K, V) bool) func(map[K]V) map[K]V {
|
||||
return G.FilterWithIndex[map[K]V](f)
|
||||
}
|
||||
|
||||
// IsNil checks if the map is set to nil
|
||||
func IsNil[K comparable, V any](m map[K]V) bool {
|
||||
return G.IsNil(m)
|
||||
}
|
||||
|
||||
// IsNonNil checks if the map is set to nil
|
||||
func IsNonNil[K comparable, V any](m map[K]V) bool {
|
||||
return G.IsNonNil(m)
|
||||
}
|
||||
|
||||
// ConstNil return a nil map
|
||||
func ConstNil[K comparable, V any]() map[K]V {
|
||||
return (map[K]V)(nil)
|
||||
}
|
58
record/record_test.go
Normal file
58
record/record_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/ibm/fp-go/internal/utils"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
data := map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
}
|
||||
keys := Keys(data)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c"}, keys)
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
data := map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
}
|
||||
keys := Values(data)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"A", "B", "C"}, keys)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
data := map[string]string{
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
"c": "c",
|
||||
}
|
||||
expected := map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
}
|
||||
assert.Equal(t, expected, Map[string](utils.Upper)(data))
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
data := map[string]string{
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
"c": "c",
|
||||
}
|
||||
assert.Equal(t, O.Some("a"), Lookup[string, string]("a")(data))
|
||||
assert.Equal(t, O.None[string](), Lookup[string, string]("a1")(data))
|
||||
}
|
10
record/semigroup.go
Normal file
10
record/semigroup.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
G "github.com/ibm/fp-go/record/generic"
|
||||
S "github.com/ibm/fp-go/semigroup"
|
||||
)
|
||||
|
||||
func UnionSemigroup[K comparable, V any](s S.Semigroup[V]) S.Semigroup[map[K]V] {
|
||||
return G.UnionSemigroup[map[K]V](s)
|
||||
}
|
38
record/traverse.go
Normal file
38
record/traverse.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
G "github.com/ibm/fp-go/internal/record"
|
||||
)
|
||||
|
||||
func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(map[K]B) HKTRB,
|
||||
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
f func(K, A) HKTB) func(map[K]A) HKTRB {
|
||||
return G.TraverseWithIndex[map[K]A](fof, fmap, fap, f)
|
||||
}
|
||||
|
||||
// HKTA = HKT<A>
|
||||
// HKTB = HKT<B>
|
||||
// HKTAB = HKT<func(A)B>
|
||||
// HKTRB = HKT<map[K]B>
|
||||
func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(map[K]B) HKTRB,
|
||||
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
f func(A) HKTB) func(map[K]A) HKTRB {
|
||||
return G.Traverse[map[K]A](fof, fmap, fap, f)
|
||||
}
|
||||
|
||||
// HKTA = HKT[A]
|
||||
// HKTAA = HKT[func(A)map[K]A]
|
||||
// HKTRA = HKT[map[K]A]
|
||||
func Sequence[K comparable, A, HKTA, HKTAA, HKTRA any](
|
||||
fof func(map[K]A) HKTRA,
|
||||
fmap func(func(map[K]A) func(A) map[K]A) func(HKTRA) HKTAA,
|
||||
fap func(HKTA) func(HKTAA) HKTRA,
|
||||
ma map[K]HKTA) HKTRA {
|
||||
return G.Sequence(fof, fmap, fap, ma)
|
||||
|
||||
}
|
75
record/traverse_test.go
Normal file
75
record/traverse_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/ibm/fp-go/function"
|
||||
O "github.com/ibm/fp-go/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MapType = map[string]int
|
||||
type MapTypeString = map[string]string
|
||||
type MapTypeO = map[string]O.Option[int]
|
||||
|
||||
func TestSimpleTraversalWithIndex(t *testing.T) {
|
||||
|
||||
f := func(k string, n int) O.Option[int] {
|
||||
if k != "a" {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
tWithIndex := TraverseWithIndex(
|
||||
O.Of[MapType],
|
||||
O.Map[MapType, func(int) MapType],
|
||||
O.Ap[MapType, int],
|
||||
f)
|
||||
|
||||
assert.Equal(t, O.None[MapType](), F.Pipe1(MapType{"a": 1, "b": 2}, tWithIndex))
|
||||
assert.Equal(t, O.Some(MapType{"b": 2}), F.Pipe1(MapType{"b": 2}, tWithIndex))
|
||||
}
|
||||
|
||||
func TestSimpleTraversalNoIndex(t *testing.T) {
|
||||
|
||||
f := func(k string) O.Option[string] {
|
||||
if k != "1" {
|
||||
return O.Some(k)
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
|
||||
tWithoutIndex := Traverse(
|
||||
O.Of[MapTypeString],
|
||||
O.Map[MapTypeString, func(string) MapTypeString],
|
||||
O.Ap[MapTypeString, string],
|
||||
f)
|
||||
|
||||
assert.Equal(t, O.None[MapTypeString](), F.Pipe1(MapTypeString{"a": "1", "b": "2"}, tWithoutIndex))
|
||||
assert.Equal(t, O.Some(MapTypeString{"b": "2"}), F.Pipe1(MapTypeString{"b": "2"}, tWithoutIndex))
|
||||
}
|
||||
|
||||
func TestSequence(t *testing.T) {
|
||||
// source map
|
||||
simpleMapO := MapTypeO{"a": O.Of(1), "b": O.Of(2)}
|
||||
// convert to an option of record
|
||||
|
||||
s := Traverse(
|
||||
O.Of[MapType],
|
||||
O.Map[MapType, func(int) MapType],
|
||||
O.Ap[MapType, int],
|
||||
F.Identity[O.Option[int]],
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(MapType{"a": 1, "b": 2}), F.Pipe1(simpleMapO, s))
|
||||
|
||||
s1 := Sequence(
|
||||
O.Of[MapType],
|
||||
O.Map[MapType, func(int) MapType],
|
||||
O.Ap[MapType, int],
|
||||
simpleMapO,
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(MapType{"a": 1, "b": 2}), s1)
|
||||
}
|
Reference in New Issue
Block a user