mirror of
https://github.com/IBM/fp-go.git
synced 2025-07-17 01:32:23 +02:00
fix: refactor builder
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
72
context/readerioeither/http/builder/builder.go
Normal file
72
context/readerioeither/http/builder/builder.go
Normal 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
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
59
context/readerioeither/http/builder/builder_test.go
Normal file
59
context/readerioeither/http/builder/builder_test.go
Normal 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())()))
|
||||||
|
}
|
311
http/builder/builder.go
Normal file
311
http/builder/builder.go
Normal 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)
|
||||||
|
}
|
68
http/builder/builder_test.go
Normal file
68
http/builder/builder_test.go
Normal 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))
|
||||||
|
}
|
@ -18,213 +18,26 @@ package builder
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
E "github.com/IBM/fp-go/either"
|
E "github.com/IBM/fp-go/either"
|
||||||
ENDO "github.com/IBM/fp-go/endomorphism"
|
|
||||||
F "github.com/IBM/fp-go/function"
|
F "github.com/IBM/fp-go/function"
|
||||||
C "github.com/IBM/fp-go/http/content"
|
R "github.com/IBM/fp-go/http/builder"
|
||||||
FM "github.com/IBM/fp-go/http/form"
|
|
||||||
H "github.com/IBM/fp-go/http/headers"
|
H "github.com/IBM/fp-go/http/headers"
|
||||||
IOG "github.com/IBM/fp-go/io/generic"
|
|
||||||
IOE "github.com/IBM/fp-go/ioeither"
|
IOE "github.com/IBM/fp-go/ioeither"
|
||||||
IOEH "github.com/IBM/fp-go/ioeither/http"
|
IOEH "github.com/IBM/fp-go/ioeither/http"
|
||||||
J "github.com/IBM/fp-go/json"
|
|
||||||
LZ "github.com/IBM/fp-go/lazy"
|
LZ "github.com/IBM/fp-go/lazy"
|
||||||
L "github.com/IBM/fp-go/optics/lens"
|
|
||||||
O "github.com/IBM/fp-go/option"
|
O "github.com/IBM/fp-go/option"
|
||||||
S "github.com/IBM/fp-go/string"
|
|
||||||
T "github.com/IBM/fp-go/tuple"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
func Requester(builder *R.Builder) IOEH.Requester {
|
||||||
Builder struct {
|
|
||||||
method O.Option[string]
|
|
||||||
url string
|
|
||||||
headers http.Header
|
|
||||||
body O.Option[IOE.IOEither[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[IOE.IOEither[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[IOE.IOEither[error, []byte]],
|
|
||||||
Body.Set,
|
|
||||||
)
|
|
||||||
// WithBytes creates a [Endomorphism] for a request body using bytes
|
|
||||||
WithBytes = F.Flow2(
|
|
||||||
IOE.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,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Requester creates a requester from a builder
|
|
||||||
Requester = (*Builder).Requester
|
|
||||||
|
|
||||||
// WithoutBody creates a [Endomorphism] to remove the body
|
|
||||||
WithoutBody = F.Pipe1(
|
|
||||||
noBody,
|
|
||||||
Body.Set,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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[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) Requester() IOEH.Requester {
|
|
||||||
|
|
||||||
withBody := F.Curry3(func(data []byte, url string, method string) IOE.IOEither[error, *http.Request] {
|
withBody := F.Curry3(func(data []byte, url string, method string) IOE.IOEither[error, *http.Request] {
|
||||||
return IOE.TryCatchError(func() (*http.Request, error) {
|
return IOE.TryCatchError(func() (*http.Request, error) {
|
||||||
req, err := http.NewRequest(method, url, bytes.NewReader(data))
|
req, err := http.NewRequest(method, url, bytes.NewReader(data))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
|
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
|
||||||
H.Monoid.Concat(req.Header, builder.headers)
|
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||||
}
|
}
|
||||||
return req, err
|
return req, err
|
||||||
})
|
})
|
||||||
@ -234,127 +47,21 @@ func (builder *Builder) Requester() IOEH.Requester {
|
|||||||
return IOE.TryCatchError(func() (*http.Request, error) {
|
return IOE.TryCatchError(func() (*http.Request, error) {
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
H.Monoid.Concat(req.Header, builder.headers)
|
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||||
}
|
}
|
||||||
return req, err
|
return req, err
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// construct the final URL
|
return F.Pipe5(
|
||||||
targetUrl := F.Pipe3(
|
builder.GetBody(),
|
||||||
builder,
|
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
|
||||||
Url.Get,
|
E.Ap[func(string) IOE.IOEither[error, *http.Request]](builder.GetTargetUrl()),
|
||||||
parseUrl,
|
E.Flap[error, IOE.IOEither[error, *http.Request]](builder.GetMethod()),
|
||||||
E.Chain(F.Flow4(
|
E.GetOrElse(IOE.Left[*http.Request, error]),
|
||||||
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),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return F.Pipe6(
|
|
||||||
builder,
|
|
||||||
Body.Get,
|
|
||||||
O.Fold(LZ.Of(IOE.Of[error](withoutBody)), IOE.Map[error](withBody)),
|
|
||||||
IOG.Map[IOE.IOEither[error, func(string) func(string) IOE.IOEither[error, *http.Request]], IOE.IOEither[error, func(string) IOE.IOEither[error, *http.Request]]](E.Ap[func(string) IOE.IOEither[error, *http.Request]](targetUrl)),
|
|
||||||
IOE.Flap[error, IOE.IOEither[error, *http.Request]](builder.GetMethod()),
|
|
||||||
IOE.Flatten[error, *http.Request],
|
|
||||||
IOE.Map[error](func(req *http.Request) *http.Request {
|
IOE.Map[error](func(req *http.Request) *http.Request {
|
||||||
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
|
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||||
return req
|
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 [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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFormData creates a [Endomorphism] 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(C.FormEncoded),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithJson creates a [Endomorphism] 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(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)
|
|
||||||
}
|
|
||||||
|
@ -22,54 +22,26 @@ import (
|
|||||||
|
|
||||||
E "github.com/IBM/fp-go/either"
|
E "github.com/IBM/fp-go/either"
|
||||||
F "github.com/IBM/fp-go/function"
|
F "github.com/IBM/fp-go/function"
|
||||||
C "github.com/IBM/fp-go/http/content"
|
R "github.com/IBM/fp-go/http/builder"
|
||||||
H "github.com/IBM/fp-go/http/headers"
|
|
||||||
IO "github.com/IBM/fp-go/io"
|
IO "github.com/IBM/fp-go/io"
|
||||||
IOE "github.com/IBM/fp-go/ioeither"
|
IOE "github.com/IBM/fp-go/ioeither"
|
||||||
O "github.com/IBM/fp-go/option"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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 TestBuilderWithQuery(t *testing.T) {
|
func TestBuilderWithQuery(t *testing.T) {
|
||||||
// add some query
|
// add some query
|
||||||
withLimit := WithQueryArg("limit")("10")
|
withLimit := R.WithQueryArg("limit")("10")
|
||||||
withUrl := WithUrl("http://www.example.org?a=b")
|
withUrl := R.WithUrl("http://www.example.org?a=b")
|
||||||
|
|
||||||
b := F.Pipe2(
|
b := F.Pipe2(
|
||||||
Default,
|
R.Default,
|
||||||
withLimit,
|
withLimit,
|
||||||
withUrl,
|
withUrl,
|
||||||
)
|
)
|
||||||
|
|
||||||
req := F.Pipe2(
|
req := F.Pipe3(
|
||||||
b.Requester(),
|
b,
|
||||||
|
Requester,
|
||||||
IOE.Map[error](func(r *http.Request) *url.URL {
|
IOE.Map[error](func(r *http.Request) *url.URL {
|
||||||
return r.URL
|
return r.URL
|
||||||
}),
|
}),
|
||||||
|
Reference in New Issue
Block a user