bluer 0.17.1

BlueR: official Rust interface to the Linux Bluetooth protocol stack (BlueZ)
Documentation
use serde::Deserialize;
use std::{
    collections::{hash_map::Entry, HashMap},
    env,
    error::Error,
    fmt,
    fs::File,
    io::Write,
    path::Path,
    str::FromStr,
};
use uuid::Uuid;

#[path = "src/uuid_ext.rs"]
mod uuid_ext;
use uuid_ext::UuidExt;

struct UuidOrShort(Uuid);

impl FromStr for UuidOrShort {
    type Err = String;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.parse::<Uuid>() {
            Ok(uuid) => Ok(Self(uuid)),
            Err(_) => match u16::from_str_radix(s, 16) {
                Ok(short) => Ok(Self(Uuid::from_u16(short))),
                Err(_) => match u32::from_str_radix(s, 16) {
                    Ok(short) => Ok(Self(Uuid::from_u32(short))),
                    Err(_) => Err(s.to_string()),
                },
            },
        }
    }
}

impl fmt::Display for UuidOrShort {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Uuid::from_u128({})", self.0.as_u128())
    }
}

impl UuidOrShort {
    fn as_u128(&self) -> u128 {
        self.0.as_u128()
    }
}

#[derive(Deserialize)]
struct UuidEntry {
    name: String,
    identifier: String,
    uuid: String,
    #[serde(default)]
    source: String,
}

impl UuidEntry {
    fn rust_id(&self, prefix: &str) -> String {
        let id = self.identifier.trim_start_matches(prefix);
        let mut rid = String::new();
        let mut capital = true;
        for c in id.trim().chars() {
            if !c.is_alphanumeric() {
                capital = true;
            } else if capital {
                rid.push(c.to_ascii_uppercase());
                capital = false;
            } else {
                rid.push(c);
            }
        }
        rid
    }

    fn uuid(&self) -> Result<UuidOrShort, String> {
        self.uuid.parse::<UuidOrShort>()
    }
}

fn uniquify_names(entries: &mut [UuidEntry]) {
    let mut name_counts = HashMap::new();
    for entry in entries {
        match name_counts.entry(entry.name.clone()) {
            Entry::Occupied(e) => {
                let cnt = e.into_mut();
                *cnt += 1;
                entry.name = format!("{} {}", &entry.name, cnt);
            }
            Entry::Vacant(e) => {
                e.insert(0);
            }
        }
    }
}

fn convert_uuids(src: &str, dest: &str, name: &str, doc_name: &str, prefix: &str) -> Result<(), Box<dyn Error>> {
    println!("cargo:rerun-if-changed={src}");

    let input = File::open(src)?;
    let mut entries: Vec<UuidEntry> = serde_json::from_reader(input)?;
    uniquify_names(&mut entries);
    let mut out = File::create(Path::new(&env::var("OUT_DIR")?).join(dest))?;

    writeln!(out, "/// Assigned identifiers for {doc_name}.")?;
    writeln!(out, "///")?;
    writeln!(out, "/// Can be converted to and from UUIDs.")?;
    writeln!(out, "#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, EnumString, Display)]")?;
    writeln!(out, "#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]")?;
    writeln!(out, "#[non_exhaustive]")?;
    writeln!(out, "pub enum {name} {{")?;
    for entry in &entries {
        writeln!(out, "    /// {}", &entry.name)?;
        if !entry.source.is_empty() {
            writeln!(out, "    ///")?;
            writeln!(out, "    /// Source: {}", &entry.source)?;
        }
        writeln!(out, "    #[strum(serialize = \"{}\")]", &entry.name)?;
        writeln!(out, "    {},", entry.rust_id(prefix))?;
    }
    writeln!(out, "}}")?;

    writeln!(out, "impl From<{name}> for Uuid {{")?;
    writeln!(out, "    fn from(s: {name}) -> Uuid {{")?;
    writeln!(out, "        match s {{")?;
    for entry in &entries {
        writeln!(out, "            {}::{} => {},", name, entry.rust_id(prefix), entry.uuid()?)?;
    }
    writeln!(out, "        }}")?;
    writeln!(out, "    }}")?;
    writeln!(out, "}}")?;
    writeln!(out)?;

    writeln!(out, "impl TryFrom<Uuid> for {name} {{")?;
    writeln!(out, "    type Error = Uuid;")?;
    writeln!(out, "    fn try_from(uuid: Uuid) -> Result<Self, Uuid> {{")?;
    writeln!(out, "        #[allow(unreachable_patterns)]")?;
    writeln!(out, "        #[allow(clippy::match_overlapping_arm)]")?;
    writeln!(out, "        match uuid.as_u128() {{")?;
    for entry in entries {
        writeln!(out, "            {} => Ok(Self::{}),", entry.uuid()?.as_u128(), entry.rust_id(prefix))?;
    }
    writeln!(out, "            _ => Err(uuid),")?;
    writeln!(out, "        }}")?;
    writeln!(out, "    }}")?;
    writeln!(out, "}}")?;
    writeln!(out)?;

    Ok(())
}

