feat: complete API-driven projects with infrastructure and business acceptance
This commit is contained in:
259
contract/rules.go
Normal file
259
contract/rules.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user