// 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" "crypto/sha256" "fmt" "net/http" "net/url" A "github.com/IBM/fp-go/array" B "github.com/IBM/fp-go/bytes" 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" R "github.com/IBM/fp-go/record" 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 // // Deprecated: use [URL] instead Url = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL) // 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 the URL // // Deprecated: use [WithURL] instead WithUrl = URL.Set // WithURL creates a [Endomorphism] for the URL 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)), ) // bodyAsBytes returns a []byte with a fallback to the empty array bodyAsBytes = O.Fold(B.Empty, E.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte])) ) 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 // // Deprecated: use [GetTargetURL] instead func (builder *Builder) GetTargetUrl() E.Either[error, string] { return builder.GetTargetURL() } // 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), )), ) } // Deprecated: use [GetURL] instead func (builder *Builder) GetUrl() string { return builder.url } 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 } // Deprecated: use [SetURL] instead func (builder *Builder) SetUrl(url string) *Builder { builder.url = url 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) } // GetHash returns a hash value for the builder that can be used as a cache key func (builder *Builder) GetHash() string { return MakeHash(builder) } // 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 // // Deprecated: use [WithJSON] instead func WithJson[T any](data T) Endomorphism { return WithJSON[T](data) } // 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) } func hashWriteValue(buf *bytes.Buffer, value string) *bytes.Buffer { buf.WriteString(value) return buf } func hashWriteQuery(name string, buf *bytes.Buffer, values []string) *bytes.Buffer { buf.WriteString(name) return A.Reduce(hashWriteValue, buf)(values) } func makeBytes(b *Builder) []byte { var buf bytes.Buffer buf.WriteString(b.GetMethod()) buf.WriteString(b.GetURL()) b.GetHeaders().Write(&buf) // #nosec: G104 R.ReduceOrdWithIndex[[]string, *bytes.Buffer](S.Ord)(hashWriteQuery, &buf)(b.GetQuery()) buf.Write(bodyAsBytes(b.GetBody())) return buf.Bytes() } // MakeHash converts a [Builder] into a hash string, convenient to use as a cache key func MakeHash(b *Builder) string { return fmt.Sprintf("%x", sha256.Sum256(makeBytes(b))) }