use crate::errors::ValidationError;
#[cfg(feature = "mic-registry")]
pub use crate::mic_registry::{MicEntry, MicStatus};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Mic {
bytes: [u8; Self::LENGTH],
}
impl Mic {
pub const LENGTH: usize = 4;
pub fn parse(s: &str) -> Result<Self, ValidationError> {
let found = s.chars().count();
if found != Self::LENGTH {
return Err(ValidationError::WrongLength {
expected: Self::LENGTH,
found,
});
}
for (i, ch) in s.chars().enumerate() {
let legal = if i == 0 {
ch.is_ascii_uppercase()
} else {
ch.is_ascii_digit() || ch.is_ascii_uppercase()
};
if !legal {
return Err(ValidationError::InvalidCharacter {
position: i + 1,
found: ch,
});
}
}
let mut bytes = [0u8; Self::LENGTH];
bytes.copy_from_slice(s.as_bytes());
Ok(Self { bytes })
}
pub fn validate(s: &str) -> Result<(), ValidationError> {
Self::parse(s).map(|_| ())
}
#[cfg(feature = "mic-registry")]
pub fn parse_registered(s: &str) -> Result<Self, ValidationError> {
let mic = Self::parse(s)?;
if mic.is_registered() {
Ok(mic)
} else {
Err(ValidationError::Structure {
rule: "MIC is not in the ISO 10383 registry",
})
}
}
#[must_use]
pub const fn from_bytes_unchecked(bytes: [u8; Self::LENGTH]) -> Self {
Self { bytes }
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.bytes).unwrap_or("")
}
#[must_use]
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
#[inline]
pub fn prefix(&self) -> char {
char::from(self.bytes[0])
}
#[must_use]
#[inline]
pub fn suffix(&self) -> &str {
core::str::from_utf8(&self.bytes[1..4]).unwrap_or("")
}
#[cfg(feature = "mic-registry")]
#[must_use]
#[inline]
pub fn lookup(&self) -> Option<&'static MicEntry> {
crate::mic_registry::lookup(self.as_str())
}
#[cfg(feature = "mic-registry")]
#[must_use]
#[inline]
pub fn is_registered(&self) -> bool {
self.lookup().is_some()
}
}
impl core::fmt::Display for Mic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for Mic {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Mic {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::display;
use core::str::FromStr;
const GOLDEN: &[&str] = &[
"XNAS", "XLON", "XPAR", "XNYS", ];
const UNREGISTERED: &str = "ZZZZ";
#[test]
fn parses_golden_mics() {
for &s in GOLDEN {
let mic = Mic::parse(s).unwrap_or_else(|e| panic!("{s} should parse: {e}"));
assert_eq!(mic.as_str(), s);
}
}
#[test]
fn parses_unregistered_but_well_formed() {
let mic = Mic::parse(UNREGISTERED).unwrap();
assert_eq!(mic.as_str(), "ZZZZ");
}
#[test]
fn segment_accessors() {
let mic = Mic::parse("XNAS").unwrap();
assert_eq!(mic.prefix(), 'X');
assert_eq!(mic.suffix(), "NAS");
assert_eq!(mic.as_bytes(), b"XNAS");
assert_eq!(Mic::LENGTH, 4);
}
#[test]
fn accepts_digits_in_suffix() {
let mic = Mic::parse("A2XX").unwrap();
assert_eq!(mic.suffix(), "2XX");
}
#[test]
fn rejects_wrong_length() {
assert_eq!(
Mic::parse("XNA"),
Err(ValidationError::WrongLength {
expected: 4,
found: 3,
})
);
assert_eq!(
Mic::parse("XNASX"),
Err(ValidationError::WrongLength {
expected: 4,
found: 5,
})
);
assert_eq!(
Mic::parse(""),
Err(ValidationError::WrongLength {
expected: 4,
found: 0,
})
);
}
#[test]
fn rejects_digit_in_leading_position() {
assert_eq!(
Mic::parse("1NAS"),
Err(ValidationError::InvalidCharacter {
position: 1,
found: '1',
})
);
}
#[test]
fn rejects_lower_case() {
assert!(matches!(
Mic::parse("xnas"),
Err(ValidationError::InvalidCharacter { position: 1, .. })
));
assert!(matches!(
Mic::parse("Xnas"),
Err(ValidationError::InvalidCharacter { position: 2, .. })
));
}
#[test]
fn rejects_punctuation_in_suffix() {
assert!(matches!(
Mic::parse("XN-S"),
Err(ValidationError::InvalidCharacter { position: 3, .. })
));
}
#[test]
fn rejects_non_ascii_without_panic() {
assert!(Mic::parse("XNAé").is_err());
assert!(Mic::parse("ÉNAS").is_err());
}
#[test]
fn validate_agrees_with_parse() {
assert!(Mic::validate("XPAR").is_ok());
assert!(Mic::validate("xpar").is_err());
}
#[test]
fn round_trips_through_str() {
for &s in GOLDEN {
assert_eq!(Mic::parse(s).unwrap().as_str(), s);
}
}
#[test]
fn from_str_matches_parse() {
assert_eq!(Mic::from_str("XNAS"), Mic::parse("XNAS"));
assert!(Mic::from_str("nonsense").is_err());
}
#[test]
fn display_renders_identifier() {
let mic = Mic::parse("XNAS").unwrap();
assert_eq!(display(mic).as_str(), "XNAS");
}
#[test]
fn as_ref_str() {
let mic = Mic::parse("XNAS").unwrap();
let s: &str = mic.as_ref();
assert_eq!(s, "XNAS");
}
#[test]
fn from_bytes_unchecked_round_trip() {
let mic = Mic::from_bytes_unchecked(*b"XNAS");
assert_eq!(mic, Mic::parse("XNAS").unwrap());
}
#[test]
fn is_copy_and_eq_and_hashable() {
let a = Mic::parse("XNAS").unwrap();
let b = a; assert_eq!(a, b);
assert_ne!(a, Mic::parse("XLON").unwrap());
let keys = [a, b];
assert_eq!(keys[0], keys[1]);
}
#[cfg(feature = "mic-registry")]
#[test]
fn lookup_finds_golden_mics() {
for &s in GOLDEN {
let mic = Mic::parse(s).unwrap();
let entry = mic
.lookup()
.unwrap_or_else(|| panic!("{s} should be registered"));
assert_eq!(entry.mic, s);
}
}
#[cfg(feature = "mic-registry")]
#[test]
fn lookup_misses_unregistered() {
assert!(Mic::parse(UNREGISTERED).unwrap().lookup().is_none());
}
#[cfg(feature = "mic-registry")]
#[test]
fn is_registered_reflects_membership() {
for &s in GOLDEN {
assert!(Mic::parse(s).unwrap().is_registered());
}
assert!(!Mic::parse(UNREGISTERED).unwrap().is_registered());
}
#[cfg(feature = "mic-registry")]
#[test]
fn parse_registered_accepts_golden_mics() {
for &s in GOLDEN {
let mic = Mic::parse_registered(s)
.unwrap_or_else(|e| panic!("{s} should parse as registered: {e}"));
assert_eq!(mic.as_str(), s);
}
}
#[cfg(feature = "mic-registry")]
#[test]
fn parse_registered_rejects_unregistered() {
assert_eq!(
Mic::parse_registered(UNREGISTERED),
Err(ValidationError::Structure {
rule: "MIC is not in the ISO 10383 registry",
})
);
}
#[cfg(feature = "mic-registry")]
#[test]
fn parse_registered_rejects_structural_errors_first() {
assert_eq!(
Mic::parse_registered("XNA"),
Err(ValidationError::WrongLength {
expected: 4,
found: 3,
})
);
assert!(matches!(
Mic::parse_registered("xnas"),
Err(ValidationError::InvalidCharacter { position: 1, .. })
));
}
}