1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-25 22:21:49 +02:00
Files
fp-go/v2/context/readerioeither/http/request.go
Dr. Carsten Leue 92eb9715bd fix: implement some useful prisms
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 13:53:02 +01:00

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 ReaderIOEither 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/readerioeither"
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.ReaderIOEither[*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 ReaderIOEither monad for composable, type-safe HTTP operations.
Client interface {
// Do executes an HTTP request and returns the response wrapped in a ReaderIOEither.
// 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 ReaderIOEither that produces either an error or an *http.Response
Do(Requester) RIOE.ReaderIOEither[*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.ReaderIOEither[*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 ReaderIOEither 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 ReaderIOEither[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.ReaderIOEither[H.FullResponse] {
return func(req Requester) RIOE.ReaderIOEither[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 ReaderIOEither[[]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.ReaderIOEither[[]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 ReaderIOEither[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.ReaderIOEither[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.ReaderIOEither[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.ReaderIOEither[[]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 ReaderIOEither[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.ReaderIOEither[A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),
)
}