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 serializers #91

Merged
merged 15 commits into from
Oct 6, 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
15 changes: 11 additions & 4 deletions body.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package requests

import (
"bytes"
"encoding/json"
"io"
"net/url"
"os"
Expand Down Expand Up @@ -44,17 +43,25 @@ func BodyBytes(b []byte) BodyGetter {
}
}

// BodyJSON is a BodyGetter that marshals a JSON object.
func BodyJSON(v any) BodyGetter {
// 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 := json.Marshal(v)
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) {
Expand Down
123 changes: 123 additions & 0 deletions builder_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package requests_test
import (
"bytes"
"context"
"encoding/binary"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -583,3 +585,124 @@ func ExampleBuilder_ErrorJSON() {
// X 1
// brewing
}

func ExampleBuilder_BodySerializer() {
// Have some binary data
data := struct {
Header [3]byte
Payload uint32
}{
Header: [3]byte([]byte("ABC")),
Payload: 0xbadc0fee,
}
// Serialize it by just shoving data onto the wire
serializer := func(v any) ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, v)
return buf.Bytes(), err
}
// Make a request using the serializer
req, err := requests.
New().
BodySerializer(serializer, data).
Request(context.Background())
if err != nil {
panic(err)
}
b, err := io.ReadAll(req.Body)
if err != nil {
panic(err)
}
// Request body is just serialized bytes
fmt.Printf("%q", b)
// Output:
// "ABC\xba\xdc\x0f\xee"
}

func ExampleBuilder_ToDeserializer() {
trans := requests.ReplayString(
"HTTP/1.1 200 OK\r\n\r\nXYZ\x00\xde\xca\xff",
)
// Have some binary structure
var data struct {
Header [3]byte
Payload uint32
}
// Deserialize it by just pulling data off the wire
deserializer := func(data []byte, v any) error {
buf := bytes.NewReader(data)
return binary.Read(buf, binary.BigEndian, v)
}
// Make a request using the deserializer
err := requests.
New().
Transport(trans).
ToDeserializer(deserializer, &data).
Fetch(context.Background())
if err != nil {
panic(err)
}

// We read the data out of the response body
fmt.Printf("%q, %X", data.Header, data.Payload)
// Output:
// "XYZ", DECAFF
}

func ExampleBuilder_BodyJSON() {
// Restore defaults after this test
defaultSerializer := requests.JSONSerializer
defer func() {
requests.JSONSerializer = defaultSerializer
}()

data := struct {
A string `json:"a"`
B int `json:"b"`
C []bool `json:"c"`
}{
"Hello", 42, []bool{true, false},
}

// Build a request using the default JSON serializer
req, err := requests.
New().
BodyJSON(&data).
Request(context.Background())
if err != nil {
panic(err)
}

// JSON is packed in with no whitespace
io.Copy(os.Stdout, req.Body)
fmt.Println()

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

// Build a new request using the new indenting serializer
req, err = requests.
New().
BodyJSON(&data).
Request(context.Background())
if err != nil {
panic(err)
}

// Now the request body is indented
io.Copy(os.Stdout, req.Body)
fmt.Println()

// Output:
// {"a":"Hello","b":42,"c":[true,false]}
// {
// "a": "Hello",
// "b": 42,
// "c": [
// true,
// false
// ]
// }
}
19 changes: 18 additions & 1 deletion builder_extras.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,15 @@ 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".
func (rb *Builder) BodyJSON(v any) *Builder {
return rb.
Expand Down Expand Up @@ -169,7 +177,16 @@ func (rb *Builder) CheckPeek(n int, f func([]byte) error) *Builder {
return rb.AddValidator(CheckPeek(n, f))
}

// ToJSON sets the Builder to decode a response as a JSON object
// 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))
}
Expand Down
14 changes: 10 additions & 4 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package requests
import (
"bufio"
"bytes"
"encoding/json"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -39,20 +38,27 @@ func consumeBody(res *http.Response) (err error) {
return err
}

// ToJSON decodes a response as a JSON object.
func ToJSON(v any) ResponseHandler {
// 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 = json.Unmarshal(data, v); err != nil {
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 {
Expand Down
9 changes: 1 addition & 8 deletions reqxml/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@ package reqxml

import (
"encoding/xml"
"io"

"github.com/carlmjohnson/requests"
)

// Body is a BodyGetter that marshals a XML object.
func Body(v any) requests.BodyGetter {
return func() (io.ReadCloser, error) {
b, err := xml.Marshal(v)
if err != nil {
return nil, err
}
return requests.BodyBytes(b)()
}
return requests.BodySerializer(xml.Marshal, v)
}

// BodyConfig sets the Builder's request body to the marshaled XML.
Expand Down
13 changes: 1 addition & 12 deletions reqxml/to.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,11 @@ package reqxml

import (
"encoding/xml"
"io"
"net/http"

"github.com/carlmjohnson/requests"
)

// To decodes a response as an XML object.
func To(v any) requests.ResponseHandler {
return func(res *http.Response) error {
data, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if err = xml.Unmarshal(data, v); err != nil {
return err
}
return nil
}
return requests.ToDeserializer(xml.Unmarshal, v)
}
24 changes: 24 additions & 0 deletions serializer.go
Original file line number Diff line number Diff line change
@@ -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
)