use super::types::*;
use crate::error::{Error, Result};
use http::HeaderMap;
pub fn parse_preferences(headers: &HeaderMap) -> Result<Preferences> {
let mut prefs = Preferences::default();
let mut seen_any = false;
for value in headers.get_all("prefer") {
let value = value.to_str().map_err(|_| Error::InvalidHeader("Prefer"))?;
seen_any = true;
for pref in value.split(',').map(|s| s.trim()) {
parse_preference(&mut prefs, pref);
}
}
if !seen_any {
return Ok(prefs);
}
if prefs.handling == PreferHandling::Strict && !prefs.invalid.is_empty() {
return Err(Error::InvalidPreferences(prefs.invalid));
}
Ok(prefs)
}
fn parse_preference(prefs: &mut Preferences, pref: &str) {
let pref = pref.trim();
if let Some((key, value)) = pref.split_once('=') {
let key = key.trim();
let value = value.trim().trim_matches('"');
match key {
"resolution" => {
prefs.resolution = match value {
"merge-duplicates" => Some(PreferResolution::MergeDuplicates),
"ignore-duplicates" => Some(PreferResolution::IgnoreDuplicates),
_ => None,
};
if prefs.resolution.is_some() {
prefs.applied.push(format!("resolution={}", value));
}
}
"return" => {
prefs.representation = match value {
"representation" => PreferRepresentation::Full,
"headers-only" => PreferRepresentation::HeadersOnly,
"minimal" => PreferRepresentation::None,
_ => PreferRepresentation::None,
};
prefs.applied.push(format!("return={}", value));
}
"count" => {
prefs.count = match value {
"exact" => Some(PreferCount::Exact),
"planned" => Some(PreferCount::Planned),
"estimated" => Some(PreferCount::Estimated),
_ => None,
};
if prefs.count.is_some() {
prefs.applied.push(format!("count={}", value));
}
}
"tx" => {
prefs.transaction = match value {
"commit" => PreferTransaction::Commit,
"rollback" => PreferTransaction::Rollback,
_ => PreferTransaction::Commit,
};
prefs.applied.push(format!("tx={}", value));
}
"missing" => {
prefs.missing = match value {
"default" => PreferMissing::ApplyDefaults,
"null" => PreferMissing::ApplyNulls,
_ => PreferMissing::ApplyDefaults,
};
prefs.applied.push(format!("missing={}", value));
}
"handling" => {
prefs.handling = match value {
"strict" => PreferHandling::Strict,
"lenient" => PreferHandling::Lenient,
_ => PreferHandling::Strict,
};
prefs.applied.push(format!("handling={}", value));
}
"timezone" => {
prefs.timezone = Some(value.to_string());
prefs.applied.push(format!("timezone={}", value));
}
"params" => {}
"max-affected" => {
if let Ok(n) = value.parse::<i64>() {
prefs.max_affected = Some(n);
prefs.applied.push(format!("max-affected={}", n));
}
}
_ => {
prefs.invalid.push(pref.to_string());
}
}
return;
}
match pref {
"return=representation" => prefs.representation = PreferRepresentation::Full,
"return=headers-only" => prefs.representation = PreferRepresentation::HeadersOnly,
"return=minimal" => prefs.representation = PreferRepresentation::None,
"count=exact" => prefs.count = Some(PreferCount::Exact),
"count=planned" => prefs.count = Some(PreferCount::Planned),
"count=estimated" => prefs.count = Some(PreferCount::Estimated),
"resolution=merge-duplicates" => prefs.resolution = Some(PreferResolution::MergeDuplicates),
"resolution=ignore-duplicates" => {
prefs.resolution = Some(PreferResolution::IgnoreDuplicates)
}
"tx=commit" => prefs.transaction = PreferTransaction::Commit,
"tx=rollback" => prefs.transaction = PreferTransaction::Rollback,
"params=single-object" => {} "params=multiple-objects" => {}
_ => {
prefs.invalid.push(pref.to_string());
}
}
}
pub fn preference_applied(prefs: &Preferences, relevance: PreferenceScope) -> Option<String> {
let asked = |name: &str| {
prefs
.applied
.iter()
.find(|pref| pref.starts_with(name))
.cloned()
};
let mut values = Vec::new();
if relevance.resolution {
values.extend(asked("resolution="));
}
if relevance.missing {
values.extend(asked("missing="));
}
if relevance.representation {
values.extend(asked("return="));
}
values.extend(asked("count="));
values.extend(asked("handling="));
values.extend(asked("timezone="));
if relevance.max_affected && prefs.handling == PreferHandling::Strict {
values.extend(asked("max-affected="));
}
match values.is_empty() {
true => None,
false => Some(values.join(", ")),
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct PreferenceScope {
pub resolution: bool,
pub representation: bool,
pub missing: bool,
pub max_affected: bool,
}
impl PreferenceScope {
pub fn read() -> Self {
Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
fn headers_with_prefer(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("prefer", HeaderValue::from_str(value).unwrap());
headers
}
#[test]
fn test_parse_return_representation() {
let headers = headers_with_prefer("return=representation");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.representation, PreferRepresentation::Full);
}
#[test]
fn test_parse_count_exact() {
let headers = headers_with_prefer("count=exact");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.count, Some(PreferCount::Exact));
}
#[test]
fn test_parse_resolution() {
let headers = headers_with_prefer("resolution=merge-duplicates");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.resolution, Some(PreferResolution::MergeDuplicates));
}
#[test]
fn test_parse_multiple() {
let headers = headers_with_prefer("return=representation, count=exact, tx=rollback");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.representation, PreferRepresentation::Full);
assert_eq!(prefs.count, Some(PreferCount::Exact));
assert_eq!(prefs.transaction, PreferTransaction::Rollback);
}
#[test]
fn test_parse_timezone() {
let headers = headers_with_prefer("timezone=America/New_York");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.timezone, Some("America/New_York".to_string()));
}
#[test]
fn test_parse_max_affected() {
let headers = headers_with_prefer("max-affected=100");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(prefs.max_affected, Some(100));
}
#[test]
fn test_preference_applied() {
let headers = headers_with_prefer("return=representation, count=exact");
let prefs = parse_preferences(&headers).unwrap();
let writing = PreferenceScope {
representation: true,
..PreferenceScope::read()
};
let applied = preference_applied(&prefs, writing).unwrap();
assert!(applied.contains("return=representation"));
assert!(applied.contains("count=exact"));
}
#[test]
fn preference_applied_leaves_out_what_a_read_cannot_honour() {
let headers = headers_with_prefer("return=representation, count=exact");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(
preference_applied(&prefs, PreferenceScope::read()).as_deref(),
Some("count=exact")
);
}
#[test]
fn preference_applied_leaves_out_a_transaction_end_nothing_honours() {
let headers = headers_with_prefer("tx=rollback");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(
preference_applied(&prefs, PreferenceScope::read()),
None,
"tx= must not be reported while the transaction always commits"
);
let headers = headers_with_prefer("return=representation, tx=commit");
let prefs = parse_preferences(&headers).unwrap();
let writing = PreferenceScope {
representation: true,
..PreferenceScope::read()
};
assert_eq!(
preference_applied(&prefs, writing).as_deref(),
Some("return=representation")
);
}
#[test]
fn preference_applied_reports_only_what_was_asked_for() {
let headers = headers_with_prefer("handling=lenient");
let prefs = parse_preferences(&headers).unwrap();
assert_eq!(
preference_applied(&prefs, PreferenceScope::read()).as_deref(),
Some("handling=lenient")
);
assert_eq!(
preference_applied(&Preferences::default(), PreferenceScope::read()),
None
);
}
}