use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Model {
A1Mini,
A1,
P1P,
P1S,
X1,
X1Carbon,
X1E,
H2D,
Unknown(String),
}
impl Model {
pub fn from_config_str(s: &str) -> Self {
let normalized: String = s
.chars()
.filter(|c| !matches!(c, ' ' | '-' | '_'))
.flat_map(char::to_lowercase)
.collect();
match normalized.as_str() {
"a1mini" => Model::A1Mini,
"a1" => Model::A1,
"p1p" => Model::P1P,
"p1s" => Model::P1S,
"x1" => Model::X1,
"x1c" | "x1carbon" => Model::X1Carbon,
"x1e" => Model::X1E,
"h2d" => Model::H2D,
_ => Model::Unknown(s.trim().to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
Model::A1Mini => "a1mini",
Model::A1 => "a1",
Model::P1P => "p1p",
Model::P1S => "p1s",
Model::X1 => "x1",
Model::X1Carbon => "x1c",
Model::X1E => "x1e",
Model::H2D => "h2d",
Model::Unknown(s) => s,
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Model::Unknown(_))
}
pub fn from_device_code(code: &str) -> Self {
match code.trim() {
"N1" => Model::A1Mini,
"N2S" => Model::A1,
"C11" => Model::P1P,
"C12" => Model::P1S,
"C13" => Model::X1E,
"BL-P001" | "3DPrinter-X1-Carbon" => Model::X1Carbon,
"BL-P002" | "3DPrinter-X1" => Model::X1,
"O1D" => Model::H2D,
other => Model::Unknown(other.to_string()),
}
}
pub fn device_code(&self) -> Option<&'static str> {
Some(match self {
Model::A1Mini => "N1",
Model::A1 => "N2S",
Model::P1P => "C11",
Model::P1S => "C12",
Model::X1 => "BL-P002",
Model::X1Carbon => "BL-P001",
Model::X1E => "C13",
Model::H2D => "O1D",
Model::Unknown(_) => return None,
})
}
}
impl fmt::Display for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
const KNOWN: [Model; 8] = [
Model::A1Mini,
Model::A1,
Model::P1P,
Model::P1S,
Model::X1,
Model::X1Carbon,
Model::X1E,
Model::H2D,
];
#[test]
fn parses_canonical_config_names() {
assert_eq!(Model::from_config_str("a1mini"), Model::A1Mini);
assert_eq!(Model::from_config_str("a1"), Model::A1);
assert_eq!(Model::from_config_str("p1s"), Model::P1S);
assert_eq!(Model::from_config_str("x1"), Model::X1);
assert_eq!(Model::from_config_str("x1c"), Model::X1Carbon);
assert_eq!(Model::from_config_str("h2d"), Model::H2D);
}
#[test]
fn config_parsing_is_lenient_about_case_and_separators() {
assert_eq!(Model::from_config_str("A1 mini"), Model::A1Mini);
assert_eq!(Model::from_config_str("A1-Mini"), Model::A1Mini);
assert_eq!(Model::from_config_str("a1_mini"), Model::A1Mini);
assert_eq!(Model::from_config_str("X1Carbon"), Model::X1Carbon);
}
#[test]
fn unknown_model_is_kept_verbatim() {
let m = Model::from_config_str("z9 ultra");
assert_eq!(m, Model::Unknown("z9 ultra".to_string()));
assert!(!m.is_known());
}
#[test]
fn resolves_vendor_canonical_device_codes() {
assert_eq!(Model::from_device_code("N1"), Model::A1Mini); assert_eq!(Model::from_device_code("N2S"), Model::A1); assert_eq!(Model::from_device_code("C11"), Model::P1P);
assert_eq!(Model::from_device_code("C12"), Model::P1S);
assert_eq!(Model::from_device_code("C13"), Model::X1E);
assert_eq!(Model::from_device_code("BL-P001"), Model::X1Carbon);
assert_eq!(Model::from_device_code("BL-P002"), Model::X1);
assert_eq!(Model::from_device_code("O1D"), Model::H2D);
}
#[test]
fn x1_and_x1_carbon_are_distinct_codes() {
assert_ne!(Model::X1, Model::X1Carbon);
assert_eq!(Model::X1.device_code(), Some("BL-P002"));
assert_eq!(Model::X1Carbon.device_code(), Some("BL-P001"));
}
#[test]
fn legacy_ssdp_strings_normalise_to_modern_models() {
assert_eq!(
Model::from_device_code("3DPrinter-X1-Carbon"),
Model::X1Carbon
);
assert_eq!(Model::from_device_code("3DPrinter-X1"), Model::X1);
}
#[test]
fn unrecognised_device_code_is_unknown() {
assert_eq!(
Model::from_device_code("ZZ9"),
Model::Unknown("ZZ9".to_string())
);
assert_eq!(Model::Unknown("ZZ9".into()).device_code(), None);
}
#[test]
fn device_codes_round_trip_for_all_known_models() {
for m in KNOWN {
let code = m.device_code().expect("known model has a device code");
assert_eq!(Model::from_device_code(code), m, "round-trip for {m}");
}
}
#[test]
fn config_names_round_trip_for_all_known_models() {
for m in KNOWN {
assert!(m.is_known());
assert_eq!(Model::from_config_str(m.as_str()), m);
}
}
}