nationify 0.2.0

A library that provide information about countries
Documentation
use core::panic;
use std::{env, fs, path::PathBuf};

#[derive(Debug, serde::Deserialize)]
struct RawCountry {
    // Only include fields you actually emit. Missing ones default via Option.
    iso_code: Option<String>,
    alpha3: Option<String>,
    continent: Option<String>,
    country_code: Option<String>,
    currency_code: Option<String>,
    distance_unit: Option<String>,
    gec: Option<String>,
    geo: Option<RawGeo>,
    international_prefix: Option<String>,
    ioc: Option<String>,
    iso_long_name: Option<String>,
    iso_short_name: Option<String>,
    languages_official: Option<Vec<String>>,
    languages_spoken: Option<Vec<String>>,
    national_destination_code_lengths: Option<Vec<u8>>,
    national_number_lengths: Option<Vec<u8>>,
    national_prefix: Option<String>,
    nationality: Option<String>,
    number: Option<String>,
    postal_code: Option<bool>,
    postal_code_format: Option<String>,
    region: Option<String>,
    start_of_week: Option<String>,
    subregion: Option<String>,
    un_locode: Option<String>,
    unofficial_names: Option<Vec<String>>,
    world_region: Option<String>,
}

#[derive(Debug, serde::Deserialize)]
struct RawGeo {
    latitude: f64,
    longitude: f64,
    max_latitude: f64,
    max_longitude: f64,
    min_latitude: f64,
    min_longitude: f64,
    bounds: RawBounds,
}

#[derive(Debug, serde::Deserialize)]
struct RawBounds {
    northeast: RawLatLng,
    southwest: RawLatLng,
}

#[derive(Debug, serde::Deserialize)]
struct RawLatLng {
    lat: f64,
    lng: f64,
}

fn feature(name: &str) -> bool {
    env::var(format!("CARGO_FEATURE_{}", name.to_ascii_uppercase())).is_ok()
}

fn escape(s: &str) -> String {
    // Use serde_json escaping then strip quotes.
    let j = serde_json::to_string(s).unwrap();
    j[1..j.len() - 1].to_string()
}

fn fmt_f64(v: f64) -> String {
    let s = v.to_string();
    if s.contains('.') || s.contains('e') || s.contains('E') {
        s
    } else {
        format!("{}.0", s)
    }
}

fn continent_code(continent: &str) -> &str {
    match continent {
        "Africa" => "AF",
        "Antarctica" => "AN",
        "Asia" => "AS",
        "Europe" => "EU",
        "North America" => "NA",
        "Oceania" => "OC",
        "South America" => "SA",
        _ => panic!("Unknown continent: {}", continent),
    }
}

