feat: complete contract-driven business development and MySQL integration
This commit is contained in:
172
contract/bind.go
172
contract/bind.go
@@ -1,6 +1,7 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
)
|
||||
@@ -19,9 +21,17 @@ import (
|
||||
// strictly decoded, and limited to 1 MiB unless maxBytes is supplied. Query
|
||||
// parameters never overwrite path/body/header values. form means URL query only.
|
||||
func (b *Binder) Bind(r *http.Request, params map[string]string, target any, maxBytes ...int64) error {
|
||||
return b.BindWithPolicy(r, params, target, RequestPolicy{}, maxBytes...)
|
||||
}
|
||||
|
||||
// BindWithPolicy merges route overrides over the App request policy.
|
||||
func (b *Binder) BindWithPolicy(r *http.Request, params map[string]string, target any, policy RequestPolicy, maxBytes ...int64) error {
|
||||
if b == nil || r == nil || b.schemaOnly {
|
||||
return apierrors.Internal(errors.New("contract: nil binder or request"))
|
||||
}
|
||||
if b.HasMultipart() {
|
||||
return apierrors.Internal(errors.New("multipart fields require BindMultipart"))
|
||||
}
|
||||
v := reflect.ValueOf(target)
|
||||
if v.Kind() != reflect.Pointer || v.IsNil() || v.Elem().Type() != b.typ {
|
||||
return apierrors.Internal(errors.New("contract: target type mismatch"))
|
||||
@@ -33,54 +43,9 @@ func (b *Binder) Bind(r *http.Request, params map[string]string, target any, max
|
||||
if len(maxBytes) == 1 {
|
||||
limit = maxBytes[0]
|
||||
}
|
||||
fresh := reflect.New(b.typ).Elem()
|
||||
body := map[string]any{}
|
||||
if len(b.root.schema.Properties) > 0 {
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil || ct != "application/json" {
|
||||
return apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json")
|
||||
}
|
||||
if r.Body == nil {
|
||||
return invalid(errors.New("body required"))
|
||||
}
|
||||
if r.ContentLength > limit {
|
||||
return tooLarge()
|
||||
}
|
||||
reader := &io.LimitedReader{R: r.Body, N: limit + 1}
|
||||
dec := json.NewDecoder(reader)
|
||||
dec.UseNumber()
|
||||
value, err := decodeValue(dec, 0)
|
||||
if reader.N == 0 {
|
||||
return tooLarge()
|
||||
}
|
||||
if err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
return tooLarge()
|
||||
}
|
||||
return invalid(err)
|
||||
}
|
||||
_, err = dec.Token()
|
||||
if reader.N == 0 {
|
||||
return tooLarge()
|
||||
}
|
||||
if err != io.EOF {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
return tooLarge()
|
||||
}
|
||||
return invalid(errors.New("exactly one JSON value required"))
|
||||
}
|
||||
var ok bool
|
||||
body, ok = value.(map[string]any)
|
||||
if !ok {
|
||||
return invalid(errors.New("body must be an object"))
|
||||
}
|
||||
for key := range body {
|
||||
if _, ok = b.root.schema.Properties[key]; !ok {
|
||||
return invalid(fmt.Errorf("unknown body field %s", key))
|
||||
}
|
||||
}
|
||||
policy = MergePolicy(RequestPolicyFromContext(r.Context()), policy)
|
||||
if err := policy.Validate(); err != nil {
|
||||
return apierrors.Internal(err)
|
||||
}
|
||||
query := url.Values{}
|
||||
if r.URL != nil {
|
||||
@@ -90,10 +55,121 @@ func (b *Binder) Bind(r *http.Request, params map[string]string, target any, max
|
||||
return invalid(errors.New("malformed query"))
|
||||
}
|
||||
}
|
||||
if policy.UnknownQuery == "reject" {
|
||||
allowed := map[string]bool{}
|
||||
for _, f := range b.root.fields {
|
||||
if f.source == "form" {
|
||||
allowed[f.name] = true
|
||||
}
|
||||
}
|
||||
for key := range query {
|
||||
if !allowed[key] {
|
||||
return invalid(fmt.Errorf("unknown query field %s", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
fresh := reflect.New(b.typ).Elem()
|
||||
body := map[string]any{}
|
||||
bodyAbsent := false
|
||||
bodyMode := policy.Body
|
||||
if bodyMode == "" || bodyMode == "auto" {
|
||||
if len(b.root.schema.Properties) > 0 {
|
||||
bodyMode = "required"
|
||||
} else {
|
||||
bodyMode = "auto"
|
||||
}
|
||||
}
|
||||
if (bodyMode == "none" || bodyMode == "emptyObject") && len(b.root.schema.Properties) > 0 {
|
||||
return apierrors.Internal(errors.New("request body policy conflicts with JSON fields"))
|
||||
}
|
||||
if bodyMode == "none" {
|
||||
if r.Body != nil {
|
||||
var one [1]byte
|
||||
n, err := io.ReadFull(r.Body, one[:])
|
||||
if n > 0 {
|
||||
return invalid(errors.New("request body is forbidden"))
|
||||
}
|
||||
if err != io.EOF {
|
||||
return invalid(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if bodyMode == "required" || bodyMode == "optional" || bodyMode == "emptyObject" {
|
||||
var bodyReader io.Reader = r.Body
|
||||
if bodyReader == nil {
|
||||
bodyReader = strings.NewReader("")
|
||||
}
|
||||
if r.ContentLength > limit {
|
||||
return tooLarge()
|
||||
}
|
||||
reader := &io.LimitedReader{R: bodyReader, N: limit + 1}
|
||||
var checked io.Reader = reader
|
||||
if policy.UTF8 == "reject" {
|
||||
checked = &utf8Reader{r: checked}
|
||||
}
|
||||
buffered := bufio.NewReader(checked)
|
||||
_, peekErr := buffered.Peek(1)
|
||||
absent := peekErr == io.EOF
|
||||
bodyAbsent = absent
|
||||
if absent && bodyMode != "optional" {
|
||||
return invalid(errors.New("body required"))
|
||||
}
|
||||
if peekErr != nil && !absent {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(peekErr, &maxErr) {
|
||||
return tooLarge()
|
||||
}
|
||||
return invalid(peekErr)
|
||||
}
|
||||
if !absent {
|
||||
ct, mediaParams, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil || ct != "application/json" || (policy.Charset == "utf8" && mediaParams["charset"] != "" && !strings.EqualFold(mediaParams["charset"], "utf-8")) {
|
||||
return apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json with an allowed charset")
|
||||
}
|
||||
dec := json.NewDecoder(buffered)
|
||||
dec.UseNumber()
|
||||
value, err := decodeValue(dec, 0)
|
||||
if reader.N == 0 {
|
||||
return tooLarge()
|
||||
}
|
||||
if err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
return tooLarge()
|
||||
}
|
||||
return invalid(err)
|
||||
}
|
||||
_, err = dec.Token()
|
||||
if reader.N == 0 {
|
||||
return tooLarge()
|
||||
}
|
||||
if err != io.EOF {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
return tooLarge()
|
||||
}
|
||||
return invalid(errors.New("exactly one JSON value required"))
|
||||
}
|
||||
var ok bool
|
||||
body, ok = value.(map[string]any)
|
||||
if !ok {
|
||||
return invalid(errors.New("body must be an object"))
|
||||
}
|
||||
for key := range body {
|
||||
if _, ok = b.root.schema.Properties[key]; !ok {
|
||||
return invalid(fmt.Errorf("unknown body field %s", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range b.root.fields {
|
||||
var value any
|
||||
var present bool
|
||||
if f.source == "json" {
|
||||
if bodyAbsent && bodyMode == "optional" {
|
||||
continue
|
||||
}
|
||||
value, present = body[f.name]
|
||||
} else {
|
||||
var raw []string
|
||||
|
||||
Reference in New Issue
Block a user