260 lines
7.1 KiB
Go
260 lines
7.1 KiB
Go
package contract
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
|
)
|
|
|
|
func applyRange(s *openapi.Schema, value string) error {
|
|
if len(value) < 3 || (value[0] != '[' && value[0] != '(') || (value[len(value)-1] != ']' && value[len(value)-1] != ')') {
|
|
return errors.New("invalid range: use [min:max], (min:max), or mixed bounds")
|
|
}
|
|
lo, hi, ok := strings.Cut(value[1:len(value)-1], ":")
|
|
if !ok || (lo == "" && hi == "") {
|
|
return errors.New("invalid numeric range")
|
|
}
|
|
if lo != "" {
|
|
v, err := bound(lo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.Minimum == nil || v > *s.Minimum || (v == *s.Minimum && value[0] == '(') {
|
|
s.Minimum = &v
|
|
s.ExclusiveMinimum = value[0] == '('
|
|
}
|
|
}
|
|
if hi != "" {
|
|
v, err := bound(hi)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.Maximum == nil || v < *s.Maximum || (v == *s.Maximum && value[len(value)-1] == ')') {
|
|
s.Maximum = &v
|
|
s.ExclusiveMaximum = value[len(value)-1] == ')'
|
|
}
|
|
}
|
|
return checkBounds(s)
|
|
}
|
|
func bound(s string) (float64, error) {
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil || math.IsInf(v, 0) || math.IsNaN(v) || math.Abs(v) > 1<<53 {
|
|
return 0, errors.New("constraint bound must be finite and within ±2^53")
|
|
}
|
|
return v, nil
|
|
}
|
|
func checkBounds(s *openapi.Schema) error {
|
|
if s.Minimum != nil && s.Maximum != nil && (*s.Minimum > *s.Maximum || (*s.Minimum == *s.Maximum && (s.ExclusiveMinimum || s.ExclusiveMaximum))) {
|
|
return errors.New("empty numeric range")
|
|
}
|
|
if s.MinLength != nil && s.MaxLength != nil && *s.MinLength > *s.MaxLength {
|
|
return errors.New("empty string length range")
|
|
}
|
|
if s.MinItems != nil && s.MaxItems != nil && *s.MinItems > *s.MaxItems {
|
|
return errors.New("empty array length range")
|
|
}
|
|
return nil
|
|
}
|
|
func applyBinding(f *field) error {
|
|
if f.binding == "" {
|
|
return nil
|
|
}
|
|
n := base(f.value)
|
|
k := n.typ.Kind()
|
|
s := f.value.schema
|
|
seen := map[string]bool{}
|
|
for _, rule := range strings.Split(f.binding, ",") {
|
|
name, value, _ := strings.Cut(rule, "=")
|
|
if seen[name] {
|
|
return fmt.Errorf("duplicate binding rule %s", name)
|
|
}
|
|
seen[name] = true
|
|
switch name {
|
|
case "required":
|
|
if rule != name || f.optional || f.hasDefault {
|
|
return errors.New("required conflicts with optional/default")
|
|
}
|
|
if !isScalar(k) {
|
|
return errors.New("binding required supports scalars; other fields are already required by presence")
|
|
}
|
|
s.Nullable = false
|
|
switch k {
|
|
case reflect.String:
|
|
one := 1
|
|
s.MinLength = &one
|
|
case reflect.Bool:
|
|
s.Not = &openapi.Schema{Enum: []any{false}}
|
|
default:
|
|
s.Not = &openapi.Schema{Enum: []any{0}}
|
|
}
|
|
case "email":
|
|
if rule != name || k != reflect.String {
|
|
return errors.New("email requires a string")
|
|
}
|
|
s.Format = "email"
|
|
case "min", "max", "len":
|
|
if k == reflect.String || k == reflect.Slice {
|
|
v, err := strconv.Atoi(value)
|
|
if err != nil || v < 0 {
|
|
return errors.New("length constraints must be nonnegative integers")
|
|
}
|
|
if k == reflect.String {
|
|
if name == "min" || name == "len" {
|
|
if s.MinLength == nil || v > *s.MinLength {
|
|
s.MinLength = &v
|
|
}
|
|
}
|
|
if name == "max" || name == "len" {
|
|
s.MaxLength = &v
|
|
}
|
|
} else {
|
|
if name == "min" || name == "len" {
|
|
s.MinItems = &v
|
|
}
|
|
if name == "max" || name == "len" {
|
|
s.MaxItems = &v
|
|
}
|
|
}
|
|
} else if isNumber(k) && name != "len" {
|
|
v, err := bound(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if name == "min" {
|
|
if s.Minimum == nil || v > *s.Minimum {
|
|
s.Minimum = &v
|
|
s.ExclusiveMinimum = false
|
|
}
|
|
} else {
|
|
if s.Maximum == nil || v < *s.Maximum {
|
|
s.Maximum = &v
|
|
s.ExclusiveMaximum = false
|
|
}
|
|
}
|
|
} else {
|
|
return fmt.Errorf("%s requires a number, string or slice", name)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported binding rule %q; use required,min,max,len,email or source-tag options/range", name)
|
|
}
|
|
}
|
|
return checkBounds(s)
|
|
}
|
|
|
|
func parseScalar(raw string, t reflect.Type) (any, error) {
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return raw, nil
|
|
case reflect.Bool:
|
|
if raw == "true" {
|
|
return true, nil
|
|
}
|
|
if raw == "false" {
|
|
return false, nil
|
|
}
|
|
return nil, errors.New("expected true or false")
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
v, err := strconv.ParseInt(raw, 10, t.Bits())
|
|
if err != nil {
|
|
return nil, errors.New("invalid integer")
|
|
}
|
|
return json.Number(strconv.FormatInt(v, 10)), nil
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
v, err := strconv.ParseUint(raw, 10, t.Bits())
|
|
if err != nil {
|
|
return nil, errors.New("invalid unsigned integer")
|
|
}
|
|
return json.Number(strconv.FormatUint(v, 10)), nil
|
|
case reflect.Float32, reflect.Float64:
|
|
v, err := strconv.ParseFloat(raw, t.Bits())
|
|
if err != nil || math.IsInf(v, 0) || math.IsNaN(v) {
|
|
return nil, errors.New("invalid finite number")
|
|
}
|
|
return json.Number(strconv.FormatFloat(v, 'g', -1, t.Bits())), nil
|
|
}
|
|
return nil, errors.New("expected scalar")
|
|
}
|
|
|
|
func checkField(f field, v reflect.Value) error {
|
|
if v.Kind() == reflect.Pointer {
|
|
if v.IsNil() {
|
|
if strings.Contains(","+f.binding+",", ",required,") {
|
|
return errors.New("must not be null")
|
|
}
|
|
return nil
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
if strings.Contains(","+f.binding+",", ",required,") && v.IsZero() {
|
|
return errors.New("must not be zero or empty")
|
|
}
|
|
s := f.value.schema
|
|
if s.Format == "email" && valuesValidator.Var(v.Interface(), "email") != nil {
|
|
return errors.New("invalid email")
|
|
}
|
|
if len(s.Enum) > 0 {
|
|
found := false
|
|
for _, item := range s.Enum {
|
|
option := reflect.New(v.Type()).Elem()
|
|
if setValue(base(f.value), option, item) == nil && reflect.DeepEqual(option.Interface(), v.Interface()) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return errors.New("not an allowed option")
|
|
}
|
|
}
|
|
if isNumber(v.Kind()) {
|
|
var number float64
|
|
switch v.Kind() {
|
|
case reflect.Float32, reflect.Float64:
|
|
number = v.Float()
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
number = float64(v.Uint())
|
|
if v.Uint() > 1<<53 {
|
|
number = math.Inf(1)
|
|
}
|
|
default:
|
|
number = float64(v.Int())
|
|
if v.Int() > 1<<53 {
|
|
number = math.Inf(1)
|
|
} else if v.Int() < -(1 << 53) {
|
|
number = math.Inf(-1)
|
|
}
|
|
}
|
|
if s.Minimum != nil && (number < *s.Minimum || (s.ExclusiveMinimum && number == *s.Minimum)) {
|
|
return errors.New("below minimum")
|
|
}
|
|
if s.Maximum != nil && (number > *s.Maximum || (s.ExclusiveMaximum && number == *s.Maximum)) {
|
|
return errors.New("above maximum")
|
|
}
|
|
}
|
|
if v.Kind() == reflect.String {
|
|
size := utf8.RuneCountInString(v.String())
|
|
if s.MinLength != nil && size < *s.MinLength {
|
|
return errors.New("string too short")
|
|
}
|
|
if s.MaxLength != nil && size > *s.MaxLength {
|
|
return errors.New("string too long")
|
|
}
|
|
}
|
|
if v.Kind() == reflect.Slice {
|
|
size := v.Len()
|
|
if s.MinItems != nil && size < *s.MinItems {
|
|
return errors.New("too few items")
|
|
}
|
|
if s.MaxItems != nil && size > *s.MaxItems {
|
|
return errors.New("too many items")
|
|
}
|
|
}
|
|
return nil
|
|
}
|