Skip to content

Commit 4b0cfd6

Browse files
committed
Initial pass for AcceptLanguage header
1 parent 3a0a047 commit 4b0cfd6

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

src/content/accept_language.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! Client header advertising which languages the client is able to understand.
2+
3+
use crate::content::LanguageProposal;
4+
use crate::headers::{Header, HeaderValue, Headers, ACCEPT_LANGUAGE};
5+
6+
use std::fmt::{self, Debug, Write};
7+
8+
/// Client header advertising which languages the client is able to understand.
9+
pub struct AcceptLanguage {
10+
wildcard: bool,
11+
entries: Vec<LanguageProposal>,
12+
}
13+
14+
impl AcceptLanguage {
15+
/// Create a new instance of `AcceptLanguage`.
16+
pub fn new() -> Self {
17+
Self {
18+
entries: vec![],
19+
wildcard: false,
20+
}
21+
}
22+
23+
/// Create an instance of `AcceptLanguage` from a `Headers` instance.
24+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
25+
let mut entries = vec![];
26+
let headers = match headers.as_ref().get(ACCEPT_LANGUAGE) {
27+
Some(headers) => headers,
28+
None => return Ok(None),
29+
};
30+
31+
let mut wildcard = false;
32+
33+
for value in headers {
34+
for part in value.as_str().trim().split(',') {
35+
let part = part.trim();
36+
37+
if part.is_empty() {
38+
continue;
39+
} else if part == "*" {
40+
wildcard = true;
41+
continue;
42+
}
43+
44+
let entry = LanguageProposal::from_str(part)?;
45+
entries.push(entry);
46+
}
47+
}
48+
49+
Ok(Some(Self { wildcard, entries }))
50+
}
51+
}
52+
53+
impl Header for AcceptLanguage {
54+
fn header_name(&self) -> crate::headers::HeaderName {
55+
ACCEPT_LANGUAGE
56+
}
57+
58+
fn header_value(&self) -> crate::headers::HeaderValue {
59+
let mut output = String::new();
60+
for (n, directive) in self.entries.iter().enumerate() {
61+
let directive: HeaderValue = directive.clone().into();
62+
match n {
63+
0 => write!(output, "{}", directive).unwrap(),
64+
_ => write!(output, ", {}", directive).unwrap(),
65+
};
66+
}
67+
68+
if self.wildcard {
69+
match output.len() {
70+
0 => write!(output, "*").unwrap(),
71+
_ => write!(output, ", *").unwrap(),
72+
};
73+
}
74+
75+
// SAFETY: the internal string is validated to be ASCII.
76+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
77+
}
78+
}
79+
80+
impl Debug for AcceptLanguage {
81+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82+
let mut list = f.debug_list();
83+
for directive in &self.entries {
84+
list.entry(directive);
85+
}
86+
list.finish()
87+
}
88+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use crate::ensure;
2+
use crate::headers::HeaderValue;
3+
use crate::language::LanguageRange;
4+
use crate::utils::parse_weight;
5+
6+
use std::cmp::{Ordering, PartialEq};
7+
use std::ops::{Deref, DerefMut};
8+
use std::str::FromStr;
9+
10+
/// A proposed `LanguageRange` in `AcceptLanguage`.
11+
#[derive(Debug, Clone, PartialEq)]
12+
pub struct LanguageProposal {
13+
/// The proposed language.
14+
pub(crate) language: LanguageRange,
15+
16+
/// The weight of the proposal.
17+
///
18+
/// This is a number between 0.0 and 1.0, and is max 3 decimal points.
19+
weight: Option<f32>,
20+
}
21+
22+
impl LanguageProposal {
23+
/// Create a new instance of `LanguageProposal`.
24+
pub fn new(language: impl Into<LanguageRange>, weight: Option<f32>) -> crate::Result<Self> {
25+
if let Some(weight) = weight {
26+
ensure!(
27+
weight.is_sign_positive() && weight <= 1.0,
28+
"LanguageProposal should have a weight between 0.0 and 1.0"
29+
)
30+
}
31+
32+
Ok(Self {
33+
language: language.into(),
34+
weight,
35+
})
36+
}
37+
38+
/// Get the proposed language.
39+
pub fn language_range(&self) -> &LanguageRange {
40+
&self.language
41+
}
42+
43+
/// Get the weight of the proposal.
44+
pub fn weight(&self) -> Option<f32> {
45+
self.weight
46+
}
47+
48+
pub(crate) fn from_str(s: &str) -> crate::Result<Self> {
49+
let mut parts = s.split(';');
50+
let language = LanguageRange::from_str(parts.next().unwrap())?;
51+
let weight = parts.next().map(parse_weight).transpose()?;
52+
Ok(Self::new(language, weight)?)
53+
}
54+
}
55+
56+
impl From<LanguageRange> for LanguageProposal {
57+
fn from(language: LanguageRange) -> Self {
58+
Self {
59+
language,
60+
weight: None,
61+
}
62+
}
63+
}
64+
65+
impl PartialEq<LanguageRange> for LanguageProposal {
66+
fn eq(&self, other: &LanguageRange) -> bool {
67+
self.language == *other
68+
}
69+
}
70+
71+
impl PartialEq<LanguageRange> for &LanguageProposal {
72+
fn eq(&self, other: &LanguageRange) -> bool {
73+
self.language == *other
74+
}
75+
}
76+
77+
impl Deref for LanguageProposal {
78+
type Target = LanguageRange;
79+
fn deref(&self) -> &Self::Target {
80+
&self.language
81+
}
82+
}
83+
84+
impl DerefMut for LanguageProposal {
85+
fn deref_mut(&mut self) -> &mut Self::Target {
86+
&mut self.language
87+
}
88+
}
89+
90+
impl PartialOrd for LanguageProposal {
91+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
92+
match (self.weight, other.weight) {
93+
(Some(left), Some(right)) => left.partial_cmp(&right),
94+
(Some(_), None) => Some(Ordering::Greater),
95+
(None, Some(_)) => Some(Ordering::Less),
96+
(None, None) => None,
97+
}
98+
}
99+
}
100+
101+
impl From<LanguageProposal> for HeaderValue {
102+
fn from(entry: LanguageProposal) -> HeaderValue {
103+
let s = match entry.weight {
104+
Some(weight) => format!("{};q={:.3}", entry.language, weight),
105+
None => entry.language.to_string(),
106+
};
107+
unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
108+
}
109+
}
110+
111+
#[cfg(test)]
112+
mod test {
113+
use super::*;
114+
115+
#[test]
116+
fn smoke() {
117+
let _ = LanguageProposal::new("en", Some(1.0)).unwrap();
118+
}
119+
120+
#[test]
121+
fn error_code_500() {
122+
let err = LanguageProposal::new("en", Some(1.1)).unwrap_err();
123+
assert_eq!(err.status(), 500);
124+
}
125+
}

