dm_database_sqllog2db/stats/
config.rs1use crate::error::{ConfigError, Error};
4
5#[derive(Debug, Clone, Default, serde::Deserialize)]
7pub struct StatsConfig {
8 #[serde(default)]
9 pub from: Option<String>,
10 #[serde(default)]
11 pub to: Option<String>,
12 #[serde(default)]
13 pub top: Option<u32>,
14}
15
16pub fn validate_stats_time_range(stats: &StatsConfig) -> crate::error::Result<()> {
18 if let Some(from) = &stats.from {
19 validate_time_str(from).map_err(|reason| {
20 Error::Config(ConfigError::InvalidValue {
21 field: "stats.from".to_string(),
22 value: from.clone(),
23 reason,
24 })
25 })?;
26 }
27 if let Some(to) = &stats.to {
28 validate_time_str(to).map_err(|reason| {
29 Error::Config(ConfigError::InvalidValue {
30 field: "stats.to".to_string(),
31 value: to.clone(),
32 reason,
33 })
34 })?;
35 }
36 if let (Some(from), Some(to)) = (&stats.from, &stats.to) {
37 let cmp_len = from.len().min(to.len());
40 if from.as_bytes()[..cmp_len] > to.as_bytes()[..cmp_len] {
41 return Err(Error::Config(ConfigError::InvalidValue {
42 field: "stats.from".to_string(),
43 value: from.clone(),
44 reason: format!("stats.from ({from}) must be <= stats.to ({to})"),
45 }));
46 }
47 }
48 Ok(())
49}
50
51pub fn validate_time_str(s: &str) -> Result<(), String> {
61 let err = || r#"格式不合法,支持 "YYYY-MM-DD" 或 "YYYY-MM-DD HH:MM:SS""#.to_string();
62
63 if !s.is_ascii() {
64 return Err(err());
65 }
66
67 let bytes = s.as_bytes();
68 match bytes.len() {
69 10 => {
70 if check_date_part(bytes) {
71 Ok(())
72 } else {
73 Err(err())
74 }
75 }
76 19 => {
77 if check_date_part(bytes) && check_time_part(bytes) {
78 Ok(())
79 } else {
80 Err(err())
81 }
82 }
83 _ => Err(err()),
84 }
85}
86
87fn check_date_part(bytes: &[u8]) -> bool {
89 debug_assert!(bytes.len() >= 10, "check_date_part: need at least 10 bytes");
90 if !(bytes[4] == b'-' && bytes[7] == b'-') {
91 return false;
92 }
93 if !bytes[..4].iter().all(u8::is_ascii_digit) {
94 return false;
95 }
96 if !bytes[5..7].iter().all(u8::is_ascii_digit) {
97 return false;
98 }
99 if !bytes[8..10].iter().all(u8::is_ascii_digit) {
100 return false;
101 }
102 let month = (bytes[5] - b'0') * 10 + (bytes[6] - b'0');
103 let day = (bytes[8] - b'0') * 10 + (bytes[9] - b'0');
104 if !(1..=12).contains(&month) {
105 return false;
106 }
107 let max_day: u8 = match month {
108 2 => 29,
109 4 | 6 | 9 | 11 => 30,
110 _ => 31,
111 };
112 (1..=max_day).contains(&day)
113}
114
115fn check_time_part(bytes: &[u8]) -> bool {
117 debug_assert!(bytes.len() >= 19, "check_time_part: need at least 19 bytes");
118 if !(bytes[10] == b' ' && bytes[13] == b':' && bytes[16] == b':') {
119 return false;
120 }
121 if !bytes[11..13]
122 .iter()
123 .chain(bytes[14..16].iter())
124 .chain(bytes[17..19].iter())
125 .all(u8::is_ascii_digit)
126 {
127 return false;
128 }
129 let hour = (bytes[11] - b'0') * 10 + (bytes[12] - b'0');
130 let min = (bytes[14] - b'0') * 10 + (bytes[15] - b'0');
131 let sec = (bytes[17] - b'0') * 10 + (bytes[18] - b'0');
132 hour <= 23 && min <= 59 && sec <= 59
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_validate_time_str_accepts_date_only() {
141 assert!(validate_time_str("2024-01-01").is_ok());
142 }
143
144 #[test]
145 fn test_validate_time_str_accepts_datetime() {
146 assert!(validate_time_str("2024-12-31 23:59:59").is_ok());
147 }
148
149 #[test]
150 fn test_validate_time_str_rejects_no_separator() {
151 let result = validate_time_str("20240101");
152 assert!(result.is_err());
153 let msg = result.unwrap_err();
154 assert!(
155 msg.contains("YYYY-MM-DD"),
156 "error should contain YYYY-MM-DD: {msg}"
157 );
158 assert!(
159 msg.contains("YYYY-MM-DD HH:MM:SS"),
160 "error should contain YYYY-MM-DD HH:MM:SS: {msg}"
161 );
162 }
163
164 #[test]
165 fn test_validate_time_str_rejects_not_a_date() {
166 let result = validate_time_str("not-a-date");
167 assert!(result.is_err());
168 }
169
170 #[test]
171 fn test_validate_time_str_rejects_short_date() {
172 let result = validate_time_str("2024-1-1");
173 assert!(result.is_err());
174 }
175
176 #[test]
177 fn test_validate_time_str_rejects_t_separator() {
178 let result = validate_time_str("2024-01-01T12:00:00");
179 assert!(result.is_err());
180 }
181
182 #[test]
183 fn test_validate_time_str_rejects_slash_separator() {
184 let result = validate_time_str("2024/01/01");
185 assert!(result.is_err());
186 }
187
188 #[test]
189 fn test_validate_time_str_rejects_empty() {
190 let result = validate_time_str("");
191 assert!(result.is_err());
192 }
193
194 #[test]
195 fn test_validate_time_str_rejects_month_zero() {
196 assert!(validate_time_str("2024-00-01").is_err());
197 }
198
199 #[test]
200 fn test_validate_time_str_rejects_month_13() {
201 assert!(validate_time_str("2024-13-01").is_err());
202 }
203
204 #[test]
205 fn test_validate_time_str_rejects_day_zero() {
206 assert!(validate_time_str("2024-01-00").is_err());
207 }
208
209 #[test]
210 fn test_validate_time_str_rejects_day_32() {
211 assert!(validate_time_str("2024-01-32").is_err());
212 }
213
214 #[test]
215 fn test_validate_time_str_rejects_hour_24() {
216 assert!(validate_time_str("2024-01-01 24:00:00").is_err());
217 }
218
219 #[test]
220 fn test_validate_time_str_rejects_minute_60() {
221 assert!(validate_time_str("2024-01-01 00:60:00").is_err());
222 }
223
224 #[test]
225 fn test_validate_time_str_rejects_second_60() {
226 assert!(validate_time_str("2024-01-01 00:00:60").is_err());
227 }
228
229 #[test]
230 fn test_stats_config_default_all_none() {
231 let cfg = StatsConfig::default();
232 assert!(cfg.from.is_none());
233 assert!(cfg.to.is_none());
234 assert!(cfg.top.is_none());
235 }
236
237 #[test]
238 fn test_stats_config_deserialize_empty_toml() {
239 #[derive(serde::Deserialize)]
240 struct W {
241 stats: StatsConfig,
242 }
243 let w: W = toml::from_str("[stats]").unwrap();
244 assert!(w.stats.from.is_none());
245 assert!(w.stats.to.is_none());
246 assert!(w.stats.top.is_none());
247 }
248
249 #[test]
250 fn test_stats_config_deserialize_partial_toml() {
251 #[derive(serde::Deserialize)]
252 struct W {
253 stats: StatsConfig,
254 }
255 let w: W = toml::from_str("[stats]\nfrom = \"2024-01-01\"\ntop = 10").unwrap();
256 assert_eq!(w.stats.from, Some("2024-01-01".to_string()));
257 assert!(w.stats.to.is_none());
258 assert_eq!(w.stats.top, Some(10));
259 }
260
261 #[test]
262 fn test_validate_stats_time_range_rejects_from_after_to() {
263 let cfg = StatsConfig {
264 from: Some("2024-01-31".to_string()),
265 to: Some("2024-01-01".to_string()),
266 top: None,
267 };
268 let result = validate_stats_time_range(&cfg);
269 assert!(result.is_err(), "from > to should return Err");
270 match result.unwrap_err() {
271 crate::error::Error::Config(crate::error::ConfigError::InvalidValue {
272 field,
273 value,
274 reason,
275 }) => {
276 assert_eq!(field, "stats.from");
277 assert_eq!(value, "2024-01-31");
278 assert!(
279 reason.contains("must be <="),
280 "reason should contain 'must be <=': {reason}"
281 );
282 assert!(
283 reason.contains("2024-01-31"),
284 "reason should contain from value: {reason}"
285 );
286 assert!(
287 reason.contains("2024-01-01"),
288 "reason should contain to value: {reason}"
289 );
290 }
291 err => panic!("expected ConfigError::InvalidValue, got: {err:?}"),
292 }
293 }
294
295 #[test]
296 fn test_validate_stats_time_range_accepts_equal_from_to() {
297 let cfg = StatsConfig {
298 from: Some("2024-01-15".to_string()),
299 to: Some("2024-01-15".to_string()),
300 top: None,
301 };
302 assert!(
303 validate_stats_time_range(&cfg).is_ok(),
304 "from == to should be accepted"
305 );
306 }
307
308 #[test]
309 fn test_validate_stats_time_range_accepts_from_only() {
310 let cfg_from_only = StatsConfig {
311 from: Some("2024-01-15".to_string()),
312 to: None,
313 top: None,
314 };
315 assert!(
316 validate_stats_time_range(&cfg_from_only).is_ok(),
317 "only from should be accepted"
318 );
319 let cfg_to_only = StatsConfig {
320 from: None,
321 to: Some("2024-01-15".to_string()),
322 top: None,
323 };
324 assert!(
325 validate_stats_time_range(&cfg_to_only).is_ok(),
326 "only to should be accepted"
327 );
328 }
329
330 #[test]
331 fn test_validate_stats_time_range_accepts_ordered() {
332 let cfg = StatsConfig {
333 from: Some("2024-01-01".to_string()),
334 to: Some("2024-01-31".to_string()),
335 top: None,
336 };
337 assert!(
338 validate_stats_time_range(&cfg).is_ok(),
339 "from < to should be accepted"
340 );
341 }
342
343 #[test]
344 fn test_validate_stats_time_range_accepts_datetime_from_with_date_to() {
345 let cfg = StatsConfig {
346 from: Some("2024-01-15 00:00:00".to_string()),
347 to: Some("2024-01-15".to_string()),
348 top: None,
349 };
350 assert!(
351 validate_stats_time_range(&cfg).is_ok(),
352 "datetime from at start of to-date should be accepted"
353 );
354 }
355
356 #[test]
357 fn test_validate_time_str_rejects_feb_31() {
358 assert!(validate_time_str("2024-02-31").is_err());
359 }
360
361 #[test]
362 fn test_validate_time_str_rejects_apr_31() {
363 assert!(validate_time_str("2024-04-31").is_err());
364 }
365
366 #[test]
367 fn test_validate_time_str_accepts_feb_29() {
368 assert!(validate_time_str("2024-02-29").is_ok());
369 }
370}