postrust_core/api_request/
preferences.rs

1//! Prefer header parsing (RFC 7240).
2//!
3//! Parses the HTTP Prefer header to extract PostgREST preferences.
4
5use super::types::*;
6use crate::error::{Error, Result};
7use http::HeaderMap;
8
9/// Parse Prefer header into Preferences struct.
10pub fn parse_preferences(headers: &HeaderMap) -> Result<Preferences> {
11    let mut prefs = Preferences::default();
12
13    let prefer = match headers.get("prefer") {
14        Some(v) => v.to_str().map_err(|_| Error::InvalidHeader("Prefer"))?,
15        None => return Ok(prefs),
16    };
17
18    for pref in prefer.split(',').map(|s| s.trim()) {
19        parse_preference(&mut prefs, pref);
20    }
21
22    Ok(prefs)
23}
24
25fn parse_preference(prefs: &mut Preferences, pref: &str) {
26    let pref = pref.trim();
27
28    // Handle key=value preferences
29    if let Some((key, value)) = pref.split_once('=') {
30        let key = key.trim();
31        let value = value.trim().trim_matches('"');
32
33        match key {
34            "resolution" => {
35                prefs.resolution = match value {
36                    "merge-duplicates" => Some(PreferResolution::MergeDuplicates),
37                    "ignore-duplicates" => Some(PreferResolution::IgnoreDuplicates),
38                    _ => None,
39                };
40            }
41            "return" => {
42                prefs.representation = match value {
43                    "representation" => PreferRepresentation::Full,
44                    "headers-only" => PreferRepresentation::HeadersOnly,
45                    "minimal" => PreferRepresentation::None,
46                    _ => PreferRepresentation::None,
47                };
48            }
49            "count" => {
50                prefs.count = match value {
51                    "exact" => Some(PreferCount::Exact),
52                    "planned" => Some(PreferCount::Planned),
53                    "estimated" => Some(PreferCount::Estimated),
54                    _ => None,
55                };
56            }
57            "tx" => {
58                prefs.transaction = match value {
59                    "commit" => PreferTransaction::Commit,
60                    "rollback" => PreferTransaction::Rollback,
61                    _ => PreferTransaction::Commit,
62                };
63            }
64            "missing" => {
65                prefs.missing = match value {
66                    "default" => PreferMissing::ApplyDefaults,
67                    "null" => PreferMissing::ApplyNulls,
68                    _ => PreferMissing::ApplyDefaults,
69                };
70            }
71            "handling" => {
72                prefs.handling = match value {
73                    "strict" => PreferHandling::Strict,
74                    "lenient" => PreferHandling::Lenient,
75                    _ => PreferHandling::Strict,
76                };
77            }
78            "timezone" => {
79                prefs.timezone = Some(value.to_string());
80            }
81            "max-affected" => {
82                if let Ok(n) = value.parse::<i64>() {
83                    prefs.max_affected = Some(n);
84                }
85            }
86            _ => {
87                prefs.invalid.push(pref.to_string());
88            }
89        }
90        return;
91    }
92
93    // Handle standalone preferences
94    match pref {
95        "return=representation" => prefs.representation = PreferRepresentation::Full,
96        "return=headers-only" => prefs.representation = PreferRepresentation::HeadersOnly,
97        "return=minimal" => prefs.representation = PreferRepresentation::None,
98        "count=exact" => prefs.count = Some(PreferCount::Exact),
99        "count=planned" => prefs.count = Some(PreferCount::Planned),
100        "count=estimated" => prefs.count = Some(PreferCount::Estimated),
101        "resolution=merge-duplicates" => prefs.resolution = Some(PreferResolution::MergeDuplicates),
102        "resolution=ignore-duplicates" => {
103            prefs.resolution = Some(PreferResolution::IgnoreDuplicates)
104        }
105        "tx=commit" => prefs.transaction = PreferTransaction::Commit,
106        "tx=rollback" => prefs.transaction = PreferTransaction::Rollback,
107        "params=single-object" => {} // RPC parameter mode
108        "params=multiple-objects" => {}
109        _ => {
110            prefs.invalid.push(pref.to_string());
111        }
112    }
113}
114
115/// Build Preference-Applied header from applied preferences.
116pub fn preference_applied(prefs: &Preferences) -> Option<String> {
117    let mut applied = Vec::new();
118
119    if prefs.resolution.is_some() {
120        let val = match prefs.resolution {
121            Some(PreferResolution::MergeDuplicates) => "resolution=merge-duplicates",
122            Some(PreferResolution::IgnoreDuplicates) => "resolution=ignore-duplicates",
123            None => "",
124        };
125        if !val.is_empty() {
126            applied.push(val);
127        }
128    }
129
130    match prefs.representation {
131        PreferRepresentation::Full => applied.push("return=representation"),
132        PreferRepresentation::HeadersOnly => applied.push("return=headers-only"),
133        PreferRepresentation::None => {}
134    }
135
136    if let Some(count) = &prefs.count {
137        let val = match count {
138            PreferCount::Exact => "count=exact",
139            PreferCount::Planned => "count=planned",
140            PreferCount::Estimated => "count=estimated",
141        };
142        applied.push(val);
143    }
144
145    match prefs.transaction {
146        PreferTransaction::Rollback => applied.push("tx=rollback"),
147        PreferTransaction::Commit => {}
148    }
149
150    if applied.is_empty() {
151        None
152    } else {
153        Some(applied.join(", "))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use http::HeaderValue;
161
162    fn headers_with_prefer(value: &str) -> HeaderMap {
163        let mut headers = HeaderMap::new();
164        headers.insert("prefer", HeaderValue::from_str(value).unwrap());
165        headers
166    }
167
168    #[test]
169    fn test_parse_return_representation() {
170        let headers = headers_with_prefer("return=representation");
171        let prefs = parse_preferences(&headers).unwrap();
172        assert_eq!(prefs.representation, PreferRepresentation::Full);
173    }
174
175    #[test]
176    fn test_parse_count_exact() {
177        let headers = headers_with_prefer("count=exact");
178        let prefs = parse_preferences(&headers).unwrap();
179        assert_eq!(prefs.count, Some(PreferCount::Exact));
180    }
181
182    #[test]
183    fn test_parse_resolution() {
184        let headers = headers_with_prefer("resolution=merge-duplicates");
185        let prefs = parse_preferences(&headers).unwrap();
186        assert_eq!(prefs.resolution, Some(PreferResolution::MergeDuplicates));
187    }
188
189    #[test]
190    fn test_parse_multiple() {
191        let headers = headers_with_prefer("return=representation, count=exact, tx=rollback");
192        let prefs = parse_preferences(&headers).unwrap();
193        assert_eq!(prefs.representation, PreferRepresentation::Full);
194        assert_eq!(prefs.count, Some(PreferCount::Exact));
195        assert_eq!(prefs.transaction, PreferTransaction::Rollback);
196    }
197
198    #[test]
199    fn test_parse_timezone() {
200        let headers = headers_with_prefer("timezone=America/New_York");
201        let prefs = parse_preferences(&headers).unwrap();
202        assert_eq!(prefs.timezone, Some("America/New_York".to_string()));
203    }
204
205    #[test]
206    fn test_parse_max_affected() {
207        let headers = headers_with_prefer("max-affected=100");
208        let prefs = parse_preferences(&headers).unwrap();
209        assert_eq!(prefs.max_affected, Some(100));
210    }
211
212    #[test]
213    fn test_preference_applied() {
214        let mut prefs = Preferences::default();
215        prefs.representation = PreferRepresentation::Full;
216        prefs.count = Some(PreferCount::Exact);
217
218        let applied = preference_applied(&prefs).unwrap();
219        assert!(applied.contains("return=representation"));
220        assert!(applied.contains("count=exact"));
221    }
222}