src/content/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,29 @@
3333
3434
pub mod accept;
3535
pub mod accept_encoding;
36+
pub mod accept_language;
3637
pub mod content_encoding;
3738

3839
mod content_length;
3940
mod content_location;
4041
mod content_type;
4142
mod encoding;
4243
mod encoding_proposal;
44+
mod language_range_proposal;
4345
mod media_type_proposal;
4446

4547
#[doc(inline)]
4648
pub use accept::Accept;
4749
#[doc(inline)]
4850
pub use accept_encoding::AcceptEncoding;
4951
#[doc(inline)]
52+
pub use accept_language::AcceptLanguage;
53+
#[doc(inline)]
5054
pub use content_encoding::ContentEncoding;
5155
pub use content_length::ContentLength;
5256
pub use content_location::ContentLocation;
5357
pub use content_type::ContentType;
5458
pub use encoding::Encoding;
5559
pub use encoding_proposal::EncodingProposal;
60+
pub use language_range_proposal::LanguageProposal;
5661
pub use media_type_proposal::MediaTypeProposal;

src/language/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ mod parse;
77
use crate::headers::HeaderValue;
88
use std::{fmt::{self, Display}, borrow::Cow, str::FromStr};
99

10-
#[derive(Debug)]
10+
/// An RFC 4647 language range.
11+
#[derive(Debug, Clone, PartialEq)]
1112
pub struct LanguageRange {
1213
pub(crate) tags: Vec<Cow<'static, str>>
1314
}
@@ -40,3 +41,9 @@ impl FromStr for LanguageRange {
4041
parse::parse(s)
4142
}
4243
}
44+
45+
impl<'a> From<&'a str> for LanguageRange {
46+
fn from(value: &'a str) -> Self {
47+
Self::from_str(value).unwrap()
48+
}
49+
}

0 commit comments

Comments
 (0)