package serviceadapter import ( "fmt" "strings" ) // FieldSchema describes validation schema for a field or request/response body. type FieldSchema struct { Type string `json:"type"` // string, number, boolean, array, object Nullable bool `json:"nullable"` // accept null values Strict bool `json:"strict"` // reject unknown fields (object only) Required []string `json:"required"` // required field names (object only) Fields map[string]FieldSchema `json:"fields"` // field schemas (object only) Items *FieldSchema `json:"items"` // item schema (array only) } // ValidationError describes a single validation failure. type ValidationError struct { Field string Reason string } // Validator validates bodies against a schema. type Validator struct { schema *FieldSchema } // NewValidator creates a new validator for a schema. func NewValidator(schemaStr string) (*Validator, error) { if schemaStr == "" { return nil, nil // No validation } schema, err := parseSchema(schemaStr) if err != nil { return nil, err } return &Validator{schema: schema}, nil } // Validate validates a body (map or []interface{}) against the schema. func (v *Validator) Validate(body interface{}) []ValidationError { if v == nil || v.schema == nil { return nil } return v.validateValue(body, v.schema, "") } func (v *Validator) validateValue(value interface{}, schema *FieldSchema, path string) []ValidationError { var errors []ValidationError // Handle null if value == nil { if !schema.Nullable { errors = append(errors, ValidationError{ Field: path, Reason: "null not allowed", }) } return errors } switch schema.Type { case "object": obj, ok := value.(map[string]interface{}) if !ok { return []ValidationError{{ Field: path, Reason: fmt.Sprintf("type_mismatch: want object got %T", value), }} } // Check required fields for _, required := range schema.Required { if _, ok := obj[required]; !ok { errors = append(errors, ValidationError{ Field: required, Reason: "missing", }) } } // Check field types for fieldName, fieldValue := range obj { if fieldSchema, ok := schema.Fields[fieldName]; ok { errors = append(errors, v.validateValue(fieldValue, &fieldSchema, fieldName)...) } else if schema.Strict { errors = append(errors, ValidationError{ Field: fieldName, Reason: "unknown_field", }) } } case "array": arr, ok := value.([]interface{}) if !ok { return []ValidationError{{ Field: path, Reason: fmt.Sprintf("type_mismatch: want array got %T", value), }} } if schema.Items != nil { for i, item := range arr { itemPath := fmt.Sprintf("%s[%d]", path, i) errors = append(errors, v.validateValue(item, schema.Items, itemPath)...) } } case "string": if _, ok := value.(string); !ok { return []ValidationError{{ Field: path, Reason: fmt.Sprintf("type_mismatch: want string got %T", value), }} } case "number": switch value.(type) { case float64, int, int32, int64: // OK default: return []ValidationError{{ Field: path, Reason: fmt.Sprintf("type_mismatch: want number got %T", value), }} } case "boolean": if _, ok := value.(bool); !ok { return []ValidationError{{ Field: path, Reason: fmt.Sprintf("type_mismatch: want boolean got %T", value), }} } } return errors } // parseSchema parses a simple schema DSL (flat key:type format for now). // Real DSL defined in design doc — stub implementation here. func parseSchema(schemaStr string) (*FieldSchema, error) { if strings.TrimSpace(schemaStr) == "" { return nil, nil } // Stub: for now accept any non-empty schema and validate as permissive object schema := &FieldSchema{ Type: "object", Fields: make(map[string]FieldSchema), } // Very basic parsing: "field1: string, field2: number" parts := strings.Split(schemaStr, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } kv := strings.Split(part, ":") if len(kv) != 2 { continue } fieldName := strings.TrimSpace(kv[0]) fieldType := strings.TrimSpace(kv[1]) schema.Fields[fieldName] = FieldSchema{ Type: fieldType, Nullable: false, } } return schema, nil }