use crate::protocol::error::{Error, Result};
use chrono::{DateTime, Utc};
pub mod names {
pub const STREAM_TTL: &str = "Stream-TTL";
pub const STREAM_EXPIRES_AT: &str = "Stream-Expires-At";
pub const STREAM_CLOSED: &str = "Stream-Closed";
pub const STREAM_NEXT_OFFSET: &str = "Stream-Next-Offset";
pub const STREAM_UP_TO_DATE: &str = "Stream-Up-To-Date";
pub const STREAM_CURSOR: &str = "Stream-Cursor";
pub const STREAM_SEQ: &str = "Stream-Seq";
pub const PRODUCER_ID: &str = "Producer-Id";
pub const PRODUCER_EPOCH: &str = "Producer-Epoch";
pub const PRODUCER_SEQ: &str = "Producer-Seq";
pub const PRODUCER_EXPECTED_SEQ: &str = "Producer-Expected-Seq";
pub const PRODUCER_RECEIVED_SEQ: &str = "Producer-Received-Seq";
}
pub fn parse_ttl(value: &str) -> Result<u64> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::InvalidTtl("empty value".to_string()));
}
if trimmed.len() > 1 && trimmed.starts_with('0') {
return Err(Error::InvalidTtl(format!(
"leading zeros not allowed: '{trimmed}'"
)));
}
if trimmed.contains('.') {
return Err(Error::InvalidTtl(format!(
"decimal values not allowed: '{trimmed}'"
)));
}
if trimmed.contains('e') || trimmed.contains('E') {
return Err(Error::InvalidTtl(format!(
"scientific notation not allowed: '{trimmed}'"
)));
}
if trimmed.starts_with('-') {
return Err(Error::InvalidTtl(format!(
"negative values not allowed: '{trimmed}'"
)));
}
if trimmed.starts_with('+') {
return Err(Error::InvalidTtl(format!(
"leading plus sign not allowed: '{trimmed}'"
)));
}
trimmed
.parse::<u64>()
.map_err(|e| Error::InvalidTtl(format!("invalid integer '{trimmed}': {e}")))
}
pub fn parse_expires_at(value: &str) -> Result<DateTime<Utc>> {
value
.parse::<DateTime<Utc>>()
.map_err(|e| Error::InvalidHeader {
header: names::STREAM_EXPIRES_AT.to_string(),
reason: format!("invalid ISO 8601 timestamp: {e}"),
})
}
#[must_use]
pub fn parse_bool(value: &str) -> bool {
value.trim().eq_ignore_ascii_case("true")
}
#[must_use]
pub fn normalize_content_type(content_type: &str) -> String {
content_type
.split(';')
.next()
.unwrap_or(content_type)
.trim()
.to_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ttl_valid() {
assert_eq!(parse_ttl("0").unwrap(), 0);
assert_eq!(parse_ttl("1").unwrap(), 1);
assert_eq!(parse_ttl("3600").unwrap(), 3600);
assert_eq!(parse_ttl("86400").unwrap(), 86400);
assert_eq!(parse_ttl(" 100 ").unwrap(), 100); }
#[test]
fn test_parse_ttl_invalid() {
assert!(parse_ttl("01").is_err());
assert!(parse_ttl("00").is_err());
assert!(parse_ttl("0123").is_err());
assert!(parse_ttl("1.5").is_err());
assert!(parse_ttl("3600.0").is_err());
assert!(parse_ttl("1e3").is_err());
assert!(parse_ttl("1E3").is_err());
assert!(parse_ttl("-1").is_err());
assert!(parse_ttl("+1").is_err());
assert!(parse_ttl("+123").is_err());
assert!(parse_ttl("").is_err());
assert!(parse_ttl(" ").is_err());
assert!(parse_ttl("abc").is_err());
}
#[test]
fn test_parse_bool() {
assert!(parse_bool("true"));
assert!(parse_bool("TRUE"));
assert!(parse_bool("True"));
assert!(parse_bool(" true "));
assert!(!parse_bool("false"));
assert!(!parse_bool("1"));
assert!(!parse_bool(""));
assert!(!parse_bool("yes"));
}
#[test]
fn test_normalize_content_type() {
assert_eq!(normalize_content_type("text/plain"), "text/plain");
assert_eq!(normalize_content_type("TEXT/PLAIN"), "text/plain");
assert_eq!(
normalize_content_type("text/plain; charset=utf-8"),
"text/plain"
);
assert_eq!(
normalize_content_type("application/json;charset=utf-8"),
"application/json"
);
assert_eq!(normalize_content_type(" TEXT/PLAIN "), "text/plain");
}
}