foukoapi 0.1.0-alpha.1

Cross-platform bot framework in Rust. Write your handlers once, run the same bot on Telegram and Discord with shared accounts, embeds, keyboards and SQLite storage.
Documentation
//! Small helper utilities shared between adapters, built-in commands
//! and user-defined bots.

/// Capitalise the first character of a string (`"telegram"` → `"Telegram"`).
///
/// Handles multibyte UTF-8 correctly. Leaves an empty string as-is.
pub fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
        None => String::new(),
    }
}

/// Render a 0..=total progress as a width-cell bar using filled ▰ and
/// empty ▱ glyphs. `total` of 0 is treated as 1 to avoid divide-by-zero.
///
/// ```
/// use foukoapi::util::progress_bar;
/// assert_eq!(progress_bar(3, 10, 10), "▰▰▰▱▱▱▱▱▱▱");
/// ```
pub fn progress_bar(done: u64, total: u64, width: usize) -> String {
    let width = width.max(1);
    let total = total.max(1);
    let filled = ((done as f64 / total as f64) * width as f64).round() as usize;
    let filled = filled.min(width);
    let empty = width - filled;
    let mut s = String::with_capacity(width * 3);
    s.push_str(&"\u{25B0}".repeat(filled));
    s.push_str(&"\u{25B1}".repeat(empty));
    s
}

/// Minimal URL-encoder that keeps A-Z / a-z / 0-9 / - _ . ~ as-is and
/// percent-encodes every other byte. Good enough for city names and
/// other short query-string arguments without pulling in another crate.
pub fn urlencode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for byte in s.as_bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(*byte as char);
            }
            _ => out.push_str(&format!("%{:02X}", byte)),
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capitalize_ascii() {
        assert_eq!(capitalize("telegram"), "Telegram");
        assert_eq!(capitalize(""), "");
    }

    #[test]
    fn capitalize_unicode() {
        assert_eq!(capitalize("ёлка"), "Ёлка");
    }

    #[test]
    fn progress_bar_edges() {
        assert_eq!(progress_bar(0, 10, 4), "▱▱▱▱");
        assert_eq!(progress_bar(10, 10, 4), "▰▰▰▰");
        assert_eq!(progress_bar(5, 0, 4), "▰▰▰▰"); // total=0 => 1 => fully filled
    }

    #[test]
    fn urlencode_basic() {
        assert_eq!(urlencode("hello"), "hello");
        assert_eq!(urlencode("hello world"), "hello%20world");
        assert_eq!(urlencode("a&b=c"), "a%26b%3Dc");
    }
}