mirror of
https://github.com/IBM/fp-go.git
synced 2025-08-10 22:31:32 +02:00
fix: add WithQueryArg to request builder
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
@@ -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 (
|
||||
@@ -41,6 +42,9 @@ var (
|
||||
// FormMonoid 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]
|
||||
|
||||
|
@@ -15,6 +15,18 @@
|
||||
|
||||
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"
|
||||
@@ -22,3 +34,25 @@ const (
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
58
http/headers/headers_test.go
Normal file
58
http/headers/headers_test.go
Normal 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))
|
||||
}
|
@@ -21,10 +21,13 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
IOG "github.com/IBM/fp-go/io/generic"
|
||||
IOE "github.com/IBM/fp-go/ioeither"
|
||||
IOEH "github.com/IBM/fp-go/ioeither/http"
|
||||
J "github.com/IBM/fp-go/json"
|
||||
@@ -32,6 +35,7 @@ import (
|
||||
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 (
|
||||
@@ -40,6 +44,7 @@ type (
|
||||
url string
|
||||
headers http.Header
|
||||
body O.Option[IOE.IOEither[error, []byte]]
|
||||
query url.Values
|
||||
}
|
||||
|
||||
// Endomorphism returns an [ENDO.Endomorphism] that transforms a builder
|
||||
@@ -63,26 +68,36 @@ var (
|
||||
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]]()
|
||||
noHeader = O.None[string]()
|
||||
noBody = O.None[IOE.IOEither[error, []byte]]()
|
||||
noQueryArg = O.None[string]()
|
||||
|
||||
// WithMethod creates a [BuilderBuilder] for a certain method
|
||||
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 [BuilderBuilder] for a certain method
|
||||
// WithUrl creates a [Endomorphism] for a certain method
|
||||
WithUrl = Url.Set
|
||||
// WithHeaders creates a [BuilderBuilder] for a set of headers
|
||||
// WithHeaders creates a [Endomorphism] for a set of headers
|
||||
WithHeaders = Headers.Set
|
||||
// WithBody creates a [BuilderBuilder] for a request body
|
||||
// WithBody creates a [Endomorphism] for a request body
|
||||
WithBody = F.Flow2(
|
||||
O.Of[IOE.IOEither[error, []byte]],
|
||||
Body.Set,
|
||||
)
|
||||
// WithBytes creates a [BuilderBuilder] for a request body using bytes
|
||||
// WithBytes creates a [Endomorphism] for a request body using bytes
|
||||
WithBytes = F.Flow2(
|
||||
IOE.Of[error, []byte],
|
||||
WithBody,
|
||||
@@ -101,7 +116,7 @@ var (
|
||||
// WithDelete adds the [http.MethodDelete] method
|
||||
WithDelete = WithMethod(http.MethodDelete)
|
||||
|
||||
// WithBearer creates a [BuilderBuilder] to add a Bearer [H.Authorization] header
|
||||
// WithBearer creates a [Endomorphism] to add a Bearer [H.Authorization] header
|
||||
WithBearer = F.Flow2(
|
||||
S.Format[string]("Bearer %s"),
|
||||
WithAuthorization,
|
||||
@@ -110,13 +125,22 @@ var (
|
||||
// Requester creates a requester from a builder
|
||||
Requester = (*Builder).Requester
|
||||
|
||||
// WithoutBody creates a [BuilderBuilder] to remove the body
|
||||
// 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()
|
||||
@@ -138,6 +162,15 @@ 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
|
||||
}
|
||||
@@ -184,30 +217,62 @@ 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(
|
||||
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(H.ContentLength, strconv.FormatInt(rdr.Size(), 10))
|
||||
}
|
||||
}
|
||||
return req, err
|
||||
})
|
||||
}),
|
||||
|
||||
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.headers)
|
||||
}
|
||||
return req, err
|
||||
})
|
||||
})
|
||||
|
||||
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.headers)
|
||||
}
|
||||
return req, err
|
||||
})
|
||||
})
|
||||
|
||||
// construct the final URL
|
||||
targetUrl := 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),
|
||||
)),
|
||||
)
|
||||
|
||||
return F.Pipe5(
|
||||
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],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,7 +294,7 @@ func Header(name string) L.Lens[*Builder, O.Option[string]] {
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeader creates a [BuilderBuilder] for a certain header
|
||||
// WithHeader creates a [Endomorphism] for a certain header
|
||||
func WithHeader(name string) func(value string) Endomorphism {
|
||||
return F.Flow2(
|
||||
O.Of[string],
|
||||
@@ -237,12 +302,12 @@ func WithHeader(name string) func(value string) Endomorphism {
|
||||
)
|
||||
}
|
||||
|
||||
// WithoutHeader creates a [BuilderBuilder] to remove a certain header
|
||||
// WithoutHeader creates a [Endomorphism] to remove a certain header
|
||||
func WithoutHeader(name string) Endomorphism {
|
||||
return Header(name).Set(noHeader)
|
||||
}
|
||||
|
||||
// WithFormData creates a [BuilderBuilder] to send form data payload
|
||||
// WithFormData creates a [Endomorphism] to send form data payload
|
||||
func WithFormData(value url.Values) Endomorphism {
|
||||
return F.Flow2(
|
||||
F.Pipe4(
|
||||
@@ -256,7 +321,7 @@ func WithFormData(value url.Values) Endomorphism {
|
||||
)
|
||||
}
|
||||
|
||||
// WithJson creates a [BuilderBuilder] to send JSON payload
|
||||
// WithJson creates a [Endomorphism] to send JSON payload
|
||||
func WithJson[T any](data T) Endomorphism {
|
||||
return F.Flow2(
|
||||
F.Pipe3(
|
||||
@@ -268,3 +333,24 @@ func WithJson[T any](data T) Endomorphism {
|
||||
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)
|
||||
}
|
||||
|
@@ -16,16 +16,21 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/either"
|
||||
F "github.com/IBM/fp-go/function"
|
||||
C "github.com/IBM/fp-go/http/content"
|
||||
H "github.com/IBM/fp-go/http/headers"
|
||||
IO "github.com/IBM/fp-go/io"
|
||||
IOE "github.com/IBM/fp-go/ioeither"
|
||||
O "github.com/IBM/fp-go/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuiler(t *testing.T) {
|
||||
func TestBuilder(t *testing.T) {
|
||||
|
||||
name := H.ContentType
|
||||
withContentType := WithHeader(name)
|
||||
@@ -51,3 +56,31 @@ func TestBuiler(t *testing.T) {
|
||||
assert.Equal(t, O.Of(C.TextPlain), b2.GetHeader(name))
|
||||
assert.Equal(t, O.None[string](), b3.GetHeader(name))
|
||||
}
|
||||
|
||||
func TestBuilderWithQuery(t *testing.T) {
|
||||
// add some query
|
||||
withLimit := WithQueryArg("limit")("10")
|
||||
withUrl := WithUrl("http://www.example.org?a=b")
|
||||
|
||||
b := F.Pipe2(
|
||||
Default,
|
||||
withLimit,
|
||||
withUrl,
|
||||
)
|
||||
|
||||
req := F.Pipe2(
|
||||
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"))
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, E.IsRight(req()))
|
||||
}
|
||||
|
Reference in New Issue
Block a user