Files
iqudo-top1/internal/resultcontract/schema.go
2026-08-18 16:27:02 +08:00

197 lines
5.5 KiB
Go

package resultcontract
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var keyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`)
type Schema struct {
Fields []Field `json:"fields"`
}
type Field struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Options []any `json:"options,omitempty"`
Fields []Field `json:"fields,omitempty"`
Items *Field `json:"items,omitempty"`
Min *float64 `json:"min,omitempty"`
Max *float64 `json:"max,omitempty"`
Default interface{} `json:"default,omitempty"`
}
func ParseAndValidate(raw []byte) (Schema, error) {
var schema Schema
if len(raw) == 0 {
return schema, nil
}
if err := json.Unmarshal(raw, &schema); err != nil {
return schema, fmt.Errorf("result_schema 格式不正确")
}
if err := validateFields(schema.Fields, 0); err != nil {
return schema, err
}
return schema, nil
}
func ValidateSchema(value map[string]interface{}) error {
raw, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("result_schema 格式不正确")
}
_, err = ParseAndValidate(raw)
return err
}
// ValidateResult permits nil and an empty object so a run can finish without a business outcome.
func ValidateResult(schema Schema, value map[string]interface{}) error {
if len(value) == 0 {
return nil
}
return validateObject(schema.Fields, value, "final_result")
}
func validateFields(fields []Field, depth int) error {
if depth > 8 {
return fmt.Errorf("result_schema 嵌套层级不能超过 8 层")
}
seen := map[string]bool{}
for _, field := range fields {
if !keyPattern.MatchString(field.Key) || seen[field.Key] {
return fmt.Errorf("结果字段标识必须唯一且格式正确")
}
seen[field.Key] = true
if field.Name == "" {
return fmt.Errorf("结果字段 %s 缺少名称", field.Key)
}
if !supportedType(field.Type) {
return fmt.Errorf("结果字段 %s 使用了不支持的类型 %s", field.Key, field.Type)
}
if field.Type == "object" {
if len(field.Fields) == 0 {
return fmt.Errorf("对象字段 %s 必须定义 fields", field.Key)
}
if err := validateFields(field.Fields, depth+1); err != nil {
return err
}
}
if field.Type == "array" && field.Items != nil {
item := *field.Items
if item.Type == "" {
return fmt.Errorf("数组字段 %s 的 items 必须定义类型", field.Key)
}
if item.Type == "object" {
if len(item.Fields) == 0 {
return fmt.Errorf("数组字段 %s 的对象项必须定义 fields", field.Key)
}
if err := validateFields(item.Fields, depth+1); err != nil {
return err
}
} else if !supportedType(item.Type) || item.Type == "array" {
return fmt.Errorf("数组字段 %s 的 items 类型不正确", field.Key)
}
}
}
return nil
}
func supportedType(value string) bool {
switch value {
case "string", "text", "textarea", "number", "integer", "boolean", "select", "multiselect", "date", "array", "object", "any":
return true
default:
return false
}
}
func validateObject(fields []Field, value map[string]interface{}, path string) error {
definitions := make(map[string]Field, len(fields))
for _, field := range fields {
definitions[field.Key] = field
if field.Required {
if item, ok := value[field.Key]; !ok || item == nil || item == "" {
return fmt.Errorf("%s.%s 为必填字段", path, field.Key)
}
}
}
for key, item := range value {
field, ok := definitions[key]
if !ok {
return fmt.Errorf("%s.%s 未在结果格式中定义", path, key)
}
if err := validateValue(field, item, path+"."+key); err != nil {
return err
}
}
return nil
}
func validateValue(field Field, value interface{}, path string) error {
if value == nil {
if field.Required {
return fmt.Errorf("%s 为必填字段", path)
}
return nil
}
switch field.Type {
case "string", "text", "textarea", "select", "date":
text, ok := value.(string)
if !ok {
return fmt.Errorf("%s 必须是字符串", path)
}
if field.Type == "select" && len(field.Options) > 0 && !contains(field.Options, text) {
return fmt.Errorf("%s 不在允许选项中", path)
}
case "number", "integer":
number, ok := value.(float64)
if !ok || (field.Type == "integer" && number != float64(int64(number))) {
return fmt.Errorf("%s 必须是%s", path, map[bool]string{true: "整数", false: "数字"}[field.Type == "integer"])
}
if field.Min != nil && number < *field.Min || field.Max != nil && number > *field.Max {
return fmt.Errorf("%s 超出允许范围", path)
}
case "boolean":
if _, ok := value.(bool); !ok {
return fmt.Errorf("%s 必须是布尔值", path)
}
case "object":
object, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("%s 必须是对象", path)
}
return validateObject(field.Fields, object, path)
case "array", "multiselect":
items, ok := value.([]interface{})
if !ok {
return fmt.Errorf("%s 必须是数组", path)
}
for index, item := range items {
if field.Type == "multiselect" && len(field.Options) > 0 && !contains(field.Options, item) {
return fmt.Errorf("%s[%d] 不在允许选项中", path, index)
}
if field.Items != nil {
if err := validateValue(*field.Items, item, fmt.Sprintf("%s[%d]", path, index)); err != nil {
return err
}
}
}
}
return nil
}
func contains(options []any, value interface{}) bool {
want := fmt.Sprint(value)
for _, option := range options {
if strings.EqualFold(fmt.Sprint(option), want) {
return true
}
}
return false
}