diff --git a/id/src/double.rs b/id/src/double.rs index 8a36076..6480d16 100644 --- a/id/src/double.rs +++ b/id/src/double.rs @@ -2,21 +2,13 @@ use std::{fmt::Display, str::FromStr}; use rand::{distributions::Standard, prelude::Distribution, Rng}; -use crate::{Error, Id, SingleId}; +use crate::{utils::is_delimiter, Error, Id, SingleId}; #[derive(Debug, Clone, PartialEq)] pub struct DoubleId{ inner: (SingleId, SingleId) } -impl DoubleId { - #[cfg(test)] - pub fn validate(&self) -> bool { - self.inner.0.validate() && self.inner.1.validate() && (u32::from(self.clone()) < Self::SIZE) - } -} - - impl Id for DoubleId{ type SizeType = u32; const SIZE: Self::SizeType = (SingleId::SIZE as u32).pow(2); @@ -41,6 +33,11 @@ impl Id for DoubleId{ const MAX: Self = Self{ inner: (SingleId::MAX, SingleId::MAX) }; + + #[cfg(test)] + fn is_valid(&self) -> bool { + self.inner.0.is_valid() && self.inner.1.is_valid() && (u32::from(self.clone()) < Self::SIZE) + } } impl Display for DoubleId { @@ -56,20 +53,25 @@ impl FromStr for DoubleId { Ok(Self { inner : match s.len() { 7 => { - match s.chars().nth(3).unwrap() { - '-' => { - Ok((SingleId::from_str(&s[0..3])?, SingleId::from_str(&s[4..7])?)) - }, - x => { - Err(Error::InvalidDelimiter(x)) - } + let delimiter = s[3..4].chars().next().unwrap(); + if is_delimiter(delimiter) { + Ok((SingleId::from_str(&s[0..3])?,SingleId::from_str(&s[4..7])?)) + } else { + Err(Error::InvalidDelimiter{ + found: vec![delimiter], + raw: s.to_string() + }) } } 6 => { Ok((SingleId::from_str(&s[0..3])?,SingleId::from_str(&s[3..6])?)) } - _ => Err(Error::InvalidLength(s.to_string())) + x => Err(Error::InvalidLength{ + expected: (6, 7), + found: x, + raw: s.to_string() + }) }? }) } @@ -95,7 +97,10 @@ impl TryFrom for DoubleId { SingleId::try_from(u16::try_from(value % (SingleId::SIZE as u32)).unwrap())? )}) } else { - Err(Error::OutsideOfRange(value as u64)) + Err(Error::OutsideOfRange{ + expected: Self::SIZE as usize, + found: value as usize + }) } } } @@ -114,9 +119,10 @@ mod tests { where R: Rng { - let chunk: SingleId = rand.r#gen(); - let s = chunk.to_string(); - assert_eq!(chunk,SingleId::from_str(&s).unwrap()) + let id: DoubleId = rand.r#gen(); + assert!(id.is_valid()); + assert_eq!(id,DoubleId::from_str(&id.to_string()).unwrap()); + assert_eq!(id, DoubleId::try_from(u32::from(id.clone())).unwrap()) } #[test] fn random_x10() { diff --git a/id/src/error.rs b/id/src/error.rs index 7b6ad6a..5febe6c 100644 --- a/id/src/error.rs +++ b/id/src/error.rs @@ -1,12 +1,28 @@ #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Outside of range: {0}")] - OutsideOfRange(u64), + #[error("expected under {expected}, found {found}")] + OutsideOfRange{ + expected: usize, + found: usize, + }, #[error("Invalid chunk: {0}")] InvalidChunk(String), - #[error("Invalid delemeter: {0}")] - InvalidDelimiter(char), - #[error("Invalid length: {0}")] - InvalidLength(String) + #[error("Length of id expected {} or {} but found {found}: {raw}", .expected.0, .expected.1 )] + InvalidLength{ + expected: (u8, u8), + found: usize, + raw: String + }, + #[error("Number of chunks expected {expected} but {found}: {raw}")] + InvalidLengthOfChunks{ + expected: u8, + found: usize, + raw: String, + }, + #[error("Delimiter expected '-' or '_' but '{found:?}' found: {raw}")] + InvalidDelimiter{ + found: Vec, + raw: String, + } } diff --git a/id/src/lib.rs b/id/src/lib.rs index 97b4870..4ae05c7 100644 --- a/id/src/lib.rs +++ b/id/src/lib.rs @@ -2,6 +2,7 @@ mod single; mod double; mod error; mod triple; +mod utils; use rand::Rng; pub use single::*; @@ -9,7 +10,6 @@ pub use double::*; pub use triple::*; pub use error::*; - const DOUBLE_ID_SIZE: u32 = (SingleId::SIZE as u32).pow(2); const TRIPLE_ID_SIZE: u64 = (SingleId::SIZE as u64).pow(3); @@ -18,4 +18,7 @@ pub trait Id { const NIL: Self; const MAX: Self; const SIZE: Self::SizeType; -} \ No newline at end of file + #[cfg(test)] + fn is_valid(&self) -> bool; +} + diff --git a/id/src/single.rs b/id/src/single.rs index 67cf56a..d5367d5 100644 --- a/id/src/single.rs +++ b/id/src/single.rs @@ -90,7 +90,10 @@ fn str_to_u16(s: &str) -> Result { } fn u16_to_string(int: u16) -> Result { if int >= CUBED_BASE { - return Err(Error::OutsideOfRange(int as u64)) + return Err(Error::OutsideOfRange{ + expected: CUBED_BASE as usize, + found: int as usize + }) } let first_char = char::from(CHARACTERS[usize::try_from(int / SQUARED_BASE).unwrap()]); let second_char = char::from(CHARACTERS[usize::try_from((int % SQUARED_BASE)/ BASE).unwrap()]); @@ -103,12 +106,6 @@ pub struct SingleId{ inner: u16 } -impl SingleId { - #[cfg(test)] - pub fn validate(&self) -> bool { - self.inner < Self::SIZE - } -} impl Id for SingleId { type SizeType = u16; @@ -135,8 +132,11 @@ impl Id for SingleId { const MAX: SingleId = SingleId{ inner: Self::SIZE-1 }; - - + + #[cfg(test)] + fn is_valid(&self) -> bool { + self.inner < Self::SIZE + } } impl Display for SingleId { @@ -170,7 +170,10 @@ impl TryFrom for SingleId { if value < Self::SIZE { Ok(Self{inner: value}) } else { - Err(Error::OutsideOfRange(value as u64)) + Err(Error::OutsideOfRange{ + expected: Self::SIZE as usize, + found: value as usize + }) } } } @@ -192,12 +195,11 @@ mod tests { R: Rng { let chunk: SingleId = rand.r#gen(); - assert!(chunk.validate()); + assert!(chunk.is_valid()); let s = chunk.to_string(); assert_eq!(chunk,SingleId::from_str(&s).unwrap()); let i = u16::from(chunk.clone()); assert_eq!(chunk, SingleId::try_from(i).unwrap()); - } #[test] fn random_x10() { diff --git a/id/src/triple.rs b/id/src/triple.rs index cb71dbf..2d68e7a 100644 --- a/id/src/triple.rs +++ b/id/src/triple.rs @@ -1,6 +1,145 @@ -use crate::{Error, SingleId}; +use crate::{utils::is_delimiter, DoubleId, Error, SingleId}; +use std::{fmt::Display, str::FromStr}; + +use rand::{distributions::Standard, prelude::Distribution, Rng}; + +use crate::Id; + +#[derive(Debug, Clone, PartialEq)] pub struct TripleId { inner: (SingleId, SingleId, SingleId) } +impl Id for TripleId{ + type SizeType = u64; + const SIZE: Self::SizeType = (SingleId::SIZE as u64).pow(3); + /// ``` + /// use caretta_id::{Id, TripleId}; + /// use std::str::FromStr; + /// + /// assert_eq!(TripleId::NIL, TripleId::from_str("000-000-000").unwrap()); + /// assert_eq!(TripleId::NIL, TripleId::try_from(0).unwrap()); + /// ``` + const NIL: Self = Self{ + inner: (SingleId::NIL, SingleId::NIL, SingleId::NIL) + }; + + /// ``` + /// use caretta_id::{Id, TripleId}; + /// use std::str::FromStr; + /// + /// assert_eq!(TripleId::MAX, TripleId::from_str("zzz-zzz-zzz").unwrap()); + /// assert_eq!(TripleId::MAX, TripleId::try_from(46411484401952).unwrap()); + /// ``` + const MAX: Self = Self{ + inner: (SingleId::MAX, SingleId::MAX, SingleId::MAX) + }; + + #[cfg(test)] + fn is_valid(&self) -> bool { + self.inner.0.is_valid() && self.inner.1.is_valid() && self.inner.2.is_valid() && (u64::from(self.clone()) < Self::SIZE) + } +} + +impl Display for TripleId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.inner.0, self.inner.1, self.inner.2) + } +} + +impl FromStr for TripleId { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self { + inner : match s.len() { + 11 => { + let delimiter = [ + s[3..4].chars().next().unwrap(), + s[7..8].chars().next().unwrap(), + ]; + if is_delimiter(delimiter[0]) && is_delimiter(delimiter[1]) { + Ok((SingleId::from_str(&s[0..3])?,SingleId::from_str(&s[4..7])?,SingleId::from_str(&s[8..11])?)) + } else { + Err(Error::InvalidDelimiter{ + found: Vec::from(delimiter), + raw: s.to_string() + }) + } + + } + 9 => { + Ok((SingleId::from_str(&s[0..3])?,SingleId::from_str(&s[3..6])?,SingleId::from_str(&s[6..9])?)) + } + x => { + Err(Self::Err::InvalidLength{ + expected: (9, 11), + found: x, + raw: s.to_string() + }) + } + } ? + }) + } +} + + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> TripleId { + TripleId { + inner: (rng.r#gen(), rng.r#gen(), rng.r#gen()) + } + } +} + +impl TryFrom for TripleId { + type Error = Error; + + fn try_from(value: u64) -> Result { + if value < Self::SIZE { + Ok(Self{ + inner: ( + SingleId::try_from(u16::try_from(value / (DoubleId::SIZE as u64)).unwrap())?, + SingleId::try_from(u16::try_from((value % (DoubleId::SIZE as u64)) /(SingleId::SIZE as u64)).unwrap())?, + SingleId::try_from(u16::try_from(value % (SingleId::SIZE as u64)).unwrap())? + )}) + } else { + Err(Error::OutsideOfRange{ + expected: Self::SIZE as usize, + found: value as usize + }) + } + } +} + +impl From for u64 { + fn from(value: TripleId) -> Self { + (u16::from(value.inner.0) as u64) * (DoubleId::SIZE as u64) + + (u16::from(value.inner.1) as u64) * (SingleId::SIZE as u64) + + (u16::from(value.inner.2) as u64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_random(rand: &mut R) + where + R: Rng + { + let id: TripleId = rand.r#gen(); + assert!(id.is_valid()); + assert_eq!(id, TripleId::from_str(&id.to_string()).unwrap()); + assert_eq!(id, TripleId::try_from(u64::from(id.clone())).unwrap()); + } + #[test] + fn random_x10() { + let mut rng = rand::thread_rng(); + for _ in 0..10 { + assert_random(&mut rng); + } + + } +} \ No newline at end of file diff --git a/id/src/utils.rs b/id/src/utils.rs new file mode 100644 index 0000000..a8809aa --- /dev/null +++ b/id/src/utils.rs @@ -0,0 +1,11 @@ +use std::str::FromStr; + +use crate::SingleId; + + +pub fn is_delimiter(c: char) -> bool { + match c { + '-' | '_' => true, + _ => false, + } +} \ No newline at end of file