rust-web-server 18.16.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
use super::*;
use crate::header::Header;

fn catalogs() -> Catalogs {
    Catalogs::new("en")
        .add("en", "# English\ngreeting = \"Hello, {name}!\"\nonly_en = 'raw \\n text' # literal\nitems.one = \"{count} item\"\nitems.other = \"{count} items\"\nbraces = \"{{name}} is {name}\"\n")
        .unwrap()
        .add("en-GB", "colour = \"colour\"\n")
        .unwrap()
        .add("uk", "greeting = \"Привіт, {name}!\"\nitems.one = \"{count} річ\"\nitems.few = \"{count} речі\"\nitems.many = \"{count} речей\"\nescaped = \"a\\tb \\\"c\\\" \\u00e9\"\n")
        .unwrap()
}

#[test]
fn accept_language_orders_by_q_then_header_order() {
    assert_eq!(AcceptLanguage(vec!["uk-ua".into(), "uk".into(), "en".into(), "*".into()]), AcceptLanguage::parse("en;q=0.5, uk-UA, *;q=0.1, uk;q=0.9"));
    assert_eq!(AcceptLanguage(vec!["de".into()]), AcceptLanguage::parse("fr;q=0, de;q=0.000001, de"));
    assert_eq!(AcceptLanguage(vec!["de".into()]), AcceptLanguage::parse("bad tag, x;q=2, y;q=abc, de, ;q=1, <script>"));
    assert_eq!(AcceptLanguage::default(), AcceptLanguage::parse(""));
    let long = vec!["en"; 1000].join(",");
    assert_eq!(MAX_LANGUAGE_RANGES, AcceptLanguage::parse(&long).0.len());
}

#[test]
fn negotiation_is_rfc_4647_lookup_then_a_regional_match() {
    let available = ["en", "en-GB", "uk", "pt-BR"];
    let pick = |header: &str| AcceptLanguage::parse(header).negotiate(&available);
    assert_eq!(Some("en-GB"), pick("en-GB"));
    assert_eq!(Some("uk"), pick("uk-UA"));
    assert_eq!(Some("en"), pick("en-US"));
    assert_eq!(Some("pt-BR"), pick("pt"));
    assert_eq!(Some("uk"), pick("fr, uk;q=0.2"));
    assert_eq!(None, pick("fr, *"));
    assert_eq!(None, pick(""));
}

#[test]
fn messages_fall_back_and_fill_placeholders() {
    let c = catalogs();
    let uk = c.translator("uk-UA,uk;q=0.9");
    assert_eq!("uk", uk.locale());
    assert_eq!("Привіт, Олена!", uk.t_with("greeting", &[("name", "Олена")]));
    assert_eq!("raw \\n text", uk.t("only_en"), "missing in uk: the default's");
    assert_eq!("no.such.key", uk.t("no.such.key"));
    assert_eq!("a\tb \"c\" é", uk.t("escaped"));

    let gb = c.translator("en-GB");
    assert_eq!("colour", gb.t("colour"));
    assert_eq!("Hello, {name}!", gb.t("greeting"), "base language, unknown placeholder kept");
    assert_eq!("{name} is x", gb.t_with("braces", &[("name", "x")]));

    assert_eq!("en", c.translator("fr").locale(), "nothing matches: the default");
    assert_eq!("de", c.for_language("de").locale());
    assert_eq!("Hello, A!", c.for_language("de").t_with("greeting", &[("name", "A")]));
}

#[test]
fn plurals_follow_each_language() {
    let c = catalogs();
    let uk = c.for_language("uk");
    let en = c.for_language("en");
    assert_eq!(["1 річ", "3 речі", "5 речей", "11 речей", "21 річ", "22 речі", "112 речей"], [1, 3, 5, 11, 21, 22, 112].map(|n| uk.plural("items", n)));
    assert_eq!(["0 items", "1 item", "2 items"], [0, 1, 2].map(|n| en.plural("items", n)));
    assert_eq!("missing.other", en.plural("missing", 2));
    assert_eq!(("one", "few", "many", "many"), (plural_category("pl", 1), plural_category("pl", 23), plural_category("pl", 21), plural_category("pl", 12)));
    assert_eq!(("one", "one", "other"), (plural_category("fr", 0), plural_category("pt-BR", 1), plural_category("pt-PT", 0)));
    assert_eq!(("few", "other"), (plural_category("cs", 4), plural_category("cs", 5)));
    assert_eq!("other", plural_category("ja", 1));
}

#[test]
fn catalog_errors_name_the_line() {
    for (text, expect) in [
        ("a = 1", "line 1: the value must be quoted"),
        ("\n\nkey", "line 3: expected key"),
        ("a b = \"x\"", "isn't a key"),
        ("a = \"open", "no closing quote"),
        ("a = \"x\" trailing", "unexpected"),
        ("a = \"\\q\"", "unknown escape"),
        ("a = \"\\uZZZZ\"", "bad \\u escape"),
    ] {
        let e = Catalog::parse(text).unwrap_err();
        assert!(e.contains(expect), "{text:?}: {e}");
    }
    let e = Catalogs::new("en").add("fr", "x").unwrap_err();
    assert!(e.starts_with("fr: line 1"), "{e}");
}

#[test]
fn catalogs_load_from_a_directory() {
    let dir = crate::test_env::TempPath::new("i18n_catalogs");
    std::fs::create_dir_all(&*dir).unwrap();
    std::fs::write(dir.join("en.toml"), "hi = \"Hi\"\n").unwrap();
    std::fs::write(dir.join("uk.toml"), "hi = \"Привіт\"\n").unwrap();
    std::fs::write(dir.join("notes.txt"), "ignored").unwrap();
    let c = Catalogs::from_dir(&*dir, "en").unwrap();
    assert_eq!(vec!["en", "uk"], c.languages());
    assert_eq!("Привіт", c.translator("uk").t("hi"));
    std::fs::write(dir.join("bad.toml"), "oops").unwrap();
    assert!(Catalogs::from_dir(&*dir, "en").unwrap_err().contains("bad.toml"));
    let empty = crate::test_env::TempPath::new("i18n_empty");
    std::fs::create_dir_all(&*empty).unwrap();
    assert!(Catalogs::from_dir(&*empty, "en").unwrap_err().contains("no <language>.toml"));
}

#[test]
fn requests_and_responses() {
    let c = catalogs();
    let request = Request::parse(b"GET / HTTP/1.1\r\nHost: a\r\nAccept-Language: uk, en;q=0.8\r\n\r\n").unwrap();
    use crate::extract::FromRequest;
    assert_eq!(AcceptLanguage(vec!["uk".into(), "en".into()]), <AcceptLanguage as FromRequest>::from_request(&request).unwrap());
    let t = c.for_request(&request);
    let mut response = Response::text(crate::response::STATUS_CODE_REASON_PHRASE.n200_ok, &t.t_with("greeting", &[("name", "A")]));
    response.headers.push(Header::new(Header::_VARY, "Accept-Encoding".to_string()));
    let response = t.localize(t.localize(response));
    let header = |name: &str| response.headers.iter().filter(|h| h.name.eq_ignore_ascii_case(name)).map(|h| h.value.to_string()).collect::<Vec<_>>();
    assert_eq!(vec!["uk"], header("content-language"));
    assert_eq!(vec!["Accept-Encoding, Accept-Language"], header("vary"));
    let bare = Request::parse(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n").unwrap();
    assert_eq!("en", c.for_request(&bare).locale());
}