155 lines
5.6 KiB
Go
155 lines
5.6 KiB
Go
package apigen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"git.yuntuo.hk/k2safe/go-omni/contract"
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"mime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func (p *parser) parseBody(r *Route) error {
|
|
props, err := p.metadata()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for key, value := range props {
|
|
switch key {
|
|
case "maxParts":
|
|
r.MaxParts, err = strconv.Atoi(value)
|
|
if err != nil || r.MaxParts < 1 || r.MaxParts > 10000 {
|
|
return p.err("maxParts must be 1..10000")
|
|
}
|
|
case "maxFieldBytes", "maxFileBytes":
|
|
limit, e := strconv.ParseInt(value, 10, 64)
|
|
if e != nil || limit < 1 || limit > 1<<30 {
|
|
return p.err("part limits must be 1..1073741824 bytes")
|
|
}
|
|
if key == "maxFieldBytes" {
|
|
r.MaxFieldBytes = limit
|
|
} else {
|
|
r.MaxFileBytes = limit
|
|
}
|
|
case "mode":
|
|
r.BodyMode = value
|
|
case "maxBytes":
|
|
r.MaxBodyBytes, err = strconv.ParseInt(value, 10, 64)
|
|
if err != nil || r.MaxBodyBytes <= 0 || r.MaxBodyBytes > 1<<40 {
|
|
return p.err("maxBytes must be 1..1099511627776 bytes")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown body property %s", key)
|
|
}
|
|
}
|
|
if r.MaxBodyBytes == 0 {
|
|
r.MaxBodyBytes = contract.DefaultMaxBodyBytes
|
|
}
|
|
return nil
|
|
}
|
|
func validateRouteExtensions(r Route) error {
|
|
if (r.MaxParts > 0 || r.MaxFieldBytes > 0 || r.MaxFileBytes > 0) && r.BodyMode != "multipart" {
|
|
return errors.New("part limits require multipart mode")
|
|
}
|
|
|
|
if err := r.RequestPolicy.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if (r.BodyMode == "raw" || r.BodyMode == "multipart") && r.RequestPolicy.Body != "" && r.RequestPolicy.Body != "auto" {
|
|
return errors.New("raw/multipart cannot declare a JSON body policy")
|
|
}
|
|
if r.BodyMode != "" && r.BodyMode != "json" && r.BodyMode != "raw" && r.BodyMode != "multipart" {
|
|
return errors.New("body mode must be json, raw or multipart")
|
|
}
|
|
if r.CustomBinding && (r.Manual || r.Request == "") {
|
|
return errors.New("@binding custom requires an automatic handler with a request DTO")
|
|
}
|
|
if r.MaxBodyBytes > 0 && (r.Method == "get" || r.Method == "head") {
|
|
return errors.New("GET/HEAD cannot declare a body limit")
|
|
}
|
|
if (r.Method == "get" || r.Method == "head") && r.RequestPolicy.Body != "" && r.RequestPolicy.Body != "auto" && r.RequestPolicy.Body != "none" {
|
|
return errors.New("GET/HEAD cannot declare a JSON body policy")
|
|
}
|
|
if r.Produces != "" {
|
|
media, params, err := mime.ParseMediaType(r.Produces)
|
|
if err != nil || len(params) != 0 || media != r.Produces || strings.Contains(media, "*") {
|
|
return errors.New("produces requires a concrete media type without parameters")
|
|
}
|
|
if r.Response != "" || r.Status == 204 || r.Method == "head" {
|
|
return errors.New("@produces requires a stream response without a DTO, 204 or HEAD")
|
|
}
|
|
}
|
|
if r.Manual && r.Status == 204 && r.Response != "" {
|
|
return errors.New("204 cannot declare a response")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decorateRequestOptions(op map[string]any, r Route) {
|
|
if r.BodyMode == "raw" || r.BodyMode == "multipart" {
|
|
r.RequestPolicy.Body = "auto"
|
|
}
|
|
op["x-omni-request-policy"] = r.RequestPolicy
|
|
if r.BodyMode == "multipart" {
|
|
limits, _ := (contract.MultipartOptions{MaxBodyBytes: r.MaxBodyBytes, MaxParts: r.MaxParts, MaxFieldBytes: r.MaxFieldBytes, MaxFileBytes: r.MaxFileBytes}).Normalized()
|
|
op["x-omni-multipart-limits"] = map[string]any{"maxBytes": limits.MaxBodyBytes, "maxParts": limits.MaxParts, "maxFieldBytes": limits.MaxFieldBytes, "maxFileBytes": limits.MaxFileBytes}
|
|
}
|
|
if r.BodyMode != "raw" && r.BodyMode != "multipart" {
|
|
if body, ok := op["requestBody"].(map[string]any); ok && r.RequestPolicy.Body == "optional" {
|
|
body["required"] = false
|
|
}
|
|
if _, ok := op["requestBody"]; !ok && (r.RequestPolicy.Body == "emptyObject" || r.RequestPolicy.Body == "required" || r.RequestPolicy.Body == "optional") {
|
|
op["requestBody"] = map[string]any{"required": r.RequestPolicy.Body != "optional", "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{"type": "object", "additionalProperties": false}}}}
|
|
}
|
|
}
|
|
|
|
if r.Manual {
|
|
op["x-omni-handler-mode"] = "manual"
|
|
}
|
|
if r.CustomBinding {
|
|
op["x-omni-binding"] = "custom"
|
|
}
|
|
if r.MaxBodyBytes > 0 {
|
|
op["x-omni-max-body-bytes"] = r.MaxBodyBytes
|
|
}
|
|
if r.BodyMode == "raw" || r.BodyMode == "multipart" {
|
|
media := "application/octet-stream"
|
|
schema := map[string]any{"type": "string", "format": "binary"}
|
|
if r.BodyMode == "multipart" {
|
|
media = "multipart/form-data"
|
|
schema = map[string]any{"type": "object", "additionalProperties": true}
|
|
}
|
|
op["requestBody"] = map[string]any{"required": true, "content": map[string]any{media: map[string]any{"schema": schema}}}
|
|
}
|
|
if r.Produces != "" {
|
|
schema := map[string]any{"type": "string"}
|
|
if r.Produces == "application/octet-stream" {
|
|
schema["format"] = "binary"
|
|
}
|
|
if r.Produces == "application/json" {
|
|
schema = map[string]any{}
|
|
}
|
|
op["responses"].(map[string]any)[strconv.Itoa(r.Status)] = map[string]any{"description": "Success", "content": map[string]any{r.Produces: map[string]any{"schema": schema}}}
|
|
}
|
|
}
|
|
|
|
func parseRequestPolicy(props map[string]string) (contract.RequestPolicy, error) {
|
|
data, err := json.Marshal(props)
|
|
if err != nil {
|
|
return contract.RequestPolicy{}, err
|
|
}
|
|
return decodeRequestPolicy(string(data))
|
|
}
|
|
func decodeRequestPolicy(raw string) (contract.RequestPolicy, error) {
|
|
var p contract.RequestPolicy
|
|
if err := apierrors.DecodeCatalogJSON([]byte(raw), &p); err != nil {
|
|
return p, fmt.Errorf("request policy: %w", err)
|
|
}
|
|
return p, p.Validate()
|
|
}
|
|
func policyLiteral(p contract.RequestPolicy) string {
|
|
return fmt.Sprintf("contract.RequestPolicy{UnknownQuery:%q,UTF8:%q,Charset:%q,Body:%q}", p.UnknownQuery, p.UTF8, p.Charset, p.Body)
|
|
}
|