Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add XML support; restore Post(); Add Params() and Headers() #82

Merged
merged 16 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions body.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"net/url"
"os"
"strings"

"github.com/carlmjohnson/requests/internal/core"
)

// BodyGetter provides a Builder with a source for a request body.
Expand All @@ -20,7 +18,7 @@ func BodyReader(r io.Reader) BodyGetter {
if rc, ok := r.(io.ReadCloser); ok {
return rc, nil
}
return core.RC(r), nil
return rc(r), nil
}
}

Expand All @@ -42,7 +40,7 @@ func BodyWriter(f func(w io.Writer) error) BodyGetter {
// BodyBytes is a BodyGetter that returns the provided raw bytes.
func BodyBytes(b []byte) BodyGetter {
return func() (io.ReadCloser, error) {
return core.RC(bytes.NewReader(b)), nil
return rc(bytes.NewReader(b)), nil
}
}

Expand All @@ -53,14 +51,14 @@ func BodyJSON(v any) BodyGetter {
if err != nil {
return nil, err
}
return core.RC(bytes.NewReader(b)), nil
return rc(bytes.NewReader(b)), nil
}
}

// BodyForm is a BodyGetter that builds an encoded form body.
func BodyForm(data url.Values) BodyGetter {
return func() (r io.ReadCloser, err error) {
return core.RC(strings.NewReader(data.Encode())), nil
return rc(strings.NewReader(data.Encode())), nil
}
}

Expand Down
33 changes: 16 additions & 17 deletions builder_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"net/http"
"net/url"

