postrust_core/api_request/
preferences.rs1use super::types::*;
6use crate::error::{Error, Result};
7use http::HeaderMap;
8
9pub fn parse_preferences(headers: &HeaderMap) -> Result<Preferences> {
11 let mut prefs = Preferences::default();
12
13 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 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 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 "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 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" => {} "params=multiple-objects" => {}
136 _ => {
137 prefs.invalid.push(pref.to_string());
138 }
139 }
140}
141
142pub 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 values.extend(asked("handling="));
179 values.extend(asked("timezone="));
180 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#[derive(Clone, Copy, Debug, Default)]
198pub struct PreferenceScope {
199 pub resolution: bool,
201 pub representation: bool,
203 pub missing: bool,
205 pub max_affected: bool,
207}
208
209impl PreferenceScope {
210 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 #[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 #[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 #[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}