1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-09-05 20:26:12 +02:00

Compare commits

...

28 Commits

Author SHA1 Message Date
Dr. Carsten Leue
5a9f405362 fix: better handling of HTTP errors
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-20 09:57:23 +01:00
Dr. Carsten Leue
89c34254a9 fix: add Join
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-17 11:57:26 +01:00
Dr. Carsten Leue
a14feff1d6 fix: add missing MapLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-15 12:04:55 +01:00
Dr. Carsten Leue
f3642bad60 fix: remove 1.22
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-15 10:52:16 +01:00
Dr. Carsten Leue
997627b318 fix: node version
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-15 10:51:17 +01:00
Dr. Carsten Leue
1e1411c003 fix: add LogJson
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-15 10:46:50 +01:00
Dr. Carsten Leue
9ed9f8a171 fix: implement foldMap for records
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-12 17:19:38 +01:00
Dr. Carsten Leue
e7428549e4 fix: add consistent Delay and After functions to IO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-12 13:20:50 +01:00
Dr. Carsten Leue
aef0048119 fix: add Wrap and Unwrap to endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-11 17:18:35 +01:00
Dr. Carsten Leue
709d74b135 fix: refactor builder
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-11 14:12:53 +01:00
Dr. Carsten Leue
38c6541254 fix: handling of headers in builder
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-09 23:30:55 +01:00
Dr. Carsten Leue
813b83b423 fix: add WithQueryArg to request builder
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-08 21:35:39 +01:00
Dr. Carsten Leue
9139dedbbe fix: add lazy support for iterators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-01-08 14:22:08 +01:00
Elliot Chen
5e15119653 run go mod tidy (#97)
Co-authored-by: zichang.chen <zichange.chen@grabtaxi.com>
2024-01-08 08:08:59 +01:00
Elliot Chen
986aa21055 add idea filter in gitignore (#94)
Co-authored-by: zichang.chen <zichange.chen@grabtaxi.com>
2024-01-02 21:56:40 +01:00
renovate[bot]
3a4c46ec1e fix(deps): update module github.com/urfave/cli/v2 to v2.27.1 2023-12-30 18:29:29 +00:00
renovate[bot]
96c3ee20ff fix(deps): update module github.com/urfave/cli/v2 to v2.27.0 2023-12-27 02:11:30 +00:00
Dr. Carsten Leue
7af9acfd99 fix: Chain for Endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-22 16:43:19 +01:00
Dr. Carsten Leue
36eefbcd27 fix: name the endomorphism for the Y combinator
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-22 14:41:55 +01:00
Dr. Carsten Leue
973138c822 fix: add prepend method
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-21 16:46:47 +01:00
Dr. Carsten Leue
12ef79184b fix: typing for Y combinator
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-21 16:25:35 +01:00
Dr. Carsten Leue
5ac47440a1 fix: add a single element cache
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-21 14:10:46 +01:00
Dr. Carsten Leue
3aa55c74d4 fix: add WithBearer
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-19 14:33:52 +01:00
Dr. Carsten Leue
a6f55a199c fix: experiment with doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-19 12:18:07 +01:00
Dr. Carsten Leue
2b500d15da Merge branch 'main' of github.com:IBM/fp-go 2023-12-18 21:40:39 +01:00
Dr. Carsten Leue
599b8256b6 fix: generate DI variations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-18 21:40:30 +01:00
renovate[bot]
cf70b47984 chore(deps): update actions/setup-node action to v4.0.1 2023-12-18 15:59:48 +00:00
Dr. Carsten Leue
7bceb856f8 fix: move DI to separate package
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-12-18 10:06:47 +01:00
66 changed files with 2782 additions and 602 deletions

View File

@@ -17,8 +17,8 @@ on:
env:
# Currently no way to detect automatically
DEFAULT_BRANCH: main
GO_VERSION: 1.20.6 # renovate: datasource=golang-version depName=golang
NODE_VERSION: 18
GO_VERSION: 1.21.6 # renovate: datasource=golang-version depName=golang
NODE_VERSION: 20
DRY_RUN: true
jobs:
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x' ]
go-version: [ '1.20.x', '1.21.x']
steps:
# full checkout for semantic-release
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -60,7 +60,7 @@ jobs:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
fp-go.exe
main.exe
build/
build/
.idea

View File

@@ -17,6 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/array/generic"
EM "github.com/IBM/fp-go/endomorphism"
F "github.com/IBM/fp-go/function"
"github.com/IBM/fp-go/internal/array"
M "github.com/IBM/fp-go/monoid"
@@ -89,16 +90,16 @@ func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
}
// Filter returns a new array with all elements from the original array that match a predicate
func Filter[A any](pred func(A) bool) func([]A) []A {
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
return G.Filter[[]A](pred)
}
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
func FilterWithIndex[A any](pred func(int, A) bool) func([]A) []A {
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
return G.FilterWithIndex[[]A](pred)
}
func FilterRef[A any](pred func(*A) bool) func([]A) []A {
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
return F.Bind2nd(filterRef[A], pred)
}
@@ -227,7 +228,7 @@ func Last[A any](as []A) O.Option[A] {
return G.Last(as)
}
func PrependAll[A any](middle A) func([]A) []A {
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
return func(as []A) []A {
count := len(as)
dst := count * 2
@@ -242,7 +243,7 @@ func PrependAll[A any](middle A) func([]A) []A {
}
}
func Intersperse[A any](middle A) func([]A) []A {
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
prepend := PrependAll(middle)
return func(as []A) []A {
if IsEmpty(as) {
@@ -271,7 +272,7 @@ func Lookup[A any](idx int) func([]A) O.Option[A] {
return G.Lookup[[]A](idx)
}
func UpsertAt[A any](a A) func([]A) []A {
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
return G.UpsertAt[[]A](a)
}
@@ -304,7 +305,7 @@ func ConstNil[A any]() []A {
return array.ConstNil[[]A]()
}
func SliceRight[A any](start int) func([]A) []A {
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
return G.SliceRight[[]A](start)
}
@@ -333,8 +334,8 @@ func Fold[A any](m M.Monoid[A]) func([]A) A {
return G.Fold[[]A](m)
}
func Push[A any](a A) func([]A) []A {
return G.Push[[]A](a)
func Push[A any](a A) EM.Endomorphism[[]A] {
return G.Push[EM.Endomorphism[[]A]](a)
}
func MonadFlap[B, A any](fab []func(A) B, a A) []B {
@@ -344,3 +345,7 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
func Flap[B, A any](a A) func([]func(A) B) []B {
return G.Flap[func(A) B, []func(A) B, []B, A, B](a)
}
func Prepend[A any](head A) EM.Endomorphism[[]A] {
return G.Prepend[EM.Endomorphism[[]A]](head)
}

View File

@@ -335,7 +335,7 @@ func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
}
}
func Push[GA ~[]A, A any](a A) func(GA) GA {
func Push[ENDO ~func(GA) GA, GA ~[]A, A any](a A) ENDO {
return F.Bind2nd(array.Push[GA, A], a)
}
@@ -346,3 +346,7 @@ func MonadFlap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](fab GFAB, a A) GB
func Flap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](a A) func(GFAB) GB {
return F.Bind2nd(MonadFlap[FAB, GFAB, GB, A, B], a)
}
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
return array.Prepend[ENDO](head)
}

View File

@@ -24,10 +24,10 @@ import (
func concat[T any](left, right []T) []T {
// some performance checks
ll := len(left)
lr := len(right)
if ll == 0 {
return right
}
lr := len(right)
if lr == 0 {
return left
}

View File

@@ -17,6 +17,7 @@ package nonempty
import (
G "github.com/IBM/fp-go/array/generic"
EM "github.com/IBM/fp-go/endomorphism"
F "github.com/IBM/fp-go/function"
"github.com/IBM/fp-go/internal/array"
S "github.com/IBM/fp-go/semigroup"
@@ -65,6 +66,12 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
}
}
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.ReduceRight(as, f, initial)
}
}
func Tail[A any](as NonEmptyArray[A]) []A {
return as[1:]
}
@@ -122,3 +129,8 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
return array.Reduce(Tail(as), s.Concat, Head(as))
}
}
// Prepend prepends a single value to an array
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
}

View File

@@ -34,5 +34,6 @@ func Commands() []*C.Command {
IOEitherCommand(),
IOCommand(),
IOOptionCommand(),
DICommand(),
}
}

231
cli/di.go Normal file
View File

