mediatype 0.21.0

MIME Media-type parsing
Documentation
use std::{fmt::Write, fs};

#[test]
fn generated_code_is_fresh() {
    let mut fresh = generate_consts(
        "Name",
        "src/consts/names.txt",
        "src/consts/names.rs",
        &[
            ("vnd.", "vnd", "Vendor subtypes starting with `vnd.`."),
            ("x-", "x_", "Unregistered subtypes starting with `x-`."),
            ("", "", ""),
        ],
        NAMES_HEADER,
    );

    fresh = fresh
        && generate_consts(
            "Value",
            "src/consts/values.txt",
            "src/consts/values.rs",
            &[("", "", "")],
            VALUES_HEADER,
        );

    if !fresh {
        panic!("generated code is not fresh, please commit updates");
    }
}

fn generate_consts(
    ty: &str,
    input: &str,
    dst: &str,
    prefixes: &[(&str, &str, &str)],
    header: &str,
) -> bool {
    let mut prefixes = prefixes
        .iter()
        .map(|&(pf, name, comment)| (pf, name, comment, String::with_capacity(1024)))
        .collect::<Vec<_>>();

    let input = fs::read_to_string(input).expect("failed to read input file");
    for line in input.lines() {
        let (ident, name) = if let Some(pair) = line.split_once('=') {
            pair
        } else {
            (line, line)
        };

        if let Some((pf, _, _, out)) = prefixes.iter_mut().find(|(pf, ..)| ident.starts_with(pf)) {
            let ident = upper_snake_case(ident.trim_start_matches(*pf));
            let indent = if pf.is_empty() { "" } else { "    " };
            writeln!(out, "{}/// `{}`", indent, name).unwrap();
            writeln!(
                out,
                "{}pub const {}: crate::{} = crate::{}::new_unchecked(\"{}\");",
                indent, ident, ty, ty, name
            )
            .unwrap();
        }
    }

    let mut new = String::with_capacity(1024);
    new.push_str(header);
    new.push_str("\n\n");
    for (pf, name, comment, out) in prefixes {
        if !pf.is_empty() {
            writeln!(new, "/// {}", comment).unwrap();
            writeln!(new, "pub mod {} {{", name).unwrap();
        }

        new.push_str(&out);

        if !pf.is_empty() {
            new.push_str("}\n\n");
        }
    }

    let old = fs::read_to_string(dst).expect("failed to read destination file");
    if old != new {
        fs::write(dst, &new).expect("failed to write destination file");
    }

    old == new
}

fn upper_snake_case(s: &str) -> String {
    let s = s
        .split_inclusive(char::is_uppercase)
        .map(|chunk| {
            if chunk.ends_with(char::is_uppercase) {
                let prefix = chunk.trim_end_matches(char::is_uppercase);
                if prefix.ends_with(char::is_lowercase) {
                    return format!("{}_{}", prefix, chunk.split_at(chunk.len() - 1).1);
                }
            }
            chunk.to_string()
        })
        .collect::<String>()
        .replace('+', "_plus")
        .replace(|c| !char::is_ascii_alphanumeric(&c), "_")
        .to_ascii_uppercase();

    if s.starts_with(char::is_numeric) {
        format!("_{}", s)
    } else {
        s
    }
}

const NAMES_HEADER: &str = r#"//! Predefined names, @generated in tests/codegen.rs
//!
//! # Sources
//! - <https://en.wikipedia.org/wiki/Media_type>
//! - <https://www.iana.org/assignments/media-types/media-types.xhtml>
//! - <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_type>
//! - <https://developer.mozilla.org/en-US/docs/Glossary/Quality_values>
//! - <https://datatracker.ietf.org/doc/html/rfc3676>
//! - <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types>"#;

const VALUES_HEADER: &str = r#"//! Predefined parameter values, @generated in tests/codegen.rs
//!
//! # Sources
//! - <https://www.iana.org/assignments/character-sets/character-sets.xhtml>
//! - <https://datatracker.ietf.org/doc/html/rfc3676>"#;