package infra import ( "database/sql" "fmt" "regexp" "strconv" "strings" ) // validCRUDTypes enumera los tipos SQLite aceptados por las funciones CRUD. var validCRUDTypes = map[string]bool{ "TEXT": true, "INTEGER": true, "REAL": true, "BLOB": true, } // isValidCRUDType indica si el tipo string corresponde a uno soportado. func isValidCRUDType(t string) bool { return validCRUDTypes[strings.ToUpper(t)] } // crudFieldByName busca un campo por nombre. Retorna nil si no existe. func crudFieldByName(res CRUDResource, name string) *CRUDField { for i := range res.Fields { if res.Fields[i].Name == name { return &res.Fields[i] } } return nil } // crudColumnNames retorna la lista de nombres de columnas de una tabla sqlite. // Usa PRAGMA table_info — unica forma portable en SQLite. func crudColumnNames(db *sql.DB, table string) ([]string, error) { rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%q)", table)) if err != nil { return nil, fmt.Errorf("crud column names: %w", err) } defer rows.Close() var cols []string for rows.Next() { var cid int var name, ctype string var notnull, pk int var dflt sql.NullString if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { return nil, fmt.Errorf("crud column names: %w", err) } cols = append(cols, name) } return cols, nil } // crudScanRow escanea una fila generica a map[string]any usando las columnas proporcionadas. // Usa []any con apuntadores para que database/sql decida el tipo Go. func crudScanRow(rows *sql.Rows, cols []string) (map[string]any, error) { values := make([]any, len(cols)) scanArgs := make([]any, len(cols)) for i := range values { scanArgs[i] = &values[i] } if err := rows.Scan(scanArgs...); err != nil { return nil, err } row := make(map[string]any, len(cols)) for i, col := range cols { v := values[i] // Normalizar bytes a string (SQLite TEXT llega como []byte cuando no se tipa) if b, ok := v.([]byte); ok { row[col] = string(b) } else { row[col] = v } } return row, nil } // crudValidateField valida un valor contra las reglas de un campo. // Retorna nil si todo ok, error con mensaje descriptivo si falla alguna regla. func crudValidateField(field CRUDField, value any) error { if value == nil { if field.Required { return fmt.Errorf("field %q is required", field.Name) } return nil } switch strings.ToUpper(field.Type) { case "TEXT": s, ok := value.(string) if !ok { return fmt.Errorf("field %q must be a string", field.Name) } return crudValidateText(field, s) case "INTEGER": n, err := crudCoerceInt(value) if err != nil { return fmt.Errorf("field %q must be an integer", field.Name) } return crudValidateNumber(field, float64(n)) case "REAL": f, err := crudCoerceFloat(value) if err != nil { return fmt.Errorf("field %q must be a number", field.Name) } return crudValidateNumber(field, f) case "BLOB": return nil } return nil } // crudValidateText aplica min_length, max_length, pattern, enum a un string. func crudValidateText(field CRUDField, s string) error { if v, ok := field.Validations["min_length"]; ok { n, err := strconv.Atoi(v) if err == nil && len(s) < n { return fmt.Errorf("field %q must have at least %d characters", field.Name, n) } } if v, ok := field.Validations["max_length"]; ok { n, err := strconv.Atoi(v) if err == nil && len(s) > n { return fmt.Errorf("field %q must have at most %d characters", field.Name, n) } } if v, ok := field.Validations["pattern"]; ok { re, err := regexp.Compile(v) if err == nil && !re.MatchString(s) { return fmt.Errorf("field %q does not match pattern %q", field.Name, v) } } if v, ok := field.Validations["enum"]; ok { options := strings.Split(v, ",") matched := false for _, opt := range options { if strings.TrimSpace(opt) == s { matched = true break } } if !matched { return fmt.Errorf("field %q must be one of: %s", field.Name, v) } } return nil } // crudValidateNumber aplica min y max a un valor numerico. func crudValidateNumber(field CRUDField, f float64) error { if v, ok := field.Validations["min"]; ok { min, err := strconv.ParseFloat(v, 64) if err == nil && f < min { return fmt.Errorf("field %q must be >= %s", field.Name, v) } } if v, ok := field.Validations["max"]; ok { max, err := strconv.ParseFloat(v, 64) if err == nil && f > max { return fmt.Errorf("field %q must be <= %s", field.Name, v) } } return nil } // crudCoerceInt intenta convertir un valor a int64. func crudCoerceInt(v any) (int64, error) { switch n := v.(type) { case int: return int64(n), nil case int64: return n, nil case float64: if n != float64(int64(n)) { return 0, fmt.Errorf("not an integer") } return int64(n), nil case string: return strconv.ParseInt(n, 10, 64) } return 0, fmt.Errorf("not an integer") } // crudCoerceFloat intenta convertir un valor a float64. func crudCoerceFloat(v any) (float64, error) { switch n := v.(type) { case int: return float64(n), nil case int64: return float64(n), nil case float64: return n, nil case string: return strconv.ParseFloat(n, 64) } return 0, fmt.Errorf("not a number") }