Add stub for doll-master and doll services, add required vendored libs

This commit is contained in:
eleksir
2025-10-27 21:02:31 +03:00
parent f2948d705a
commit 7bd6c7e854
49 changed files with 10721 additions and 10 deletions

1
vendor/github.com/carlmjohnson/requests/.gitignore generated vendored Normal file
View File

@@ -0,0 +1 @@
img/

21
vendor/github.com/carlmjohnson/requests/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Carl Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

314
vendor/github.com/carlmjohnson/requests/README.md generated vendored Normal file
View File

@@ -0,0 +1,314 @@
# Requests [![GoDoc](https://godoc.org/github.com/carlmjohnson/requests?status.svg)](https://godoc.org/github.com/carlmjohnson/requests) [![Go Report Card](https://goreportcard.com/badge/github.com/carlmjohnson/requests)](https://goreportcard.com/report/github.com/carlmjohnson/requests) [![Coverage Status](https://coveralls.io/repos/github/earthboundkid/requests/badge.svg)](https://coveralls.io/github/earthboundkid/requests) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
![Requests logo](https://github.com/earthboundkid/requests/assets/222245/e59a9f82-c6c7-46d8-96b2-ce2f7dd58dce)
## _HTTP requests for Gophers._
**The problem**: Go's net/http is powerful and versatile, but using it correctly for client requests can be extremely verbose.
**The solution**: The requests.Builder type is a convenient way to build, send, and handle HTTP requests. Builder has a fluent API with methods returning a pointer to the same struct, which allows for declaratively describing a request by method chaining.
Requests also comes with tools for building custom http transports, include a request recorder and replayer for testing.
*[See this note on the canonical project URL.](https://gist.github.com/earthboundkid/8915002ae0e531cecdfc58bc6453ac80)*
## Features
- Simplifies HTTP client usage compared to net/http
- Can't forget to close response body
- Checks status codes by default
- Supports context.Context
- JSON serialization and deserialization helpers
- Easily manipulate URLs and query parameters
- Request recording and replaying for tests
- Customizable transports and validators that are compatible with the standard library and third party libraries
- No third party dependencies
- Good test coverage
## Examples
### Simple GET into a string
<table>
<thead>
<tr>
<th><strong>code with net/http</strong></th>
<th><strong>code with requests</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>
```go
req, err := http.NewRequestWithContext(ctx,
http.MethodGet, "http://example.com", nil)
if err != nil {
// ...
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
// ...
}
s := string(b)
```
</td>
<td>
```go
var s string
err := requests.
URL("http://example.com").
ToString(&s).
Fetch(ctx)
```
</td>
</tr>
<tr><td>11+ lines</td><td>5 lines</td></tr>
</tbody>
</table>
### POST a raw body
<table>
<thead>
<tr>
<th><strong>code with net/http</strong></th>
<th><strong>code with requests</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>
```go
body := bytes.NewReader(([]byte(`hello, world`))
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://postman-echo.com/post", body)
if err != nil {
// ...
}
req.Header.Set("Content-Type", "text/plain")
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
_, err := io.ReadAll(res.Body)
if err != nil {
// ...
}
```
</td>
<td>
```go
err := requests.
URL("https://postman-echo.com/post").
BodyBytes([]byte(`hello, world`)).
ContentType("text/plain").
Fetch(ctx)
```
</td>
</tr>
<tr><td>12+ lines</td><td>5 lines</td></tr></tbody></table>
### GET a JSON object
<table>
<thead>
<tr>
<th><strong>code with net/http</strong></th>
<th><strong>code with requests</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>
```go
var post placeholder
u, err := url.Parse("https://jsonplaceholder.typicode.com")
if err != nil {
// ...
}
u.Path = fmt.Sprintf("/posts/%d", 1)
req, err := http.NewRequestWithContext(ctx,
http.MethodGet, u.String(), nil)
if err != nil {
// ...
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
// ...
}
err := json.Unmarshal(b, &post)
if err != nil {
// ...
}
```
</td><td>
```go
var post placeholder
err := requests.
URL("https://jsonplaceholder.typicode.com").
Pathf("/posts/%d", 1).
ToJSON(&post).
Fetch(ctx)
```
</td>
</tr>
<tr><td>18+ lines</td><td>7 lines</td></tr></tbody></table>
### POST a JSON object and parse the response
```go
var res placeholder
req := placeholder{
Title: "foo",
Body: "baz",
UserID: 1,
}
err := requests.
URL("/posts").
Host("jsonplaceholder.typicode.com").
BodyJSON(&req).
ToJSON(&res).
Fetch(ctx)
// net/http equivalent left as an exercise for the reader
```
### Set custom headers for a request
```go
// Set headers
var headers postman
err := requests.
URL("https://postman-echo.com/get").
UserAgent("bond/james-bond").
ContentType("secret").
Header("martini", "shaken").
Fetch(ctx)
```
### Easily manipulate URLs and query parameters
```go
u, err := requests.
URL("https://prod.example.com/get?a=1&b=2").
Hostf("%s.example.com", "dev1").
Param("b", "3").
ParamInt("c", 4).
URL()
if err != nil { /* ... */ }
fmt.Println(u.String()) // https://dev1.example.com/get?a=1&b=3&c=4
```
### Record and replay responses
```go
// record a request to the file system
var s1, s2 string
err := requests.URL("http://example.com").
Transport(reqtest.Record(nil, "somedir")).
ToString(&s1).
Fetch(ctx)
check(err)
// now replay the request in tests
err = requests.URL("http://example.com").
Transport(reqtest.Replay("somedir")).
ToString(&s2).
Fetch(ctx)
check(err)
assert(s1 == s2) // true
```
## FAQs
[See wiki](https://github.com/earthboundkid/requests/wiki) for more details.
### Why not just use the standard library HTTP client?
Brad Fitzpatrick, long time maintainer of the net/http package, [wrote an extensive list of problems with the standard library HTTP client](https://github.com/bradfitz/exp-httpclient/blob/master/problems.md). His four main points (ignoring issues that can't be resolved by a wrapper around the standard library) are:
> - Too easy to not call Response.Body.Close.
> - Too easy to not check return status codes
> - Context support is oddly bolted on
> - Proper usage is too many lines of boilerplate
Requests solves these issues by always closing the response body, checking status codes by default, always requiring a `context.Context`, and simplifying the boilerplate with a descriptive UI based on fluent method chaining.
### Why requests and not some other helper library?
There are two major flaws in other libraries as I see it. One is that in other libraries support for `context.Context` tends to be bolted on if it exists at all. Two, many hide the underlying `http.Client` in such a way that it is difficult or impossible to replace or mock out. Beyond that, I believe that none have achieved the same core simplicity that the requests library has.
### How do I just get some JSON?
```go
var data SomeDataType
err := requests.
URL("https://example.com/my-json").
ToJSON(&data).
Fetch(ctx)
```
### How do I post JSON and read the response JSON?
```go
body := MyRequestType{}
var resp MyResponseType
err := requests.
URL("https://example.com/my-json").
BodyJSON(&body).
ToJSON(&resp).
Fetch(ctx)
```
### How do I just save a file to disk?
It depends on exactly what you need in terms of file atomicity and buffering, but this will work for most cases:
```go
err := requests.
URL("http://example.com").
ToFile("myfile.txt").
Fetch(ctx)
```
For more advanced use case, use `ToWriter`.
### How do I save a response to a string?
```go
var s string
err := requests.
URL("http://example.com").
ToString(&s).
Fetch(ctx)
```
### How do I validate the response status?
By default, if no other validators are added to a builder, requests will check that the response is in the 2XX range. If you add another validator, you can add `builder.CheckStatus(200)` or `builder.AddValidator(requests.DefaultValidator)` to the validation stack.
To disable all response validation, run `builder.AddValidator(nil)`.
## Contributing
Please [create a discussion](https://github.com/earthboundkid/requests/discussions) before submitting a pull request for a new feature.

9
vendor/github.com/carlmjohnson/requests/SECURITY.md generated vendored Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
Only the most recently tagged version of this repository is supported.
## Reporting a Vulnerability
Email me@carlmjohnson.net with any security concerns or vulnerabilities.

77
vendor/github.com/carlmjohnson/requests/body.go generated vendored Normal file
View File

@@ -0,0 +1,77 @@
package requests
import (
"bytes"
"io"
"net/url"
"os"
"strings"
)
// BodyGetter provides a Builder with a source for a request body.
type BodyGetter = func() (io.ReadCloser, error)
// BodyReader is a BodyGetter that returns an io.Reader.
func BodyReader(r io.Reader) BodyGetter {
return func() (io.ReadCloser, error) {
if rc, ok := r.(io.ReadCloser); ok {
return rc, nil
}
return rc(r), nil
}
}
// BodyWriter is a BodyGetter that pipes writes into a request body.
func BodyWriter(f func(w io.Writer) error) BodyGetter {
return func() (io.ReadCloser, error) {
r, w := io.Pipe()
go func() {
var err error
defer func() {
w.CloseWithError(err)
}()
err = f(w)
}()
return r, nil
}
}
// BodyBytes is a BodyGetter that returns the provided raw bytes.
func BodyBytes(b []byte) BodyGetter {
return func() (io.ReadCloser, error) {
return rc(bytes.NewReader(b)), nil
}
}
// BodySerializer is a BodyGetter
// that uses the provided [Serializer]
// to build the body of a request from v.
func BodySerializer(s Serializer, v any) BodyGetter {
return func() (io.ReadCloser, error) {
b, err := s(v)
if err != nil {
return nil, err
}
return rc(bytes.NewReader(b)), nil
}
}
// BodyJSON is a [BodySerializer]
// that uses [JSONSerializer] to marshal the object.
func BodyJSON(v any) BodyGetter {
return BodySerializer(JSONSerializer, v)
}
// BodyForm is a BodyGetter that builds an encoded form body.
func BodyForm(data url.Values) BodyGetter {
return func() (r io.ReadCloser, err error) {
return rc(strings.NewReader(data.Encode())), nil
}
}
// BodyFile is a BodyGetter that reads the provided file path.
func BodyFile(name string) BodyGetter {
return func() (r io.ReadCloser, err error) {
return os.Open(name)
}
}

281
vendor/github.com/carlmjohnson/requests/builder_core.go generated vendored Normal file
View File

@@ -0,0 +1,281 @@
package requests
import (
"cmp"
"context"
"fmt"
"net/http"
"net/url"
"slices"
"github.com/carlmjohnson/requests/internal/minitrue"
)
// Builder is a convenient way to build, send, and handle HTTP requests.
// Builder has a fluent API with methods returning a pointer to the same
// struct, which allows for declaratively describing a request by method chaining.
//
// Builder can build a url.URL,
// build an http.Request,
// or handle a full http.Client request and response with validation.
//
// # Build a url.URL with Builder.URL
//
// Set the base URL by creating a new Builder with [requests.URL]
// or by calling [Builder.BaseURL]
// then customize it with
// [Builder.Scheme], [Builder.Host], [Builder.Hostf], [Builder.Path],
// [Builder.Pathf], [Builder.Param], and [Builder.ParamInt].
// [Builder.ParamOptional] can be used to add a query parameter
// only if it has not been otherwise set.
//
// # Build an http.Request with Builder.Request
//
// Set the method for a request with [Builder.Method]
// or use the [Builder.Delete], [Builder.Head], [Builder.Patch], [Builder.Post], and [Builder.Put] methods.
// By default, requests without a body are GET,
// and those with a body are POST.
//
// Set headers with [Builder.Header]
// or set conventional header keys with
// [Builder.Accept], [Builder.BasicAuth], [Builder.Bearer], [Builder.CacheControl],
// [Builder.ContentType], [Builder.Cookie], and [Builder.UserAgent].
// [Builder.HeaderOptional] can be used to add a header
// only if it has not been otherwise set.
//
// Set the body of the request, if any, with [Builder.Body]
// or use built in [Builder.BodyBytes], [Builder.BodyFile], [Builder.BodyForm],
// [Builder.BodyJSON], [Builder.BodyReader], or [Builder.BodyWriter].
//
// # Handle a request and response with Builder.Do or Builder.Fetch
//
// Set the http.Client to use for a request with [Builder.Client]
// and/or set an http.RoundTripper with [Builder.Transport].
//
// Add a response validator to the Builder with [Builder.AddValidator]
// or use the built in [Builder.CheckStatus], [Builder.CheckContentType],
// [Builder.CheckPeek], [Builder.CopyHeaders], and [Builder.ErrorJSON].
// If no validator has been added, Builder will use [DefaultValidator].
//
// Set a handler for a response with [Builder.Handle]
// or use the built in [Builder.ToHeaders], [Builder.ToJSON], [Builder.ToString],
// [Builder.ToBytesBuffer], or [Builder.ToWriter].
//
// [Builder.Fetch] creates an http.Request with [Builder.Request]
// and validates and handles it with [Builder.Do].
//
// # Other methods
//
// [Builder.Config] can be used to set several options on a Builder at once.
// [New] creates a new Builder and applies [Config] options to it.
//
// In many cases, it will be possible to set most options for an API endpoint
// in a Builder at the package or struct level
// and then call [Builder.Clone] in a function
// to add request specific details for the URL, parameters, headers, body, or handler.
//
// Errors returned by Builder methods will have an [ErrorKind] indicating their origin.
//
// The zero value of Builder is usable.
type Builder struct {
ub urlBuilder
rb requestBuilder
cl *http.Client
rt http.RoundTripper
validators []ResponseHandler
handler ResponseHandler
}
// BaseURL sets the base URL that other URL methods modify.
// It is usually more convenient to use [URL] instead.
func (rb *Builder) BaseURL(baseurl string) *Builder {
rb.ub.BaseURL(baseurl)
return rb
}
// Scheme sets the scheme for a Builder's URL.
// It overrides the scheme set by BaseURL.
func (rb *Builder) Scheme(scheme string) *Builder {
rb.ub.Scheme(scheme)
return rb
}
// Host sets the host for a Builder's URL.
// It overrides the host set by BaseURL.
func (rb *Builder) Host(host string) *Builder {
rb.ub.Host(host)
return rb
}
// Path joins a path to a Builder's URL per the path joining rules of RFC 3986.
// If the path begins with /, it overrides any existing path.
// If the path begins with ./ or ../, the final path will be rewritten in its absolute form when creating a request.
func (rb *Builder) Path(path string) *Builder {
rb.ub.Path(path)
return rb
}
// Param sets a query parameter on a Builder's URL.
// It overwrites the existing values of a key.
func (rb *Builder) Param(key string, values ...string) *Builder {
rb.ub.Param(key, values...)
return rb
}
// ParamOptional sets a query parameter on a Builder's URL
// only if it is not set by some other call to Param or ParamOptional
// and one of the values is a non-blank string.
func (rb *Builder) ParamOptional(key string, values ...string) *Builder {
rb.ub.ParamOptional(key, values...)
return rb
}
// Header sets a header on a request. It overwrites the existing values of a key.
func (rb *Builder) Header(key string, values ...string) *Builder {
rb.rb.Header(key, values...)
return rb
}
// HeaderOptional sets a header on a request
// only if it has not already been set by another call to Header or HeaderOptional
// and one of the values is a non-blank string.
func (rb *Builder) HeaderOptional(key string, values ...string) *Builder {
rb.rb.HeaderOptional(key, values...)
return rb
}
// Cookie adds a cookie to a request.
// Unlike other headers, adding a cookie does not overwrite existing values.
func (rb *Builder) Cookie(name, value string) *Builder {
rb.rb.Cookie(name, value)
return rb
}
// Method sets the HTTP method for a request.
// By default, requests without a body are GET,
// and those with a body are POST.
func (rb *Builder) Method(method string) *Builder {
rb.rb.Method(method)
return rb
}
// Body sets the BodyGetter to use to build the body of a request.
// The provided BodyGetter is used as an http.Request.GetBody func.
// It implicitly sets method to POST.
func (rb *Builder) Body(src BodyGetter) *Builder {
rb.rb.Body(src)
return rb
}
// Client sets the http.Client to use for requests. If nil, it uses http.DefaultClient.
func (rb *Builder) Client(cl *http.Client) *Builder {
rb.cl = cl
return rb
}
// Transport sets the http.RoundTripper to use for requests.
// If set, it makes a shallow copy of the http.Client before modifying it.
func (rb *Builder) Transport(rt http.RoundTripper) *Builder {
rb.rt = rt
return rb
}
// AddValidator adds a response validator to the Builder.
// Adding a validator disables DefaultValidator.
// To disable all validation, just add nil.
func (rb *Builder) AddValidator(h ResponseHandler) *Builder {
rb.validators = append(rb.validators, h)
return rb
}
// Handle sets the response handler for a Builder.
// To use multiple handlers, use ChainHandlers.
func (rb *Builder) Handle(h ResponseHandler) *Builder {
rb.handler = h
return rb
}
// Config allows Builder to be extended by functions that set several options at once.
func (rb *Builder) Config(cfgs ...Config) *Builder {
for _, cfg := range cfgs {
cfg(rb)
}
return rb
}
// Clone creates a new Builder suitable for independent mutation.
func (rb *Builder) Clone() *Builder {
rb2 := *rb
rb2.ub = *rb.ub.Clone()
rb2.rb = *rb.rb.Clone()
rb2.validators = slices.Clip(rb2.validators)
return &rb2
}
func joinerrs(a, b error) error {
return fmt.Errorf("%w: %w", a, b)
}
// URL builds a *url.URL from the base URL and options set on the Builder.
// If a valid url.URL cannot be built,
// URL() nevertheless returns a new url.URL,
// so it is always safe to call u.String().
func (rb *Builder) URL() (u *url.URL, err error) {
u, err = rb.ub.URL()
if err != nil {
return u, joinerrs(ErrURL, err)
}
return u, nil
}
// Request builds a new http.Request with its context set.
func (rb *Builder) Request(ctx context.Context) (req *http.Request, err error) {
u, err := rb.URL()
if err != nil {
return nil, err
}
req, err = rb.rb.Request(ctx, u)
if err != nil {
return nil, joinerrs(ErrRequest, err)
}
return req, nil
}
// Do calls the underlying http.Client and validates and handles any resulting response. The response body is closed after all validators and the handler run.
func (rb *Builder) Do(req *http.Request) (err error) {
cl := cmp.Or(rb.cl, http.DefaultClient)
if rb.rt != nil {
cl2 := *cl
cl2.Transport = rb.rt
cl = &cl2
}
validators := rb.validators
if len(validators) == 0 {
validators = []ResponseHandler{DefaultValidator}
}
h := minitrue.Cond(rb.handler != nil,
rb.handler,
consumeBody)
code, err := do(cl, req, validators, h)
switch code {
case doOK:
return nil
case doConnect:
err = joinerrs(ErrTransport, err)
case doValidate:
err = joinerrs(ErrValidator, err)
case doHandle:
err = joinerrs(ErrHandler, err)
}
return err
}
// Fetch builds a request, sends it, and handles the response.
func (rb *Builder) Fetch(ctx context.Context) (err error) {
req, err := rb.Request(ctx)
if err != nil {
return err
}
return rb.Do(req)
}

View File

@@ -0,0 +1,239 @@
package requests
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
)
// URL creates a new Builder suitable for method chaining.
// It is equivalent to calling BaseURL on an empty Builder.
func URL(baseurl string) *Builder {
return (&Builder{}).BaseURL(baseurl)
}
// New creates a new Builder suitable for method chaining by applying the specified Configs.
// It is equivalent to calling Config on an empty Builder.
// The zero value of Builder is usable,
// so it is not necessary to call New
// when you do not have any Configs to apply.
func New(cfgs ...Config) *Builder {
return (&Builder{}).Config(cfgs...)
}
// Head sets HTTP method to HEAD.
func (rb *Builder) Head() *Builder {
return rb.Method(http.MethodHead)
}
// Post sets HTTP method to POST.
//
// Note that setting a Body causes a request to be POST by default.
func (rb *Builder) Post() *Builder {
return rb.Method(http.MethodPost)
}
// Put sets HTTP method to PUT.
func (rb *Builder) Put() *Builder {
return rb.Method(http.MethodPut)
}
// Patch sets HTTP method to PATCH.
func (rb *Builder) Patch() *Builder {
return rb.Method(http.MethodPatch)
}
// Delete sets HTTP method to DELETE.
func (rb *Builder) Delete() *Builder {
return rb.Method(http.MethodDelete)
}
// Hostf calls Host with fmt.Sprintf.
func (rb *Builder) Hostf(format string, a ...any) *Builder {
return rb.Host(fmt.Sprintf(format, a...))
}
// Pathf calls Path with fmt.Sprintf.
//
// Note that for security reasons, you must not use %s
// with a user provided string!
func (rb *Builder) Pathf(format string, a ...any) *Builder {
return rb.Path(fmt.Sprintf(format, a...))
}
// ParamInt converts value to a string and calls Param.
func (rb *Builder) ParamInt(key string, value int) *Builder {
return rb.Param(key, strconv.Itoa(value))
}
// Params calls Param with all the members of m.
func (rb *Builder) Params(m map[string][]string) *Builder {
for k, vv := range m {
rb.Param(k, vv...)
}
return rb
}
// Headers calls Header with all the members of m.
func (rb *Builder) Headers(m map[string][]string) *Builder {
for k, vv := range m {
rb.Header(k, vv...)
}
return rb
}
// Accept sets the Accept header for a request.
func (rb *Builder) Accept(contentTypes string) *Builder {
return rb.Header("Accept", contentTypes)
}
// CacheControl sets the client-side Cache-Control directive for a request.
func (rb *Builder) CacheControl(directive string) *Builder {
return rb.Header("Cache-Control", directive)
}
// ContentType sets the Content-Type header on a request.
func (rb *Builder) ContentType(ct string) *Builder {
return rb.Header("Content-Type", ct)
}
// UserAgent sets the User-Agent header.
func (rb *Builder) UserAgent(s string) *Builder {
return rb.Header("User-Agent", s)
}
// BasicAuth sets the Authorization header to a basic auth credential.
func (rb *Builder) BasicAuth(username, password string) *Builder {
auth := username + ":" + password
v := base64.StdEncoding.EncodeToString([]byte(auth))
return rb.Header("Authorization", "Basic "+v)
}
// Bearer sets the Authorization header to a bearer token.
func (rb *Builder) Bearer(token string) *Builder {
return rb.Header("Authorization", "Bearer "+token)
}
// BodyReader sets the Builder's request body to r.
func (rb *Builder) BodyReader(r io.Reader) *Builder {
return rb.Body(BodyReader(r))
}
// BodyWriter pipes writes from w to the Builder's request body.
func (rb *Builder) BodyWriter(f func(w io.Writer) error) *Builder {
return rb.Body(BodyWriter(f))
}
// BodyBytes sets the Builder's request body to b.
func (rb *Builder) BodyBytes(b []byte) *Builder {
return rb.Body(BodyBytes(b))
}
// BodySerializer sets the Builder's request body
// to the serialized object.
func (rb *Builder) BodySerializer(s Serializer, v any) *Builder {
return rb.
Body(BodySerializer(s, v))
}
// BodyJSON sets the Builder's request body to the marshaled JSON.
// It uses [JSONSerializer] to marshal the object.
// It also sets ContentType to "application/json"
// if it is not otherwise set.
func (rb *Builder) BodyJSON(v any) *Builder {
return rb.
Body(BodyJSON(v)).
HeaderOptional("Content-Type", "application/json")
}
// BodyForm sets the Builder's request body to the encoded form.
// It also sets the ContentType to "application/x-www-form-urlencoded".
func (rb *Builder) BodyForm(data url.Values) *Builder {
return rb.
Body(BodyForm(data)).
ContentType("application/x-www-form-urlencoded")
}
// BodyFile sets the Builder's request body to read from the given file path.
func (rb *Builder) BodyFile(name string) *Builder {
return rb.Body(BodyFile(name))
}
// CheckStatus adds a validator for status code of a response.
func (rb *Builder) CheckStatus(acceptStatuses ...int) *Builder {
return rb.AddValidator(CheckStatus(acceptStatuses...))
}
// CheckContentType adds a validator for the content type header of a response.
func (rb *Builder) CheckContentType(cts ...string) *Builder {
return rb.AddValidator(CheckContentType(cts...))
}
// CheckPeek adds a validator that peeks at the first n bytes of a response body.
func (rb *Builder) CheckPeek(n int, f func([]byte) error) *Builder {
return rb.AddValidator(CheckPeek(n, f))
}
// ToDeserializer sets the Builder to decode a response into v
// using a [Deserializer].
func (rb *Builder) ToDeserializer(d Deserializer, v any) *Builder {
return rb.
Handle(ToDeserializer(d, v))
}
// ToJSON sets the Builder to decode a response as a JSON object.
//
// It uses [JSONDeserializer] to unmarshal the object.
func (rb *Builder) ToJSON(v any) *Builder {
return rb.Handle(ToJSON(v))
}
// ToString sets the Builder to write the response body to the provided string pointer.
func (rb *Builder) ToString(sp *string) *Builder {
return rb.Handle(ToString(sp))
}
// ToBytesBuffer sets the Builder to write the response body to the provided bytes.Buffer.
func (rb *Builder) ToBytesBuffer(buf *bytes.Buffer) *Builder {
return rb.Handle(ToBytesBuffer(buf))
}
// ToWriter sets the Builder to copy the response body into w.
func (rb *Builder) ToWriter(w io.Writer) *Builder {
return rb.Handle(ToWriter(w))
}
// ToFile sets the Builder to write the response body to the given file name.
// The file and its parent directories are created automatically.
// For more advanced use cases, use ToWriter.
func (rb *Builder) ToFile(name string) *Builder {
return rb.Handle(ToFile(name))
}
// CopyHeaders adds a validator which copies the response headers to h.
// Note that because CopyHeaders adds a validator,
// the DefaultValidator is disabled and must be added back manually
// if status code validation is desired.
func (rb *Builder) CopyHeaders(h map[string][]string) *Builder {
return rb.
AddValidator(CopyHeaders(h))
}
// ToHeaders sets the method to HEAD and adds a handler which copies the response headers to h.
// To just copy headers, see Builder.CopyHeaders.
func (rb *Builder) ToHeaders(h map[string][]string) *Builder {
return rb.
Head().
Handle(ChainHandlers(CopyHeaders(h), consumeBody))
}
// ErrorJSON adds a validator that applies DefaultValidator
// and decodes the response as a JSON object
// if the DefaultValidator check fails.
func (rb *Builder) ErrorJSON(v any) *Builder {
return rb.AddValidator(ErrorJSON(v))
}

77
vendor/github.com/carlmjohnson/requests/config.go generated vendored Normal file
View File

@@ -0,0 +1,77 @@
package requests
import (
"compress/gzip"
"fmt"
"io"
"mime/multipart"
"net/http/httptest"
)
// Config allows Builder to be extended by setting several options at once.
// For example, a Config might set a Body and its ContentType.
type Config = func(rb *Builder)
// GzipConfig writes a gzip stream to its request body using a callback.
// It also sets the appropriate Content-Encoding header and automatically
// closes and the stream when the callback returns.
func GzipConfig(level int, h func(gw *gzip.Writer) error) Config {
return func(rb *Builder) {
rb.
Header("Content-Encoding", "gzip").
BodyWriter(func(w io.Writer) error {
gw, err := gzip.NewWriterLevel(w, level)
if err != nil {
return err
}
if err = h(gw); err != nil {
gw.Close()
return err
}
return gw.Close()
})
}
}
// TestServerConfig returns a Config
// which sets the Builder's BaseURL to s.URL
// and the Builder's Client to s.Client().
//
// Deprecated: Use reqtest.Server.
func TestServerConfig(s *httptest.Server) Config {
return func(rb *Builder) {
rb.
BaseURL(s.URL).
Client(s.Client())
}
}
// BodyMultipart returns a Config
// that uses a multipart.Writer for the request body.
// If boundary is "", a multipart boundary is chosen at random.
// The content type of the request is set to multipart/form-data
// with the correct boundary.
// The multipart.Writer is automatically closed if the callback succeeds.
func BodyMultipart(boundary string, h func(multi *multipart.Writer) error) Config {
return func(rb *Builder) {
if boundary == "" {
multi := multipart.NewWriter(nil)
boundary = multi.Boundary()
}
rb.
ContentType("multipart/form-data; boundary=" + boundary).
BodyWriter(func(w io.Writer) error {
multi := multipart.NewWriter(w)
if err := multi.SetBoundary(boundary); err != nil {
return fmt.Errorf("setting boundary: %w", err)
}
if err := h(multi); err != nil {
return err
}
if err := multi.Close(); err != nil {
return fmt.Errorf("closing multipart writer: %w", err)
}
return nil
})
}
}

18
vendor/github.com/carlmjohnson/requests/cookies.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
package requests
import (
"net/http"
"net/http/cookiejar"
"golang.org/x/net/publicsuffix"
)
// NewCookieJar returns a cookie jar using the standard public suffix list.
func NewCookieJar() http.CookieJar {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
// As of Go 1.16, cookiejar.New err is hardcoded nil
if err != nil {
panic(err)
}
return jar
}

36
vendor/github.com/carlmjohnson/requests/core_do.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
package requests
import (
"net/http"
)
type doResponse int
const (
doOK doResponse = iota
doConnect
doValidate
doHandle
)
func do(cl *http.Client, req *http.Request, validators []ResponseHandler, h ResponseHandler) (doResponse, error) {
res, err := cl.Do(req)
if err != nil {
return doConnect, err
}
defer res.Body.Close()
for _, v := range validators {
if v == nil {
continue
}
if err = v(res); err != nil {
return doValidate, err
}
}
if err = h(res); err != nil {
return doHandle, err
}
return doOK, nil
}

106
vendor/github.com/carlmjohnson/requests/core_req.go generated vendored Normal file
View File

@@ -0,0 +1,106 @@
package requests
import (
"cmp"
"context"
"io"
"net/http"
"net/url"
"slices"
"github.com/carlmjohnson/requests/internal/minitrue"
)
// nopCloser is like io.NopCloser(),
// but it is a concrete type so we can strip it out
// before setting a body on a request.
// See https://github.com/carlmjohnson/requests/discussions/49
type nopCloser struct {
io.Reader
}
func rc(r io.Reader) nopCloser {
return nopCloser{r}
}
func (nopCloser) Close() error { return nil }
var _ io.ReadCloser = nopCloser{}
type requestBuilder struct {
headers []multimap
cookies []kvpair
getBody BodyGetter
method string
}
func (rb *requestBuilder) Header(key string, values ...string) {
rb.headers = append(rb.headers, multimap{key, values, false})
}
func (rb *requestBuilder) HeaderOptional(key string, values ...string) {
rb.headers = append(rb.headers, multimap{key, values, true})
}
func (rb *requestBuilder) Cookie(name, value string) {
rb.cookies = append(rb.cookies, kvpair{name, value})
}
func (rb *requestBuilder) Method(method string) {
rb.method = method
}
func (rb *requestBuilder) Body(src BodyGetter) {
rb.getBody = src
}
// Clone creates a new Builder suitable for independent mutation.
func (rb *requestBuilder) Clone() *requestBuilder {
rb2 := *rb
rb2.headers = slices.Clip(rb2.headers)
rb2.cookies = slices.Clip(rb2.cookies)
return &rb2
}
// Request builds a new http.Request with its context set.
func (rb *requestBuilder) Request(ctx context.Context, u *url.URL) (req *http.Request, err error) {
var body io.Reader
if rb.getBody != nil {
if body, err = rb.getBody(); err != nil {
return nil, err
}
if nopper, ok := body.(nopCloser); ok {
body = nopper.Reader
}
}
method := cmp.Or(rb.method,
minitrue.Cond(rb.getBody == nil,
http.MethodGet,
http.MethodPost))
req, err = http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
}
req.GetBody = rb.getBody
for _, kv := range rb.headers {
if !kv.optional {
req.Header[http.CanonicalHeaderKey(kv.key)] = kv.values
}
}
for _, kv := range rb.headers {
if kv.optional &&
req.Header.Get(kv.key) == "" &&
cmp.Or(kv.values...) != "" {
req.Header[http.CanonicalHeaderKey(kv.key)] = kv.values
}
}
for _, kv := range rb.cookies {
req.AddCookie(&http.Cookie{
Name: kv.key,
Value: kv.value,
})
}
return req, nil
}

94
vendor/github.com/carlmjohnson/requests/core_url.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
package requests
import (
"cmp"
"net/url"
"slices"
)
type multimap struct {
key string
values []string
optional bool
}
type kvpair struct {
key, value string
}
type urlBuilder struct {
baseurl string
scheme, host string
paths []string
params []multimap
}
func (ub *urlBuilder) BaseURL(baseurl string) {
ub.baseurl = baseurl
}
func (ub *urlBuilder) Scheme(scheme string) {
ub.scheme = scheme
}
func (ub *urlBuilder) Host(host string) {
ub.host = host
}
func (ub *urlBuilder) Path(path string) {
ub.paths = append(ub.paths, path)
}
func (ub *urlBuilder) Param(key string, values ...string) {
ub.params = append(ub.params, multimap{key, values, false})
}
func (ub *urlBuilder) ParamOptional(key string, values ...string) {
ub.params = append(ub.params, multimap{key, values, true})
}
func (ub *urlBuilder) Clone() *urlBuilder {
ub2 := *ub
ub2.paths = slices.Clip(ub2.paths)
ub2.params = slices.Clip(ub2.params)
return &ub2
}
func (ub *urlBuilder) URL() (u *url.URL, err error) {
u, err = url.Parse(ub.baseurl)
if err != nil {
return new(url.URL), err
}
u.Scheme = cmp.Or(
ub.scheme,
u.Scheme,
"https",
)
u.Host = cmp.Or(ub.host, u.Host)
for _, p := range ub.paths {
u.Path = u.ResolveReference(&url.URL{Path: p}).Path
}
if len(ub.params) > 0 {
q := u.Query()
for _, kv := range ub.params {
if !kv.optional {
q[kv.key] = kv.values
}
}
for _, kv := range ub.params {
if kv.optional &&
q.Get(kv.key) == "" &&
cmp.Or(kv.values...) != "" {
q[kv.key] = kv.values
}
}
u.RawQuery = q.Encode()
}
// Reparsing, in case the path rewriting broke the URL
u, err = url.Parse(u.String())
if err != nil {
return new(url.URL), err
}
return u, nil
}

3
vendor/github.com/carlmjohnson/requests/doc.go generated vendored Normal file
View File

@@ -0,0 +1,3 @@
// Package requests is a convenience wrapper around net/http to make it faster
// and easier to build requests and custom transports.
package requests

20
vendor/github.com/carlmjohnson/requests/errorkind.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
package requests
// ErrorKind indicates where an error was returned in the process of building, validating, and handling a request.
// Errors returned by Builder can be tested for their ErrorKind using errors.Is or errors.As.
type ErrorKind int8
//go:generate stringer -type=ErrorKind
// Enum values for type ErrorKind
const (
ErrURL ErrorKind = iota // error building URL
ErrRequest // error building the request
ErrTransport // error connecting
ErrValidator // validator error
ErrHandler // handler error
)
func (ek ErrorKind) Error() string {
return ek.String()
}

View File

@@ -0,0 +1,27 @@
// Code generated by "stringer -type=ErrorKind"; DO NOT EDIT.
package requests
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ErrURL-0]
_ = x[ErrRequest-1]
_ = x[ErrTransport-2]
_ = x[ErrValidator-3]
_ = x[ErrHandler-4]
}
const _ErrorKind_name = "ErrURLErrRequestErrTransportErrValidatorErrHandler"
var _ErrorKind_index = [...]uint8{0, 6, 16, 28, 40, 50}
func (i ErrorKind) String() string {
if i < 0 || i >= ErrorKind(len(_ErrorKind_index)-1) {
return "ErrorKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ErrorKind_name[_ErrorKind_index[i]:_ErrorKind_index[i+1]]
}

139
vendor/github.com/carlmjohnson/requests/handler.go generated vendored Normal file
View File

@@ -0,0 +1,139 @@
package requests
import (
"bufio"
"bytes"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"golang.org/x/net/html"
)
// ResponseHandler is used to validate or handle the response to a request.
type ResponseHandler = func(*http.Response) error
// ChainHandlers allows for the composing of validators or response handlers.
func ChainHandlers(handlers ...ResponseHandler) ResponseHandler {
return func(r *http.Response) error {
for _, h := range handlers {
if h == nil {
continue
}
if err := h(r); err != nil {
return err
}
}
return nil
}
}
func consumeBody(res *http.Response) (err error) {
const maxDiscardSize = 640 * 1 << 10
if _, err = io.CopyN(io.Discard, res.Body, maxDiscardSize); err == io.EOF {
err = nil
}
return err
}
// ToDeserializer decodes a response into v using a [Deserializer].
func ToDeserializer(d Deserializer, v any) ResponseHandler {
return func(res *http.Response) error {
data, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if err = d(data, v); err != nil {
return err
}
return nil
}
}
// ToJSON decodes a response as a JSON object.
//
// It uses [JSONDeserializer] to unmarshal the object.
func ToJSON(v any) ResponseHandler {
return ToDeserializer(JSONDeserializer, v)
}
// ToString writes the response body to the provided string pointer.
func ToString(sp *string) ResponseHandler {
return func(res *http.Response) error {
var buf strings.Builder
_, err := io.Copy(&buf, res.Body)
if err == nil {
*sp = buf.String()
}
return err
}
}
// ToBytesBuffer writes the response body to the provided bytes.Buffer.
func ToBytesBuffer(buf *bytes.Buffer) ResponseHandler {
return func(res *http.Response) error {
_, err := io.Copy(buf, res.Body)
return err
}
}
// ToBufioReader takes a callback which wraps the response body in a bufio.Reader.
func ToBufioReader(f func(r *bufio.Reader) error) ResponseHandler {
return func(res *http.Response) error {
return f(bufio.NewReader(res.Body))
}
}
// ToBufioScanner takes a callback which wraps the response body in a bufio.Scanner.
func ToBufioScanner(f func(r *bufio.Scanner) error) ResponseHandler {
return func(res *http.Response) error {
return f(bufio.NewScanner(res.Body))
}
}
// ToHTML parses the page with x/net/html.Parse.
//
// Deprecated: Use reqhtml.To.
func ToHTML(n *html.Node) ResponseHandler {
return ToBufioReader(func(r *bufio.Reader) error {
n2, err := html.Parse(r)
if err != nil {
return err
}
*n = *n2
return nil
})
}
// ToWriter copies the response body to w.
func ToWriter(w io.Writer) ResponseHandler {
return ToBufioReader(func(r *bufio.Reader) error {
_, err := io.Copy(w, r)
return err
})
}
// ToFile writes the response body at the provided file path.
// The file and its parent directories are created automatically.
func ToFile(name string) ResponseHandler {
return func(res *http.Response) error {
_ = os.MkdirAll(filepath.Dir(name), 0777)
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, res.Body)
return err
}
}
// ToHeaders is an alias for backwards compatibility.
//
// Deprecated: Use CopyHeaders
var ToHeaders = CopyHeaders

View File

@@ -0,0 +1,9 @@
// Package minitrue - Whatever the Package holds to be the truth, is truth.
package minitrue
func Cond[T any](val bool, a, b T) T {
if val {
return a
}
return b
}

131
vendor/github.com/carlmjohnson/requests/recorder.go generated vendored Normal file
View File

@@ -0,0 +1,131 @@
package requests
import (
"bufio"
"bytes"
"crypto/md5"
"encoding/base64"
"errors"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
)
// Record returns an http.RoundTripper that writes out its
// requests and their responses to text files in basepath.
// Requests are named according to a hash of their contents.
// Responses are named according to the request that made them.
//
// Deprecated: Use reqtest.Record.
func Record(rt http.RoundTripper, basepath string) Transport {
if rt == nil {
rt = http.DefaultTransport
}
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("problem while recording transport: %w", err)
}
}()
_ = os.MkdirAll(basepath, 0755)
b, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, err
}
reqname, resname := buildName(b)
name := filepath.Join(basepath, reqname)
if err = os.WriteFile(name, b, 0644); err != nil {
return nil, err
}
if res, err = rt.RoundTrip(req); err != nil {
return
}
b, err = httputil.DumpResponse(res, true)
if err != nil {
return nil, err
}
name = filepath.Join(basepath, resname)
if err = os.WriteFile(name, b, 0644); err != nil {
return nil, err
}
return
})
}
// Replay returns an http.RoundTripper that reads its
// responses from text files in basepath.
// Responses are looked up according to a hash of the request.
//
// Deprecated: Use reqtest.Replay.
func Replay(basepath string) Transport {
return ReplayFS(os.DirFS(basepath))
}
var errNotFound = errors.New("response not found")
// ReplayFS returns an http.RoundTripper that reads its
// responses from text files in the fs.FS.
// Responses are looked up according to a hash of the request.
// Response file names may optionally be prefixed with comments for better human organization.
//
// Deprecated: Use reqtest.ReplayFS.
func ReplayFS(fsys fs.FS) Transport {
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("problem while replaying transport: %w", err)
}
}()
b, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, err
}
_, name := buildName(b)
glob := "*" + name
matches, err := fs.Glob(fsys, glob)
if err != nil {
return nil, err
}
if len(matches) == 0 {
return nil, fmt.Errorf("%w: no replay file matches %q", errNotFound, glob)
}
if len(matches) > 1 {
return nil, fmt.Errorf("ambiguous response: multiple replay files match %q", glob)
}
b, err = fs.ReadFile(fsys, matches[0])
if err != nil {
return nil, err
}
r := bufio.NewReader(bytes.NewReader(b))
return http.ReadResponse(r, req)
})
}
func buildName(b []byte) (reqname, resname string) {
h := md5.New()
h.Write(b)
s := base64.URLEncoding.EncodeToString(h.Sum(nil))
return s[:8] + ".req.txt", s[:8] + ".res.txt"
}
// Caching returns an http.RoundTripper that attempts to read its
// responses from text files in basepath. If the response is absent,
// it caches the result of issuing the request with rt in basepath.
// Requests are named according to a hash of their contents.
// Responses are named according to the request that made them.
//
// Deprecated: Use reqtest.Caching.
func Caching(rt http.RoundTripper, basepath string) Transport {
replay := Replay(basepath).RoundTrip
record := Record(rt, basepath).RoundTrip
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
res, err = replay(req)
if errors.Is(err, errNotFound) {
res, err = record(req)
}
return
})
}

