Skip to main content

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    // A client may send one `Prefer` header with a comma-separated list, or
14    // several headers, or both -- RFC 7240 allows all three and PostgREST's
15    // own suite uses more than one. Reading only the first drops the rest.
16    let mut seen_any = false;
17    for value in headers.get_all("prefer") {
18        let value = value.to_str().map_err(|_| Error::InvalidHeader("Prefer"))?;
19        seen_any = true;
20        for pref in value.split(',').map(|s| s.trim()) {
21            parse_preference(&mut prefs, pref);
22        }
23    }
24
25    if !seen_any {
26        return Ok(prefs);
27    }
28
29    // `handling=strict` asks to be told about preferences the server does not
30    // implement rather than have them quietly dropped.
31    if prefs.handling == PreferHandling::Strict && !prefs.invalid.is_empty() {
32        return Err(Error::InvalidPreferences(prefs.invalid));
33    }
34
35    Ok(prefs)
36}
37
38fn parse_preference(prefs: &mut Preferences, pref: &str) {
39    let pref = pref.trim();
40
41    // Handle key=value preferences
42    if let Some((key, value)) = pref.split_once('=') {
43        let key = key.trim();
44        let value = value.trim().trim_matches('"');
45
46        match key {
47            "resolution" => {
48                prefs.resolution = match value {
49                    "merge-duplicates" => Some(PreferResolution::MergeDuplicates),
50                    "ignore-duplicates" => Some(PreferResolution::IgnoreDuplicates),
51                    _ => None,
52                };
53                if prefs.resolution.is_some() {
54                    prefs.applied.push(format!("resolution={}", value));
55                }
56            }
57            "return" => {
58                prefs.representation = match value {
59                    "representation" => PreferRepresentation::Full,
60                    "headers-only" => PreferRepresentation::HeadersOnly,
61                    "minimal" => PreferRepresentation::None,
62                    _ => PreferRepresentation::None,
63                };
64                prefs.applied.push(format!("return={}", value));
65            }
66            "count" => {
67                prefs.count = match value {
68                    "exact" => Some(PreferCount::Exact),
69                    "planned" => Some(PreferCount::Planned),
70                    "estimated" => Some(PreferCount::Estimated),
71                    _ => None,
72                };
73                if prefs.count.is_some() {
74                    prefs.applied.push(format!("count={}", value));
75                }
76            }
77            "tx" => {
78                prefs.transaction = match value {
79                    "commit" => PreferTransaction::Commit,
80                    "rollback" => PreferTransaction::Rollback,
81                    _ => PreferTransaction::Commit,
82                };
83                prefs.applied.push(format!("tx={}", value));
84            }
85            "missing" => {
86                prefs.missing = match value {
87                    "default" => PreferMissing::ApplyDefaults,
88                    "null" => PreferMissing::ApplyNulls,
89                    _ => PreferMissing::ApplyDefaults,
90                };
91                prefs.applied.push(format!("missing={}", value));
92            }
93            "handling" => {
94                prefs.handling = match value {
95                    "strict" => PreferHandling::Strict,
96                    "lenient" => PreferHandling::Lenient,
97                    _ => PreferHandling::Strict,
98                };
99                prefs.applied.push(format!("handling={}", value));
100            }
101            "timezone" => {
102                prefs.timezone = Some(value.to_string());
103                prefs.applied.push(format!("timezone={}", value));
104            }
105            // Understood, and applied where the RPC path reads the body.
106            "params" => {}
107            "max-affected" => {
108                if let Ok(n) = value.parse::<i64>() {
109                    prefs.max_affected = Some(n);
110                    prefs.applied.push(format!("max-affected={}", n));
111                }
112            }
113            _ => {
114                prefs.invalid.push(pref.to_string());
115            }
116        }
117        return;
118    }
119
120    // Handle standalone preferences
121    match pref {
122        "return=representation" => prefs.representation = PreferRepresentation::Full,
123        "return=headers-only" => prefs.representation = PreferRepresentation::HeadersOnly,
124        "return=minimal" => prefs.representation = PreferRepresentation::None,
125        "count=exact" => prefs.count = Some(PreferCount::Exact),
126        "count=planned" => prefs.count = Some(PreferCount::Planned),
127        "count=estimated" => prefs.count = Some(PreferCount::Estimated),
128        "resolution=merge-duplicates" => prefs.resolution = Some(PreferResolution::MergeDuplicates),
129        "resolution=ignore-duplicates" => {
130            prefs.resolution = Some(PreferResolution::IgnoreDuplicates)
131        }
132        "tx=commit" => prefs.transaction = PreferTransaction::Commit,
133        "tx=rollback" => prefs.transaction = PreferTransaction::Rollback,
134        "params=single-object" => {} // RPC parameter mode
135        "params=multiple-objects" => {}
136        _ => {
137            prefs.invalid.push(pref.to_string());
138        }
139    }
140}
141
142/// Build the `Preference-Applied` header.
143///
144/// Only preferences that were asked for, and only those the request could
145/// honour: `return=representation` says nothing on a read, and `missing`
146/// nothing on a delete. PostgREST filters the same way and in this order,
147/// and a client comparing the header against what it sent will notice.
148///
149/// `applied` records what was asked for, since a parsed field cannot say
150/// whether its value was requested or is merely its default.
151pub fn preference_applied(prefs: &Preferences, relevance: PreferenceScope) -> Option<String> {
152    let asked = |name: &str| {
153        prefs
154            .applied
155            .iter()
156            .find(|pref| pref.starts_with(name))
157            .cloned()
158    };
159
160    let mut values = Vec::new();
161    if relevance.resolution {
162        values.extend(asked("resolution="));
163    }
164    if relevance.missing {
165        values.extend(asked("missing="));
166    }
167    if relevance.representation {
168        values.extend(asked("return="));
169    }
170    values.extend(asked("count="));
171    // `tx=` is deliberately absent. Ending the transaction the client's way
172    // is something PostgREST does only where `db-tx-end` is configured to let
173    // the request decide; by default the preference is not honoured, and so
174    // is not reported. There is no such setting here and nothing reads
175    // `Preferences::transaction`, which makes the answer the same one for a
176    // stronger reason: a rollback that was asked for and never happened must
177    // not come back described as applied.
178    values.extend(asked("handling="));
179    values.extend(asked("timezone="));
180    // `max-affected` only has an effect under strict handling, so leniently
181    // it was not applied.
182    if relevance.max_affected && prefs.handling == PreferHandling::Strict {
183        values.extend(asked("max-affected="));
184    }
185
186    match values.is_empty() {
187        true => None,
188        false => Some(values.join(", ")),
189    }
190}
191
192/// Which preferences a given request could honour.
193///
194/// A preference that cannot apply to the request is not reported back, however
195/// plainly it was asked for -- saying it was applied when it was ignored is
196/// worse than saying nothing.
197#[derive(Clone, Copy, Debug, Default)]
198pub struct PreferenceScope {
199    /// Insert, where duplicates can be resolved.
200    pub resolution: bool,
201    /// Insert or update, where a missing column can take a default.
202    pub representation: bool,
203    /// Insert or update.
204    pub missing: bool,
205    /// Update, delete or a function call.
206    pub max_affected: bool,
207}
208
209impl PreferenceScope {
210    /// What a read can honour: none of the write-only preferences.
211    pub fn read() -> Self {
212        Self::default()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use http::HeaderValue;
220
221    fn headers_with_prefer(value: &str) -> HeaderMap {
222        let mut headers = HeaderMap::new();
223        headers.insert("prefer", HeaderValue::from_str(value).unwrap());
224        headers
225    }
226
227    #[test]
228    fn test_parse_return_representation() {
229        let headers = headers_with_prefer("return=representation");
230        let prefs = parse_preferences(&headers).unwrap();
231        assert_eq!(prefs.representation, PreferRepresentation::Full);
232    }
233
234    #[test]
235    fn test_parse_count_exact() {
236        let headers = headers_with_prefer("count=exact");
237        let prefs = parse_preferences(&headers).unwrap();
238        assert_eq!(prefs.count, Some(PreferCount::Exact));
239    }
240
241    #[test]
242    fn test_parse_resolution() {
243        let headers = headers_with_prefer("resolution=merge-duplicates");
244        let prefs = parse_preferences(&headers).unwrap();
245        assert_eq!(prefs.resolution, Some(PreferResolution::MergeDuplicates));
246    }
247
248    #[test]
249    fn test_parse_multiple() {
250        let headers = headers_with_prefer("return=representation, count=exact, tx=rollback");
251        let prefs = parse_preferences(&headers).unwrap();
252        assert_eq!(prefs.representation, PreferRepresentation::Full);
253        assert_eq!(prefs.count, Some(PreferCount::Exact));
254        assert_eq!(prefs.transaction, PreferTransaction::Rollback);
255    }
256
257    #[test]
258    fn test_parse_timezone() {
259        let headers = headers_with_prefer("timezone=America/New_York");
260        let prefs = parse_preferences(&headers).unwrap();
261        assert_eq!(prefs.timezone, Some("America/New_York".to_string()));
262    }
263
264    #[test]
265    fn test_parse_max_affected() {
266        let headers = headers_with_prefer("max-affected=100");
267        let prefs = parse_preferences(&headers).unwrap();
268        assert_eq!(prefs.max_affected, Some(100));
269    }
270
271    #[test]
272    fn test_preference_applied() {
273        let headers = headers_with_prefer("return=representation, count=exact");
274        let prefs = parse_preferences(&headers).unwrap();
275        let writing = PreferenceScope {
276            representation: true,
277            ..PreferenceScope::read()
278        };
279
280        let applied = preference_applied(&prefs, writing).unwrap();
281        assert!(applied.contains("return=representation"));
282        assert!(applied.contains("count=exact"));
283    }
284
285    /// A preference the request could not act on is not reported as applied.
286    #[test]
287    fn preference_applied_leaves_out_what_a_read_cannot_honour() {
288        let headers = headers_with_prefer("return=representation, count=exact");
289        let prefs = parse_preferences(&headers).unwrap();
290
291        assert_eq!(
292            preference_applied(&prefs, PreferenceScope::read()).as_deref(),
293            Some("count=exact")
294        );
295    }
296
297    /// A preference nothing here acts on is not reported as applied, however
298    /// plainly it was asked for. `tx=rollback` is parsed and then ignored --
299    /// the transaction commits either way -- so a client told the preference
300    /// was applied would believe its write had been undone.
301    #[test]
302    fn preference_applied_leaves_out_a_transaction_end_nothing_honours() {
303        let headers = headers_with_prefer("tx=rollback");
304        let prefs = parse_preferences(&headers).unwrap();
305
306        assert_eq!(
307            preference_applied(&prefs, PreferenceScope::read()),
308            None,
309            "tx= must not be reported while the transaction always commits"
310        );
311
312        let headers = headers_with_prefer("return=representation, tx=commit");
313        let prefs = parse_preferences(&headers).unwrap();
314        let writing = PreferenceScope {
315            representation: true,
316            ..PreferenceScope::read()
317        };
318        assert_eq!(
319            preference_applied(&prefs, writing).as_deref(),
320            Some("return=representation")
321        );
322    }
323
324    /// A preference the server merely defaulted to is not one it applied.
325    #[test]
326    fn preference_applied_reports_only_what_was_asked_for() {
327        let headers = headers_with_prefer("handling=lenient");
328        let prefs = parse_preferences(&headers).unwrap();
329
330        assert_eq!(
331            preference_applied(&prefs, PreferenceScope::read()).as_deref(),
332            Some("handling=lenient")
333        );
334        assert_eq!(
335            preference_applied(&Preferences::default(), PreferenceScope::read()),
336            None
337        );
338    }
339}