197 lines
5.5 KiB
Go
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
|
|
}
|