Skip to content
Closed
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
9 changes: 9 additions & 0 deletions body.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package requests

import (
"bytes"
jsonv1 "encoding/json"
"io"
"net/url"
"os"
Expand Down Expand Up @@ -62,6 +63,14 @@ func BodyJSON(v any) BodyGetter {
return BodySerializer(JSONSerializer, v)
}

// BodyJSONV1 is a [BodySerializer]
// that uses the encoding/json (v1) package to marshal the object.
// Use it to keep the marshaling behavior of earlier versions of requests,
// such as omitempty omitting zero numbers.
func BodyJSONV1(v any) BodyGetter {
return BodySerializer(jsonv1.Marshal, v)
}

// BodyForm is a BodyGetter that builds an encoded form body.
func BodyForm(data url.Values) BodyGetter {
return func() (r io.ReadCloser, err error) {
Expand Down
27 changes: 18 additions & 9 deletions builder_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"context"
"encoding/binary"
"encoding/csv"
"encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -145,7 +146,7 @@ func ExampleBuilder_ToFile() {
}

type placeholder struct {
ID int `json:"id,omitempty"`
ID int `json:"id,omitzero"`
Title string `json:"title"`
Body string `json:"body"`
UserID int `json:"userId"`
Expand Down Expand Up @@ -307,8 +308,8 @@ func ExampleBuilder_Bearer() {
}
// But our response is accepted when we provide a bearer token
var res struct {
Authenticated bool
Token string
Authenticated bool `json:"authenticated"`
Token string `json:"token"`
}
err = requests.
URL("http://httpbin.org/bearer").
Expand Down Expand Up @@ -542,8 +543,12 @@ func ExampleBuilder_ErrorJSON() {

{"x": 1}`)

var goodJSON struct{ X int }
var errJSON struct{ Error string }
var goodJSON struct {
X int `json:"x"`
}
var errJSON struct {
Error string `json:"error"`
}
err := requests.
URL("http://example.com/").
Transport(trans).
Expand All @@ -561,8 +566,12 @@ func ExampleBuilder_ErrorJSON() {

{"error": "brewing"}`)

var goodJSON struct{ X int }
var errJSON struct{ Error string }
var goodJSON struct {
X int `json:"x"`
}
var errJSON struct {
Error string `json:"error"`
}
err := requests.
URL("http://example.com/").
Transport(trans).
Expand Down Expand Up @@ -676,7 +685,7 @@ func ExampleBuilder_BodyJSON() {

// Change the default JSON serializer to indent with two spaces
requests.JSONSerializer = func(v any) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
return json.Marshal(v, jsontext.WithIndent(" "))
}

// Build a new request using the new indenting serializer
Expand Down
20 changes: 20 additions & 0 deletions builder_extras.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ func (rb *Builder) BodyJSON(v any) *Builder {
HeaderOptional("Content-Type", "application/json")
}

// BodyJSONV1 is like [Builder.BodyJSON]
// but marshals the object using [BodyJSONV1].
func (rb *Builder) BodyJSONV1(v any) *Builder {
return rb.
Body(BodyJSONV1(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 {
Expand Down Expand Up @@ -192,6 +200,12 @@ func (rb *Builder) ToJSON(v any) *Builder {
return rb.Handle(ToJSON(v))
}

// ToJSONV1 is like [Builder.ToJSON]
// but decodes the response using [ToJSONV1].
func (rb *Builder) ToJSONV1(v any) *Builder {
return rb.Handle(ToJSONV1(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))
Expand Down Expand Up @@ -237,3 +251,9 @@ func (rb *Builder) ToHeaders(h map[string][]string) *Builder {
func (rb *Builder) ErrorJSON(v any) *Builder {
return rb.AddValidator(ErrorJSON(v))
}

// ErrorJSONV1 is like [Builder.ErrorJSON]
// but decodes the response using [ToJSONV1].
func (rb *Builder) ErrorJSONV1(v any) *Builder {
return rb.AddValidator(ErrorJSONV1(v))
}
2 changes: 1 addition & 1 deletion cookies_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func ExampleNewCookieJar() {
}
// And we'll see that they're reused on subsequent requests
var cookies struct {
Cookies map[string]string
Cookies map[string]string `json:"cookies"`
}
err = requests.
URL("http://httpbin.org/cookies").
Expand Down
9 changes: 9 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package requests
import (
"bufio"
"bytes"
jsonv1 "encoding/json"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -57,6 +58,14 @@ func ToJSON(v any) ResponseHandler {
return ToDeserializer(JSONDeserializer, v)
}

// ToJSONV1 decodes a response as a JSON object
// using the encoding/json (v1) package.
// Use it to keep the unmarshaling behavior of earlier versions of requests,
// such as case-insensitive matching of object keys to struct fields.
func ToJSONV1(v any) ResponseHandler {
return ToDeserializer(jsonv1.Unmarshal, v)
}

// ToString writes the response body to the provided string pointer.
func ToString(sp *string) ResponseHandler {
return func(res *http.Response) error {
Expand Down
66 changes: 66 additions & 0 deletions json_v1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package requests_test

import (
"context"
"errors"
"io"
"testing"

"github.com/gophertools/requests"
"github.com/gophertools/requests/internal/be"
"github.com/gophertools/requests/reqtest"
)

func TestJSONV1(t *testing.T) {
t.Parallel()
type obj struct {
Name string
Count int `json:"count,omitempty"`
}
ctx := context.Background()

t.Run("BodyJSON", func(t *testing.T) {
req, err := requests.New().BodyJSON(obj{Name: "a"}).Request(ctx)
be.NilErr(t, err)
b, err := io.ReadAll(req.Body)
be.NilErr(t, err)
be.Equal(t, `{"Name":"a","count":0}`, string(b))
})
t.Run("BodyJSONV1", func(t *testing.T) {
req, err := requests.New().BodyJSONV1(obj{Name: "a"}).Request(ctx)
be.NilErr(t, err)
be.Equal(t, "application/json", req.Header.Get("Content-Type"))
b, err := io.ReadAll(req.Body)
be.NilErr(t, err)
be.Equal(t, `{"Name":"a"}`, string(b))
})
t.Run("ToJSON", func(t *testing.T) {
var got obj
err := requests.New().
Transport(reqtest.ReplayString("HTTP/1.1 200 OK\n\n{\"name\": \"a\"}")).
ToJSON(&got).
Fetch(ctx)
be.NilErr(t, err)
be.Equal(t, "", got.Name)
})
t.Run("ToJSONV1", func(t *testing.T) {
var got obj
err := requests.New().
Transport(reqtest.ReplayString("HTTP/1.1 200 OK\n\n{\"name\": \"a\"}")).
ToJSONV1(&got).
Fetch(ctx)
be.NilErr(t, err)
be.Equal(t, "a", got.Name)
})
t.Run("ErrorJSONV1", func(t *testing.T) {
var good, bad obj
err := requests.New().
Transport(reqtest.ReplayString("HTTP/1.1 418 I'm a teapot\n\n{\"name\": \"brewing\"}")).
ToJSONV1(&good).
ErrorJSONV1(&bad).
Fetch(ctx)
be.True(t, errors.Is(err, requests.ErrInvalidHandled))
be.Equal(t, "brewing", bad.Name)
be.Equal(t, "", good.Name)
})
}
5 changes: 3 additions & 2 deletions reqtest/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package reqtest

import (
"bytes"
"encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"io"
"net/http"
Expand All @@ -20,7 +21,7 @@ import (
// wrapped marshaling error.
func ReplayJSON(code int, obj any) requests.Transport {
return requests.RoundTripFunc(func(req *http.Request) (*http.Response, error) {
data, err := json.MarshalIndent(obj, "", " ")
data, err := json.Marshal(obj, jsontext.WithIndent(" "))
if err != nil {
return nil, fmt.Errorf("marshal error in ReplayJSON: %v", err)
}
Expand Down
10 changes: 7 additions & 3 deletions serializer.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package requests

import (
"encoding/json"
"encoding/json/v2"
)

// Serializer is a function
Expand All @@ -17,8 +17,12 @@ 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
JSONSerializer Serializer = func(v any) ([]byte, error) {
return json.Marshal(v)
}
// JSONDeserializer is used by [ToJSON] and [Builder.ToJSON].
// The default deserializer may be changed in a future version of requests.
JSONDeserializer Deserializer = json.Unmarshal
JSONDeserializer Deserializer = func(data []byte, v any) error {
return json.Unmarshal(data, v)
}
)
6 changes: 6 additions & 0 deletions validator_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ var ErrInvalidHandled = errors.New("handled recovery from invalid response")
func ErrorJSON(v any) ResponseHandler {
return ValidatorHandler(DefaultValidator, ToJSON(v))
}

// ErrorJSONV1 is like [ErrorJSON]
// but decodes the response using [ToJSONV1].
func ErrorJSONV1(v any) ResponseHandler {
return ValidatorHandler(DefaultValidator, ToJSONV1(v))
}
Loading