19
vendor/github.com/carlmjohnson/requests/redirects.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
package requests
import "net/http"
// CheckRedirectPolicy is a function suitable for use as CheckRedirect on an http.Client.
type CheckRedirectPolicy = func(req *http.Request, via []*http.Request) error
// MaxFollow returns a CheckRedirectPolicy that follows a maximum of n redirects.
func MaxFollow(n int) CheckRedirectPolicy {
return func(req *http.Request, via []*http.Request) error {
if len(via) > n {
return http.ErrUseLastResponse
}
return nil
}
}
// NoFollow is a CheckRedirectPolicy that does not follow redirects.
var NoFollow CheckRedirectPolicy = MaxFollow(0)

24
vendor/github.com/carlmjohnson/requests/serializer.go generated vendored Normal file
View File

@@ -0,0 +1,24 @@
package requests
import (
"encoding/json"
)
// Serializer is a function
// that can convert arbitrary data
// to bytes in some format.
type Serializer = func(v any) ([]byte, error)
// Deserializer is a function
// that can read data in some format
// and store the result in v.
type Deserializer = func(data []byte, v any) error
var (
// JSONSerializer is used by BodyJSON and Builder.BodyJSON.
// The default serializer may be changed in a future version of requests.
JSONSerializer Serializer = json.Marshal
// JSONDeserializer is used by ToJSON and Builder.ToJSON.
// The default deserializer may be changed in a future version of requests.
JSONDeserializer Deserializer = json.Unmarshal
)

