Files
go-omni/contract/rules.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
}