package common import ( "encoding/base64" "encoding/json" "fmt" "os" "strings" ) // ImageFileToBase64 reads an image file and returns a data:image/...;base64 string. func ImageFileToBase64(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } ext := strings.ToLower(pathExt(path)) mime := "image/png" switch ext { case ".jpg", ".jpeg": mime = "image/jpeg" case ".gif": mime = "image/gif" case ".webp": mime = "image/webp" } return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil } // pathExt extracts the extension from a path. func pathExt(path string) string { for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- { if path[i] == '.' { return path[i:] } } return "" } // BuildSchemaRequest validates input values against a JSON schema definition, // fills in default values for missing optional fields, // and returns the result matching the schema's nested structure. // // The schema format follows test.json convention: // // { // "section": { // "field_name": { // "type": "string|integer|number|boolean|array|object", // "required": true|false, // "default": value, // "enum": [...], // "min": number, // "max": number, // "max_chars": number, // "min_items": number, // "max_items": number // } // } // } // // input is a flat map like {"prompt": "hello", "duration": 5}. // Fields not present in input but with a "default" in the schema are filled automatically. // Nodes without "type" are treated as grouping sections and recursed into. // When validate is false, required/type/range/enum checks are skipped (only structure + defaults). func BuildSchemaRequest(schema map[string]any, input map[string]any, validate bool) (map[string]any, error) { result := make(map[string]any) for key, val := range schema { fieldDef, ok := val.(map[string]any) if !ok { result[key] = val continue } if _, hasType := fieldDef["type"]; hasType { processed, err := processField(key, fieldDef, input, validate) if err != nil { return nil, err } if processed != nil { result[key] = processed } continue } nested, err := BuildSchemaRequest(fieldDef, input, validate) if err != nil { return nil, err } if len(nested) > 0 { result[key] = nested } } return result, nil } func processField(name string, def map[string]any, input map[string]any, validate bool) (any, error) { fieldType, _ := def["type"].(string) required, _ := def["required"].(bool) rawVal, exists := input[name] if !exists { if validate && required { return nil, fmt.Errorf("%s", def["description"]) } if dflt, ok := def["default"]; ok { return convertDefault(dflt, fieldType), nil } return nil, nil } if !validate { return rawVal, nil } switch fieldType { case "string": s, ok := rawVal.(string) if !ok { return nil, fmt.Errorf("'%s' must be a string", name) } if maxChars, ok := def["max_chars"].(float64); ok && len([]rune(s)) > int(maxChars) { return nil, fmt.Errorf("'%s' exceeds max length of %d", name, int(maxChars)) } if enum, ok := def["enum"].([]any); ok && len(enum) > 0 { if !containsValue(enum, s) { return nil, fmt.Errorf("'%s' must be one of %v", name, enum) } } return s, nil case "integer": v, err := toInt(rawVal) if err != nil { return nil, fmt.Errorf("'%s' must be an integer", name) } if minVal, ok := def["min"].(float64); ok && v < int(minVal) { return nil, fmt.Errorf("'%s' must be >= %d", name, int(minVal)) } if maxVal, ok := def["max"].(float64); ok && v > int(maxVal) { return nil, fmt.Errorf("'%s' must be <= %d", name, int(maxVal)) } return v, nil case "number": v, ok := rawVal.(float64) if !ok { if iv, err := toInt(rawVal); err == nil { v = float64(iv) } else { return nil, fmt.Errorf("'%s' must be a number", name) } } if minVal, ok := def["min"].(float64); ok && v < minVal { return nil, fmt.Errorf("'%s' must be >= %v", name, minVal) } if maxVal, ok := def["max"].(float64); ok && v > maxVal { return nil, fmt.Errorf("'%s' must be <= %v", name, maxVal) } return v, nil case "boolean": _, ok := rawVal.(bool) if !ok { return nil, fmt.Errorf("'%s' must be a boolean", name) } return rawVal, nil case "array": arr, ok := rawVal.([]any) if !ok { return nil, fmt.Errorf("'%s' must be an array", name) } if minItems, ok := def["min_items"].(float64); ok && len(arr) < int(minItems) { return nil, fmt.Errorf("'%s' must have at least %d items", name, int(minItems)) } if maxItems, ok := def["max_items"].(float64); ok && len(arr) > int(maxItems) { return nil, fmt.Errorf("'%s' must have at most %d items", name, int(maxItems)) } if itemsDef, ok := def["items"].(map[string]any); ok { items, err := processArrayItems(arr, itemsDef, validate) if err != nil { return nil, fmt.Errorf("'%s': %w", name, err) } return items, nil } return arr, nil } return rawVal, nil } func processArrayItems(arr []any, itemsDef map[string]any, validate bool) ([]any, error) { itemType, _ := itemsDef["type"].(string) if itemType != "object" { return arr, nil } props, _ := itemsDef["properties"].(map[string]any) if props == nil { return arr, nil } result := make([]any, len(arr)) for i, item := range arr { itemMap, ok := item.(map[string]any) if !ok { result[i] = item continue } processed, err := BuildSchemaRequest(props, itemMap, validate) if err != nil { return nil, fmt.Errorf("item[%d]: %w", i, err) } result[i] = processed } return result, nil } func toInt(v any) (int, error) { switch val := v.(type) { case float64: return int(val), nil case int: return val, nil case int64: return int(val), nil case json.Number: n, err := val.Int64() return int(n), err default: return 0, fmt.Errorf("cannot convert %T to int", v) } } func convertDefault(dflt any, fieldType string) any { if fieldType == "integer" { if f, ok := dflt.(float64); ok { return int(f) } } return dflt } func containsValue(arr []any, val any) bool { for _, v := range arr { if v == val { return true } } return false }