diff --git a/context/readerioeither/http/builder/builder.go b/context/readerioeither/http/builder/builder.go new file mode 100644 index 0000000..b10f1d3 --- /dev/null +++ b/context/readerioeither/http/builder/builder.go @@ -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 + }), + ) +} diff --git a/context/readerioeither/http/builder/builder_test.go b/context/readerioeither/http/builder/builder_test.go new file mode 100644 index 0000000..0385983 --- /dev/null +++ b/context/readerioeither/http/builder/builder_test.go @@ -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())())) +} diff --git a/http/builder/builder.go b/http/builder/builder.go new file mode 100644 index 0000000..54b6bd3 --- /dev/null +++ b/http/builder/builder.go @@ -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) +} diff --git a/http/builder/builder_test.go b/http/builder/builder_test.go new file mode 100644 index 0000000..ef92fda --- /dev/null +++ b/http/builder/builder_test.go @@ -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)) +} diff --git a/ioeither/http/builder/builder.go b/ioeither/http/builder/builder.go index a3db010..9398379 100644 --- a/ioeither/http/builder/builder.go +++ b/ioeither/http/builder/builder.go @@ -18,213 +18,26 @@ package builder import ( "bytes" "net/http" - "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" + R "github.com/IBM/fp-go/http/builder" 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" 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[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 { +func Requester(builder *R.Builder) IOEH.Requester { 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) + H.Monoid.Concat(req.Header, builder.GetHeaders()) } return req, err }) @@ -234,127 +47,21 @@ func (builder *Builder) Requester() IOEH.Requester { return IOE.TryCatchError(func() (*http.Request, error) { req, err := http.NewRequest(method, url, nil) if err == nil { - H.Monoid.Concat(req.Header, builder.headers) + H.Monoid.Concat(req.Header, builder.GetHeaders()) } 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.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], + return F.Pipe5( + builder.GetBody(), + 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 [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) -} diff --git a/ioeither/http/builder/builder_test.go b/ioeither/http/builder/builder_test.go index 2605e12..8ac27ec 100644 --- a/ioeither/http/builder/builder_test.go +++ b/ioeither/http/builder/builder_test.go @@ -22,54 +22,26 @@ import ( 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" + R "github.com/IBM/fp-go/http/builder" 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 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) { // add some query - withLimit := WithQueryArg("limit")("10") - withUrl := WithUrl("http://www.example.org?a=b") + withLimit := R.WithQueryArg("limit")("10") + withUrl := R.WithUrl("http://www.example.org?a=b") b := F.Pipe2( - Default, + R.Default, withLimit, withUrl, ) - req := F.Pipe2( - b.Requester(), + req := F.Pipe3( + b, + Requester, IOE.Map[error](func(r *http.Request) *url.URL { return r.URL }),