use std::{fmt, str::FromStr};
use anyhow::{Context, Result, ensure};
use iroh::EndpointAddr;
use iroh_gossip::proto::TopicId;
use serde::{Deserialize, Serialize};
const PREFIX: &str = "chan";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomTicket {
pub topic: TopicId,
pub name: String,
pub peers: Vec<EndpointAddr>,
}
impl RoomTicket {
pub fn new(topic: TopicId, name: impl Into<String>, peers: Vec<EndpointAddr>) -> Self {
Self {
topic,
name: name.into(),
peers,
}
}
}
impl fmt::Display for RoomTicket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let bytes = postcard::to_stdvec(self).expect("a ticket is always encodable");
let mut text = data_encoding::BASE32_NOPAD.encode(&bytes);
text.make_ascii_lowercase();
write!(f, "{PREFIX}{text}")
}
}
impl FromStr for RoomTicket {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let s = s.trim();
let body = s
.get(..PREFIX.len())
.filter(|head| head.eq_ignore_ascii_case(PREFIX))
.map(|_| &s[PREFIX.len()..])
.with_context(|| format!("a ticket starts with `{PREFIX}`"))?;
ensure!(!body.is_empty(), "the ticket is empty");
let bytes = data_encoding::BASE32_NOPAD
.decode(body.to_ascii_uppercase().as_bytes())
.context("the ticket is not valid base32")?;
postcard::from_bytes(&bytes).context("the ticket is malformed")
}
}
#[cfg(test)]
mod tests {
use super::*;
use iroh::SecretKey;
fn addr() -> EndpointAddr {
EndpointAddr::new(SecretKey::generate().public())
}
#[test]
fn round_trips_through_a_string() {
let ticket = RoomTicket::new(TopicId::from_bytes([9; 32]), "lab", vec![addr(), addr()]);
let parsed: RoomTicket = ticket.to_string().parse().unwrap();
assert_eq!(parsed, ticket);
}
#[test]
fn survives_the_journey_through_a_chat_client() {
let ticket = RoomTicket::new(TopicId::from_bytes([1; 32]), "lab", vec![addr()]);
let mangled = format!(" {} ", ticket.to_string().to_uppercase());
assert_eq!(mangled.parse::<RoomTicket>().unwrap(), ticket);
}
#[test]
fn bad_input_says_what_is_wrong() {
assert!("".parse::<RoomTicket>().is_err());
assert!("chan".parse::<RoomTicket>().is_err());
assert!("nonsense".parse::<RoomTicket>().is_err());
assert!("chan!!!!".parse::<RoomTicket>().is_err());
let ticket = RoomTicket::new(TopicId::from_bytes([1; 32]), "lab", vec![addr()]);
assert!(ticket.to_string()[..12].parse::<RoomTicket>().is_err());
}
}