Files
go-omni/cmd/go-omni/apigen/requestOptions.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)
}