@@ -0,0 +1,231 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func generateMakeProvider(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeProvider%d creates a [DIE.Provider] for an [InjectionToken] from a function with %d dependencies\n", i, i)
fmt.Fprintf(f, "func MakeProvider%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
fmt.Fprintf(f, " token InjectionToken[R],\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") DIE.Provider {\n")
fmt.Fprint(f, " return DIE.MakeProvider(\n")
fmt.Fprint(f, " token,\n")
fmt.Fprintf(f, " MakeProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ))\n")
fmt.Fprintf(f, "}\n")
}
func generateMakeTokenWithDefault(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeTokenWithDefault%d creates an [InjectionToken] with a default implementation with %d dependencies\n", i, i)
fmt.Fprintf(f, "func MakeTokenWithDefault%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
fmt.Fprintf(f, " name string,\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") InjectionToken[R] {\n")
fmt.Fprintf(f, " return MakeTokenWithDefault[R](name, MakeProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ))\n")
fmt.Fprintf(f, "}\n")
}
func generateMakeProviderFactory(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeProviderFactory%d creates a [DIE.ProviderFactory] from a function with %d arguments and %d dependencies\n", i, i, i)
fmt.Fprintf(f, "func MakeProviderFactory%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") DIE.ProviderFactory {\n")
fmt.Fprint(f, " return DIE.MakeProviderFactory(\n")
fmt.Fprint(f, " A.From[DIE.Dependency](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " ),\n")
fmt.Fprintf(f, " eraseProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ),\n")
fmt.Fprint(f, " )\n")
fmt.Fprintf(f, "}\n")
}
func generateEraseProviderFactory(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// eraseProviderFactory%d creates a function that takes a variadic number of untyped arguments and from a function of %d strongly typed arguments and %d dependencies\n", i, i, i)
fmt.Fprintf(f, "func eraseProviderFactory%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {\n")
fmt.Fprintf(f, " ft := eraseTuple(T.Tupled%d(f))\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d := lookupAt[T%d](%d, d%d)\n", j+1, j+1, j, j+1)
}
fmt.Fprint(f, " return func(params ...any) IOE.IOEither[error, any] {\n")
fmt.Fprintf(f, " return ft(E.SequenceT%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d(params),\n", j+1)
}
fmt.Fprint(f, " ))\n")
fmt.Fprint(f, " }\n")
fmt.Fprintf(f, "}\n")
}
func generateDIHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprint(f, `
import (
E "github.com/IBM/fp-go/either"
IOE "github.com/IBM/fp-go/ioeither"
T "github.com/IBM/fp-go/tuple"
A "github.com/IBM/fp-go/array"
DIE "github.com/IBM/fp-go/di/erasure"
)
`)
for i := 1; i <= count; i++ {
generateEraseProviderFactory(f, i)
generateMakeProviderFactory(f, i)
generateMakeTokenWithDefault(f, i)
generateMakeProvider(f, i)
}
return nil
}
func DICommand() *C.Command {
return &C.Command{
Name: "di",
Usage: "generate code for the dependency injection package",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package builder
import (
"bytes"
"context"
"net/http"
"strconv"
RIOE "github.com/IBM/fp-go/context/readerioeither"
RIOEH "github.com/IBM/fp-go/context/readerioeither/http"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
R "github.com/IBM/fp-go/http/builder"
H "github.com/IBM/fp-go/http/headers"
LZ "github.com/IBM/fp-go/lazy"
O "github.com/IBM/fp-go/option"
)
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
return func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
if err == nil {
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
}
})
})
withoutBody := F.Curry2(func(url string, method string) RIOE.ReaderIOEither[*http.Request] {
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
return func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
if err == nil {
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
}
})
})
return F.Pipe5(
builder.GetBody(),
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
E.Ap[func(string) RIOE.ReaderIOEither[*http.Request]](builder.GetTargetUrl()),
E.Flap[error, RIOE.ReaderIOEither[*http.Request]](builder.GetMethod()),
E.GetOrElse(RIOE.Left[*http.Request]),
RIOE.Map(func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req
}),
)
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package builder
import (
"context"
"net/http"
"net/url"
"testing"
RIOE "github.com/IBM/fp-go/context/readerioeither"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
R "github.com/IBM/fp-go/http/builder"
IO "github.com/IBM/fp-go/io"
"github.com/stretchr/testify/assert"
)
func TestBuilderWithQuery(t *testing.T) {
// add some query
withLimit := R.WithQueryArg("limit")("10")
withUrl := R.WithUrl("http://www.example.org?a=b")
b := F.Pipe2(
R.Default,
withLimit,
withUrl,
)
req := F.Pipe3(
b,
Requester,
RIOE.Map(func(r *http.Request) *url.URL {
return r.URL
}),
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
return IO.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))
assert.Equal(t, "b", q.Get("a"))
})
}),
)
assert.True(t, E.IsRight(req(context.Background())()))
}

38
di/app.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package di
import (
DIE "github.com/IBM/fp-go/di/erasure"
F "github.com/IBM/fp-go/function"
IO "github.com/IBM/fp-go/io"
IOE "github.com/IBM/fp-go/ioeither"
)
var (
// InjMain is the [InjectionToken] for the main application
InjMain = MakeToken[any]("APP")
// Main is the resolver for the main application
Main = Resolve(InjMain)
)
// RunMain runs the main application from a set of [DIE.Provider]s
var RunMain = F.Flow3(
DIE.MakeInjector,
Main,
IOE.Fold(IO.Of[error], F.Constant1[any](IO.Of[error](nil))),
)

43
di/doc.go Normal file
View File

@@ -0,0 +1,43 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package di implements functions and data types supporting dependency injection patterns
//
// The fundamental building block is the concept of a [Dependency]. This describes the abstract concept of a function, service or value together with its type.
// Examples for dependencies can be as simple as configuration values such as the API URL for a service, the current username, a [Dependency] could be the map
// of the configuration environment, an http client or as complex as a service interface. Important is that a [Dependency] only defines the concept but
// not the implementation.
//
// The implementation of a [Dependency] is called a [Provider], the dependency of an `API URL` could e.g. be realized by a provider that consults the environment to read the information
// or a config file or simply hardcode it.
// In many cases the implementation of a [Provider] depends in turn on other [Dependency] (but never directly on other [Provider]s), a provider for an `API URL` that reads
// the information from the environment would e.g. depend on a [Dependency] that represents this environment.
//
// It is the resposibility of the [InjectableFactory] to create an instance of a [Dependency]. All instances are considered singletons. Create an [InjectableFactory] via the [MakeInjector] method. Use [Resolve] to create a strongly typed
// factory for a particular [InjectionToken].
//
// In most cases it is not required to use [InjectableFactory] directly, instead you would create a [Provider] via the [MakeProvider2] method (suffix indicates the number of dependencies). In this call
// you give a number of (strongly typed) [Dependency] identifiers and a (strongly typed) factory function for the implementation. The dependency injection framework makes
// sure to resolve the dependencies before calling the factory method.
//
// For convenience purposes it can be helpful to attach a default implementation of a [Dependency]. In this case use the [MakeTokenWithDefault2] method (suffix indicates the number of dependencies)
// to define the [InjectionToken].
//
// [Provider]: [github.com/IBM/fp-go/di/erasure.Provider]
// [InjectableFactory]: [github.com/IBM/fp-go/di/erasure.InjectableFactory]
// [MakeInjector]: [github.com/IBM/fp-go/di/erasure.MakeInjector]
package di
//go:generate go run .. di --count 10 --filename gen.go

View File

@@ -7,7 +7,6 @@
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -115,6 +114,9 @@ func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
}
// MakeInjector creates an [InjectableFactory] based on a set of [Provider]s
//
// The resulting [InjectableFactory] can then be used to retrieve service instances given their [Dependency]. The implementation
// makes sure to transitively resolve the required dependencies.
func MakeInjector(providers []Provider) InjectableFactory {
type Result = IOE.IOEither[error, any]

View File

@@ -32,6 +32,7 @@ import (
)
type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOE.IOEither[error, any]
ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any]
@@ -83,6 +84,8 @@ var (
mergeMaps = R.UnionLastMonoid[int, any]()
collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any])
mapDeps = F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]])
handlers = map[int]handler{
Identity: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
@@ -171,7 +174,7 @@ func MakeProviderFactory(
fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory {
return F.Flow3(
F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]])(deps),
mapDeps(deps),
handleMapping(foldDeps(deps)),
IOE.Chain(F.Unvariadic0(fct)),
)

