use crate::error::MatrixError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyncToken {
pub stream_id: i64,
pub typing_gen: u64,
}
pub fn format(stream_id: i64, typing_gen: u64) -> String {
format!("s{stream_id}_{typing_gen}")
}
pub fn parse(raw: &str) -> Result<SyncToken, MatrixError> {
let digits = raw.strip_prefix('s').unwrap_or(raw);
let (stream_part, typing_part) = match digits.split_once('_') {
Some((stream_part, typing_part)) => (stream_part, Some(typing_part)),
None => (digits, None),
};
let stream_id = stream_part.parse::<i64>().map_err(|_| MatrixError::invalid_param("malformed sync token"))?;
let typing_gen = match typing_part {
Some(raw_gen) => raw_gen.parse::<u64>().map_err(|_| MatrixError::invalid_param("malformed sync token"))?,
None => 0,
};
Ok(SyncToken { stream_id, typing_gen })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sync_token_round_trips_the_full_form() {
for (stream_id, typing_gen) in [(0_i64, 0_u64), (1, 1), (42, 7), (1_000_000, 12_345)] {
let token = format(stream_id, typing_gen);
assert_eq!(parse(&token).expect("parse"), SyncToken { stream_id, typing_gen });
}
}
#[test]
fn sync_token_accepts_the_legacy_bare_stream_id_form_with_typing_gen_zero() {
assert_eq!(parse("s42").expect("parse"), SyncToken { stream_id: 42, typing_gen: 0 });
}
#[test]
fn sync_token_accepts_a_bare_integer_too() {
assert_eq!(parse("42").expect("parse"), SyncToken { stream_id: 42, typing_gen: 0 });
}
#[test]
fn sync_token_rejects_garbage() {
let err = parse("not-a-token").unwrap_err();
assert_eq!(err.errcode, "M_INVALID_PARAM");
}
#[test]
fn sync_token_rejects_a_malformed_typing_gen() {
let err = parse("s42_not-a-number").unwrap_err();
assert_eq!(err.errcode, "M_INVALID_PARAM");
}
}