1
0
mirror of https://github.com/ggicci/httpin.git synced 2024-11-24 08:32:45 +02:00

feat: add json & xml body decoder

This commit is contained in:
Ggicci 2021-07-16 19:08:57 +08:00
parent f634165786
commit 2526bcff53
5 changed files with 177 additions and 13 deletions

26
body.go
View File

@ -1,6 +1,26 @@
package httpin
func bodyDecoder(ctx *DirectiveContext) error {
// TODO(ggicci): implement this
return nil
import (
"encoding/json"
"encoding/xml"
"net/http"
"reflect"
)
type JSONBody struct{}
type XMLBody struct{}
var (
typeJSONBody = reflect.TypeOf(JSONBody{})
typeXMLBody = reflect.TypeOf(XMLBody{})
)
func decodeJSONBody(req *http.Request, rv reflect.Value) error {
obj := rv.Interface()
return json.NewDecoder(req.Body).Decode(&obj)
}
func decodeXMLBody(req *http.Request, rv reflect.Value) error {
obj := rv.Interface()
return xml.NewDecoder(req.Body).Decode(&obj)
}

View File

@ -1 +1,107 @@
package httpin
import (
"io"
"net/http"
"reflect"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
type LanguageLevel struct {
Language string `json:"lang" xml:"lang"`
Level int `json:"level" xml:"level"`
}
type BodyPayload struct {
Name string `json:"name" xml:"name"`
Age int `json:"age" xml:"age"`
IsNative bool `json:"is_native" xml:"is_native"`
Hobbies []string `json:"hobbies" xml:"hobbies"`
Languages []*LanguageLevel `json:"languages" xml:"languages"`
}
type JSONBodyPayload struct {
JSONBody
BodyPayload
}
type XMLBodyPayload struct {
XMLBody
BodyPayload
}
func TestJSONBody(t *testing.T) {
Convey("json: parse HTTP body in JSON", t, func() {
resolver, err := buildResolverTree(reflect.TypeOf(JSONBodyPayload{}))
So(err, ShouldBeNil)
So(resolver, ShouldNotBeNil)
r, _ := http.NewRequest("GET", "https://example.com", nil)
r.Body = io.NopCloser(strings.NewReader(`{
"name": "Elia",
"is_native": false,
"age": 14,
"hobbies": ["Gaming", "Drawing"],
"languages": [
{"lang": "English", "level": 10},
{"lang": "Japanese", "level": 3}
]
}`))
res, err := resolver.resolve(r)
So(err, ShouldBeNil)
So(res.Interface(), ShouldResemble, &JSONBodyPayload{
BodyPayload: BodyPayload{
Name: "Elia",
Age: 14,
IsNative: false,
Hobbies: []string{"Gaming", "Drawing"},
Languages: []*LanguageLevel{
{"English", 10},
{"Japanese", 3},
},
},
})
})
}
func TestXMLBody(t *testing.T) {
Convey("json: parse HTTP body in XML", t, func() {
resolver, err := buildResolverTree(reflect.TypeOf(XMLBodyPayload{}))
So(err, ShouldBeNil)
So(resolver, ShouldNotBeNil)
r, _ := http.NewRequest("GET", "https://example.com", nil)
r.Body = io.NopCloser(strings.NewReader(`<BodyPayload>
<name>Elia</name>
<age>14</age>
<is_native>false</is_native>
<hobbies>Gaming</hobbies>
<hobbies>Drawing</hobbies>
<languages>
<lang>English</lang>
<level>10</level>
</languages>
<languages>
<lang>Japanese</lang>
<level>3</level>
</languages>
</BodyPayload>`))
res, err := resolver.resolve(r)
So(err, ShouldBeNil)
So(res.Interface(), ShouldResemble, &XMLBodyPayload{
BodyPayload: BodyPayload{
Name: "Elia",
Age: 14,
IsNative: false,
Hobbies: []string{"Gaming", "Drawing"},
Languages: []*LanguageLevel{
{"English", 10},
{"Japanese", 3},
},
},
})
})
}

View File

@ -17,7 +17,6 @@ func init() {
RegisterDirectiveExecutor("form", DirectiveExecutorFunc(formValueExtractor))
RegisterDirectiveExecutor("header", DirectiveExecutorFunc(headerValueExtractor))
RegisterDirectiveExecutor("body", DirectiveExecutorFunc(bodyDecoder))
RegisterDirectiveExecutor("required", DirectiveExecutorFunc(required))
}

View File

@ -59,8 +59,6 @@ type ProductQuery struct {
SortDesc []bool `in:"form=sort_desc"`
Pagination
Authorization
Patch map[string]interface{} `in:"body=json"`
}
type ObjectID struct {

View File

@ -17,10 +17,42 @@ type fieldResolver struct {
Fields []*fieldResolver
}
func (r *fieldResolver) isBodyResolverAnnotation() bool {
return r.Type == typeJSONBody || r.Type == typeXMLBody
}
func (r *fieldResolver) isRoot() bool {
return r.Field.Name == ""
}
func (r *fieldResolver) resolve(req *http.Request) (reflect.Value, error) {
rv := reflect.New(r.Type)
// Execute directives.
// Resolve HTTP request body if necessary.
if r.isRoot() {
// Check if there's an annotation field.
var bodyType reflect.Type
for _, field := range r.Fields {
if field.isBodyResolverAnnotation() {
bodyType = field.Type
break
}
}
switch bodyType {
case typeJSONBody:
if err := decodeJSONBody(req, rv); err != nil {
return rv, fmt.Errorf("decode JSON body: %w", err)
}
return rv, nil
case typeXMLBody:
if err := decodeXMLBody(req, rv); err != nil {
return rv, fmt.Errorf("decode XML body: %w", err)
}
return rv, nil
}
}
// Then execute directives.
if len(r.Directives) > 0 {
inheritableContext := context.Background()
for _, dir := range r.Directives {
@ -82,22 +114,31 @@ func buildResolverTree(t reflect.Type) (*fieldResolver, error) {
}
func buildFieldResolver(parent *fieldResolver, field reflect.StructField) (*fieldResolver, error) {
directives, err := parseStructTag(field)
if err != nil {
return nil, fmt.Errorf("parse struct tag failed: %w", err)
}
t := field.Type
path := make([]string, len(parent.Path)+1)
copy(path, parent.Path)
path[len(path)-1] = field.Name
root := &fieldResolver{
Type: t,
Field: field,
Path: path,
Directives: directives,
Directives: make([]*directive, 0),
}
if field.Anonymous && t.Kind() == reflect.Struct && len(directives) == 0 {
// Skip parsing struct tags if met body resolver annotation field.
if root.isBodyResolverAnnotation() {
return root, nil
}
// Parse the struct tag and build the directives.
if directives, err := parseStructTag(field); err != nil {
return nil, fmt.Errorf("parse struct tag failed: %w", err)
} else {
root.Directives = directives
}
if field.Anonymous && t.Kind() == reflect.Struct && len(root.Directives) == 0 {
for i := 0; i < t.NumField(); i++ {
fieldResolver, err := buildFieldResolver(root, t.Field(i))
if err != nil {