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"] pub 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 { match s.parse::() { 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 { self.uuid.parse::() } } 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> { println!("cargo:rerun-if-changed={src}"); let input = File::open(src)?; let mut entries: Vec = 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 for {name} {{")?; writeln!(out, " type Error = Uuid;")?; writeln!(out, " fn try_from(uuid: Uuid) -> Result {{")?; 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> { println!("cargo:rerun-if-changed={src}"); let input = File::open(src)?; let mut entries: Vec = serde_json::from_reader(input)?; let mut out = File::create(Path::new(&env::var("OUT_DIR")?).join(dest))?; let mut seen_names: HashMap = 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 for {name} {{")?; writeln!(out, " type Error = u16;")?; writeln!(out, " fn try_from(code: u16) -> Result {{")?; 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> { 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> { if env::var_os("CARGO_FEATURE_ID").is_some() { build_ids()?; } Ok(()) }