Skip to main content

dbrest_core/api_request/
preferences.rs

1//! HTTP Prefer header parsing
2//!
3//! Parses HTTP `Prefer` headers into structured preference objects.
4//! Parses RFC 7240 Prefer headers into structured preference types.
5
6use compact_str::CompactString;
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10// ==========================================================================
11// Preference enums
12// ==========================================================================
13
14/// How to handle duplicate values during upsert.
15#[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/// How to return mutated data.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum PreferRepresentation {
41    /// Return the body
42    Full,
43    /// Return the Location header (for POST)
44    HeadersOnly,
45    /// Return nothing
46    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/// How to count results.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum PreferCount {
71    /// Exact count (slower)
72    Exact,
73    /// Query planner estimate
74    Planned,
75    /// Use planner if count exceeds max-rows, otherwise exact
76    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/// Whether to commit or rollback the transaction.
99#[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/// How to handle missing columns in insert/update.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum PreferMissing {
125    /// Use column defaults
126    ApplyDefaults,
127    /// Use null
128    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/// How to handle unrecognized preferences.
149#[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/// Response plurality preference.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174pub enum PreferPlurality {
175    /// Return array (default)
176    Plural,
177    /// Return single object (error if != 1 row)
178    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// ==========================================================================
199// Preferences struct
200// ==========================================================================
201
202/// All recognized preferences from Prefer headers.
203///
204/// Parsed HTTP `Prefer` header values.
205#[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    /// Preference strings that were not recognized
217    pub invalid_prefs: Vec<CompactString>,
218}
219
220impl Preferences {
221    /// Parse preferences from HTTP headers.
222    ///
223    /// Parse preferences from HTTP headers.
224    ///
225    /// - `allow_tx_override`: whether to allow `tx=commit`/`tx=rollback`
226    /// - `valid_timezones`: set of accepted timezone names
227    /// - `headers`: HTTP headers (name, value) pairs
228    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        // Collect all Prefer header values, split by comma
234        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        // Parse each preference category (first match wins)
248        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        // Parse timezone preference
261        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        // Parse max-affected preference
274        let max_affected = prefs
275            .iter()
276            .find_map(|p| p.strip_prefix("max-affected=").and_then(|s| s.parse().ok()));
277
278        // Build set of all accepted preference strings
279        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        // Find invalid preferences
301        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    /// Check if we should execute a count query.
327    pub fn should_count(&self) -> bool {
328        self.count == Some(PreferCount::Exact) || self.count == Some(PreferCount::Estimated)
329    }
330
331    /// Check if we should use EXPLAIN for count.
332    pub fn should_explain_count(&self) -> bool {
333        self.count == Some(PreferCount::Planned) || self.count == Some(PreferCount::Estimated)
334    }
335
336    /// Build the Preference-Applied response header value.
337    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        // timezone and max_affected in applied header only if handling=strict
363        // (first occurrence wins per spec)
364
365        if parts.is_empty() {
366            None
367        } else {
368            Some(parts.join(", "))
369        }
370    }
371
372    /// Check if handling is strict.
373    pub fn is_strict(&self) -> bool {
374        self.handling == Some(PreferHandling::Strict)
375    }
376}
377
378// ==========================================================================
379// Tests
380// ==========================================================================
381
382#[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        // Per spec, first occurrence wins
435        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}