Skip to content

Commit 9ebd380

Browse files
committed
Added lots of httprequest and response
1 parent 506f330 commit 9ebd380

File tree

7 files changed

+966
-56
lines changed

7 files changed

+966
-56
lines changed

pkg/httprequest/body.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package httprequest
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"mime"
7+
"mime/multipart"
8+
"net/http"
9+
"reflect"
10+
"slices"
11+
"strings"
12+
13+
// Namespace imports
14+
. "github.com/djthorpe/go-errors"
15+
)
16+
17+
///////////////////////////////////////////////////////////////////////////////
18+
// GLOBALS
19+
20+
const (
21+
ContentTypeJson = "application/json"
22+
ContentTypeTextXml = "text/xml"
23+
ContentTypeApplicationXml = "application/xml"
24+
ContentTypeText = "text/"
25+
ContentTypeBinary = "application/octet-stream"
26+
ContentTypeFormData = "multipart/form-data"
27+
ContentTypeUrlEncoded = "application/x-www-form-urlencoded"
28+
)
29+
30+
const (
31+
maxMemory = 10 << 20 // 10 MB in-memory cache for multipart form
32+
)
33+
34+
///////////////////////////////////////////////////////////////////////////////
35+
// PUBLIC METHODS
36+
37+
// Body reads the body of an HTTP request and decodes it into v.
38+
// You can include the mimetypes that are acceptable, otherwise it will read
39+
// the body based on the content type. If does not close the body of the
40+
// request, which should be done by the caller.
41+
// v can be a io.Writer, in which case the body is copied into it.
42+
// v can be a struct, in which case the body is decoded into it.
43+
func Body(v any, r *http.Request, accept ...string) error {
44+
// Parse the content type
45+
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
46+
if err != nil {
47+
return err
48+
} else {
49+
contentType = strings.ToLower(contentType)
50+
}
51+
52+
// Check whether we'll accept it
53+
if len(accept) > 0 && !slices.Contains(accept, contentType) {
54+
return ErrBadParameter.Withf("unexpected content type %q", contentType)
55+
}
56+
57+
// If v is an io.Writer, then copy the body into it
58+
if v, ok := v.(io.Writer); ok {
59+
if _, err := io.Copy(v, r.Body); err != nil {
60+
return err
61+
}
62+
return nil
63+
}
64+
65+
// Read the body - we can read JSON, form data or url encoded data
66+
// TODO: We should also be able to read XML
67+
switch {
68+
case contentType == ContentTypeJson:
69+
return readJson(v, r)
70+
case contentType == ContentTypeFormData:
71+
return readFormData(v, r)
72+
case contentType == ContentTypeUrlEncoded:
73+
return readForm(v, r)
74+
case strings.HasPrefix(contentType, ContentTypeText):
75+
return readText(v, r)
76+
}
77+
78+
// Report error
79+
return ErrBadParameter.Withf("unsupported content type %q", contentType)
80+
}
81+
82+
///////////////////////////////////////////////////////////////////////////////
83+
// PRIVATE METHODS
84+
85+
func readJson(v any, r *http.Request) error {
86+
return json.NewDecoder(r.Body).Decode(v)
87+
}
88+
89+
func readForm(v any, r *http.Request) error {
90+
if err := r.ParseForm(); err != nil {
91+
return err
92+
} else {
93+
return Query(v, r.Form)
94+
}
95+
}
96+
97+
func readFormData(v any, r *http.Request) error {
98+
if err := r.ParseMultipartForm(maxMemory); err != nil {
99+
return err
100+
} else if err := readFiles(v, r.MultipartForm.File); err != nil {
101+
return err
102+
} else {
103+
return Query(v, r.MultipartForm.Value)
104+
}
105+
}
106+
107+
func readFiles(v any, files map[string][]*multipart.FileHeader) error {
108+
return mapFields(v, func(key string, value reflect.Value) error {
109+
src, exists := files[key]
110+
if !exists || len(src) == 0 {
111+
setZeroValue(value)
112+
return nil
113+
}
114+
// Set a *multipart.FileHeader or []*multipart.FileHeader with the source
115+
return setFile(value, src)
116+
})
117+
}
118+
119+
func readText(v any, r *http.Request) error {
120+
switch v := v.(type) {
121+
case *string:
122+
data, err := io.ReadAll(r.Body)
123+
if err != nil {
124+
return err
125+
}
126+
*v = string(data)
127+
default:
128+
return ErrBadParameter.Withf("unsupported type: %T", v)
129+
}
130+
131+
// Return success
132+
return nil
133+
}

