use core::fmt;
use crate::caps::Capabilities;
use crate::error::Error;
use crate::limits::{MAX_APP, MAX_ID, MAX_ID_DEPTH, MAX_ID_SEGMENT};
use crate::text::Text;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum State {
Idle,
Working,
Done,
Blocked,
Error,
}
impl State {
pub const ALL: [State; 5] = [
State::Idle,
State::Working,
State::Done,
State::Blocked,
State::Error,
];
pub const fn as_str(self) -> &'static str {
match self {
State::Idle => "idle",
State::Working => "working",
State::Done => "done",
State::Blocked => "blocked",
State::Error => "error",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
State::ALL.into_iter().find(|st| st.as_str() == s)
}
pub const fn is_transient(self) -> bool {
matches!(self, State::Working | State::Blocked)
}
pub const fn allows_progress(self) -> bool {
matches!(self, State::Working | State::Blocked)
}
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Kind {
Permission,
Question,
Auth,
}
impl Kind {
pub const ALL: [Kind; 3] = [Kind::Permission, Kind::Question, Kind::Auth];
pub const fn as_str(self) -> &'static str {
match self {
Kind::Permission => "permission",
Kind::Question => "question",
Kind::Auth => "auth",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
Kind::ALL.into_iter().find(|k| k.as_str() == s)
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn is_name_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'+' | b'-')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Id<'a>(&'a str);
impl<'a> Id<'a> {
pub fn new(s: &'a str) -> Result<Self, Error> {
if s.is_empty() || s.len() > MAX_ID {
return Err(Error::InvalidId);
}
let mut depth = 0;
for seg in s.split('/') {
depth += 1;
if seg.is_empty()
|| seg.len() > MAX_ID_SEGMENT
|| depth > MAX_ID_DEPTH
|| !seg.bytes().all(is_name_byte)
{
return Err(Error::InvalidId);
}
}
Ok(Id(s))
}
pub const fn as_str(&self) -> &'a str {
self.0
}
pub fn segments(&self) -> impl DoubleEndedIterator<Item = &'a str> + 'a {
self.0.split('/')
}
pub fn parent(&self) -> Option<Id<'a>> {
self.0.rfind('/').map(|i| Id(&self.0[..i]))
}
pub fn contains(&self, other: &Id<'_>) -> bool {
other.0 == self.0
|| (other.0.len() > self.0.len()
&& other.0.starts_with(self.0)
&& other.0.as_bytes()[self.0.len()] == b'/')
}
}
impl fmt::Display for Id<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct App<'a>(&'a str);
impl<'a> App<'a> {
pub fn new(s: &'a str) -> Result<Self, Error> {
if s.is_empty() || s.len() > MAX_APP || !s.bytes().all(is_name_byte) {
return Err(Error::InvalidApp);
}
Ok(App(s))
}
pub const fn as_str(&self) -> &'a str {
self.0
}
}
impl fmt::Display for App<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Report<'a> {
pub state: State,
pub id: Option<Id<'a>>,
pub kind: Option<Kind>,
pub progress: Option<u8>,
pub app: Option<App<'a>>,
pub title: Option<Text<'a>>,
pub msg: Option<Text<'a>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Message<'a> {
Report(Report<'a>),
Clear {
id: Option<Id<'a>>,
},
Feature(Capabilities),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_navigation() {
let id = Id::new("a/b/c").unwrap();
let mut segs = id.segments();
assert_eq!((segs.next(), segs.next_back()), (Some("a"), Some("c")));
assert_eq!(id.parent(), Some(Id::new("a/b").unwrap()));
assert_eq!(Id::new("a").unwrap().parent(), None);
let a = Id::new("a").unwrap();
assert!(a.contains(&id));
assert!(a.contains(&a));
assert!(!a.contains(&Id::new("ab").unwrap()));
assert!(!id.contains(&a));
}
#[test]
fn wire_names_round_trip() {
for s in State::ALL {
assert_eq!(State::from_wire(s.as_str()), Some(s));
}
for k in Kind::ALL {
assert_eq!(Kind::from_wire(k.as_str()), Some(k));
}
assert_eq!(State::from_wire("clear"), None);
assert!(State::Working.is_transient() && State::Blocked.is_transient());
assert!(
!State::Done.is_transient()
&& !State::Error.is_transient()
&& !State::Idle.is_transient()
);
}
}