1use compact_str::CompactString;
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum PreferResolution {
17 MergeDuplicates,
18 IgnoreDuplicates,
19}
20
21impl PreferResolution {
22 fn header_value(&self) -> &'static str {
23 match self {
24 PreferResolution::MergeDuplicates => "resolution=merge-duplicates",
25 PreferResolution::IgnoreDuplicates => "resolution=ignore-duplicates",
26 }
27 }
28
29 fn parse(s: &str) -> Option<Self> {
30 match s {
31 "resolution=merge-duplicates" => Some(PreferResolution::MergeDuplicates),
32 "resolution=ignore-duplicates" => Some(PreferResolution::IgnoreDuplicates),
33 _ => None,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum PreferRepresentation {
41 Full,
43 HeadersOnly,
45 None,
47}
48
49impl PreferRepresentation {
50 fn header_value(&self) -> &'static str {
51 match self {
52 PreferRepresentation::Full => "return=representation",
53 PreferRepresentation::HeadersOnly => "return=headers-only",
54 PreferRepresentation::None => "return=minimal",
55 }
56 }
57
58 fn parse(s: &str) -> Option<Self> {
59 match s {
60 "return=representation" => Some(PreferRepresentation::Full),
61 "return=headers-only" => Some(PreferRepresentation::HeadersOnly),
62 "return=minimal" => Some(PreferRepresentation::None),
63 _ => None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum PreferCount {
71 Exact,
73 Planned,
75 Estimated,
77}
78
79impl PreferCount {
80 fn header_value(&self) -> &'static str {
81 match self {
82 PreferCount::Exact => "count=exact",
83 PreferCount::Planned => "count=planned",
84 PreferCount::Estimated => "count=estimated",
85 }
86 }
87
88 fn parse(s: &str) -> Option<Self> {
89 match s {
90 "count=exact" => Some(PreferCount::Exact),
91 "count=planned" => Some(PreferCount::Planned),
92 "count=estimated" => Some(PreferCount::Estimated),
93 _ => None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100pub enum PreferTransaction {
101 Commit,
102 Rollback,
103}
104
105impl PreferTransaction {
106 fn header_value(&self) -> &'static str {
107 match self {
108 PreferTransaction::Commit => "tx=commit",
109 PreferTransaction::Rollback => "tx=rollback",
110 }
111 }
112
113 fn parse(s: &str) -> Option<Self> {
114 match s {
115 "tx=commit" => Some(PreferTransaction::Commit),
116 "tx=rollback" => Some(PreferTransaction::Rollback),
117 _ => None,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum PreferMissing {
125 ApplyDefaults,
127 ApplyNulls,
129}
130
131impl PreferMissing {
132 fn header_value(&self) -> &'static str {
133 match self {
134 PreferMissing::ApplyDefaults => "missing=default",
135 PreferMissing::ApplyNulls => "missing=null",
136 }
137 }
138
139 fn parse(s: &str) -> Option<Self> {
140 match s {
141 "missing=default" => Some(PreferMissing::ApplyDefaults),
142 "missing=null" => Some(PreferMissing::ApplyNulls),
143 _ => None,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum PreferHandling {
151 Strict,
152 Lenient,
153}
154
155impl PreferHandling {
156 fn header_value(&self) -> &'static str {
157 match self {
158 PreferHandling::Strict => "handling=strict",
159 PreferHandling::Lenient => "handling=lenient",
160 }
161 }
162
163 fn parse(s: &str) -> Option<Self> {
164 match s {
165 "handling=strict" => Some(PreferHandling::Strict),
166 "handling=lenient" => Some(PreferHandling::Lenient),
167 _ => None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174pub enum PreferPlurality {
175 Plural,
177 Singular,
179}
180
181impl PreferPlurality {
182 fn header_value(&self) -> &'static str {
183 match self {
184 PreferPlurality::Plural => "plurality=plural",
185 PreferPlurality::Singular => "plurality=singular",
186 }
187 }
188
189 fn parse(s: &str) -> Option<Self> {
190 match s {
191 "plurality=plural" => Some(PreferPlurality::Plural),
192 "plurality=singular" => Some(PreferPlurality::Singular),
193 _ => None,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
206pub struct Preferences {
207 pub resolution: Option<PreferResolution>,
208 pub representation: Option<PreferRepresentation>,
209 pub count: Option<PreferCount>,
210 pub transaction: Option<PreferTransaction>,
211 pub missing: Option<PreferMissing>,
212 pub handling: Option<PreferHandling>,
213 pub plurality: Option<PreferPlurality>,
214 pub timezone: Option<CompactString>,
215 pub max_affected: Option<i64>,
216 pub invalid_prefs: Vec<CompactString>,
218}
219
220impl Preferences {
221 pub fn from_headers(
229 allow_tx_override: bool,
230 valid_timezones: &HashSet<String>,
231 headers: &[(impl AsRef<str>, impl AsRef<str>)],
232 ) -> Self {
233 let prefs: Vec<String> = headers
235 .iter()
236 .filter(|(name, _)| name.as_ref().eq_ignore_ascii_case("prefer"))
237 .flat_map(|(_, value)| {
238 value
239 .as_ref()
240 .split(',')
241 .map(|s| s.trim().to_string())
242 .collect::<Vec<_>>()
243 })
244 .filter(|s| !s.is_empty())
245 .collect();
246
247 let resolution = prefs.iter().find_map(|p| PreferResolution::parse(p));
249 let representation = prefs.iter().find_map(|p| PreferRepresentation::parse(p));
250 let count = prefs.iter().find_map(|p| PreferCount::parse(p));
251 let transaction = if allow_tx_override {
252 prefs.iter().find_map(|p| PreferTransaction::parse(p))
253 } else {
254 None
255 };
256 let missing = prefs.iter().find_map(|p| PreferMissing::parse(p));
257 let handling = prefs.iter().find_map(|p| PreferHandling::parse(p));
258 let plurality = prefs.iter().find_map(|p| PreferPlurality::parse(p));
259
260 let timezone_pref = prefs
262 .iter()
263 .find_map(|p| p.strip_prefix("timezone=").map(|s| s.to_string()));
264 let timezone = timezone_pref.as_ref().and_then(|tz| {
265 if valid_timezones.contains(tz) {
266 Some(CompactString::from(tz.as_str()))
267 } else {
268 None
269 }
270 });
271 let timezone_accepted = timezone.is_some();
272
273 let max_affected = prefs
275 .iter()
276 .find_map(|p| p.strip_prefix("max-affected=").and_then(|s| s.parse().ok()));
277
278 let accepted: HashSet<&str> = [
280 PreferResolution::MergeDuplicates.header_value(),
281 PreferResolution::IgnoreDuplicates.header_value(),
282 PreferRepresentation::Full.header_value(),
283 PreferRepresentation::HeadersOnly.header_value(),
284 PreferRepresentation::None.header_value(),
285 PreferCount::Exact.header_value(),
286 PreferCount::Planned.header_value(),
287 PreferCount::Estimated.header_value(),
288 PreferTransaction::Commit.header_value(),
289 PreferTransaction::Rollback.header_value(),
290 PreferMissing::ApplyDefaults.header_value(),
291 PreferMissing::ApplyNulls.header_value(),
292 PreferHandling::Strict.header_value(),
293 PreferHandling::Lenient.header_value(),
294 PreferPlurality::Plural.header_value(),
295 PreferPlurality::Singular.header_value(),
296 ]
297 .into_iter()
298 .collect();
299
300 let invalid_prefs: Vec<CompactString> = prefs
302 .iter()
303 .filter(|p| {
304 let p_str = p.as_str();
305 !(accepted.contains(p_str)
306 || p_str.starts_with("max-affected=")
307 || (p_str.starts_with("timezone=") && timezone_accepted))
308 })
309 .map(|p| CompactString::from(p.as_str()))
310 .collect();
311
312 Preferences {
313 resolution,
314 representation,
315 count,
316 transaction,
317 missing,
318 handling,
319 plurality,
320 timezone,
321 max_affected,
322 invalid_prefs,
323 }
324 }
325
326 pub fn should_count(&self) -> bool {
328 self.count == Some(PreferCount::Exact) || self.count == Some(PreferCount::Estimated)
329 }
330
331 pub fn should_explain_count(&self) -> bool {
333 self.count == Some(PreferCount::Planned) || self.count == Some(PreferCount::Estimated)
334 }
335
336 pub fn applied_header(&self) -> Option<String> {
338 let mut parts: Vec<&str> = Vec::new();
339
340 if let Some(r) = &self.resolution {
341 parts.push(r.header_value());
342 }
343 if let Some(m) = &self.missing {
344 parts.push(m.header_value());
345 }
346 if let Some(r) = &self.representation {
347 parts.push(r.header_value());
348 }
349 if let Some(c) = &self.count {
350 parts.push(c.header_value());
351 }
352 if let Some(t) = &self.transaction {
353 parts.push(t.header_value());
354 }
355 if let Some(h) = &self.handling {
356 parts.push(h.header_value());
357 }
358 if let Some(p) = &self.plurality {
359 parts.push(p.header_value());
360 }
361
362 if parts.is_empty() {
366 None
367 } else {
368 Some(parts.join(", "))
369 }
370 }
371
372 pub fn is_strict(&self) -> bool {
374 self.handling == Some(PreferHandling::Strict)
375 }
376}
377
378#[cfg(test)]
383mod tests {
384 use super::*;
385
386 fn empty_tz() -> HashSet<String> {
387 HashSet::new()
388 }
389
390 fn sample_tz() -> HashSet<String> {
391 let mut tz = HashSet::new();
392 tz.insert("America/Los_Angeles".to_string());
393 tz.insert("UTC".to_string());
394 tz
395 }
396
397 #[test]
398 fn test_single_preference() {
399 let headers = vec![("Prefer", "return=representation")];
400 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
401 assert_eq!(prefs.representation, Some(PreferRepresentation::Full));
402 assert!(prefs.resolution.is_none());
403 }
404
405 #[test]
406 fn test_multiple_prefs_comma_separated() {
407 let headers = vec![(
408 "Prefer",
409 "resolution=ignore-duplicates, count=exact, return=representation",
410 )];
411 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
412 assert_eq!(prefs.resolution, Some(PreferResolution::IgnoreDuplicates));
413 assert_eq!(prefs.count, Some(PreferCount::Exact));
414 assert_eq!(prefs.representation, Some(PreferRepresentation::Full));
415 }
416
417 #[test]
418 fn test_multiple_prefer_headers() {
419 let headers = vec![
420 ("Prefer", "resolution=ignore-duplicates"),
421 ("Prefer", "count=exact"),
422 ("Prefer", "missing=null"),
423 ("Prefer", "handling=lenient"),
424 ];
425 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
426 assert_eq!(prefs.resolution, Some(PreferResolution::IgnoreDuplicates));
427 assert_eq!(prefs.count, Some(PreferCount::Exact));
428 assert_eq!(prefs.missing, Some(PreferMissing::ApplyNulls));
429 assert_eq!(prefs.handling, Some(PreferHandling::Lenient));
430 }
431
432 #[test]
433 fn test_first_preference_wins() {
434 let headers = vec![("Prefer", "tx=commit, tx=rollback")];
436 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
437 assert_eq!(prefs.transaction, Some(PreferTransaction::Commit));
438 }
439
440 #[test]
441 fn test_first_preference_wins_across_headers() {
442 let headers = vec![
443 ("Prefer", "resolution=ignore-duplicates"),
444 ("Prefer", "resolution=merge-duplicates"),
445 ];
446 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
447 assert_eq!(prefs.resolution, Some(PreferResolution::IgnoreDuplicates));
448 }
449
450 #[test]
451 fn test_tx_override_disabled() {
452 let headers = vec![("Prefer", "tx=commit")];
453 let prefs = Preferences::from_headers(false, &empty_tz(), &headers);
454 assert!(prefs.transaction.is_none());
455 }
456
457 #[test]
458 fn test_invalid_preferences() {
459 let headers = vec![("Prefer", "invalid, handling=strict")];
460 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
461 assert_eq!(prefs.handling, Some(PreferHandling::Strict));
462 assert_eq!(prefs.invalid_prefs.len(), 1);
463 assert_eq!(prefs.invalid_prefs[0].as_str(), "invalid");
464 }
465
466 #[test]
467 fn test_timezone_preference() {
468 let headers = vec![("Prefer", "timezone=America/Los_Angeles")];
469 let prefs = Preferences::from_headers(true, &sample_tz(), &headers);
470 assert_eq!(prefs.timezone.as_deref(), Some("America/Los_Angeles"));
471 }
472
473 #[test]
474 fn test_timezone_invalid() {
475 let headers = vec![("Prefer", "timezone=Invalid/Zone")];
476 let prefs = Preferences::from_headers(true, &sample_tz(), &headers);
477 assert!(prefs.timezone.is_none());
478 assert_eq!(prefs.invalid_prefs.len(), 1);
479 }
480
481 #[test]
482 fn test_max_affected() {
483 let headers = vec![("Prefer", "max-affected=100")];
484 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
485 assert_eq!(prefs.max_affected, Some(100));
486 }
487
488 #[test]
489 fn test_max_affected_not_invalid() {
490 let headers = vec![("Prefer", "max-affected=5999")];
491 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
492 assert_eq!(prefs.max_affected, Some(5999));
493 assert!(prefs.invalid_prefs.is_empty());
494 }
495
496 #[test]
497 fn test_case_insensitive_header_name() {
498 let headers = vec![("prefer", "count=exact")];
499 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
500 assert_eq!(prefs.count, Some(PreferCount::Exact));
501 }
502
503 #[test]
504 fn test_whitespace_handling() {
505 let headers = vec![(
506 "Prefer",
507 "count=exact, tx=commit ,return=representation , missing=default, handling=strict",
508 )];
509 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
510 assert_eq!(prefs.count, Some(PreferCount::Exact));
511 assert_eq!(prefs.transaction, Some(PreferTransaction::Commit));
512 assert_eq!(prefs.representation, Some(PreferRepresentation::Full));
513 assert_eq!(prefs.missing, Some(PreferMissing::ApplyDefaults));
514 assert_eq!(prefs.handling, Some(PreferHandling::Strict));
515 }
516
517 #[test]
518 fn test_should_count() {
519 let mut p = Preferences::default();
520 assert!(!p.should_count());
521
522 p.count = Some(PreferCount::Exact);
523 assert!(p.should_count());
524
525 p.count = Some(PreferCount::Estimated);
526 assert!(p.should_count());
527
528 p.count = Some(PreferCount::Planned);
529 assert!(!p.should_count());
530 }
531
532 #[test]
533 fn test_should_explain_count() {
534 let mut p = Preferences::default();
535 assert!(!p.should_explain_count());
536
537 p.count = Some(PreferCount::Planned);
538 assert!(p.should_explain_count());
539
540 p.count = Some(PreferCount::Estimated);
541 assert!(p.should_explain_count());
542
543 p.count = Some(PreferCount::Exact);
544 assert!(!p.should_explain_count());
545 }
546
547 #[test]
548 fn test_applied_header() {
549 let mut p = Preferences::default();
550 assert!(p.applied_header().is_none());
551
552 p.resolution = Some(PreferResolution::IgnoreDuplicates);
553 p.count = Some(PreferCount::Exact);
554 let h = p.applied_header().unwrap();
555 assert!(h.contains("resolution=ignore-duplicates"));
556 assert!(h.contains("count=exact"));
557 }
558
559 #[test]
560 fn test_is_strict() {
561 let mut p = Preferences::default();
562 assert!(!p.is_strict());
563
564 p.handling = Some(PreferHandling::Strict);
565 assert!(p.is_strict());
566 }
567
568 #[test]
569 fn test_empty_headers() {
570 let headers: Vec<(&str, &str)> = vec![];
571 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
572 assert_eq!(prefs, Preferences::default());
573 }
574
575 #[test]
576 fn test_comprehensive_parse() {
577 let headers = vec![(
578 "Prefer",
579 "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100",
580 )];
581 let prefs = Preferences::from_headers(true, &sample_tz(), &headers);
582 assert_eq!(prefs.resolution, Some(PreferResolution::IgnoreDuplicates));
583 assert_eq!(prefs.count, Some(PreferCount::Exact));
584 assert_eq!(prefs.timezone.as_deref(), Some("America/Los_Angeles"));
585 assert_eq!(prefs.max_affected, Some(100));
586 assert!(prefs.invalid_prefs.is_empty());
587 }
588
589 #[test]
590 fn test_all_return_values() {
591 let headers = vec![("Prefer", "return=minimal")];
592 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
593 assert_eq!(prefs.representation, Some(PreferRepresentation::None));
594
595 let headers = vec![("Prefer", "return=headers-only")];
596 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
597 assert_eq!(
598 prefs.representation,
599 Some(PreferRepresentation::HeadersOnly)
600 );
601 }
602
603 #[test]
604 fn test_all_count_values() {
605 for (val, expected) in [
606 ("count=exact", PreferCount::Exact),
607 ("count=planned", PreferCount::Planned),
608 ("count=estimated", PreferCount::Estimated),
609 ] {
610 let headers = vec![("Prefer", val)];
611 let prefs = Preferences::from_headers(true, &empty_tz(), &headers);
612 assert_eq!(prefs.count, Some(expected));
613 }
614 }
615}