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 }