"github.com/carlmjohnson/requests/internal/core"
"github.com/carlmjohnson/requests/internal/minitrue"
"github.com/carlmjohnson/requests/internal/slicex"
)
Expand All @@ -30,7 +29,7 @@ import (
// # 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], and [Builder.Put] methods.
// 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.
//
Expand Down Expand Up @@ -74,8 +73,8 @@ import (
//
// The zero value of Builder is usable.
type Builder struct {
ub core.URLBuilder
rb core.RequestBuilder
ub urlBuilder
rb requestBuilder
cl *http.Client
rt http.RoundTripper
validators []ResponseHandler
Expand Down Expand Up @@ -223,32 +222,32 @@ func (rb *Builder) Request(ctx context.Context) (req *http.Request, err error) {

// 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 := minitrue.First(rb.cl, http.DefaultClient)
cl := minitrue.Or(rb.cl, http.DefaultClient)
if rb.rt != nil {
cl2 := *cl
cl2.Transport = rb.rt
cl = &cl2
}
res, err := cl.Do(req)
if err != nil {
return joinerrs(ErrTransport, err)
}
defer res.Body.Close()

validators := rb.validators
if len(validators) == 0 {
validators = []ResponseHandler{DefaultValidator}
}
if err = ChainHandlers(validators...)(res); err != nil {
return joinerrs(ErrValidator, err)
}
h := minitrue.Cond(rb.handler != nil,
rb.handler,
consumeBody)
if err = h(res); err != nil {
return joinerrs(ErrHandler, err)

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 nil
return err
}

// Fetch builds a request, sends it, and handles the response.
Expand Down
45 changes: 45 additions & 0 deletions builder_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,29 @@ func Example_queryParam() {
// https://dev1.example.com/get?a=1&b=3&c=4
}

func ExampleBuilder_Params() {
// Conditionally add parameters
values := url.Values{"a": {"1"}}
values.Set("b", "3")
if "cond" != "example" {
values.Add("b", "4")
values.Set("c", "5")
}

// Then add them to the URL
u, err := requests.
URL("https://www.example.com/get?a=0&z=6").
Params(values).
URL()
if err != nil {
fmt.Println("Error!", err)
}
fmt.Println(u.String())

// Output:
// https://www.example.com/get?a=1&b=3&b=4&c=5&z=6
}

func ExampleBuilder_Header() {
// Set headers
var headers postman
Expand All @@ -252,6 +275,28 @@ func ExampleBuilder_Header() {
// shaken
}

func ExampleBuilder_Headers() {
// Set headers conditionally
h := make(http.Header)
if "x-forwarded-for" != "true" {
h.Add("x-forwarded-for", "127.0.0.1")
}
if "has-trace-id" != "true" {
h.Add("x-trace-id", "abc123")
}
// Then add them to a request
req, err := requests.
URL("https://example.com").
Headers(h).
Request(context.Background())
if err != nil {
fmt.Println("Error!", err)
}
fmt.Println(req.Header)
// Output:
// map[X-Forwarded-For:[127.0.0.1] X-Trace-Id:[abc123]]

}
func ExampleBuilder_Bearer() {
// We get a 401 response if no bearer token is provided
err := requests.
Expand Down
23 changes: 23 additions & 0 deletions builder_extras.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ 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)
Expand Down Expand Up @@ -63,6 +70,22 @@ 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)
Expand Down
3 changes: 1 addition & 2 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/carlmjohnson/requests"
"github.com/carlmjohnson/requests/internal/be"
"github.com/carlmjohnson/requests/internal/core"
)

func TestClone(t *testing.T) {
Expand Down Expand Up @@ -132,7 +131,7 @@ func TestScheme(t *testing.T) {

func TestPath(t *testing.T) {
t.Parallel()
for name, tc := range core.PathCases {
for name, tc := range requests.PathCases {
t.Run(name, func(t *testing.T) {
var b requests.Builder
b.BaseURL(tc.Base)
Expand Down
36 changes: 36 additions & 0 deletions core_do.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 19 additions & 21 deletions internal/core/req.go → core_req.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package core
package requests

import (
"context"
Expand All @@ -10,70 +10,68 @@ import (
"github.com/carlmjohnson/requests/internal/slicex"
)

// NopCloser is like io.NopCloser(),
// 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 {
type nopCloser struct {
io.Reader
}

func RC(r io.Reader) NopCloser {
return NopCloser{r}
func rc(r io.Reader) nopCloser {
return nopCloser{r}
}

func (NopCloser) Close() error { return nil }
func (nopCloser) Close() error { return nil }

var _ io.ReadCloser = NopCloser{}
var _ io.ReadCloser = nopCloser{}

type BodyGetter = func() (io.ReadCloser, error)

type RequestBuilder struct {
type requestBuilder struct {
headers []multimap
cookies []kvpair
getBody BodyGetter
method string
}

func (rb *RequestBuilder) Header(key string, values ...string) {
func (rb *requestBuilder) Header(key string, values ...string) {
rb.headers = append(rb.headers, multimap{key, values})
}

func (rb *RequestBuilder) Cookie(name, value string) {
func (rb *requestBuilder) Cookie(name, value string) {
rb.cookies = append(rb.cookies, kvpair{name, value})
}

func (rb *RequestBuilder) Method(method string) {
func (rb *requestBuilder) Method(method string) {
rb.method = method
}

func (rb *RequestBuilder) Body(src BodyGetter) {
func (rb *requestBuilder) Body(src BodyGetter) {
rb.getBody = src
}

// Clone creates a new Builder suitable for independent mutation.
func (rb *RequestBuilder) Clone() *RequestBuilder {
func (rb *requestBuilder) Clone() *requestBuilder {
rb2 := *rb
slicex.Clip(&rb2.headers)
slicex.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) {
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 {
if nopper, ok := body.(nopCloser); ok {
body = nopper.Reader
}
}
method := minitrue.First(rb.method,
minitrue.Cond(rb.getBody != nil,
http.MethodPost,
http.MethodGet))
method := minitrue.Or(rb.method,
minitrue.Cond(rb.getBody == nil,
http.MethodGet,
http.MethodPost))

req, err = http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
Expand Down
Loading