diff --git a/context/readerioeither/http/request.go b/context/readerioeither/http/request.go index 95c5fbf..8caac6e 100644 --- a/context/readerioeither/http/request.go +++ b/context/readerioeither/http/request.go @@ -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], + )), ) } diff --git a/http/types.go b/http/types.go new file mode 100644 index 0000000..60dbc00 --- /dev/null +++ b/http/types.go @@ -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] +) diff --git a/http/utils.go b/http/utils.go new file mode 100644 index 0000000..7c57c1c --- /dev/null +++ b/http/utils.go @@ -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), + ) +} diff --git a/http/utils_test.go b/http/utils_test.go new file mode 100644 index 0000000..ed515ab --- /dev/null +++ b/http/utils_test.go @@ -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) +} diff --git a/readereither/reader.go b/readereither/reader.go index e510c5f..90dd920 100644 --- a/readereither/reader.go +++ b/readereither/reader.go @@ -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) } diff --git a/readereither/sequence_test.go b/readereither/sequence_test.go index 1044d81..0bd5bc5 100644 --- a/readereither/sequence_test.go +++ b/readereither/sequence_test.go @@ -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)) diff --git a/readerioeither/reader.go b/readerioeither/reader.go index 887f3bd..88d1be2 100644 --- a/readerioeither/reader.go +++ b/readerioeither/reader.go @@ -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) } diff --git a/readerioeither/reader_test.go b/readerioeither/reader_test.go index bd7de78..6a0adf9 100644 --- a/readerioeither/reader_test.go +++ b/readerioeither/reader_test.go @@ -34,7 +34,7 @@ func TestOrLeft(t *testing.T) { ) g2 := F.Pipe1( - Left[context.Context, string, int]("a"), + Left[context.Context, int]("a"), f, ) diff --git a/record/eq.go b/record/eq.go new file mode 100644 index 0000000..6dc62be --- /dev/null +++ b/record/eq.go @@ -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) +} diff --git a/record/generic/eq.go b/record/generic/eq.go new file mode 100644 index 0000000..c367cb6 --- /dev/null +++ b/record/generic/eq.go @@ -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) + }) +} diff --git a/record/generic/monoid.go b/record/generic/monoid.go new file mode 100644 index 0000000..4f827d7 --- /dev/null +++ b/record/generic/monoid.go @@ -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](), + ) +} diff --git a/record/generic/record.go b/record/generic/record.go new file mode 100644 index 0000000..f58463e --- /dev/null +++ b/record/generic/record.go @@ -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) +} diff --git a/record/generic/semigroup.go b/record/generic/semigroup.go new file mode 100644 index 0000000..03e8622 --- /dev/null +++ b/record/generic/semigroup.go @@ -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) + }) +} diff --git a/record/monoid.go b/record/monoid.go new file mode 100644 index 0000000..667f32e --- /dev/null +++ b/record/monoid.go @@ -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) +} diff --git a/record/monoid_test.go b/record/monoid_test.go new file mode 100644 index 0000000..4442ecc --- /dev/null +++ b/record/monoid_test.go @@ -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)) +} diff --git a/record/record.go b/record/record.go new file mode 100644 index 0000000..eb44628 --- /dev/null +++ b/record/record.go @@ -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) +} diff --git a/record/record_test.go b/record/record_test.go new file mode 100644 index 0000000..a216959 --- /dev/null +++ b/record/record_test.go @@ -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)) +} diff --git a/record/semigroup.go b/record/semigroup.go new file mode 100644 index 0000000..ff41a71 --- /dev/null +++ b/record/semigroup.go @@ -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) +} diff --git a/record/traverse.go b/record/traverse.go new file mode 100644 index 0000000..c546eb9 --- /dev/null +++ b/record/traverse.go @@ -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 +// HKTB = HKT +// HKTAB = HKT +// HKTRB = HKT +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) + +} diff --git a/record/traverse_test.go b/record/traverse_test.go new file mode 100644 index 0000000..c44a089 --- /dev/null +++ b/record/traverse_test.go @@ -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) +}