durable_streams_server/protocol/
headers.rs1use crate::protocol::error::{Error, Result};
2use chrono::{DateTime, Utc};
3
4pub mod names {
6 pub const STREAM_TTL: &str = "Stream-TTL";
7 pub const STREAM_EXPIRES_AT: &str = "Stream-Expires-At";
8 pub const STREAM_CLOSED: &str = "Stream-Closed";
9 pub const STREAM_NEXT_OFFSET: &str = "Stream-Next-Offset";
10 pub const STREAM_UP_TO_DATE: &str = "Stream-Up-To-Date";
11 pub const STREAM_CURSOR: &str = "Stream-Cursor";
12 pub const STREAM_SEQ: &str = "Stream-Seq";
13 pub const PRODUCER_ID: &str = "Producer-Id";
14 pub const PRODUCER_EPOCH: &str = "Producer-Epoch";
15 pub const PRODUCER_SEQ: &str = "Producer-Seq";
16 pub const PRODUCER_EXPECTED_SEQ: &str = "Producer-Expected-Seq";
17 pub const PRODUCER_RECEIVED_SEQ: &str = "Producer-Received-Seq";
18}
19
20pub fn parse_ttl(value: &str) -> Result<u64> {
30 let trimmed = value.trim();
31
32 if trimmed.is_empty() {
34 return Err(Error::InvalidTtl("empty value".to_string()));
35 }
36
37 if trimmed.len() > 1 && trimmed.starts_with('0') {
39 return Err(Error::InvalidTtl(format!(
40 "leading zeros not allowed: '{trimmed}'"
41 )));
42 }
43
44 if trimmed.contains('.') {
46 return Err(Error::InvalidTtl(format!(
47 "decimal values not allowed: '{trimmed}'"
48 )));
49 }
50
51 if trimmed.contains('e') || trimmed.contains('E') {
53 return Err(Error::InvalidTtl(format!(
54 "scientific notation not allowed: '{trimmed}'"
55 )));
56 }
57
58 if trimmed.starts_with('-') {
60 return Err(Error::InvalidTtl(format!(
61 "negative values not allowed: '{trimmed}'"
62 )));
63 }
64
65 if trimmed.starts_with('+') {
67 return Err(Error::InvalidTtl(format!(
68 "leading plus sign not allowed: '{trimmed}'"
69 )));
70 }
71
72 trimmed
74 .parse::<u64>()
75 .map_err(|e| Error::InvalidTtl(format!("invalid integer '{trimmed}': {e}")))
76}
77
78pub fn parse_expires_at(value: &str) -> Result<DateTime<Utc>> {
84 value
85 .parse::<DateTime<Utc>>()
86 .map_err(|e| Error::InvalidHeader {
87 header: names::STREAM_EXPIRES_AT.to_string(),
88 reason: format!("invalid ISO 8601 timestamp: {e}"),
89 })
90}
91
92#[must_use]
96pub fn parse_bool(value: &str) -> bool {
97 value.trim().eq_ignore_ascii_case("true")
98}
99
100#[must_use]
105pub fn normalize_content_type(content_type: &str) -> String {
106 content_type
107 .split(';')
108 .next()
109 .unwrap_or(content_type)
110 .trim()
111 .to_lowercase()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_parse_ttl_valid() {
120 assert_eq!(parse_ttl("0").unwrap(), 0);
121 assert_eq!(parse_ttl("1").unwrap(), 1);
122 assert_eq!(parse_ttl("3600").unwrap(), 3600);
123 assert_eq!(parse_ttl("86400").unwrap(), 86400);
124 assert_eq!(parse_ttl(" 100 ").unwrap(), 100); }
126
127 #[test]
128 fn test_parse_ttl_invalid() {
129 assert!(parse_ttl("01").is_err());
131 assert!(parse_ttl("00").is_err());
132 assert!(parse_ttl("0123").is_err());
133
134 assert!(parse_ttl("1.5").is_err());
136 assert!(parse_ttl("3600.0").is_err());
137
138 assert!(parse_ttl("1e3").is_err());
140 assert!(parse_ttl("1E3").is_err());
141
142 assert!(parse_ttl("-1").is_err());
144
145 assert!(parse_ttl("+1").is_err());
147 assert!(parse_ttl("+123").is_err());
148
149 assert!(parse_ttl("").is_err());
151 assert!(parse_ttl(" ").is_err());
152
153 assert!(parse_ttl("abc").is_err());
155 }
156
157 #[test]
158 fn test_parse_bool() {
159 assert!(parse_bool("true"));
160 assert!(parse_bool("TRUE"));
161 assert!(parse_bool("True"));
162 assert!(parse_bool(" true "));
163
164 assert!(!parse_bool("false"));
165 assert!(!parse_bool("1"));
166 assert!(!parse_bool(""));
167 assert!(!parse_bool("yes"));
168 }
169
170 #[test]
171 fn test_normalize_content_type() {
172 assert_eq!(normalize_content_type("text/plain"), "text/plain");
173 assert_eq!(normalize_content_type("TEXT/PLAIN"), "text/plain");
174 assert_eq!(
175 normalize_content_type("text/plain; charset=utf-8"),
176 "text/plain"
177 );
178 assert_eq!(
179 normalize_content_type("application/json;charset=utf-8"),
180 "application/json"
181 );
182 assert_eq!(normalize_content_type(" TEXT/PLAIN "), "text/plain");
183 }
184}