128
vendor/github.com/carlmjohnson/requests/transport.go generated vendored Normal file
View File

@@ -0,0 +1,128 @@
package requests
import (
"bufio"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
// Transport is an alias of http.RoundTripper for documentation purposes.
type Transport = http.RoundTripper
// RoundTripFunc is an adaptor to use a function as an http.RoundTripper.
type RoundTripFunc func(req *http.Request) (res *http.Response, err error)
// RoundTrip implements http.RoundTripper.
func (rtf RoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return rtf(r)
}
var _ Transport = RoundTripFunc(nil)
// ReplayString returns an http.RoundTripper that always responds with a
// request built from rawResponse. It is intended for use in one-off tests.
//
// Deprecated: Use reqtest.ReplayString.
func ReplayString(rawResponse string) Transport {
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
r := bufio.NewReader(strings.NewReader(rawResponse))
res, err = http.ReadResponse(r, req)
return
})
}
// UserAgentTransport returns a wrapped http.RoundTripper that sets the User-Agent header on requests to s.
func UserAgentTransport(rt http.RoundTripper, s string) Transport {
if rt == nil {
rt = http.DefaultTransport
}
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
r2 := *req
r2.Header = r2.Header.Clone()
r2.Header.Set("User-Agent", s)
return rt.RoundTrip(&r2)
})
}
// PermitURLTransport returns a wrapped http.RoundTripper that rejects any requests whose URL doesn't match the provided regular expression string.
//
// PermitURLTransport will panic if the regexp does not compile.
func PermitURLTransport(rt http.RoundTripper, regex string) Transport {
if rt == nil {
rt = http.DefaultTransport
}
re := regexp.MustCompile(regex)
reErr := fmt.Errorf("requested URL not permitted by regexp: %s", regex)
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
if u := req.URL.String(); !re.MatchString(u) {
return nil, reErr
}
return rt.RoundTrip(req)
})
}
// LogTransport returns a wrapped http.RoundTripper
// that calls fn with details when a response has finished.
// A response is considered finished
// when the wrapper http.RoundTripper returns an error
// or the Response.Body is closed,
// whichever comes first.
// To simplify logging code,
// a nil *http.Response is replaced with a new http.Response.
func LogTransport(rt http.RoundTripper, fn func(req *http.Request, res *http.Response, err error, duration time.Duration)) Transport {
if rt == nil {
rt = http.DefaultTransport
}
return RoundTripFunc(func(req *http.Request) (res *http.Response, err error) {
start := time.Now()
res, err = rt.RoundTrip(req)
if err != nil {
res2 := res
if res == nil {
res2 = new(http.Response)
}
fn(req, res2, err, time.Since(start))
return
}
res.Body = closeLogger{res.Body, func() {
fn(req, res, err, time.Since(start))
}}
return
})
}
type closeLogger struct {
io.ReadCloser
fn func()
}
func (cl closeLogger) Close() error {
cl.fn()
return cl.ReadCloser.Close()
}
// DoerTransport converts a Doer into a Transport.
// It exists for compatibility with other libraries.
// A Doer is an interface with a Do method.
// Users should prefer Transport,
// because Do is the interface of http.Client
// which has higher level concerns.
func DoerTransport(cl interface {
Do(req *http.Request) (*http.Response, error)
}) Transport {
return RoundTripFunc(cl.Do)
}
// ErrorTransport always returns the specified error instead of connecting.
// It is intended for use in testing
// or to prevent accidental use of http.DefaultClient.
func ErrorTransport(err error) Transport {
return RoundTripFunc(func(req *http.Request) (*http.Response, error) {
return nil, err
})
}