pkg/httprequest/body_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package httprequest_test
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"mime/multipart"
7+
"net/http"
8+
"net/http/httptest"
9+
"net/url"
10+
"strconv"
11+
"testing"
12+
13+
"github.com/mutablelogic/go-server/pkg/httprequest"
14+
"github.com/mutablelogic/go-server/pkg/httpresponse"
15+
"github.com/stretchr/testify/assert"
16+
17+
// Namespace imports
18+
. "github.com/djthorpe/go-errors"
19+
)
20+
21+
func Test_body_00(t *testing.T) {
22+
assert := assert.New(t)
23+
24+
t.Run("EOF", func(t *testing.T) {
25+
// We should receive an EOF error here - no body
26+
req := httptest.NewRequest("GET", "/", nil)
27+
req.Header.Set("Content-Type", "application/json")
28+
err := httprequest.Body(nil, req)
29+
assert.ErrorIs(err, io.EOF)
30+
})
31+
32+
t.Run("ContentType1", func(t *testing.T) {
33+
// We should receive a badparameter error - invalid content type
34+
req := httptest.NewRequest("GET", "/", nil)
35+
req.Header.Set("Content-Type", "application/test")
36+
err := httprequest.Body(nil, req)
37+
assert.ErrorIs(err, ErrBadParameter)
38+
})
39+
40+
t.Run("ContentType2", func(t *testing.T) {
41+
// We should receive a badparameter error - invalid content type
42+
req := httptest.NewRequest("GET", "/", nil)
43+
req.Header.Set("Content-Type", "text/plain")
44+
err := httprequest.Body(nil, req, httpresponse.ContentTypeJSON)
45+
assert.ErrorIs(err, ErrBadParameter)
46+
})
47+
48+
t.Run("Writer", func(t *testing.T) {
49+
var in, out bytes.Buffer
50+
in.WriteString("Hello, World!")
51+
52+
// We should receive the text
53+
req := httptest.NewRequest("GET", "/", &in)
54+
req.Header.Set("Content-Type", "text/plain")
55+
err := httprequest.Body(&out, req)
56+
assert.NoError(err)
57+
assert.Equal("Hello, World!", out.String())
58+
})
59+
60+
t.Run("Text", func(t *testing.T) {
61+
var in bytes.Buffer
62+
var out string
63+
in.WriteString("Hello, World!")
64+
65+
// We should receive the text
66+
req := httptest.NewRequest("GET", "/", &in)
67+
req.Header.Set("Content-Type", "text/plain")
68+
err := httprequest.Body(&out, req)
69+
assert.NoError(err)
70+
assert.Equal("Hello, World!", out)
71+
})
72+
73+
t.Run("JSON", func(t *testing.T) {
74+
var in bytes.Buffer
75+
var out string
76+
in.WriteString(strconv.Quote("Hello, World!"))
77+
78+
// We should receive the JSON and decode it
79+
req := httptest.NewRequest("GET", "/", &in)
80+
req.Header.Set("Content-Type", "application/json")
81+
err := httprequest.Body(&out, req)
82+
assert.NoError(err)
83+
assert.Equal("Hello, World!", out)
84+
})
85+
86+
t.Run("Form", func(t *testing.T) {
87+
values := url.Values{
88+
"key1": []string{"Hello, World!"},
89+
}
90+
var in bytes.Buffer
91+
var out struct {
92+
Key1 string `json:"key1"`
93+
}
94+
in.WriteString(values.Encode())
95+
96+
// POST form data
97+
req := httptest.NewRequest("POST", "/", &in)
98+
req.Header.Set("Content-Type", httprequest.ContentTypeUrlEncoded)
99+
err := httprequest.Body(&out, req)
100+
assert.NoError(err)
101+
assert.Equal("Hello, World!", out.Key1)
102+
})
103+
104+
t.Run("File", func(t *testing.T) {
105+
var in bytes.Buffer
106+
var out struct {
107+
File *multipart.FileHeader `json:"file"`
108+
}
109+
110+
// instantiate multipart request
111+
multipartWriter := multipart.NewWriter(&in)
112+
113+
// add form field
114+
filePart, _ := multipartWriter.CreateFormFile("file", "file.txt")
115+
filePart.Write([]byte("Hello, World!"))
116+
117+
// Close the body - so it can be read
118+
multipartWriter.Close()
119+
120+
// Create the request
121+
req := httptest.NewRequest(http.MethodPost, "/file", &in)
122+
req.Header.Set("Content-Type", multipartWriter.FormDataContentType())
123+
124+
// Parse the request body
125+
err := httprequest.Body(&out, req)
126+
assert.NoError(err)
127+
assert.NotNil(out.File)
128+
129+
// Let's read the contents of the file
130+
file, err := out.File.Open()
131+
assert.NoError(err)
132+
defer file.Close()
133+
134+
// Read the contents of the file
135+
data, err := io.ReadAll(file)
136+
assert.NoError(err)
137+
assert.Equal("Hello, World!", string(data))
138+
})
139+
}

0 commit comments

Comments
 (0)