mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
287 lines
10 KiB
Go
287 lines
10 KiB
Go
// Copyright (c) 2023 - 2025 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 http provides functional HTTP client utilities built on top of ReaderIOResult monad.
|
|
// It offers a composable way to make HTTP requests with context support, error handling,
|
|
// and response parsing capabilities. The package follows functional programming principles
|
|
// to ensure type-safe, testable, and maintainable HTTP operations.
|
|
//
|
|
// The main abstractions include:
|
|
// - Requester: A reader that constructs HTTP requests with context
|
|
// - Client: An interface for executing HTTP requests
|
|
// - Response readers: Functions to parse responses as bytes, text, or JSON
|
|
//
|
|
// Example usage:
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// request := MakeGetRequest("https://api.example.com/data")
|
|
// result := ReadJSON[MyType](client)(request)
|
|
// response := result(context.Background())()
|
|
package http
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
|
|
B "github.com/IBM/fp-go/v2/bytes"
|
|
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
|
F "github.com/IBM/fp-go/v2/function"
|
|
H "github.com/IBM/fp-go/v2/http"
|
|
IOE "github.com/IBM/fp-go/v2/ioeither"
|
|
IOEF "github.com/IBM/fp-go/v2/ioeither/file"
|
|
J "github.com/IBM/fp-go/v2/json"
|
|
P "github.com/IBM/fp-go/v2/pair"
|
|
)
|
|
|
|
type (
|
|
// Requester is a reader that constructs an HTTP request with context support.
|
|
// It represents a computation that, given a context, produces either an error
|
|
// or an HTTP request. This allows for composable request building with proper
|
|
// error handling and context propagation.
|
|
Requester = RIOE.ReaderIOResult[*http.Request]
|
|
|
|
// Client is an interface for executing HTTP requests in a functional way.
|
|
// It wraps the standard http.Client and provides a Do method that works
|
|
// with the ReaderIOResult monad for composable, type-safe HTTP operations.
|
|
Client interface {
|
|
// Do executes an HTTP request and returns the response wrapped in a ReaderIOResult.
|
|
// It takes a Requester (which builds the request) and returns a computation that,
|
|
// when executed with a context, performs the HTTP request and returns either
|
|
// an error or the HTTP response.
|
|
//
|
|
// Parameters:
|
|
// - req: A Requester that builds the HTTP request
|
|
//
|
|
// Returns:
|
|
// - A ReaderIOResult that produces either an error or an *http.Response
|
|
Do(Requester) RIOE.ReaderIOResult[*http.Response]
|
|
}
|
|
|
|
// client is the internal implementation of the Client interface.
|
|
// It wraps a standard http.Client and provides functional HTTP operations.
|
|
client struct {
|
|
delegate *http.Client
|
|
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
|
}
|
|
)
|
|
|
|
var (
|
|
// MakeRequest is an eitherized version of http.NewRequestWithContext.
|
|
// It creates a Requester that builds an HTTP request with the given method, URL, and body.
|
|
// This function properly handles errors and wraps them in the Either monad.
|
|
//
|
|
// Parameters:
|
|
// - method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
// - url: The target URL for the request
|
|
// - body: Optional request body (can be nil)
|
|
//
|
|
// Returns:
|
|
// - A Requester that produces either an error or an *http.Request
|
|
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
|
|
|
// makeRequest is a partially applied version of MakeRequest with the context parameter bound.
|
|
makeRequest = F.Bind13of3(MakeRequest)
|
|
|
|
// MakeGetRequest creates a GET request for the specified URL.
|
|
// It's a convenience function that specializes MakeRequest for GET requests with no body.
|
|
//
|
|
// Parameters:
|
|
// - url: The target URL for the GET request
|
|
//
|
|
// Returns:
|
|
// - A Requester that produces either an error or an *http.Request
|
|
//
|
|
// Example:
|
|
// req := MakeGetRequest("https://api.example.com/users")
|
|
MakeGetRequest = makeRequest("GET", nil)
|
|
)
|
|
|
|
func (client client) Do(req Requester) RIOE.ReaderIOResult[*http.Response] {
|
|
return F.Pipe1(
|
|
req,
|
|
RIOE.ChainIOEitherK(client.doIOE),
|
|
)
|
|
}
|
|
|
|
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
|
|
// The returned Client provides methods for executing HTTP requests in a functional,
|
|
// composable way using the ReaderIOResult monad.
|
|
//
|
|
// Parameters:
|
|
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
|
|
//
|
|
// Returns:
|
|
// - A Client that can execute HTTP requests functionally
|
|
//
|
|
// Example:
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// // or with custom client
|
|
// customClient := &http.Client{Timeout: 10 * time.Second}
|
|
// client := MakeClient(customClient)
|
|
func MakeClient(httpClient *http.Client) Client {
|
|
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
|
|
}
|
|
|
|
// ReadFullResponse sends an HTTP request, reads the complete response body as a byte array,
|
|
// and returns both the response and body as a tuple (FullResponse).
|
|
// It validates the HTTP status code and handles errors appropriately.
|
|
//
|
|
// The function performs the following steps:
|
|
// 1. Executes the HTTP request using the provided client
|
|
// 2. Validates the response status code (checks for HTTP errors)
|
|
// 3. Reads the entire response body into a byte array
|
|
// 4. Returns a tuple containing the response and body
|
|
//
|
|
// Parameters:
|
|
// - client: The HTTP client to use for executing the request
|
|
//
|
|
// Returns:
|
|
// - A function that takes a Requester and returns a ReaderIOResult[FullResponse]
|
|
// where FullResponse is a tuple of (*http.Response, []byte)
|
|
//
|
|
// Example:
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// request := MakeGetRequest("https://api.example.com/data")
|
|
// fullResp := ReadFullResponse(client)(request)
|
|
// result := fullResp(context.Background())()
|
|
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
|
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
|
return F.Flow3(
|
|
client.Do(req),
|
|
IOE.ChainEitherK(H.ValidateResponse),
|
|
IOE.Chain(func(resp *http.Response) IOE.IOEither[error, H.FullResponse] {
|
|
return F.Pipe1(
|
|
F.Pipe3(
|
|
resp,
|
|
H.GetBody,
|
|
IOE.Of[error, io.ReadCloser],
|
|
IOEF.ReadAll[io.ReadCloser],
|
|
),
|
|
IOE.Map[error](F.Bind1st(P.MakePair[*http.Response, []byte], resp)),
|
|
)
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
// ReadAll sends an HTTP request and reads the complete response body as a byte array.
|
|
// It validates the HTTP status code and returns the raw response body bytes.
|
|
// This is useful when you need to process the response body in a custom way.
|
|
//
|
|
// Parameters:
|
|
// - client: The HTTP client to use for executing the request
|
|
//
|
|
// Returns:
|
|
// - A function that takes a Requester and returns a ReaderIOResult[[]byte]
|
|
// containing the response body as bytes
|
|
//
|
|
// Example:
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// request := MakeGetRequest("https://api.example.com/data")
|
|
// readBytes := ReadAll(client)
|
|
// result := readBytes(request)(context.Background())()
|
|
func ReadAll(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
|
return F.Flow2(
|
|
ReadFullResponse(client),
|
|
RIOE.Map(H.Body),
|
|
)
|
|
}
|
|
|
|
// ReadText sends an HTTP request, reads the response body, and converts it to a string.
|
|
// It validates the HTTP status code and returns the response body as a UTF-8 string.
|
|
// This is convenient for APIs that return plain text responses.
|
|
//
|
|
// Parameters:
|
|
// - client: The HTTP client to use for executing the request
|
|
//
|
|
// Returns:
|
|
// - A function that takes a Requester and returns a ReaderIOResult[string]
|
|
// containing the response body as a string
|
|
//
|
|
// Example:
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// request := MakeGetRequest("https://api.example.com/text")
|
|
// readText := ReadText(client)
|
|
// result := readText(request)(context.Background())()
|
|
func ReadText(client Client) func(Requester) RIOE.ReaderIOResult[string] {
|
|
return F.Flow2(
|
|
ReadAll(client),
|
|
RIOE.Map(B.ToString),
|
|
)
|
|
}
|
|
|
|
// ReadJson sends an HTTP request, reads the response, and parses it as JSON.
|
|
//
|
|
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
|
// but will be removed in a future version. The capitalized version follows Go naming
|
|
// conventions for acronyms.
|
|
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
|
|
return ReadJSON[A](client)
|
|
}
|
|
|
|
// readJSON is an internal helper that reads the response body and validates JSON content type.
|
|
// It performs the following validations:
|
|
// 1. Validates HTTP status code
|
|
// 2. Validates that the response Content-Type is application/json
|
|
// 3. Reads the response body as bytes
|
|
//
|
|
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
|
func readJSON(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
|
return F.Flow3(
|
|
ReadFullResponse(client),
|
|
RIOE.ChainFirstEitherK(F.Flow2(
|
|
H.Response,
|
|
H.ValidateJSONResponse,
|
|
)),
|
|
RIOE.Map(H.Body),
|
|
)
|
|
}
|
|
|
|
// ReadJSON sends an HTTP request, reads the response, and parses it as JSON into type A.
|
|
// It validates both the HTTP status code and the Content-Type header to ensure the
|
|
// response is valid JSON before attempting to unmarshal.
|
|
//
|
|
// Type Parameters:
|
|
// - A: The target type to unmarshal the JSON response into
|
|
//
|
|
// Parameters:
|
|
// - client: The HTTP client to use for executing the request
|
|
//
|
|
// Returns:
|
|
// - A function that takes a Requester and returns a ReaderIOResult[A]
|
|
// containing the parsed JSON data
|
|
//
|
|
// Example:
|
|
//
|
|
// type User struct {
|
|
// ID int `json:"id"`
|
|
// Name string `json:"name"`
|
|
// }
|
|
//
|
|
// client := MakeClient(http.DefaultClient)
|
|
// request := MakeGetRequest("https://api.example.com/user/1")
|
|
// readUser := ReadJSON[User](client)
|
|
// result := readUser(request)(context.Background())()
|
|
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
|
|
return F.Flow2(
|
|
readJSON(client),
|
|
RIOE.ChainEitherK(J.Unmarshal[A]),
|
|
)
|
|
}
|