102
vendor/github.com/carlmjohnson/requests/validator.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package requests
import (
"bufio"
"errors"
"fmt"
"io"
"maps"
"mime"
"net/http"
"slices"
)
// DefaultValidator is the validator applied by Builder unless otherwise specified.
var DefaultValidator ResponseHandler = CheckStatus(
http.StatusOK,
http.StatusCreated,
http.StatusAccepted,
http.StatusNonAuthoritativeInfo,
http.StatusNoContent,
)
// ResponseError is the error type produced by CheckStatus and CheckContentType.
type ResponseError http.Response
// Error fulfills the error interface.
func (se *ResponseError) Error() string {
return fmt.Sprintf("response error for %s", se.Request.URL.Redacted())
}
// CheckStatus validates the response has an acceptable status code.
func CheckStatus(acceptStatuses ...int) ResponseHandler {
return func(res *http.Response) error {
if slices.Contains(acceptStatuses, res.StatusCode) {
return nil
}
return fmt.Errorf("%w: unexpected status: %d",
(*ResponseError)(res), res.StatusCode)
}
}
// HasStatusErr returns true if err is a ResponseError caused by any of the codes given.
func HasStatusErr(err error, codes ...int) bool {
if err == nil {
return false
}
if se := new(ResponseError); errors.As(err, &se) {
return slices.Contains(codes, se.StatusCode)
}
return false
}
// CheckContentType validates that a response has one of the given content type headers.
func CheckContentType(cts ...string) ResponseHandler {
return func(res *http.Response) error {
mt, _, err := mime.ParseMediaType(res.Header.Get("Content-Type"))
if err != nil {
return fmt.Errorf("%w: problem matching Content-Type",
(*ResponseError)(res))
}
if slices.Contains(cts, mt) {
return nil
}
return fmt.Errorf("%w: unexpected Content-Type: %s",
(*ResponseError)(res), mt)
}
}
type bufioCloser struct {
*bufio.Reader
io.Closer
}
// CheckPeek wraps the body of a response in a bufio.Reader and
// gives f a peek at the first n bytes for validation.
func CheckPeek(n int, f func([]byte) error) ResponseHandler {
return func(res *http.Response) error {
// ensure buffer is at least minimum size
buf := bufio.NewReader(res.Body)
// ensure large peeks will fit in the buffer
buf = bufio.NewReaderSize(buf, n)
res.Body = &bufioCloser{
buf,
res.Body,
}
b, err := buf.Peek(n)
if err != nil && err != io.EOF {
return err
}
return f(b)
}
}
// CopyHeaders copies the response headers to h.
func CopyHeaders(h map[string][]string) ResponseHandler {
return func(res *http.Response) error {
maps.Copy(h, res.Header)
return nil
}
}

View File

@@ -0,0 +1,34 @@
package requests
import (
"errors"
"fmt"
"net/http"
)
// ValidatorHandler composes a Validator and a Handler.
// If the validation check fails, it triggers the handler.
// Any errors from validator or handler will be joined to the error returned.
// If the handler succeeds, the error will matching ErrInvalidHandled.
func ValidatorHandler(v, h ResponseHandler) ResponseHandler {
return func(res *http.Response) error {
err1 := v(res)
if err1 == nil { // passes validation
return nil
}
err2 := h(res)
if err2 == nil { // successfully handled
return fmt.Errorf("%w: %w", ErrInvalidHandled, err1)
}
return errors.Join(err1, err2)
}
}
var ErrInvalidHandled = errors.New("handled recovery from invalid response")
// ErrorJSON is a ValidatorHandler that applies DefaultValidator
// and decodes the response as a JSON object
// if the DefaultValidator check fails.
func ErrorJSON(v any) ResponseHandler {
return ValidatorHandler(DefaultValidator, ToJSON(v))
}