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

feat: allow registering custom body decoders

This commit is contained in:
Ggicci 2022-05-31 11:55:53 +08:00
parent dd0ebe0d70
commit 8522ea1250
6 changed files with 108 additions and 31 deletions

78
body.go
View File

@ -7,24 +7,49 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"io"
"reflect"
"strings"
)
type JSONBody struct{}
type XMLBody struct{}
var (
bodyTypeAnnotationJSON = reflect.TypeOf(JSONBody{})
bodyTypeAnnotationXML = reflect.TypeOf(XMLBody{})
)
const (
bodyTypeJSON = "json"
bodyTypeXML = "xml"
)
type (
JSONBody struct{}
XMLBody struct{}
BodyDecoder interface {
Decode(src io.Reader, dst interface{}) error
}
)
var (
bodyTypeAnnotationJSON = reflect.TypeOf(JSONBody{})
bodyTypeAnnotationXML = reflect.TypeOf(XMLBody{})
bodyDecoders = map[string]BodyDecoder{
bodyTypeJSON: &defaultJSONBodyDecoder{},
bodyTypeXML: &defaultXMLBodyDecoder{},
}
)
func RegisterBodyDecoder(bodyType string, decoder BodyDecoder) {
if _, ok := bodyDecoders[bodyType]; ok {
panic(fmt.Errorf("httpin: %w: %q", ErrDuplicateBodyDecoder, bodyType))
}
ReplaceBodyDecoder(bodyType, decoder)
}
func ReplaceBodyDecoder(bodyType string, decoder BodyDecoder) {
if bodyType == "" {
panic("httpin: body type cannot be empty")
}
bodyDecoders[bodyType] = decoder
}
func bodyDirectiveNormalizer(dir *Directive) error {
if len(dir.Argv) == 0 {
dir.Argv = []string{bodyTypeJSON}
@ -32,10 +57,9 @@ func bodyDirectiveNormalizer(dir *Directive) error {
dir.Argv[0] = strings.ToLower(dir.Argv[0])
var bodyType = dir.Argv[0]
if bodyType != bodyTypeJSON && bodyType != bodyTypeXML {
if _, ok := bodyDecoders[bodyType]; !ok {
return fmt.Errorf("%w: %q", ErrUnknownBodyType, bodyType)
}
return nil
}
@ -46,20 +70,22 @@ func bodyTypeString(bodyType reflect.Type) string {
case bodyTypeAnnotationXML:
return bodyTypeXML
default:
panic(ErrUnknownBodyType)
panic(fmt.Errorf("httpin: %w: %q", ErrUnknownBodyType, bodyType))
}
}
func bodyDecoder(ctx *DirectiveContext) error {
var err = ErrUnknownBodyType
switch ctx.Argv[0] { // body type
case bodyTypeJSON:
err = decodeJSONBody(ctx.Request, ctx.Value)
case bodyTypeXML:
err = decodeXMLBody(ctx.Request, ctx.Value)
var (
bodyType = ctx.Argv[0]
decoder = bodyDecoders[bodyType]
)
if decoder == nil {
return ErrUnknownBodyType
}
if err != nil {
obj := ctx.Value.Interface()
if err := decoder.Decode(ctx.Request.Body, &obj); err != nil {
return err
}
@ -67,12 +93,14 @@ func bodyDecoder(ctx *DirectiveContext) error {
return nil
}
func decodeJSONBody(req *http.Request, rv reflect.Value) error {
obj := rv.Interface()
return json.NewDecoder(req.Body).Decode(&obj)
type defaultJSONBodyDecoder struct{}
func (de *defaultJSONBodyDecoder) Decode(src io.Reader, dst interface{}) error {
return json.NewDecoder(src).Decode(dst)
}
func decodeXMLBody(req *http.Request, rv reflect.Value) error {
obj := rv.Interface()
return xml.NewDecoder(req.Body).Decode(&obj)
type defaultXMLBodyDecoder struct{}
func (de *defaultXMLBodyDecoder) Decode(src io.Reader, dst interface{}) error {
return xml.NewDecoder(src).Decode(dst)
}

View File

@ -215,3 +215,51 @@ func Test_bodyTypeString(t *testing.T) {
}, ShouldPanic)
})
}
type yamlBodyDecoder struct{}
func (de *yamlBodyDecoder) Decode(src io.Reader, dst interface{}) error {
// for test only
(*(*(dst.(*interface{}))).(*map[string]interface{})) = map[string]interface{}{
"version": 3,
}
return nil
}
type YamlInput struct {
Body map[string]interface{} `in:"body=yaml"`
}
type ThingWithUnknownBodyDecoder struct {
Body map[string]interface{} `in:"body=yml"`
}
func TestCustomBodyDecoder(t *testing.T) {
Convey("body: register new body decoder", t, func() {
So(func() { RegisterBodyDecoder("yaml", &yamlBodyDecoder{}) }, ShouldNotPanic)
resolver, err := buildResolverTree(reflect.TypeOf(YamlInput{}))
So(err, ShouldBeNil)
So(resolver, ShouldNotBeNil)
r, _ := http.NewRequest("GET", "https://example.com", nil)
r.Body = io.NopCloser(strings.NewReader(`version: "3"`))
res, err := resolver.resolve(r)
So(err, ShouldBeNil)
So(res.Interface(), ShouldResemble, &YamlInput{
Body: map[string]interface{}{
"version": 3,
},
})
})
Convey("body: panic on duplicate body decoder", t, func() {
So(func() { RegisterBodyDecoder("json", &yamlBodyDecoder{}) }, ShouldPanic)
So(func() { RegisterBodyDecoder("", &yamlBodyDecoder{}) }, ShouldPanic)
})
Convey("body: unknown body decoder", t, func() {
_, err := buildResolverTree(reflect.TypeOf(ThingWithUnknownBodyDecoder{}))
So(errors.Is(err, ErrUnknownBodyType), ShouldBeTrue)
})
}

View File

@ -34,7 +34,7 @@ func isTypeDecoder(decoder interface{}) bool {
// RegisterTypeDecoder registers a specific type decoder. Panics on conflicts.
func RegisterTypeDecoder(typ reflect.Type, decoder interface{}) {
if _, ok := decoders[typ]; ok {
panic(fmt.Errorf("%w: %q", ErrDuplicateTypeDecoder, typ))
panic(fmt.Errorf("httpin: %w: %q", ErrDuplicateTypeDecoder, typ))
}
ReplaceTypeDecoder(typ, decoder)
@ -43,11 +43,11 @@ func RegisterTypeDecoder(typ reflect.Type, decoder interface{}) {
// ReplaceTypeDecoder replaces a specific type decoder.
func ReplaceTypeDecoder(typ reflect.Type, decoder interface{}) {
if decoder == nil {
panic(fmt.Errorf("%w: %q", ErrNilTypeDecoder, typ))
panic(fmt.Errorf("httpin: %w: %q", ErrNilTypeDecoder, typ))
}
if !isTypeDecoder(decoder) {
panic(fmt.Errorf("%w: %q", ErrInvalidTypeDecoder, typ))
panic(fmt.Errorf("httpin: %w: %q", ErrInvalidTypeDecoder, typ))
}
decoders[typ] = decoder

View File

@ -41,7 +41,7 @@ type DirectiveNormalizer interface {
// taken or nil executor.
func RegisterDirectiveExecutor(name string, exe DirectiveExecutor, norm DirectiveNormalizer) {
if _, ok := executors[name]; ok {
panic(fmt.Errorf("%w: %q", ErrDuplicateExecutor, name))
panic(fmt.Errorf("httpin: %w: %q", ErrDuplicateExecutor, name))
}
ReplaceDirectiveExecutor(name, exe, norm)
}
@ -50,7 +50,7 @@ func RegisterDirectiveExecutor(name string, exe DirectiveExecutor, norm Directiv
// on duplicate names.
func ReplaceDirectiveExecutor(name string, exe DirectiveExecutor, norm DirectiveNormalizer) {
if exe == nil {
panic(fmt.Errorf("%w: %q", ErrNilExecutor, name))
panic(fmt.Errorf("httpin: %w: %q", ErrNilExecutor, name))
}
executors[name] = exe
normalizers[name] = norm

View File

@ -20,6 +20,7 @@ var (
ErrNilErrorHandler = errors.New("nil error handler")
ErrMaxMemoryTooSmall = errors.New("max memory too small")
ErrNilFile = errors.New("nil file")
ErrDuplicateBodyDecoder = errors.New("duplicate body decoder")
)
type UnsupportedTypeError struct {

View File

@ -44,7 +44,7 @@ func NewInput(inputStruct interface{}, opts ...Option) middleware {
func ReplaceDefaultErrorHandler(custom ErrorHandler) {
if custom == nil {
panic(fmt.Errorf("httpin: %v", ErrNilErrorHandler))
panic(fmt.Errorf("httpin: %w", ErrNilErrorHandler))
}
globalCustomErrorHandler = custom
}