Skip to content

Commit 53f3ddb

Browse files
joeybloggsjoeybloggs
authored andcommitted
Merge branch 'add-unmarshal-logic'
2 parents b06cadc + 8942f49 commit 53f3ddb

File tree

6 files changed

+271
-1
lines changed

6 files changed

+271
-1
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
##LARS
22
<img align="right" src="https://raw.githubusercontent.com/go-playground/lars/master/examples/README/test.gif">
3-
![Project status](https://img.shields.io/badge/version-3.0-green.svg)
3+
![Project status](https://img.shields.io/badge/version-3.1-green.svg)
44
[![Build Status](https://semaphoreci.com/api/v1/projects/4351aa2d-2f94-40be-a6ef-85c248490378/679708/badge.svg)](https://semaphoreci.com/joeybloggs/lars)
55
[![Coverage Status](https://coveralls.io/repos/github/go-playground/lars/badge.svg?branch=master)](https://coveralls.io/github/go-playground/lars?branch=master)
66
[![Go Report Card](https://goreportcard.com/badge/go-playground/lars)](https://goreportcard.com/report/go-playground/lars)
@@ -179,6 +179,19 @@ func Home(c *MyContext) {
179179
}
180180
```
181181

182+
Decoding Body
183+
-------------
184+
For full example see [here](https://github.com/go-playground/lars/blob/master/examples/decode/main.go).
185+
currently JSON, XML, FORM + Multipart Form's are support out of the box.
186+
```go
187+
// first argument denotes yes or no I would like URL query parameter fields
188+
// to be included. i.e. 'id' in route '/user/:id' should it be included.
189+
// run, then change to false and you'll see user.ID is not populated.
190+
if err := c.Decode(true, maxBytes, &user); err != nil {
191+
log.Println(err)
192+
}
193+
```
194+
182195
Misc
183196
-----
184197
```go

context.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type Context interface {
5757
TextBytes(int, []byte) error
5858
Attachment(r io.Reader, filename string) (err error)
5959
Inline(r io.Reader, filename string) (err error)
60+
Decode(includeFormQueryParams bool, maxMemory int64, v interface{}) (err error)
6061
BaseContext() *Ctx
6162
}
6263

@@ -73,6 +74,7 @@ type Ctx struct {
7374
index int
7475
formParsed bool
7576
multipartFormParsed bool
77+
l *LARS
7678
}
7779

7880
var _ context.Context = &Ctx{}
@@ -82,6 +84,7 @@ func NewContext(l *LARS) *Ctx {
8284

8385
c := &Ctx{
8486
params: make(Params, l.mostParams),
87+
l: l,
8588
}
8689

8790
c.response = newResponse(nil, c)
@@ -412,6 +415,51 @@ func (c *Ctx) Inline(r io.Reader, filename string) (err error) {
412415
return
413416
}
414417

418+
// Decode takes the request and attempts to discover it's content type via
419+
// the http headers and then decode the request body into the provided struct.
420+
// Example if header was "application/json" would decode using
421+
// json.NewDecoder(io.LimitReader(c.request.Body, maxMemory)).Decode(v).
422+
func (c *Ctx) Decode(includeFormQueryParams bool, maxMemory int64, v interface{}) (err error) {
423+
424+
c.l.initFormDecoder()
425+
426+
typ := c.request.Header.Get(ContentType)
427+
428+
if idx := strings.Index(typ, ";"); idx != -1 {
429+
typ = typ[:idx]
430+
}
431+
432+
switch typ {
433+
434+
case ApplicationJSON:
435+
err = json.NewDecoder(io.LimitReader(c.request.Body, maxMemory)).Decode(v)
436+
437+
case ApplicationXML:
438+
err = xml.NewDecoder(io.LimitReader(c.request.Body, maxMemory)).Decode(v)
439+
440+
case ApplicationForm:
441+
442+
if err = c.ParseForm(); err == nil {
443+
if includeFormQueryParams {
444+
err = c.l.formDecoder.Decode(v, c.request.Form)
445+
} else {
446+
err = c.l.formDecoder.Decode(v, c.request.PostForm)
447+
}
448+
}
449+
450+
case MultipartForm:
451+
452+
if err = c.ParseMultipartForm(maxMemory); err == nil {
453+
if includeFormQueryParams {
454+
err = c.l.formDecoder.Decode(v, c.request.Form)
455+
} else {
456+
err = c.l.formDecoder.Decode(v, c.request.MultipartForm.Value)
457+
}
458+
}
459+
}
460+
return
461+
}
462+
415463
// golang.org/x/net/context functions to comply with context.Context interface and keep context update on lars.Context object
416464

417465
// Context returns the request's context. To change the context, use

context_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package lars
22

33
import (
4+
"bytes"
45
"encoding/xml"
56
"io"
7+
"mime/multipart"
68
"net/http"
79
"net/http/httptest"
10+
"net/url"
811
"os"
912
"reflect"
13+
"strings"
1014
"testing"
1115
"time"
1216

@@ -26,6 +30,127 @@ import (
2630
// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html
2731
//
2832

33+
func TestDecode(t *testing.T) {
34+
35+
type TestStruct struct {
36+
ID int `form:"id"`
37+
Posted string
38+
MultiPartPosted string
39+
}
40+
41+
test := new(TestStruct)
42+
43+
l := New()
44+
l.Post("/decode/:id", func(c Context) {
45+
err := c.Decode(true, 16<<10, test)
46+
Equal(t, err, nil)
47+
})
48+
l.Post("/decode2/:id", func(c Context) {
49+
err := c.Decode(false, 16<<10, test)
50+
Equal(t, err, nil)
51+
})
52+
53+
NotEqual(t, l.BuiltInFormDecoder(), nil)
54+
55+
hf := l.Serve()
56+
57+
form := url.Values{}
58+
form.Add("Posted", "value")
59+
60+
r, _ := http.NewRequest(POST, "/decode/13", strings.NewReader(form.Encode()))
61+
r.Header.Set(ContentType, ApplicationForm)
62+
w := httptest.NewRecorder()
63+
64+
hf.ServeHTTP(w, r)
65+
66+
Equal(t, w.Code, http.StatusOK)
67+
Equal(t, test.ID, 13)
68+
Equal(t, test.Posted, "value")
69+
Equal(t, test.MultiPartPosted, "")
70+
71+
test = new(TestStruct)
72+
r, _ = http.NewRequest(POST, "/decode2/13", strings.NewReader(form.Encode()))
73+
r.Header.Set(ContentType, ApplicationForm)
74+
w = httptest.NewRecorder()
75+
76+
hf.ServeHTTP(w, r)
77+
78+
Equal(t, w.Code, http.StatusOK)
79+
Equal(t, test.ID, 0)
80+
Equal(t, test.Posted, "value")
81+
Equal(t, test.MultiPartPosted, "")
82+
83+
body := &bytes.Buffer{}
84+
writer := multipart.NewWriter(body)
85+
86+
err := writer.WriteField("MultiPartPosted", "value")
87+
Equal(t, err, nil)
88+
89+
// Don't forget to close the multipart writer.
90+
// If you don't close it, your request will be missing the terminating boundary.
91+
err = writer.Close()
92+
Equal(t, err, nil)
93+
94+
test = new(TestStruct)
95+
r, _ = http.NewRequest(POST, "/decode/13", body)
96+
r.Header.Set(ContentType, writer.FormDataContentType())
97+
w = httptest.NewRecorder()
98+
99+
hf.ServeHTTP(w, r)
100+
Equal(t, w.Code, http.StatusOK)
101+
Equal(t, test.ID, 13)
102+
Equal(t, test.Posted, "")
103+
Equal(t, test.MultiPartPosted, "value")
104+
105+
body = &bytes.Buffer{}
106+
writer = multipart.NewWriter(body)
107+
108+
err = writer.WriteField("MultiPartPosted", "value")
109+
Equal(t, err, nil)
110+
111+
// Don't forget to close the multipart writer.
112+
// If you don't close it, your request will be missing the terminating boundary.
113+
err = writer.Close()
114+
Equal(t, err, nil)
115+
116+
test = new(TestStruct)
117+
r, _ = http.NewRequest(POST, "/decode2/13", body)
118+
r.Header.Set(ContentType, writer.FormDataContentType())
119+
w = httptest.NewRecorder()
120+
121+
hf.ServeHTTP(w, r)
122+
Equal(t, w.Code, http.StatusOK)
123+
Equal(t, test.ID, 0)
124+
Equal(t, test.Posted, "")
125+
Equal(t, test.MultiPartPosted, "value")
126+
127+
jsonBody := `{"ID":13,"Posted":"value","MultiPartPosted":"value"}`
128+
test = new(TestStruct)
129+
r, _ = http.NewRequest(POST, "/decode/13", strings.NewReader(jsonBody))
130+
r.Header.Set(ContentType, ApplicationJSON)
131+
w = httptest.NewRecorder()
132+
133+
hf.ServeHTTP(w, r)
134+
135+
Equal(t, w.Code, http.StatusOK)
136+
Equal(t, test.ID, 13)
137+
Equal(t, test.Posted, "value")
138+
Equal(t, test.MultiPartPosted, "value")
139+
140+
xmlBody := `<TestStruct><ID>13</ID><Posted>value</Posted><MultiPartPosted>value</MultiPartPosted></TestStruct>`
141+
test = new(TestStruct)
142+
r, _ = http.NewRequest(POST, "/decode/13", strings.NewReader(xmlBody))
143+
r.Header.Set(ContentType, ApplicationXML)
144+
w = httptest.NewRecorder()
145+
146+
hf.ServeHTTP(w, r)
147+
148+
Equal(t, w.Code, http.StatusOK)
149+
Equal(t, test.ID, 13)
150+
Equal(t, test.Posted, "value")
151+
Equal(t, test.MultiPartPosted, "value")
152+
}
153+
29154
func TestStream(t *testing.T) {
30155
l := New()
31156

doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,19 @@ example context + custom handlers
152152
...
153153
}
154154
155+
Decoding Body
156+
157+
For full example see https://github.com/go-playground/lars/blob/master/examples/decode/main.go
158+
currently JSON, XML, FORM + Multipart Form's are support out of the box.
159+
160+
// first argument denotes yes or no I would like URL query parameter fields
161+
// to be included. i.e. 'id' in route '/user/:id' should it be included.
162+
// run, then change to false and you'll see user.ID is not populated.
163+
if err := c.Decode(true, maxBytes, &user); err != nil {
164+
log.Println(err)
165+
}
166+
167+
155168
Misc
156169
157170
misc examples and noteworthy features

examples/decode/main.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"net/url"
7+
"time"
8+
9+
"github.com/go-playground/lars"
10+
)
11+
12+
const (
13+
// used for io.LimitReader(...) for safetly!
14+
maxBytes = 16 << 10
15+
)
16+
17+
// User ...
18+
type User struct {
19+
ID int `form:"id"`
20+
Name string
21+
}
22+
23+
func main() {
24+
25+
l := lars.New()
26+
l.Post("/user/:id", PostUser)
27+
28+
go simulateFormPost()
29+
30+
http.ListenAndServe(":3007", l.Serve())
31+
}
32+
33+
// PostUser ...
34+
func PostUser(c lars.Context) {
35+
var user User
36+
37+
// first argument denotes yes or no I would like URL query parameter fields
38+
// to be included. i.e. 'id' in route '/user/:id' should it be included.
39+
// run, then change to false and you'll see user.ID is not populated.
40+
if err := c.Decode(true, maxBytes, &user); err != nil {
41+
log.Println(err)
42+
}
43+
44+
log.Printf("%#v", user)
45+
}
46+
47+
func simulateFormPost() {
48+
time.Sleep(1000)
49+
http.PostForm("http://localhost:3007/user/13", url.Values{"Name": {"joeybloggs"}})
50+
}

lars.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"reflect"
77
"strings"
88
"sync"
9+
10+
"github.com/go-playground/form"
911
)
1012

1113
// HTTP Constant Terms and Variables
@@ -150,6 +152,10 @@ type LARS struct {
150152
// if enabled automatically handles OPTION requests; manually configured OPTION
151153
// handlers take precidence. default true
152154
automaticallyHandleOPTIONS bool
155+
156+
// form decoder + once initialization
157+
formDecoder *form.Decoder
158+
formDecoderInit sync.Once
153159
}
154160

155161
// RouteMap contains a single routes full path
@@ -207,6 +213,21 @@ func New() *LARS {
207213
return l
208214
}
209215

216+
// BuiltInFormDecoder returns the built in form decoder github.com/go-playground/form
217+
// in order for custom type to be registered.
218+
func (l *LARS) BuiltInFormDecoder() *form.Decoder {
219+
220+
l.initFormDecoder()
221+
222+
return l.formDecoder
223+
}
224+
225+
func (l *LARS) initFormDecoder() {
226+
l.formDecoderInit.Do(func() {
227+
l.formDecoder = form.NewDecoder()
228+
})
229+
}
230+
210231
// RegisterCustomHandler registers a custom handler that gets wrapped by HandlerFunc
211232
func (l *LARS) RegisterCustomHandler(customType interface{}, fn CustomHandlerFunc) {
212233

0 commit comments

Comments
 (0)