1013
di/gen.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@@ -12,6 +12,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package di
import (
@@ -21,7 +22,6 @@ import (
"github.com/IBM/fp-go/errors"
F "github.com/IBM/fp-go/function"
IOE "github.com/IBM/fp-go/ioeither"
T "github.com/IBM/fp-go/tuple"
)
func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[error, T] {
@@ -32,89 +32,25 @@ func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[e
)
}
func eraseProviderFactory0[R any](f func() IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
func eraseTuple[A, R any](f func(A) IOE.IOEither[error, R]) func(E.Either[error, A]) IOE.IOEither[error, any] {
return F.Flow3(
IOE.FromEither[error, A],
IOE.Chain(f),
IOE.Map[error](F.ToAny[R]),
)
}
func eraseProviderFactory0[R any](f IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
return func(params ...any) IOE.IOEither[error, any] {
return F.Pipe1(
f(),
IOE.Map[error](F.ToAny[R]),
)
}
}
func eraseProviderFactory1[T1 any, R any](
d1 Dependency[T1],
f func(T1) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
ft := T.Tupled1(f)
t1 := lookupAt[T1](0, d1)
return func(params ...any) IOE.IOEither[error, any] {
return F.Pipe3(
E.SequenceT1(t1(params)),
IOE.FromEither[error, T.Tuple1[T1]],
IOE.Chain(ft),
IOE.Map[error](F.ToAny[R]),
)
}
}
func eraseProviderFactory2[T1, T2 any, R any](
d1 Dependency[T1],
d2 Dependency[T2],
f func(T1, T2) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
ft := T.Tupled2(f)
t1 := lookupAt[T1](0, d1)
t2 := lookupAt[T2](1, d2)
return func(params ...any) IOE.IOEither[error, any] {
return F.Pipe3(
E.SequenceT2(t1(params), t2(params)),
IOE.FromEither[error, T.Tuple2[T1, T2]],
IOE.Chain(ft),
IOE.Map[error](F.ToAny[R]),
)
}
}
func eraseProviderFactory3[T1, T2, T3 any, R any](
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
f func(T1, T2, T3) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
ft := T.Tupled3(f)
t1 := lookupAt[T1](0, d1)
t2 := lookupAt[T2](1, d2)
t3 := lookupAt[T3](2, d3)
return func(params ...any) IOE.IOEither[error, any] {
return F.Pipe3(
E.SequenceT3(t1(params), t2(params), t3(params)),
IOE.FromEither[error, T.Tuple3[T1, T2, T3]],
IOE.Chain(ft),
IOE.Map[error](F.ToAny[R]),
)
}
}
func eraseProviderFactory4[T1, T2, T3, T4 any, R any](
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
d4 Dependency[T4],
f func(T1, T2, T3, T4) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
ft := T.Tupled4(f)
t1 := lookupAt[T1](0, d1)
t2 := lookupAt[T2](1, d2)
t3 := lookupAt[T3](2, d3)
t4 := lookupAt[T4](3, d4)
return func(params ...any) IOE.IOEither[error, any] {
return F.Pipe3(
E.SequenceT4(t1(params), t2(params), t3(params), t4(params)),
IOE.FromEither[error, T.Tuple4[T1, T2, T3, T4]],
IOE.Chain(ft),
f,
IOE.Map[error](F.ToAny[R]),
)
}
}
func MakeProviderFactory0[R any](
fct func() IOE.IOEither[error, R],
fct IOE.IOEither[error, R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.Empty[DIE.Dependency](),
@@ -122,14 +58,14 @@ func MakeProviderFactory0[R any](
)
}
// MakeTokenWithDefault0 create a unique `InjectionToken` for a specific type with an attached default provider
func MakeTokenWithDefault0[R any](name string, fct func() IOE.IOEither[error, R]) InjectionToken[R] {
// MakeTokenWithDefault0 creates a unique [InjectionToken] for a specific type with an attached default [DIE.Provider]
func MakeTokenWithDefault0[R any](name string, fct IOE.IOEither[error, R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory0(fct))
}
func MakeProvider0[R any](
token InjectionToken[R],
fct func() IOE.IOEither[error, R],
fct IOE.IOEither[error, R],
) DIE.Provider {
return DIE.MakeProvider(
token,
@@ -137,145 +73,7 @@ func MakeProvider0[R any](
)
}
func MakeProviderFactory1[T1, R any](
d1 Dependency[T1],
fct func(T1) IOE.IOEither[error, R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.From[DIE.Dependency](d1),
eraseProviderFactory1(d1, fct),
)
}
// MakeTokenWithDefault1 create a unique `InjectionToken` for a specific type with an attached default provider
func MakeTokenWithDefault1[T1, R any](name string,
d1 Dependency[T1],
fct func(T1) IOE.IOEither[error, R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory1(d1, fct))
}
func MakeProvider1[T1, R any](
token InjectionToken[R],
d1 Dependency[T1],
fct func(T1) IOE.IOEither[error, R],
) DIE.Provider {
return DIE.MakeProvider(
token,
MakeProviderFactory1(d1, fct),
)
}
func MakeProviderFactory2[T1, T2, R any](
d1 Dependency[T1],
d2 Dependency[T2],
fct func(T1, T2) IOE.IOEither[error, R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.From[DIE.Dependency](d1, d2),
eraseProviderFactory2(d1, d2, fct),
)
}
// MakeTokenWithDefault2 create a unique `InjectionToken` for a specific type with an attached default provider
func MakeTokenWithDefault2[T1, T2, R any](name string,
d1 Dependency[T1],
d2 Dependency[T2],
fct func(T1, T2) IOE.IOEither[error, R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory2(d1, d2, fct))
}
func MakeProvider2[T1, T2, R any](
token InjectionToken[R],
d1 Dependency[T1],
d2 Dependency[T2],
fct func(T1, T2) IOE.IOEither[error, R],
) DIE.Provider {
return DIE.MakeProvider(
token,
MakeProviderFactory2(d1, d2, fct),
)
}
func MakeProviderFactory3[T1, T2, T3, R any](
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
fct func(T1, T2, T3) IOE.IOEither[error, R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.From[DIE.Dependency](d1, d2, d3),
eraseProviderFactory3(d1, d2, d3, fct),
)
}
// MakeTokenWithDefault3 create a unique `InjectionToken` for a specific type with an attached default provider
func MakeTokenWithDefault3[T1, T2, T3, R any](name string,
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
fct func(T1, T2, T3) IOE.IOEither[error, R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory3(d1, d2, d3, fct))
}
func MakeProvider3[T1, T2, T3, R any](
token InjectionToken[R],
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
fct func(T1, T2, T3) IOE.IOEither[error, R],
) DIE.Provider {
return DIE.MakeProvider(
token,
MakeProviderFactory3(d1, d2, d3, fct),
)
}
func MakeProviderFactory4[T1, T2, T3, T4, R any](
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
d4 Dependency[T4],
fct func(T1, T2, T3, T4) IOE.IOEither[error, R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.From[DIE.Dependency](d1, d2, d3, d4),
eraseProviderFactory4(d1, d2, d3, d4, fct),
)
}
// MakeTokenWithDefault4 create a unique `InjectionToken` for a specific type with an attached default provider
func MakeTokenWithDefault4[T1, T2, T3, T4, R any](name string,
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
d4 Dependency[T4],
fct func(T1, T2, T3, T4) IOE.IOEither[error, R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory4(d1, d2, d3, d4, fct))
}
func MakeProvider4[T1, T2, T3, T4, R any](
token InjectionToken[R],
d1 Dependency[T1],
d2 Dependency[T2],
d3 Dependency[T3],
d4 Dependency[T4],
fct func(T1, T2, T3, T4) IOE.IOEither[error, R],
) DIE.Provider {
return DIE.MakeProvider(
token,
MakeProviderFactory4(d1, d2, d3, d4, fct),
)
}
// ConstProvider simple implementation for a provider with a constant value
func ConstProvider[R any](token InjectionToken[R], value R) DIE.Provider {
return MakeProvider0[R](token, F.Constant(IOE.Of[error](value)))
return MakeProvider0[R](token, IOE.Of[error](value))
}

View File

@@ -12,6 +12,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package di
import (
@@ -38,12 +39,10 @@ func TestSimpleProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) func() IOE.IOEither[error, string] {
return func() IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
@@ -82,12 +81,10 @@ func TestOptionalProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) func() IOE.IOEither[error, string] {
return func() IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
@@ -182,12 +179,10 @@ func TestEagerAndLazyProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) func() IOE.IOEither[error, string] {
return func() IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
@@ -307,7 +302,7 @@ func TestTokenWithDefaultProvider(t *testing.T) {
// token without a default
injToken1 := MakeToken[string]("Token1")
// token with a default
injToken2 := MakeTokenWithDefault0("Token2", F.Constant(IOE.Of[error]("Carsten")))
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten"))
// dependency
injToken3 := MakeToken[string]("Token3")
@@ -330,7 +325,7 @@ func TestTokenWithDefaultProvider(t *testing.T) {
func TestTokenWithDefaultProviderAndOverride(t *testing.T) {
// token with a default
injToken2 := MakeTokenWithDefault0("Token2", F.Constant(IOE.Of[error]("Carsten")))
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten"))
// dependency
injToken3 := MakeToken[string]("Token3")

View File

@@ -42,20 +42,21 @@ type InjectionToken[T any] interface {
// Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`.
// If the dependency cannot be resolved, the resolution process fails
Identity() Dependency[T]
// Option identifies this dependency as optional, it will be resolved eagerly and injected as `O.Option[T]`.
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as `O.None[T]`
// Option identifies this dependency as optional, it will be resolved eagerly and injected as [O.Option[T]].
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as [O.None[T]]
Option() Dependency[O.Option[T]]
// IOEither identifies this dependency as mandatory but it will be resolved lazily as a `IOE.IOEither[error, T]`. This
// IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOE.IOEither[error, T]]. This
// value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process fails
IOEither() Dependency[IOE.IOEither[error, T]]
// IOOption identifies this dependency as optional but it will be resolved lazily as a `IOO.IOOption[T]`. This
// IOOption identifies this dependency as optional but it will be resolved lazily as a [IOO.IOOption[T]]. This
// value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value.
IOOption() Dependency[IOO.IOOption[T]]
}
// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name.
// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name that can have multiple implementations.
// Implementations are provided via the [MultiInjectionToken.Item] injection token.
type MultiInjectionToken[T any] interface {
// Container returns the injection token used to request an array of all provided items
Container() InjectionToken[[]T]
@@ -146,7 +147,7 @@ func (m *multiInjectionToken[T]) Item() InjectionToken[T] {
return m.item
}
// makeToken create a unique `InjectionToken` for a specific type
// makeToken create a unique [InjectionToken] for a specific type
func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.ProviderFactory]) InjectionToken[T] {
id := genId()
toIdentity := toType[T]()
@@ -158,12 +159,12 @@ func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.Provide
}
}
// MakeToken create a unique `InjectionToken` for a specific type
// MakeToken create a unique [InjectionToken] for a specific type
func MakeToken[T any](name string) InjectionToken[T] {
return makeInjectionToken[T](name, O.None[DIE.ProviderFactory]())
}
// MakeToken create a unique `InjectionToken` for a specific type
// MakeToken create a unique [InjectionToken] for a specific type
func MakeTokenWithDefault[T any](name string, providerFactory DIE.ProviderFactory) InjectionToken[T] {
return makeInjectionToken[T](name, O.Of(providerFactory))
}

View File

@@ -12,6 +12,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package di
import (

View File

@@ -12,6 +12,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package di
import (

View File

@@ -67,16 +67,17 @@ func MapTo[E, A, B any](b B) func(Either[E, A]) Either[E, B] {
return F.Bind2nd(MonadMapTo[E, A, B], b)
}
func MonadMapLeft[E, A, B any](fa Either[E, A], f func(E) B) Either[B, A] {
return MonadFold(fa, F.Flow2(f, Left[A, B]), Right[B, A])
func MonadMapLeft[E1, A, E2 any](fa Either[E1, A], f func(E1) E2) Either[E2, A] {
return MonadFold(fa, F.Flow2(f, Left[A, E2]), Right[E2, A])
}
func Map[E, A, B any](f func(a A) B) func(fa Either[E, A]) Either[E, B] {
return Chain(F.Flow2(f, Right[E, B]))
}
func MapLeft[E, A, B any](f func(E) B) func(fa Either[E, A]) Either[B, A] {
return F.Bind2nd(MonadMapLeft[E, A, B], f)
// MapLeft applies a mapping function to the error channel
func MapLeft[A, E1, E2 any](f func(E1) E2) func(fa Either[E1, A]) Either[E2, A] {
return F.Bind2nd(MonadMapLeft[E1, A, E2], f)
}
func MonadChain[E, A, B any](fa Either[E, A], f func(a A) Either[E, B]) Either[E, B] {

View File

@@ -27,34 +27,10 @@ func Ap[A any](fa A) func(Endomorphism[A]) A {
return G.Ap[Endomorphism[A]](fa)
}
func MonadFlap[A any](fab Endomorphism[A], a A) A {
return G.MonadFlap[Endomorphism[A]](fab, a)
}
func Flap[A any](a A) func(Endomorphism[A]) A {
return G.Flap[Endomorphism[A]](a)
}
func MonadMap[A any](fa A, f Endomorphism[A]) A {
return G.MonadMap[Endomorphism[A]](fa, f)
}
func Map[A any](f Endomorphism[A]) Endomorphism[A] {
return G.Map[Endomorphism[A]](f)
}
func MonadChain[A any](ma A, f Endomorphism[A]) A {
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return G.MonadChain[Endomorphism[A]](ma, f)
}
func Chain[A any](f Endomorphism[A]) Endomorphism[A] {
return G.Chain[Endomorphism[A], A](f)
}
func MonadChainFirst[A any](fa A, f Endomorphism[A]) A {
return G.MonadChainFirst[Endomorphism[A]](fa, f)
}
func ChainFirst[A any](f Endomorphism[A]) Endomorphism[A] {
return G.ChainFirst[Endomorphism[A]](f)
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] {
return G.Chain[Endomorphism[Endomorphism[A]], Endomorphism[A], A](f)
}

View File

@@ -16,6 +16,7 @@
package generic
import (
F "github.com/IBM/fp-go/function"
I "github.com/IBM/fp-go/identity/generic"
)
@@ -27,34 +28,10 @@ func Ap[GA ~func(A) A, A any](fa A) func(GA) A {
return I.Ap[GA, A, A](fa)
}
func MonadFlap[GA ~func(A) A, A any](fab GA, a A) A {
return I.MonadFlap[GA, A, A](fab, a)
func MonadChain[GA ~func(A) A, A any](ma GA, f GA) GA {
return Compose(ma, f)
}
func Flap[GA ~func(A) A, A any](a A) func(GA) A {
return I.Flap[GA, A, A](a)
}
func MonadMap[GA ~func(A) A, A any](fa A, f GA) A {
return I.MonadMap[GA, A, A](fa, f)
}
func Map[GA ~func(A) A, A any](f GA) GA {
return I.Map[GA, A, A](f)
}
func MonadChain[GA ~func(A) A, A any](ma A, f GA) A {
return I.MonadChain[GA, A, A](ma, f)
}
func Chain[GA ~func(A) A, A any](f GA) GA {
return I.Chain[GA, A](f)
}
func MonadChainFirst[GA ~func(A) A, A any](fa A, f GA) A {
return I.MonadChainFirst[GA, A, A](fa, f)
}
func ChainFirst[GA ~func(A) A, A any](f GA) GA {
return I.ChainFirst[GA, A, A](f)
func Chain[ENDO ~func(GA) GA, GA ~func(A) A, A any](f GA) ENDO {
return Of[ENDO](F.Bind2nd(Compose[GA], f))
}

View File

@@ -28,6 +28,16 @@ func Of[ENDO ~func(A) A, F ~func(A) A, A any](f F) ENDO {
}
}
// Wrap converts any function to an [Endomorphism]
func Wrap[ENDO ~func(A) A, F ~func(A) A, A any](f F) ENDO {
return Of[ENDO](f)
}
// Unwrap converts any [Endomorphism] to a normal function
func Unwrap[F ~func(A) A, ENDO ~func(A) A, A any](f ENDO) F {
return Of[F](f)
}
func Identity[ENDO ~func(A) A, A any]() ENDO {
return Of[ENDO](F.Identity[A])
}

View File

@@ -29,6 +29,16 @@ func Of[F ~func(A) A, A any](f F) Endomorphism[A] {
return G.Of[Endomorphism[A]](f)
}
// Wrap converts any function to an [Endomorphism]
func Wrap[F ~func(A) A, A any](f F) Endomorphism[A] {
return G.Wrap[Endomorphism[A]](f)
}
// Unwrap converts any [Endomorphism] to a function
func Unwrap[F ~func(A) A, A any](f Endomorphism[A]) F {
return G.Unwrap[F](f)
}
// Identity returns the identity [Endomorphism]
func Identity[A any]() Endomorphism[A] {
return G.Identity[Endomorphism[A]]()

View File

@@ -28,3 +28,14 @@ func Memoize[K comparable, T any](f func(K) T) func(K) T {
func ContramapMemoize[A any, K comparable, T any](kf func(A) K) func(func(A) T) func(A) T {
return G.ContramapMemoize[func(A) T](kf)
}
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
func CacheCallback[
A any, K comparable, T any](kf func(A) K, getOrCreate func(K, func() func() T) func() T) func(func(A) T) func(A) T {
return G.CacheCallback[func(func(A) T) func(A) T](kf, getOrCreate)
}
// SingleElementCache creates a cache function for use with the [CacheCallback] method that has a maximum capacity of one single item
func SingleElementCache[K comparable, T any]() func(K, func() func() T) func() T {
return G.SingleElementCache[func() func() T, K]()
}

View File

@@ -16,6 +16,8 @@
package function
import (
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
@@ -48,3 +50,21 @@ func TestCache(t *testing.T) {
assert.Equal(t, 10, cached(10))
assert.Equal(t, 2, count)
}
func TestSingleElementCache(t *testing.T) {
f := func(key string) string {
return fmt.Sprintf("%s: %d", key, rand.Int())
}
cb := CacheCallback(func(s string) string { return s }, SingleElementCache[string, string]())
cf := cb(f)
v1 := cf("1")
v2 := cf("1")
v3 := cf("2")
v4 := cf("1")
assert.Equal(t, v1, v2)
assert.NotEqual(t, v2, v3)
assert.NotEqual(t, v3, v4)
assert.NotEqual(t, v1, v4)
}

View File

@@ -28,7 +28,7 @@ func Memoize[F ~func(K) T, K comparable, T any](f F) F {
// ContramapMemoize converts a unary function into a unary function that caches the value depending on the parameter
func ContramapMemoize[F ~func(A) T, KF func(A) K, A any, K comparable, T any](kf KF) func(F) F {
return CacheCallback[F](kf, getOrCreate[K, T]())
return CacheCallback[func(F) F, func() func() T](kf, getOrCreate[K, T]())
}
// getOrCreate is a naive implementation of a cache, without bounds
@@ -50,13 +50,51 @@ func getOrCreate[K comparable, T any]() func(K, func() func() T) func() T {
}
}
// SingleElementCache is a cache with a capacity of a single element
func SingleElementCache[
LLT ~func() LT, // generator of the generator
K comparable, // key into the cache
LT ~func() T, // generator of a value
T any, // the cached data type
]() func(K, LLT) LT {
var l sync.Mutex
var key K
var value LT
hasKey := false
return func(k K, gen LLT) LT {
l.Lock()
existing := value
if !hasKey || key != k {
existing = gen()
// update state
key = k
value = existing
hasKey = true
}
l.Unlock()
return existing
}
}
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
func CacheCallback[F ~func(A) T, KF func(A) K, C ~func(K, func() func() T) func() T, A any, K comparable, T any](kf KF, getOrCreate C) func(F) F {
func CacheCallback[
EM ~func(F) F, // endomorphism of the function
LLT ~func() LT, // generator of the generator
LT ~func() T, // generator of a value
F ~func(A) T, // function to actually cache
KF func(A) K, // extracts the cache key from the input
C ~func(K, LLT) LT, // the cache callback function
A any, K comparable, T any](kf KF, getOrCreate C) EM {
return func(f F) F {
return func(a A) T {
// cache entry
return getOrCreate(kf(a), func() func() T {
return L.Memoize[func() T](func() T {
return getOrCreate(kf(a), func() LT {
return L.Memoize[LT](func() T {
return f(a)
})
})()

4
go.mod
View File

@@ -4,7 +4,7 @@ go 1.20
require (
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.26.0
github.com/urfave/cli/v2 v2.27.1
)
require (
@@ -12,6 +12,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum
View File

@@ -8,12 +8,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

311
http/builder/builder.go Normal file
View File

@@ -0,0 +1,311 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package builder
import (
"net/http"
"net/url"
E "github.com/IBM/fp-go/either"
ENDO "github.com/IBM/fp-go/endomorphism"
F "github.com/IBM/fp-go/function"
C "github.com/IBM/fp-go/http/content"
FM "github.com/IBM/fp-go/http/form"
H "github.com/IBM/fp-go/http/headers"
J "github.com/IBM/fp-go/json"
LZ "github.com/IBM/fp-go/lazy"
L "github.com/IBM/fp-go/optics/lens"
O "github.com/IBM/fp-go/option"
S "github.com/IBM/fp-go/string"
T "github.com/IBM/fp-go/tuple"
)
type (
Builder struct {
method O.Option[string]
url string
headers http.Header
body O.Option[E.Either[error, []byte]]
query url.Values
}
// Endomorphism returns an [ENDO.Endomorphism] that transforms a builder
Endomorphism = ENDO.Endomorphism[*Builder]
)
var (
// Default is the default builder
Default = &Builder{method: O.Some(defaultMethod()), headers: make(http.Header), body: noBody}
defaultMethod = F.Constant(http.MethodGet)
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[*Builder]()
// Url is a [L.Lens] for the URL
Url = L.MakeLensRef((*Builder).GetUrl, (*Builder).SetUrl)
// Method is a [L.Lens] for the HTTP method
Method = L.MakeLensRef((*Builder).GetMethod, (*Builder).SetMethod)
// Body is a [L.Lens] for the request body
Body = L.MakeLensRef((*Builder).GetBody, (*Builder).SetBody)
// Headers is a [L.Lens] for the complete set of request headers
Headers = L.MakeLensRef((*Builder).GetHeaders, (*Builder).SetHeaders)
// Query is a [L.Lens] for the set of query parameters
Query = L.MakeLensRef((*Builder).GetQuery, (*Builder).SetQuery)
rawQuery = L.MakeLensRef(getRawQuery, setRawQuery)
getHeader = F.Bind2of2((*Builder).GetHeader)
delHeader = F.Bind2of2((*Builder).DelHeader)
setHeader = F.Bind2of3((*Builder).SetHeader)
noHeader = O.None[string]()
noBody = O.None[E.Either[error, []byte]]()
noQueryArg = O.None[string]()
parseUrl = E.Eitherize1(url.Parse)
parseQuery = E.Eitherize1(url.ParseQuery)
// WithQuery creates a [Endomorphism] for a complete set of query parameters
WithQuery = Query.Set
// WithMethod creates a [Endomorphism] for a certain method
WithMethod = Method.Set
// WithUrl creates a [Endomorphism] for a certain method
WithUrl = Url.Set
// WithHeaders creates a [Endomorphism] for a set of headers
WithHeaders = Headers.Set
// WithBody creates a [Endomorphism] for a request body
WithBody = F.Flow2(
O.Of[E.Either[error, []byte]],
Body.Set,
)
// WithBytes creates a [Endomorphism] for a request body using bytes
WithBytes = F.Flow2(
E.Of[error, []byte],
WithBody,
)
// WithContentType adds the [H.ContentType] header
WithContentType = WithHeader(H.ContentType)
// WithAuthorization adds the [H.Authorization] header
WithAuthorization = WithHeader(H.Authorization)
// WithGet adds the [http.MethodGet] method
WithGet = WithMethod(http.MethodGet)
// WithPost adds the [http.MethodPost] method
WithPost = WithMethod(http.MethodPost)
// WithPut adds the [http.MethodPut] method
WithPut = WithMethod(http.MethodPut)
// WithDelete adds the [http.MethodDelete] method
WithDelete = WithMethod(http.MethodDelete)
// WithBearer creates a [Endomorphism] to add a Bearer [H.Authorization] header
WithBearer = F.Flow2(
S.Format[string]("Bearer %s"),
WithAuthorization,
)
// WithoutBody creates a [Endomorphism] to remove the body
WithoutBody = F.Pipe1(
noBody,
Body.Set,
)
// WithFormData creates a [Endomorphism] to send form data payload
WithFormData = F.Flow4(
url.Values.Encode,
S.ToBytes,
WithBytes,
ENDO.Chain(WithContentType(C.FormEncoded)),
)
)
func setRawQuery(u *url.URL, raw string) *url.URL {
u.RawQuery = raw
return u
}
func getRawQuery(u *url.URL) string {
return u.RawQuery
}
func (builder *Builder) clone() *Builder {
cpy := *builder
cpy.headers = cpy.headers.Clone()
return &cpy
}
// GetTargetUrl constructs a full URL with query parameters on top of the provided URL string
func (builder *Builder) GetTargetUrl() E.Either[error, string] {
// construct the final URL
return F.Pipe3(
builder,
Url.Get,
parseUrl,
E.Chain(F.Flow4(
T.Replicate2[*url.URL],
T.Map2(
F.Flow2(
F.Curry2(setRawQuery),
E.Of[error, func(string) *url.URL],
),
F.Flow3(
rawQuery.Get,
parseQuery,
E.Map[error](F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode,
)),
),
),
T.Tupled2(E.MonadAp[*url.URL, error, string]),
E.Map[error]((*url.URL).String),
)),
)
}
func (builder *Builder) GetUrl() string {
return builder.url
}
func (builder *Builder) GetMethod() string {
return F.Pipe1(
builder.method,
O.GetOrElse(defaultMethod),
)
}
func (builder *Builder) GetHeaders() http.Header {
return builder.headers
}
func (builder *Builder) GetQuery() url.Values {
return builder.query
}
func (builder *Builder) SetQuery(query url.Values) *Builder {
builder.query = query
return builder
}
func (builder *Builder) GetBody() O.Option[E.Either[error, []byte]] {
return builder.body
}
func (builder *Builder) SetMethod(method string) *Builder {
builder.method = O.Some(method)
return builder
}
func (builder *Builder) SetUrl(url string) *Builder {
builder.url = url
return builder
}
func (builder *Builder) SetHeaders(headers http.Header) *Builder {
builder.headers = headers
return builder
}
func (builder *Builder) SetBody(body O.Option[E.Either[error, []byte]]) *Builder {
builder.body = body
return builder
}
func (builder *Builder) SetHeader(name, value string) *Builder {
builder.headers.Set(name, value)
return builder
}
func (builder *Builder) DelHeader(name string) *Builder {
builder.headers.Del(name)
return builder
}
func (builder *Builder) GetHeader(name string) O.Option[string] {
return F.Pipe2(
name,
builder.headers.Get,
O.FromPredicate(S.IsNonEmpty),
)
}
func (builder *Builder) GetHeaderValues(name string) []string {
return builder.headers.Values(name)
}
// Header returns a [L.Lens] for a single header
func Header(name string) L.Lens[*Builder, O.Option[string]] {
get := getHeader(name)
set := F.Bind1of2(setHeader(name))
del := F.Flow2(
LZ.Of[*Builder],
LZ.Map(delHeader(name)),
)
return L.MakeLens(get, func(b *Builder, value O.Option[string]) *Builder {
cpy := b.clone()
return F.Pipe1(
value,
O.Fold(del(cpy), set(cpy)),
)
})
}
// WithHeader creates a [Endomorphism] for a certain header
func WithHeader(name string) func(value string) Endomorphism {
return F.Flow2(
O.Of[string],
Header(name).Set,
)
}
// WithoutHeader creates a [Endomorphism] to remove a certain header
func WithoutHeader(name string) Endomorphism {
return Header(name).Set(noHeader)
}
// WithJson creates a [Endomorphism] to send JSON payload
func WithJson[T any](data T) Endomorphism {
return Monoid.Concat(
F.Pipe2(
data,
J.Marshal[T],
WithBody,
),
WithContentType(C.Json),
)
}
// QueryArg is a [L.Lens] for the first value of a query argument
func QueryArg(name string) L.Lens[*Builder, O.Option[string]] {
return F.Pipe1(
Query,
L.Compose[*Builder](FM.AtValue(name)),
)
}
// WithQueryArg creates a [Endomorphism] for a certain query argument
func WithQueryArg(name string) func(value string) Endomorphism {
return F.Flow2(
O.Of[string],
QueryArg(name).Set,
)
}
// WithoutQueryArg creates a [Endomorphism] that removes a query argument
func WithoutQueryArg(name string) Endomorphism {
return QueryArg(name).Set(noQueryArg)
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package builder
import (
"testing"
F "github.com/IBM/fp-go/function"
C "github.com/IBM/fp-go/http/content"
FD "github.com/IBM/fp-go/http/form"
H "github.com/IBM/fp-go/http/headers"
O "github.com/IBM/fp-go/option"
"github.com/stretchr/testify/assert"
)
func TestBuilder(t *testing.T) {
name := H.ContentType
withContentType := WithHeader(name)
withoutContentType := WithoutHeader(name)
b1 := F.Pipe1(
Default,
withContentType(C.Json),
)
b2 := F.Pipe1(
b1,
withContentType(C.TextPlain),
)
b3 := F.Pipe1(
b2,
withoutContentType,
)
assert.Equal(t, O.None[string](), Default.GetHeader(name))
assert.Equal(t, O.Of(C.Json), b1.GetHeader(name))
assert.Equal(t, O.Of(C.TextPlain), b2.GetHeader(name))
assert.Equal(t, O.None[string](), b3.GetHeader(name))
}
func TestWithFormData(t *testing.T) {
data := F.Pipe1(
FD.Default,
FD.WithValue("a")("b"),
)
res := F.Pipe1(
Default,
WithFormData(data),
)
assert.Equal(t, C.FormEncoded, Headers.Get(res).Get(H.ContentType))
}

22
http/content/content.go Normal file
View File

@@ -0,0 +1,22 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package content
const (
TextPlain = "text/plain"
Json = "application/json"
FormEncoded = "application/x-www-form-urlencoded"
)

View File

@@ -25,6 +25,7 @@ import (
LA "github.com/IBM/fp-go/optics/lens/array"
LRG "github.com/IBM/fp-go/optics/lens/record/generic"
O "github.com/IBM/fp-go/option"
RG "github.com/IBM/fp-go/record/generic"
)
type (
@@ -38,9 +39,12 @@ var (
noField = O.None[string]()
// FormMonoid is the [M.Monoid] for the [Endomorphism]
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[url.Values]()
// ValuesMonoid is a [M.Monoid] to concatenate [url.Values] maps
ValuesMonoid = RG.UnionMonoid[url.Values](A.Semigroup[string]())
// AtValues is a [L.Lens] that focusses on the values of a form field
AtValues = LRG.AtRecord[url.Values, []string]

58
http/headers/headers.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package headers
import (
"net/http"
"net/textproto"
A "github.com/IBM/fp-go/array"
F "github.com/IBM/fp-go/function"
L "github.com/IBM/fp-go/optics/lens"
LA "github.com/IBM/fp-go/optics/lens/array"
LRG "github.com/IBM/fp-go/optics/lens/record/generic"
RG "github.com/IBM/fp-go/record/generic"
)
// HTTP headers
const (
Accept = "Accept"
Authorization = "Authorization"
ContentType = "Content-Type"
ContentLength = "Content-Length"
)
var (
// Monoid is a [M.Monoid] to concatenate [http.Header] maps
Monoid = RG.UnionMonoid[http.Header](A.Semigroup[string]())
// AtValues is a [L.Lens] that focusses on the values of a header
AtValues = F.Flow2(
textproto.CanonicalMIMEHeaderKey,
LRG.AtRecord[http.Header, []string],
)
composeHead = F.Pipe1(
LA.AtHead[string](),
L.ComposeOptions[http.Header, string](A.Empty[string]()),
)
// AtValue is a [L.Lens] that focusses on first value of a header
AtValue = F.Flow2(
AtValues,
composeHead,
)
)

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package headers
import (
"net/http"
"testing"
A "github.com/IBM/fp-go/array"
"github.com/IBM/fp-go/eq"
LT "github.com/IBM/fp-go/optics/lens/testing"
O "github.com/IBM/fp-go/option"
RG "github.com/IBM/fp-go/record/generic"
S "github.com/IBM/fp-go/string"
"github.com/stretchr/testify/assert"
)
var (
sEq = eq.FromEquals(S.Eq)
valuesEq = RG.Eq[http.Header](A.Eq(sEq))
)
func TestLaws(t *testing.T) {
name := "Content-Type"
fieldLaws := LT.AssertLaws[http.Header, O.Option[string]](t, O.Eq(sEq), valuesEq)(AtValue(name))
n := O.None[string]()
s1 := O.Some("s1")
def := make(http.Header)
v1 := make(http.Header)
v1.Set(name, "v1")
v2 := make(http.Header)
v2.Set("Other-Header", "v2")
assert.True(t, fieldLaws(def, n))
assert.True(t, fieldLaws(v1, n))
assert.True(t, fieldLaws(v2, n))
assert.True(t, fieldLaws(def, s1))
assert.True(t, fieldLaws(v1, s1))
assert.True(t, fieldLaws(v2, s1))
}

View File

@@ -20,6 +20,7 @@ import (
"io"
"mime"
H "net/http"
"net/url"
"regexp"
A "github.com/IBM/fp-go/array"
@@ -33,6 +34,13 @@ import (
type (
ParsedMediaType = T.Tuple2[string, map[string]string]
HttpError struct {
statusCode int
headers H.Header
body []byte
url *url.URL
}
)
var (
@@ -72,6 +80,31 @@ func ParseMediaType(mediaType string) E.Either[error, ParsedMediaType] {
return E.TryCatchError(T.MakeTuple2(m, p), err)
}
// Error fulfills the error interface
func (r *HttpError) Error() string {
return fmt.Sprintf("invalid status code [%d] when accessing URL [%s]", r.statusCode, r.url)
}
func (r *HttpError) String() string {
return r.Error()
}
func (r *HttpError) StatusCode() int {
return r.statusCode
}
func (r *HttpError) Headers() H.Header {
return r.headers
}
func (r *HttpError) URL() *url.URL {
return r.url
}
func (r *HttpError) Body() []byte {
return r.body
}
func GetHeader(resp *H.Response) H.Header {
return resp.Header
}
@@ -84,6 +117,13 @@ func isValidStatus(resp *H.Response) bool {
return resp.StatusCode >= H.StatusOK && resp.StatusCode < H.StatusMultipleChoices
}
// StatusCodeError creates an instance of [HttpError] filled with information from the response
func StatusCodeError(resp *H.Response) error {
return fmt.Errorf("invalid status code [%d] when accessing URL [%s]", resp.StatusCode, resp.Request.URL)
// read the body
bodyRdr := GetBody(resp)
defer bodyRdr.Close()
// try to access body content
body, _ := io.ReadAll(bodyRdr)
// return an error with comprehensive information
return &HttpError{statusCode: resp.StatusCode, headers: GetHeader(resp).Clone(), body: body, url: resp.Request.URL}
}

View File

@@ -20,6 +20,7 @@ import (
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
C "github.com/IBM/fp-go/http/content"
"github.com/stretchr/testify/assert"
)
@@ -38,7 +39,7 @@ func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
func TestValidateJsonContentTypeString(t *testing.T) {
res := F.Pipe1(
validateJsonContentTypeString("application/json"),
validateJsonContentTypeString(C.Json),
NoError[ParsedMediaType](t),
)

28
internal/array/prepend.go Normal file
View File

@@ -0,0 +1,28 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
// Prepend prepends a single value to an array
func Prepend[ENDO ~func(AS) AS, AS ~[]A, A any](head A) ENDO {
return func(as AS) AS {
l := len(as)
cpy := make(AS, l+1)
copy(cpy[1:], as)
cpy[0] = head
return cpy
}
}

View File

@@ -133,6 +133,29 @@ func Delay[GA ~func() A, A any](delay time.Duration) func(GA) GA {
}
}
func after(timestamp time.Time) func() {
return func() {
// check if we need to wait
current := time.Now()
if current.Before(timestamp) {
time.Sleep(timestamp.Sub(current))
}
}
}
// After creates an operation that passes after the given timestamp
func After[GA ~func() A, A any](timestamp time.Time) func(GA) GA {
aft := after(timestamp)
return func(ga GA) GA {
return MakeIO[GA](func() A {
// wait as long as necessary
aft()
// execute after wait
return ga()
})
}
}
// Now returns the current timestamp
func Now[GA ~func() time.Time]() GA {
return MakeIO[GA](time.Now)

View File

@@ -146,3 +146,13 @@ func MonadFlap[B, A any](fab IO[func(A) B], a A) IO[B] {
func Flap[FAB ~func(A) B, GFAB ~func() FAB, GB ~func() B, A, B any](a A) func(IO[func(A) B]) IO[B] {
return G.Flap[func(A) B, IO[func(A) B], IO[B], A, B](a)
}
// Delay creates an operation that passes in the value after some delay
func Delay[A any](delay time.Duration) func(IO[A]) IO[A] {
return G.Delay[IO[A]](delay)
}
// After creates an operation that passes after the given timestamp
func After[A any](timestamp time.Time) func(IO[A]) IO[A] {
return G.After[IO[A]](timestamp)
}

View File

@@ -207,11 +207,16 @@ func MapLeft[GA1 ~func() ET.Either[E1, A], GA2 ~func() ET.Either[E2, A], E1, E2,
return F.Bind2nd(MonadMapLeft[GA1, GA2, E1, E2, A], f)
}
// Delay creates an operation that passes in the value after some delay
// Delay creates an operation that passes in the value after some [time.Duration]
func Delay[GA ~func() ET.Either[E, A], E, A any](delay time.Duration) func(GA) GA {
return IO.Delay[GA](delay)
}
// After creates an operation that passes after the given [time.Time]
func After[GA ~func() ET.Either[E, A], E, A any](timestamp time.Time) func(GA) GA {
return IO.After[GA](timestamp)
}
func MonadBiMap[GA ~func() ET.Either[E1, A], GB ~func() ET.Either[E2, B], E1, E2, A, B any](fa GA, f func(E1) E2, g func(A) B) GB {
return eithert.MonadBiMap(IO.MonadMap[GA, GB, ET.Either[E1, A], ET.Either[E2, B]], fa, f, g)
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
"encoding/json"
"log"
B "github.com/IBM/fp-go/bytes"
ET "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
)
// LogJson converts the argument to JSON and then logs it via the format string
// Can be used with [ChainFirst]
func LogJson[GA ~func() ET.Either[error, any], A any](prefix string) func(A) GA {
return func(a A) GA {
// log this
return F.Pipe3(
ET.TryCatchError(json.MarshalIndent(a, "", " ")),
ET.Map[error](B.ToString),
FromEither[func() ET.Either[error, string]],
Chain[func() ET.Either[error, string], GA](func(data string) GA {
return FromImpure[GA](func() {
log.Printf(prefix, data)
})
}),
)
}
}

View File

@@ -18,238 +18,50 @@ package builder
import (
"bytes"
"net/http"
"net/url"
"strconv"
ENDO "github.com/IBM/fp-go/endomorphism"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
R "github.com/IBM/fp-go/http/builder"
H "github.com/IBM/fp-go/http/headers"
IOE "github.com/IBM/fp-go/ioeither"
IOEH "github.com/IBM/fp-go/ioeither/http"
J "github.com/IBM/fp-go/json"
LZ "github.com/IBM/fp-go/lazy"
L "github.com/IBM/fp-go/optics/lens"
O "github.com/IBM/fp-go/option"
S "github.com/IBM/fp-go/string"
)
type (
Builder struct {
method O.Option[string]
url string
headers http.Header
body O.Option[IOE.IOEither[error, []byte]]
}
func Requester(builder *R.Builder) IOEH.Requester {
// Endomorphism returns an [ENDO.Endomorphism] that transforms a builder
Endomorphism = ENDO.Endomorphism[*Builder]
)
withBody := F.Curry3(func(data []byte, url string, method string) IOE.IOEither[error, *http.Request] {
return IOE.TryCatchError(func() (*http.Request, error) {
req, err := http.NewRequest(method, url, bytes.NewReader(data))
if err == nil {
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
})
})
var (
// Default is the default builder
Default = &Builder{method: O.Some(defaultMethod()), headers: make(http.Header), body: noBody}
withoutBody := F.Curry2(func(url string, method string) IOE.IOEither[error, *http.Request] {
return IOE.TryCatchError(func() (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
if err == nil {
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
})
})
defaultMethod = F.Constant(http.MethodGet)
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[*Builder]()
// Url is a [L.Lens] for the URL
Url = L.MakeLensRef((*Builder).GetUrl, (*Builder).SetUrl)
// Method is a [L.Lens] for the HTTP method
Method = L.MakeLensRef((*Builder).GetMethod, (*Builder).SetMethod)
// Body is a [L.Lens] for the request body
Body = L.MakeLensRef((*Builder).GetBody, (*Builder).SetBody)
// Headers is a [L.Lens] for the complete set of request headers
Headers = L.MakeLensRef((*Builder).GetHeaders, (*Builder).SetHeaders)
getHeader = F.Bind2of2((*Builder).GetHeader)
delHeader = F.Bind2of2((*Builder).DelHeader)
setHeader = F.Bind2of3((*Builder).SetHeader)
noHeader = O.None[string]()
noBody = O.None[IOE.IOEither[error, []byte]]()
// WithMethod creates a [BuilderBuilder] for a certain method
WithMethod = Method.Set
// WithUrl creates a [BuilderBuilder] for a certain method
WithUrl = Url.Set
// WithHeaders creates a [BuilderBuilder] for a set of headers
WithHeaders = Headers.Set
// WithBody creates a [BuilderBuilder] for a request body
WithBody = F.Flow2(
O.Of[IOE.IOEither[error, []byte]],
Body.Set,
)
// WithContentType adds the content type header
WithContentType = WithHeader("Content-Type")
// WithGet adds the [http.MethodGet] method
WithGet = WithMethod(http.MethodGet)
// WithPost adds the [http.MethodPost] method
WithPost = WithMethod(http.MethodPost)
// WithPut adds the [http.MethodPut] method
WithPut = WithMethod(http.MethodPut)
// WithDelete adds the [http.MethodDelete] method
WithDelete = WithMethod(http.MethodDelete)
// Requester creates a requester from a builder
Requester = (*Builder).Requester
// WithoutBody creates a [BuilderBuilder] to remove the body
WithoutBody = F.Pipe1(
noBody,
Body.Set,
)
)
func (builder *Builder) clone() *Builder {
cpy := *builder
cpy.headers = cpy.headers.Clone()
return &cpy
}
func (builder *Builder) GetUrl() string {
return builder.url
}
func (builder *Builder) GetMethod() string {
return F.Pipe1(
builder.method,
O.GetOrElse(defaultMethod),
)
}
func (builder *Builder) GetHeaders() http.Header {
return builder.headers
}
func (builder *Builder) GetBody() O.Option[IOE.IOEither[error, []byte]] {
return builder.body
}
func (builder *Builder) SetMethod(method string) *Builder {
builder.method = O.Some(method)
return builder
}
func (builder *Builder) SetUrl(url string) *Builder {
builder.url = url
return builder
}
func (builder *Builder) SetHeaders(headers http.Header) *Builder {
builder.headers = headers
return builder
}
func (builder *Builder) SetBody(body O.Option[IOE.IOEither[error, []byte]]) *Builder {
builder.body = body
return builder
}
func (builder *Builder) SetHeader(name, value string) *Builder {
builder.headers.Set(name, value)
return builder
}
func (builder *Builder) DelHeader(name string) *Builder {
builder.headers.Del(name)
return builder
}
func (builder *Builder) GetHeader(name string) O.Option[string] {
return F.Pipe2(
name,
builder.headers.Get,
O.FromPredicate(S.IsNonEmpty),
)
}
func (builder *Builder) GetHeaderValues(name string) []string {
return builder.headers.Values(name)
}
func (builder *Builder) AddHeaderHeader(name, value string) *Builder {
builder.headers.Add(name, value)
return builder
}
func (builder *Builder) Requester() IOEH.Requester {
return F.Pipe3(
return F.Pipe5(
builder.GetBody(),
O.Map(IOE.Map[error](bytes.NewReader)),
O.GetOrElse(F.Constant(IOE.Of[error, *bytes.Reader](nil))),
IOE.Chain(func(rdr *bytes.Reader) IOE.IOEither[error, *http.Request] {
return IOE.TryCatchError(func() (*http.Request, error) {
req, err := http.NewRequest(builder.GetMethod(), builder.GetUrl(), rdr)
if err == nil {
for name, value := range builder.GetHeaders() {
req.Header[name] = value
}
if rdr != nil {
req.Header.Set("Content-Length", strconv.FormatInt(rdr.Size(), 10))
}
}
return req, err
})
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
E.Ap[func(string) IOE.IOEither[error, *http.Request]](builder.GetTargetUrl()),
E.Flap[error, IOE.IOEither[error, *http.Request]](builder.GetMethod()),
E.GetOrElse(IOE.Left[*http.Request, error]),
IOE.Map[error](func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req
}),
)
}
// Header returns a [L.Lens] for a single header
func Header(name string) L.Lens[*Builder, O.Option[string]] {
get := getHeader(name)
set := F.Bind1of2(setHeader(name))
del := F.Flow2(
LZ.Of[*Builder],
LZ.Map(delHeader(name)),
)
return L.MakeLens(get, func(b *Builder, value O.Option[string]) *Builder {
cpy := b.clone()
return F.Pipe1(
value,
O.Fold(del(cpy), set(cpy)),
)
})
}
// WithHeader creates a [BuilderBuilder] for a certain header
func WithHeader(name string) func(value string) Endomorphism {
return F.Flow2(
O.Of[string],
Header(name).Set,
)
}
// WithoutHeader creates a [BuilderBuilder] to remove a certain header
func WithoutHeader(name string) Endomorphism {
return Header(name).Set(noHeader)
}
// WithFormData creates a [BuilderBuilder] to send form data payload
func WithFormData(value url.Values) Endomorphism {
return F.Flow2(
F.Pipe4(
value,
url.Values.Encode,
S.ToBytes,
IOE.Of[error, []byte],
WithBody,
),
WithContentType("application/x-www-form-urlencoded"),
)
}
// WithJson creates a [BuilderBuilder] to send JSON payload
func WithJson[T any](data T) Endomorphism {
return F.Flow2(
F.Pipe3(
data,
J.Marshal[T],
IOE.FromEither[error, []byte],
WithBody,
),
WithContentType("application/json"),
)
}

View File

@@ -16,36 +16,43 @@
package builder
import (
"net/http"
"net/url"
"testing"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
R "github.com/IBM/fp-go/http/builder"
IO "github.com/IBM/fp-go/io"
IOE "github.com/IBM/fp-go/ioeither"
"github.com/stretchr/testify/assert"
)
func TestBuiler(t *testing.T) {
func TestBuilderWithQuery(t *testing.T) {
// add some query
withLimit := R.WithQueryArg("limit")("10")
withUrl := R.WithUrl("http://www.example.org?a=b")
name := "Content-type"
withContentType := WithHeader(name)
withoutContentType := WithoutHeader(name)
b1 := F.Pipe1(
Default,
withContentType("application/json"),
b := F.Pipe2(
R.Default,
withLimit,
withUrl,
)
b2 := F.Pipe1(
b1,
withContentType("text/plain"),
req := F.Pipe3(
b,
Requester,
IOE.Map[error](func(r *http.Request) *url.URL {
return r.URL
}),
IOE.ChainFirstIOK[error](func(u *url.URL) IO.IO[any] {
return IO.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))
assert.Equal(t, "b", q.Get("a"))
})
}),
)
b3 := F.Pipe1(
b2,
withoutContentType,
)
assert.Equal(t, O.None[string](), Default.GetHeader(name))
assert.Equal(t, O.Of("application/json"), b1.GetHeader(name))
assert.Equal(t, O.Of("text/plain"), b2.GetHeader(name))
assert.Equal(t, O.None[string](), b3.GetHeader(name))
assert.True(t, E.IsRight(req()))
}

View File

@@ -13,20 +13,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package http
package di
import (
"net/http"
DI "github.com/IBM/fp-go/di"
IOE "github.com/IBM/fp-go/ioeither"
L "github.com/IBM/fp-go/lazy"
IOEH "github.com/IBM/fp-go/ioeither/http"
)
var (
// InjHttpClient is the injection token for the default http client
InjHttpClient = DI.MakeTokenWithDefault0("HTTP_CLIENT", L.Of(IOE.Of[error](http.DefaultClient)))
// InjHttpClient is the [DI.InjectionToken] for the [http.DefaultClient]
InjHttpClient = DI.MakeTokenWithDefault0("HTTP_CLIENT", IOE.Of[error](http.DefaultClient))
// InjClient is the injection token for the default [Client]
InjClient = DI.MakeTokenWithDefault1("CLIENT", InjHttpClient.IOEither(), IOE.Map[error](MakeClient))
// InjClient is the [DI.InjectionToken] for the default [IOEH.Client]
InjClient = DI.MakeTokenWithDefault1("CLIENT", InjHttpClient.IOEither(), IOE.Map[error](IOEH.MakeClient))
)

View File

@@ -16,6 +16,8 @@
package ioeither
import (
"time"
ET "github.com/IBM/fp-go/either"
I "github.com/IBM/fp-go/io"
G "github.com/IBM/fp-go/ioeither/generic"
@@ -173,7 +175,7 @@ func MonadMapLeft[E1, E2, A any](fa IOEither[E1, A], f func(E1) E2) IOEither[E2,
return G.MonadMapLeft[IOEither[E1, A], IOEither[E2, A]](fa, f)
}
func MapLeft[E1, E2, A any](f func(E1) E2) func(IOEither[E1, A]) IOEither[E2, A] {
func MapLeft[A, E1, E2 any](f func(E1) E2) func(IOEither[E1, A]) IOEither[E2, A] {
return G.MapLeft[IOEither[E1, A], IOEither[E2, A]](f)
}
@@ -276,3 +278,13 @@ func Flap[E, B, A any](a A) func(IOEither[E, func(A) B]) IOEither[E, B] {
func ToIOOption[E, A any](ioe IOEither[E, A]) IOO.IOOption[A] {
return G.ToIOOption[IOO.IOOption[A]](ioe)
}
// Delay creates an operation that passes in the value after some delay
func Delay[E, A any](delay time.Duration) func(IOEither[E, A]) IOEither[E, A] {
return G.Delay[IOEither[E, A]](delay)
}
// After creates an operation that passes after the given [time.Time]
func After[E, A any](timestamp time.Time) func(IOEither[E, A]) IOEither[E, A] {
return G.After[IOEither[E, A]](timestamp)
}

26
ioeither/logging.go Normal file
View File

@@ -0,0 +1,26 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioeither
import (
G "github.com/IBM/fp-go/ioeither/generic"
)
// LogJson converts the argument to pretty printed JSON and then logs it via the format string
// Can be used with [ChainFirst]
func LogJson[A any](prefix string) func(A) IOEither[error, any] {
return G.LogJson[IOEither[error, any], A](prefix)
}

42
ioeither/logging_test.go Normal file
View File

@@ -0,0 +1,42 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioeither
import (
"testing"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
"github.com/stretchr/testify/assert"
)
func TestLogging(t *testing.T) {
type SomeData struct {
Key string `json:"key"`
Value string `json:"value"`
}
src := &SomeData{Key: "key", Value: "value"}
res := F.Pipe1(
Of[error](src),
ChainFirst(LogJson[*SomeData]("Data: \n%s")),
)
dst := res()
assert.Equal(t, E.Of[error](src), dst)
}

View File

@@ -219,6 +219,11 @@ func Delay[GA ~func() O.Option[A], A any](delay time.Duration) func(GA) GA {
return IO.Delay[GA](delay)
}
// After creates an operation that passes after the given [time.Time]
func After[GA ~func() O.Option[A], A any](timestamp time.Time) func(GA) GA {
return IO.After[GA](timestamp)
}
// Fold convers an IOOption into an IO
func Fold[GA ~func() O.Option[A], GB ~func() B, A, B any](onNone func() GB, onSome func(A) GB) func(GA) GB {
return optiont.MatchE(IO.MonadChain[GA, GB, O.Option[A], B], onNone, onSome)

View File

@@ -16,6 +16,8 @@
package iooption
import (
"time"
ET "github.com/IBM/fp-go/either"
I "github.com/IBM/fp-go/io"
IO "github.com/IBM/fp-go/io"
@@ -164,3 +166,13 @@ func MonadChainFirstIOK[A, B any](first IOOption[A], f func(A) IO.IO[B]) IOOptio
func ChainFirstIOK[A, B any](f func(A) IO.IO[B]) func(IOOption[A]) IOOption[A] {
return G.ChainFirstIOK[IOOption[A], IO.IO[B]](f)
}
// Delay creates an operation that passes in the value after some delay
func Delay[A any](delay time.Duration) func(IOOption[A]) IOOption[A] {
return G.Delay[IOOption[A]](delay)
}
// After creates an operation that passes after the given [time.Time]
func After[A any](timestamp time.Time) func(IOOption[A]) IOOption[A] {
return G.After[IOOption[A]](timestamp)
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
F "github.com/IBM/fp-go/function"
L "github.com/IBM/fp-go/io/generic"
O "github.com/IBM/fp-go/option"
T "github.com/IBM/fp-go/tuple"
)
// FromLazy returns an iterator on top of a lazy function
func FromLazy[GU ~func() O.Option[T.Tuple2[GU, U]], LZ ~func() U, U any](l LZ) GU {
return F.Pipe1(
l,
L.Map[LZ, GU](F.Flow2(
F.Bind1st(T.MakeTuple2[GU, U], Empty[GU]()),
O.Of[T.Tuple2[GU, U]],
)),
)
}

32
iterator/stateless/io.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stateless
import (
IO "github.com/IBM/fp-go/io"
G "github.com/IBM/fp-go/iterator/stateless/generic"
L "github.com/IBM/fp-go/lazy"
)
// FromLazy returns an [Iterator] on top of a lazy function
func FromLazy[U any](l L.Lazy[U]) Iterator[U] {
return G.FromLazy[Iterator[U], L.Lazy[U]](l)
}
// FromIO returns an [Iterator] on top of an IO function
func FromIO[U any](io IO.IO[U]) Iterator[U] {
return G.FromLazy[Iterator[U], IO.IO[U]](io)
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stateless
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIteratorFromLazy(t *testing.T) {
num := rand.Int
cit := FromLazy(num)
// create arrays twice
c1 := ToArray(cit)
c2 := ToArray(cit)
assert.Equal(t, 1, len(c1))
assert.Equal(t, 1, len(c2))
assert.NotEqual(t, c1, c2)
}

View File

@@ -15,16 +15,12 @@
package lambda
type (
// RecFct is the function called recursively
RecFct[T, R any] func(T) R
internalCombinator[T, R any] func(internalCombinator[T, R]) RecFct[T, R]
)
// Y is the Y-combinator based on https://dreamsongs.com/Files/WhyOfY.pdf
func Y[TRFRM ~func(RecFct[T, R]) RecFct[T, R], T, R any](f TRFRM) RecFct[T, R] {
g := func(h internalCombinator[T, R]) RecFct[T, R] {
func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct {
type internal[RecFct ~func(T) R, T, R any] func(internal[RecFct, T, R]) RecFct
g := func(h internal[RecFct, T, R]) RecFct {
return func(t T) R {
return f(h(h))(t)
}

View File

@@ -16,12 +16,13 @@
package lambda
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFactorial(t *testing.T) {
fct := Y(func(r RecFct[int, int]) RecFct[int, int] {
fct := Y(func(r func(int) int) func(int) int {
return func(n int) int {
if n <= 0 {
return 1
@@ -29,6 +30,5 @@ func TestFactorial(t *testing.T) {
return n * r(n-1)
}
})
fmt.Println(fct(10))
assert.Equal(t, 3628800, fct(10))
}

View File

@@ -164,3 +164,12 @@ func MonadFlap[GEFAB ~func(E) ET.Either[L, func(A) B], GEB ~func(E) ET.Either[L,
func Flap[GEFAB ~func(E) ET.Either[L, func(A) B], GEB ~func(E) ET.Either[L, B], L, E, A, B any](a A) func(GEFAB) GEB {
return FC.Flap(MonadMap[GEFAB, GEB], a)
}
func MonadMapLeft[GA1 ~func(C) ET.Either[E1, A], GA2 ~func(C) ET.Either[E2, A], C, E1, E2, A any](fa GA1, f func(E1) E2) GA2 {
return eithert.MonadMapLeft(R.MonadMap[GA1, GA2, C, ET.Either[E1, A], ET.Either[E2, A]], fa, f)
}
// MapLeft applies a mapping function to the error channel
func MapLeft[GA1 ~func(C) ET.Either[E1, A], GA2 ~func(C) ET.Either[E2, A], C, E1, E2, A any](f func(E1) E2) func(GA1) GA2 {
return F.Bind2nd(MonadMapLeft[GA1, GA2, C, E1, E2, A], f)
}

View File

@@ -151,3 +151,12 @@ func MonadFlap[L, E, A, B any](fab ReaderEither[L, E, func(A) B], a A) ReaderEit
func Flap[L, E, B, A any](a A) func(ReaderEither[L, E, func(A) B]) ReaderEither[L, E, B] {
return G.Flap[ReaderEither[L, E, func(A) B], ReaderEither[L, E, B]](a)
}
func MonadMapLeft[C, E1, E2, A any](fa ReaderEither[C, E1, A], f func(E1) E2) ReaderEither[C, E2, A] {
return G.MonadMapLeft[ReaderEither[C, E1, A], ReaderEither[C, E2, A]](fa, f)
}
// MapLeft applies a mapping function to the error channel
func MapLeft[C, E1, E2, A any](f func(E1) E2) func(ReaderEither[C, E1, A]) ReaderEither[C, E2, A] {
return G.MapLeft[ReaderEither[C, E1, A], ReaderEither[C, E2, A]](f)
}

View File

@@ -440,3 +440,12 @@ func MonadFlap[GREAB ~func(R) GEAB, GREB ~func(R) GEB, GEAB ~func() ET.Either[E,
func Flap[GREAB ~func(R) GEAB, GREB ~func(R) GEB, GEAB ~func() ET.Either[E, func(A) B], GEB ~func() ET.Either[E, B], R, E, B, A any](a A) func(GREAB) GREB {
return FC.Flap(MonadMap[GREAB, GREB], a)
}
func MonadMapLeft[GREA1 ~func(R) GEA1, GREA2 ~func(R) GEA2, GEA1 ~func() ET.Either[E1, A], GEA2 ~func() ET.Either[E2, A], R, E1, E2, A any](fa GREA1, f func(E1) E2) GREA2 {
return eithert.MonadMapLeft(G.MonadMap[GREA1, GREA2], fa, f)
}
// MapLeft applies a mapping function to the error channel
func MapLeft[GREA1 ~func(R) GEA1, GREA2 ~func(R) GEA2, GEA1 ~func() ET.Either[E1, A], GEA2 ~func() ET.Either[E2, A], R, E1, E2, A any](f func(E1) E2) func(GREA1) GREA2 {
return F.Bind2nd(MonadMapLeft[GREA1, GREA2], f)
}

View File

@@ -280,3 +280,12 @@ func MonadFlap[R, E, B, A any](fab ReaderIOEither[R, E, func(A) B], a A) ReaderI
func Flap[R, E, B, A any](a A) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B] {
return G.Flap[ReaderIOEither[R, E, func(A) B], ReaderIOEither[R, E, B]](a)
}
func MonadMapLeft[R, E1, E2, A any](fa ReaderIOEither[R, E1, A], f func(E1) E2) ReaderIOEither[R, E2, A] {
return G.MonadMapLeft[ReaderIOEither[R, E1, A], ReaderIOEither[R, E2, A]](fa, f)
}
// MapLeft applies a mapping function to the error channel
func MapLeft[R, A, E1, E2 any](f func(E1) E2) func(ReaderIOEither[R, E1, A]) ReaderIOEither[R, E2, A] {
return G.MapLeft[ReaderIOEither[R, E1, A], ReaderIOEither[R, E2, A]](f)
}

View File

@@ -19,6 +19,7 @@ import (
"sort"
F "github.com/IBM/fp-go/function"
RAG "github.com/IBM/fp-go/internal/array"
FC "github.com/IBM/fp-go/internal/functor"
G "github.com/IBM/fp-go/internal/record"
Mg "github.com/IBM/fp-go/magma"
@@ -334,6 +335,62 @@ func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
return ToArray[M, GT](r)
}
// FromFoldableMap uses the reduce method for a higher kinded type to transform
// its values into a tuple. The key and value are then used to populate the map. Duplicate
// values are resolved via the provided [Mg.Magma]
func FromFoldableMap[
FCT ~func(A) T.Tuple2[K, V],
HKTA any,
FOLDABLE ~func(func(M, A) M, M) func(HKTA) M,
M ~map[K]V,
A any,
K comparable,
V any](m Mg.Magma[V], fld FOLDABLE) func(f FCT) func(fa HKTA) M {
return func(f FCT) func(fa HKTA) M {
return fld(func(dst M, a A) M {
if IsEmpty(dst) {
dst = make(M)
}
e := f(a)
k := T.First(e)
old, ok := dst[k]
if ok {
dst[k] = m.Concat(old, T.Second(e))
} else {
dst[k] = T.Second(e)
}
return dst
}, Empty[M]())
}
}
func FromFoldable[
HKTA any,
FOLDABLE ~func(func(M, T.Tuple2[K, V]) M, M) func(HKTA) M,
M ~map[K]V,
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) M {
return FromFoldableMap[func(T.Tuple2[K, V]) T.Tuple2[K, V], HKTA, FOLDABLE](m, red)(F.Identity[T.Tuple2[K, V]])
}
func FromArrayMap[
FCT ~func(A) T.Tuple2[K, V],
GA ~[]A,
M ~map[K]V,
A any,
K comparable,
V any](m Mg.Magma[V]) func(f FCT) func(fa GA) M {
return FromFoldableMap[FCT](m, F.Bind23of3(RAG.Reduce[GA, A, M]))
}
func FromArray[
GA ~[]T.Tuple2[K, V],
M ~map[K]V,
K comparable,
V any](m Mg.Magma[V]) func(fa GA) M {
return FromFoldable[GA](m, F.Bind23of3(RAG.Reduce[GA, T.Tuple2[K, V], M]))
}
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 {

View File

@@ -16,6 +16,7 @@
package record
import (
EM "github.com/IBM/fp-go/endomorphism"
Mg "github.com/IBM/fp-go/magma"
Mo "github.com/IBM/fp-go/monoid"
O "github.com/IBM/fp-go/option"
@@ -291,6 +292,44 @@ func Copy[K comparable, V any](m map[K]V) map[K]V {
}
// Clone creates a deep copy of the map using the provided endomorphism to clone the values
func Clone[K comparable, V any](f func(V) V) func(m map[K]V) map[K]V {
func Clone[K comparable, V any](f EM.Endomorphism[V]) EM.Endomorphism[map[K]V] {
return G.Clone[map[K]V](f)
}
// FromFoldableMap converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldableMap[
FOLDABLE ~func(func(map[K]V, A) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
A any,
HKTA any,
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) T.Tuple2[K, V]) func(fa HKTA) map[K]V {
return G.FromFoldableMap[func(A) T.Tuple2[K, V]](m, red)
}
// FromArrayMap converts from an array to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromArrayMap[
A any,
K comparable,
V any](m Mg.Magma[V]) func(f func(A) T.Tuple2[K, V]) func(fa []A) map[K]V {
return G.FromArrayMap[func(A) T.Tuple2[K, V], []A, map[K]V](m)
}
// FromFoldable converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldable[
HKTA any,
FOLDABLE ~func(func(map[K]V, T.Tuple2[K, V]) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) map[K]V {
return G.FromFoldable[HKTA, FOLDABLE](m, red)
}
// FromArray converts from an array to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromArray[
K comparable,
V any](m Mg.Magma[V]) func(fa []T.Tuple2[K, V]) map[K]V {
return G.FromArray[[]T.Tuple2[K, V], map[K]V](m)
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
// Copyright (c) 2023 IBM Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -23,8 +23,10 @@ import (
A "github.com/IBM/fp-go/array"
"github.com/IBM/fp-go/internal/utils"
Mg "github.com/IBM/fp-go/magma"
O "github.com/IBM/fp-go/option"
S "github.com/IBM/fp-go/string"
T "github.com/IBM/fp-go/tuple"
"github.com/stretchr/testify/assert"
)
@@ -149,3 +151,28 @@ func TestCopyVsClone(t *testing.T) {
assert.NotEqual(t, cpy, cln)
assert.Equal(t, src, cpy)
}
func TestFromArrayMap(t *testing.T) {
src1 := A.From("a", "b", "c", "a")
frm := FromArrayMap[string, string](Mg.Second[string]())
f := frm(T.Replicate2[string])
res1 := f(src1)
assert.Equal(t, map[string]string{
"a": "a",
"b": "b",
"c": "c",
}, res1)
src2 := A.From("A", "B", "C", "A")
res2 := f(src2)
assert.Equal(t, map[string]string{
"A": "A",
"B": "B",
"C": "C",
}, res2)
}

View File

@@ -32,6 +32,9 @@ var (
// Ord implements the default ordering for strings
Ord = ord.FromStrictCompare[string]()
// Join joins strings
Join = F.Curry2(F.Bind2nd[[]string, string, string])(strings.Join)
)
func Eq(left string, right string) bool {

View File

@@ -18,6 +18,7 @@ package string
import (
"testing"
A "github.com/IBM/fp-go/array"
"github.com/stretchr/testify/assert"
)
@@ -25,3 +26,13 @@ func TestEmpty(t *testing.T) {
assert.True(t, IsEmpty(""))
assert.False(t, IsEmpty("Carsten"))
}
func TestJoin(t *testing.T) {
x := Join(",")(A.From("a", "b", "c"))
assert.Equal(t, x, x)
assert.Equal(t, "a,b,c", Join(",")(A.From("a", "b", "c")))
assert.Equal(t, "a", Join(",")(A.From("a")))
assert.Equal(t, "", Join(",")(A.Empty[string]()))
}