#[derive(Deserialize)]
struct CodeEntry {
    code: u16,
    name: String,
}

impl CodeEntry {
    fn rust_id(&self) -> String {
        let mut rid = String::new();
        let mut capital = true;
        let mut first = true;
        for c in self.name.trim().chars() {
            if first && !c.is_alphabetic() {
                rid.push('N');
            }
            first = false;

            if !c.is_ascii_alphanumeric() {
                capital = true;
            } else if capital {
                rid.push(c.to_ascii_uppercase());
                capital = false;
            } else {
                rid.push(c);
            }
        }
        rid
    }
}

fn convert_ids(src: &str, dest: &str, name: &str, doc_name: &str) -> Result<(), Box<dyn Error>> {
    println!("cargo:rerun-if-changed={src}");

    let input = File::open(src)?;
    let mut entries: Vec<CodeEntry> = serde_json::from_reader(input)?;
    let mut out = File::create(Path::new(&env::var("OUT_DIR")?).join(dest))?;

    let mut seen_names: HashMap<String, usize> = HashMap::new();
    for entry in &mut entries {
        let s = seen_names.entry(entry.rust_id()).or_default();
        if *s > 0 {
            entry.name = format!("{} ({})", &entry.name, s);
        }
        *s += 1;
    }

    writeln!(out, "/// Assigned identifiers for {doc_name}.")?;
    writeln!(out, "///")?;
    writeln!(out, "/// Can be converted to and from ids.")?;
    writeln!(out, "#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, EnumString, Display)]")?;
    writeln!(out, "#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]")?;
    writeln!(out, "#[non_exhaustive]")?;
    writeln!(out, "pub enum {name} {{")?;
    for service in &entries {
        writeln!(out, "    /// {}", &service.name)?;
        writeln!(out, "    #[strum(serialize = \"{}\")]", &service.name.replace('"', "\\\""))?;
        writeln!(out, "    {},", service.rust_id())?;
    }
    writeln!(out, "}}")?;

    writeln!(out, "impl From<{name}> for u16 {{")?;
    writeln!(out, "    fn from(s: {name}) -> u16 {{")?;
    writeln!(out, "        match s {{")?;
    for entry in &entries {
        writeln!(out, "            {}::{} => {},", name, entry.rust_id(), entry.code)?;
    }
    writeln!(out, "        }}")?;
    writeln!(out, "    }}")?;
    writeln!(out, "}}")?;
    writeln!(out)?;

    writeln!(out, "impl TryFrom<u16> for {name} {{")?;
    writeln!(out, "    type Error = u16;")?;
    writeln!(out, "    fn try_from(code: u16) -> Result<Self, u16> {{")?;
    writeln!(out, "        #[allow(unreachable_patterns)]")?;
    writeln!(out, "        match code {{")?;
    for entry in entries {
        writeln!(out, "            {} => Ok(Self::{}),", entry.code, entry.rust_id())?;
    }
    writeln!(out, "            _ => Err(code),")?;
    writeln!(out, "        }}")?;
    writeln!(out, "    }}")?;
    writeln!(out, "}}")?;
    writeln!(out)?;

    Ok(())
}

fn build_ids() -> Result<(), Box<dyn Error>> {
    convert_uuids(
        "service_class_uuids.json",
        "service_class.inc",
        "ServiceClass",
        "service classes and profiles",
        " ",
    )
    .expect("service classes");

    convert_uuids(
        "bluetooth-numbers-database/v1/service_uuids.json",
        "service.inc",
        "Service",
        "GATT services",
        "org.bluetooth.service.",
    )
    .expect("services");

    convert_uuids(
        "bluetooth-numbers-database/v1/characteristic_uuids.json",
        "characteristic.inc",
        "Characteristic",
        "GATT characteristics",
        "org.bluetooth.characteristic.",
    )
    .expect("characteristics");

    convert_uuids(
        "bluetooth-numbers-database/v1/descriptor_uuids.json",
        "descriptor.inc",
        "Descriptor",
        "GATT descriptors",
        "org.bluetooth.descriptor.",
    )
    .expect("descriptors");

    convert_ids("bluetooth-numbers-database/v1/company_ids.json", "company.inc", "Manufacturer", "manufacturers")
        .expect("companys");

    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    if env::var_os("CARGO_FEATURE_ID").is_some() {
        build_ids()?;
    }

    Ok(())
}