use std::{fmt, slice};
use smallvec::SmallVec;
use thiserror::Error;
pub const MAX_NAME_LEN: usize = u16::MAX as usize;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum NameError {
#[error("{what} must not be empty")]
Empty { what: &'static str },
#[error("{what} is {len} bytes, exceeding the {max}-byte maximum")]
TooLong {
what: &'static str,
len: usize,
max: usize,
},
}
fn validate_name(s: &str, what: &'static str) -> Result<(), NameError> {
if s.is_empty() {
return Err(NameError::Empty { what });
}
if s.len() > MAX_NAME_LEN {
return Err(NameError::TooLong {
what,
len: s.len(),
max: MAX_NAME_LEN,
});
}
Ok(())
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct EventType(Box<str>);
impl EventType {
pub fn new(s: impl Into<Box<str>>) -> Result<Self, NameError> {
let s = s.into();
validate_name(&s, "event type")?;
Ok(EventType(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> Box<str> {
self.0
}
}
impl AsRef<str> for EventType {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for EventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Tag(Box<str>);
impl Tag {
pub fn new(s: impl Into<Box<str>>) -> Result<Self, NameError> {
let s = s.into();
validate_name(&s, "tag")?;
Ok(Tag(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> Box<str> {
self.0
}
}
impl AsRef<str> for Tag {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TagsError {
#[error("duplicate tag '{tag}'")]
Duplicate { tag: Tag },
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Tags(SmallVec<[Tag; 4]>);
impl Tags {
pub fn new(tags: impl IntoIterator<Item = Tag>) -> Result<Self, TagsError> {
let mut tags: SmallVec<[Tag; 4]> = tags.into_iter().collect();
tags.sort_unstable();
if let Some(i) = (1..tags.len()).find(|&i| tags[i] == tags[i - 1]) {
return Err(TagsError::Duplicate {
tag: tags.swap_remove(i),
});
}
Ok(Tags(tags))
}
pub fn empty() -> Self {
Tags(SmallVec::new())
}
pub fn into_inner(self) -> SmallVec<[Tag; 4]> {
self.0
}
pub fn as_slice(&self) -> &[Tag] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> slice::Iter<'_, Tag> {
self.0.iter()
}
}
impl IntoIterator for Tags {
type Item = Tag;
type IntoIter = smallvec::IntoIter<[Tag; 4]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tag(s: &str) -> Tag {
Tag::new(s).unwrap()
}
fn tags(items: &[&str]) -> Tags {
Tags::new(items.iter().map(|s| tag(s)).collect::<SmallVec<[Tag; 4]>>()).unwrap()
}
#[test]
fn name_rejects_empty() {
assert_eq!(
EventType::new(""),
Err(NameError::Empty { what: "event type" })
);
assert_eq!(Tag::new(""), Err(NameError::Empty { what: "tag" }));
}
#[test]
fn name_rejects_over_long() {
let big = "x".repeat(MAX_NAME_LEN + 1);
assert_eq!(
EventType::new(big),
Err(NameError::TooLong {
what: "event type",
len: MAX_NAME_LEN + 1,
max: MAX_NAME_LEN,
})
);
assert!(Tag::new("y".repeat(MAX_NAME_LEN)).is_ok());
}
#[test]
fn name_accessors_and_ordering() {
assert_eq!(EventType::new("Registered").unwrap().as_str(), "Registered");
assert_eq!(<Tag as AsRef<str>>::as_ref(&tag("course:c1")), "course:c1");
assert!(tag("course:a") < tag("course:b"));
assert_eq!(format!("{}", tag("student:s1")), "student:s1");
}
#[test]
fn tags_sorts_input() {
let t = tags(&["course:c1", "student:s1", "admin:a1"]);
let got: Vec<&str> = t.iter().map(|t| t.as_str()).collect();
assert_eq!(got, ["admin:a1", "course:c1", "student:s1"]);
}
#[test]
fn tags_rejects_duplicates() {
let input: SmallVec<[Tag; 4]> = [tag("course:c1"), tag("course:c1")].into_iter().collect();
assert_eq!(
Tags::new(input),
Err(TagsError::Duplicate {
tag: tag("course:c1")
})
);
}
#[test]
fn tags_empty_is_empty() {
assert!(Tags::empty().is_empty());
assert_eq!(Tags::empty().len(), 0);
}
}