fn main() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let json_path = PathBuf::from("data/countries.json");

    println!("cargo:rerun-if-changed={}", json_path.display());

    let data = fs::read_to_string(&json_path).expect("Failed to read data/countries.json");
    let raw: Vec<RawCountry> = serde_json::from_str(&data).expect("Failed to parse countries.json");

    // Generate constants.rs
    let mut out = String::new();
    out.push_str("// @generated by build.rs. Do not edit.\n");
    out.push_str("use crate::definitions::{Country");
    if feature("geo") {
        out.push_str(", Geo, Bounds, LatLng");
    }
    out.push_str("};\n\n");
    out.push_str("pub static COUNTRIES: &[Country] = &[\n");

    for c in &raw {
        out.push_str("    Country {\n");
        if feature("iso_code")
            && let Some(v) = &c.iso_code
        {
            out.push_str(&format!("        iso_code: \"{}\",\n", escape(v)));
        }
        if feature("alpha3")
            && let Some(v) = &c.alpha3
        {
            out.push_str(&format!("        alpha3: \"{}\",\n", escape(v)));
        }
        if feature("continent")
            && let Some(v) = &c.continent
        {
            out.push_str(&format!("        continent: \"{}\",\n", escape(v)));
            out.push_str(&format!(
                "        continent_code: \"{}\",\n",
                escape(continent_code(v))
            ));
        }
        if feature("country_code")
            && let Some(v) = &c.country_code
        {
            out.push_str(&format!("        country_code: \"{}\",\n", escape(v)));
        }
        if feature("currency_code")
            && let Some(v) = &c.currency_code
        {
            out.push_str(&format!("        currency_code: \"{}\",\n", escape(v)));
        }
        if feature("distance_unit")
            && let Some(v) = &c.distance_unit
        {
            out.push_str(&format!("        distance_unit: \"{}\",\n", escape(v)));
        }

        if feature("gec")
            && let Some(v) = &c.gec
        {
            out.push_str(&format!("        gec: \"{}\",\n", escape(v)));
        }
        if feature("geo")
            && let Some(g) = &c.geo
        {
            out.push_str("        geo: Geo {\n");
            out.push_str(&format!("            latitude: {},\n", fmt_f64(g.latitude)));
            out.push_str(&format!(
                "            longitude: {},\n",
                fmt_f64(g.longitude)
            ));
            out.push_str(&format!(
                "            max_latitude: {},\n",
                fmt_f64(g.max_latitude)
            ));
            out.push_str(&format!(
                "            max_longitude: {},\n",
                fmt_f64(g.max_longitude)
            ));
            out.push_str(&format!(
                "            min_latitude: {},\n",
                fmt_f64(g.min_latitude)
            ));
            out.push_str(&format!(
                "            min_longitude: {},\n",
                fmt_f64(g.min_longitude)
            ));
            out.push_str("            bounds: Bounds {\n");
            out.push_str(&format!(
                "                northeast: LatLng {{ lat: {}, lng: {} }},\n",
                fmt_f64(g.bounds.northeast.lat),
                fmt_f64(g.bounds.northeast.lng)
            ));
            out.push_str(&format!(
                "                southwest: LatLng {{ lat: {}, lng: {} }},\n",
                fmt_f64(g.bounds.southwest.lat),
                fmt_f64(g.bounds.southwest.lng)
            ));
            out.push_str("            },\n");
            out.push_str("        },\n");
        }
        if feature("international_prefix")
            && let Some(v) = &c.international_prefix
        {
            out.push_str(&format!(
                "        international_prefix: \"{}\",\n",
                escape(v)
            ));
        }
        if feature("ioc")
            && let Some(v) = &c.ioc
        {
            out.push_str(&format!("        ioc: \"{}\",\n", escape(v)));
        }
        if feature("iso_long_name")
            && let Some(v) = &c.iso_long_name
        {
            out.push_str(&format!("        iso_long_name: \"{}\",\n", escape(v)));
        }
        if feature("iso_short_name")
            && let Some(v) = &c.iso_short_name
        {
            out.push_str(&format!("        iso_short_name: \"{}\",\n", escape(v)));
        }
        if feature("languages_official") {
            let list = c
                .languages_official
                .as_ref()
                .map(|v| {
                    v.iter()
                        .map(|s| format!("\"{}\"", escape(s)))
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            out.push_str(&format!("        languages_official: &[{}],\n", list));
        }
        if feature("languages_spoken") {
            let list = c
                .languages_spoken
                .as_ref()
                .map(|v| {
                    v.iter()
                        .map(|s| format!("\"{}\"", escape(s)))
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            out.push_str(&format!("        languages_spoken: &[{}],\n", list));
        }
        if feature("national_destination_code_lengths")
            && let Some(v) = &c.national_destination_code_lengths
        {
            let list = v
                .iter()
                .map(|x| x.to_string())
                .collect::<Vec<_>>()
                .join(", ");
            out.push_str(&format!(
                "        national_destination_code_lengths: &[{}],\n",
                list
            ));
        }
        if feature("national_number_lengths")
            && let Some(v) = &c.national_number_lengths
        {
            let list = v
                .iter()
                .map(|x| x.to_string())
                .collect::<Vec<_>>()
                .join(", ");
            out.push_str(&format!("        national_number_lengths: &[{}],\n", list));
        }
        if feature("national_prefix") {
            match &c.national_prefix {
                Some(v) => out.push_str(&format!(
                    "        national_prefix: Some(\"{}\"),\n",
                    escape(v)
                )),
                None => out.push_str("        national_prefix: None,\n"),
            }
        }
        if feature("nationality")
            && let Some(v) = &c.nationality
        {
            out.push_str(&format!("        nationality: \"{}\",\n", escape(v)));
        }
        if feature("number")
            && let Some(v) = &c.number
        {
            out.push_str(&format!("        number: \"{}\",\n", escape(v)));
        }
        if feature("postal_code")
            && let Some(v) = c.postal_code
        {
            out.push_str(&format!("        postal_code: {},\n", v));
        }
        if feature("postal_code_format")
            && let Some(v) = &c.postal_code_format
        {
            out.push_str(&format!("        postal_code_format: \"{}\",\n", escape(v)));
        }
        if feature("region")
            && let Some(v) = &c.region
        {
            out.push_str(&format!("        region: \"{}\",\n", escape(v)));
        }
        if feature("start_of_week")
            && let Some(v) = &c.start_of_week
        {
            out.push_str(&format!("        start_of_week: \"{}\",\n", escape(v)));
        }
        if feature("subregion")
            && let Some(v) = &c.subregion
        {
            out.push_str(&format!("        subregion: \"{}\",\n", escape(v)));
        }
        if feature("un_locode")
            && let Some(v) = &c.un_locode
        {
            out.push_str(&format!("        un_locode: \"{}\",\n", escape(v)));
        }
        if feature("unofficial_names") {
            let list = c
                .unofficial_names
                .as_ref()
                .map(|v| {
                    v.iter()
                        .map(|s| format!("\"{}\"", escape(s)))
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            out.push_str(&format!("        unofficial_names: &[{}],\n", list));
        }
        if feature("world_region")
            && let Some(v) = &c.world_region
        {
            out.push_str(&format!("        world_region: \"{}\",\n", escape(v)));
        }
        out.push_str("    },\n");
    }
    out.push_str("];\n\n");

    // PHF map for iso_code
    if feature("phf") && feature("iso_code") {
        let mut map = phf_codegen::Map::new();
        for (i, c) in raw.iter().enumerate() {
            if let Some(code) = &c.iso_code {
                map.entry(code, &i.to_string());
            }
        }
        out.push_str("#[cfg(feature = \"iso_code\")]\n");
        out.push_str("pub static ISO_CODE_INDEX: phf::Map<&'static str, usize> = ");
        out.push_str(&map.build().to_string());
        out.push_str(";\n");
    }

    fs::write(out_dir.join("constants.rs"), out).expect("write constants.rs");
}