use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::core::device::{Device, DeviceClass};
use crate::core::error::{Error, Result};
use crate::core::props::Props;
#[derive(Debug, Default)]
pub struct Registry {
classes: BTreeMap<&'static str, &'static DeviceClass>,
}
impl Registry {
pub fn new() -> Registry {
Registry {
classes: BTreeMap::new(),
}
}
pub fn add(&mut self, class: &'static DeviceClass) -> Result<()> {
if self.classes.contains_key(class.name) {
return Err(Error::Config {
at: class.name.to_string(),
message: "device class registered twice".to_string(),
});
}
self.classes.insert(class.name, class);
Ok(())
}
pub fn get(&self, name: &str) -> Option<&'static DeviceClass> {
self.classes.get(name).copied()
}
pub fn classes(&self) -> impl Iterator<Item = &'static DeviceClass> + '_ {
self.classes.values().copied()
}
pub fn len(&self) -> usize {
self.classes.len()
}
pub fn is_empty(&self) -> bool {
self.classes.is_empty()
}
pub fn create(&self, name: &str, props: &Props) -> Result<Box<dyn Device>> {
let class = self.get(name).ok_or_else(|| self.unknown(name))?;
(class.construct)(props)
}
fn unknown(&self, name: &str) -> Error {
let mut message = String::from("unknown device class `");
message.push_str(name);
message.push_str("` (is its feature enabled?)");
if let Some(near) = self.nearest(name) {
message.push_str("; did you mean `");
message.push_str(near);
message.push_str("`?");
}
Error::Config {
at: String::from("registry"),
message,
}
}
fn nearest(&self, name: &str) -> Option<&'static str> {
let mut best: Option<(usize, &'static str)> = None;
for candidate in self.classes.keys().copied() {
let score = if candidate == name {
0
} else if candidate.ends_with(name) || candidate.contains(name) {
1
} else {
let tail = candidate.rsplit('.').next().unwrap_or(candidate);
let want = name.rsplit('.').next().unwrap_or(name);
let d = edit_distance(tail, want);
if d * 4 <= want.len().max(tail.len()) {
d + 1
} else {
continue;
}
};
if best.is_none_or(|(b, _)| score < b) {
best = Some((score, candidate));
}
}
best.map(|(_, name)| name)
}
}
fn edit_distance(a: &str, b: &str) -> usize {
let b_chars: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b_chars.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut diagonal = row[0];
row[0] = i + 1;
for (j, cb) in b_chars.iter().enumerate() {
let cost = usize::from(ca != *cb);
let next = (row[j + 1] + 1).min(row[j] + 1).min(diagonal + cost);
diagonal = row[j + 1];
row[j + 1] = next;
}
}
row[b_chars.len()]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::device::{Device, DeviceClass, RealizeCtx, ResetKind};
use crate::core::props::{Props, Value};
#[derive(Debug)]
struct Dummy;
impl Device for Dummy {
fn class(&self) -> &'static DeviceClass {
&DUMMY
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {}
}
static DUMMY: DeviceClass = DeviceClass {
name: "test.dummy",
version: 1,
summary: "a device that does nothing",
properties: &[],
construct: |_props| Ok(Box::new(Dummy)),
};
static OTHER: DeviceClass = DeviceClass {
name: "pci.nvme",
version: 1,
summary: "another",
properties: &[],
construct: |_props| Ok(Box::new(Dummy)),
};
fn registry() -> Registry {
let mut r = Registry::new();
r.add(&DUMMY).unwrap();
r.add(&OTHER).unwrap();
r
}
#[test]
fn a_registered_class_can_be_constructed() {
let r = registry();
let d = r.create("test.dummy", &Props::new()).unwrap();
assert_eq!(d.class().name, "test.dummy");
}
#[test]
fn registering_a_name_twice_is_refused() {
let mut r = Registry::new();
r.add(&DUMMY).unwrap();
let e = r.add(&DUMMY).unwrap_err().to_string();
assert!(e.contains("twice"), "{e}");
}
#[test]
fn an_unknown_class_blames_the_feature_first() {
let r = registry();
let e = r
.create("dev.missing", &Props::new())
.unwrap_err()
.to_string();
assert!(e.contains("dev.missing"), "{e}");
assert!(e.contains("feature"), "{e}");
}
#[test]
fn a_bare_name_suggests_its_qualified_class() {
let r = registry();
let e = r.create("nvme", &Props::new()).unwrap_err().to_string();
assert!(e.contains("did you mean `pci.nvme`?"), "{e}");
}
#[test]
fn a_typo_in_the_tail_is_suggested() {
let r = registry();
let e = r.create("pci.nvne", &Props::new()).unwrap_err().to_string();
assert!(e.contains("did you mean `pci.nvme`?"), "{e}");
}
#[test]
fn an_unrelated_name_gets_no_suggestion() {
let r = registry();
let e = r
.create("completely.different", &Props::new())
.unwrap_err()
.to_string();
assert!(!e.contains("did you mean"), "{e}");
}
#[test]
fn classes_iterate_in_name_order() {
let r = registry();
let names: Vec<&str> = r.classes().map(|c| c.name).collect();
assert_eq!(names, ["pci.nvme", "test.dummy"]);
assert_eq!(r.len(), 2);
assert!(!r.is_empty());
}
#[test]
fn properties_reach_the_constructor() {
static TAKES: DeviceClass = DeviceClass {
name: "test.sized",
version: 1,
summary: "wants a size",
properties: &[],
construct: |props| {
let mut r = props.reader();
let n: u64 = r.require("size")?;
assert_eq!(n, 2048);
Ok(Box::new(Dummy))
},
};
let mut reg = Registry::new();
reg.add(&TAKES).unwrap();
let props = Props::new().with("size", Value::Size(2048));
assert!(reg.create("test.sized", &props).is_ok());
}
}