182 lines
4.3 KiB
Go
182 lines
4.3 KiB
Go
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
|
||
|
|
}
|