From 3a0a047ebfb22a316474fb7cf7c82043fd826cf0 Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Fri, 28 Jan 2022 17:59:24 -0500 Subject: [PATCH 1/7] WIP language ranges --- src/language/mod.rs | 42 ++++++++++++++++++++++++++ src/language/parse.rs | 68 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 111 insertions(+) create mode 100644 src/language/mod.rs create mode 100644 src/language/parse.rs diff --git a/src/language/mod.rs b/src/language/mod.rs new file mode 100644 index 00000000..87917abd --- /dev/null +++ b/src/language/mod.rs @@ -0,0 +1,42 @@ +//! RFC 4647 Language Ranges. +//! +//! [Read more](https://datatracker.ietf.org/doc/html/rfc4647) + +mod parse; + +use crate::headers::HeaderValue; +use std::{fmt::{self, Display}, borrow::Cow, str::FromStr}; + +#[derive(Debug)] +pub struct LanguageRange { + pub(crate) tags: Vec> +} + +impl Display for LanguageRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut tags = self.tags.iter(); + if let Some(tag) = tags.next() { + write!(f, "{}", tag)?; + + for tag in tags { + write!(f, "-{}", tag)?; + } + } + Ok(()) + } +} + +impl From for HeaderValue { + fn from(language: LanguageRange) -> Self { + let s = language.to_string(); + unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } + } +} + +impl FromStr for LanguageRange { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + parse::parse(s) + } +} diff --git a/src/language/parse.rs b/src/language/parse.rs new file mode 100644 index 00000000..3e05d22a --- /dev/null +++ b/src/language/parse.rs @@ -0,0 +1,68 @@ +use std::borrow::Cow; + +use super::LanguageRange; + +fn split_tag(input: &str) -> Option<(&str, &str)> { + match input.find('-') { + Some(pos) if pos <= 8 => { + let (tag, rest) = input.split_at(pos); + Some((tag, &rest[1..])) + } + Some(_) => None, + None => (input.len() <= 8).then(|| (input, "")), + } +} + +// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" +// alphanum = ALPHA / DIGIT +pub(crate) fn parse(input: &str) -> crate::Result { + let tags = if input == "*" { + vec![Cow::from(input.to_string())] + } else { + let mut tags = Vec::new(); + + let (tag, mut input) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; + crate::ensure!(!tag.is_empty(), "Language tag should not be empty"); + crate::ensure!( + tag.bytes() + .all(|b| (b'a'..=b'z').contains(&b) || (b'A'..=b'Z').contains(&b)), + "Language tag should be alpha" + ); + tags.push(Cow::from(tag.to_string())); + + while !input.is_empty() { + let (tag, rest) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; + crate::ensure!(!tag.is_empty(), "Language tag should not be empty"); + crate::ensure!( + tag.bytes().all(|b| (b'a'..=b'z').contains(&b) + || (b'A'..=b'Z').contains(&b) + || (b'0'..=b'9').contains(&b)), + "Language tag should be alpha numeric" + ); + tags.push(Cow::from(tag.to_string())); + input = rest; + } + + tags + }; + + Ok(LanguageRange { tags }) +} + +#[test] +fn test() { + let range = parse("*").unwrap(); + assert_eq!(&range.tags, &["*"]); + + let range = parse("en").unwrap(); + assert_eq!(&range.tags, &["en"]); + + let range = parse("en-CA").unwrap(); + assert_eq!(&range.tags, &["en", "CA"]); + + let range = parse("zh-Hant-CN-x-private1-private2").unwrap(); + assert_eq!( + &range.tags, + &["zh", "Hant", "CN", "x", "private1", "private2"] + ); +} diff --git a/src/lib.rs b/src/lib.rs index c8821a41..1cf9825b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,7 @@ pub mod cache; pub mod conditional; pub mod content; pub mod headers; +pub mod language; pub mod mime; pub mod other; pub mod proxies; From 4b0cfd6fc1f6ea92f743b2e6e41f3f385d93d741 Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 30 Jan 2022 17:44:05 -0500 Subject: [PATCH 2/7] Initial pass for AcceptLanguage header --- src/content/accept_language.rs | 88 +++++++++++++++++ src/content/language_range_proposal.rs | 125 +++++++++++++++++++++++++ src/content/mod.rs | 5 + src/language/mod.rs | 9 +- 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/content/accept_language.rs create mode 100644 src/content/language_range_proposal.rs diff --git a/src/content/accept_language.rs b/src/content/accept_language.rs new file mode 100644 index 00000000..378a67bd --- /dev/null +++ b/src/content/accept_language.rs @@ -0,0 +1,88 @@ +//! Client header advertising which languages the client is able to understand. + +use crate::content::LanguageProposal; +use crate::headers::{Header, HeaderValue, Headers, ACCEPT_LANGUAGE}; + +use std::fmt::{self, Debug, Write}; + +/// Client header advertising which languages the client is able to understand. +pub struct AcceptLanguage { + wildcard: bool, + entries: Vec, +} + +impl AcceptLanguage { + /// Create a new instance of `AcceptLanguage`. + pub fn new() -> Self { + Self { + entries: vec![], + wildcard: false, + } + } + + /// Create an instance of `AcceptLanguage` from a `Headers` instance. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let mut entries = vec![]; + let headers = match headers.as_ref().get(ACCEPT_LANGUAGE) { + Some(headers) => headers, + None => return Ok(None), + }; + + let mut wildcard = false; + + for value in headers { + for part in value.as_str().trim().split(',') { + let part = part.trim(); + + if part.is_empty() { + continue; + } else if part == "*" { + wildcard = true; + continue; + } + + let entry = LanguageProposal::from_str(part)?; + entries.push(entry); + } + } + + Ok(Some(Self { wildcard, entries })) + } +} + +impl Header for AcceptLanguage { + fn header_name(&self) -> crate::headers::HeaderName { + ACCEPT_LANGUAGE + } + + fn header_value(&self) -> crate::headers::HeaderValue { + let mut output = String::new(); + for (n, directive) in self.entries.iter().enumerate() { + let directive: HeaderValue = directive.clone().into(); + match n { + 0 => write!(output, "{}", directive).unwrap(), + _ => write!(output, ", {}", directive).unwrap(), + }; + } + + if self.wildcard { + match output.len() { + 0 => write!(output, "*").unwrap(), + _ => write!(output, ", *").unwrap(), + }; + } + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl Debug for AcceptLanguage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut list = f.debug_list(); + for directive in &self.entries { + list.entry(directive); + } + list.finish() + } +} diff --git a/src/content/language_range_proposal.rs b/src/content/language_range_proposal.rs new file mode 100644 index 00000000..3dc0bb49 --- /dev/null +++ b/src/content/language_range_proposal.rs @@ -0,0 +1,125 @@ +use crate::ensure; +use crate::headers::HeaderValue; +use crate::language::LanguageRange; +use crate::utils::parse_weight; + +use std::cmp::{Ordering, PartialEq}; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +/// A proposed `LanguageRange` in `AcceptLanguage`. +#[derive(Debug, Clone, PartialEq)] +pub struct LanguageProposal { + /// The proposed language. + pub(crate) language: LanguageRange, + + /// The weight of the proposal. + /// + /// This is a number between 0.0 and 1.0, and is max 3 decimal points. + weight: Option, +} + +impl LanguageProposal { + /// Create a new instance of `LanguageProposal`. + pub fn new(language: impl Into, weight: Option) -> crate::Result { + if let Some(weight) = weight { + ensure!( + weight.is_sign_positive() && weight <= 1.0, + "LanguageProposal should have a weight between 0.0 and 1.0" + ) + } + + Ok(Self { + language: language.into(), + weight, + }) + } + + /// Get the proposed language. + pub fn language_range(&self) -> &LanguageRange { + &self.language + } + + /// Get the weight of the proposal. + pub fn weight(&self) -> Option { + self.weight + } + + pub(crate) fn from_str(s: &str) -> crate::Result { + let mut parts = s.split(';'); + let language = LanguageRange::from_str(parts.next().unwrap())?; + let weight = parts.next().map(parse_weight).transpose()?; + Ok(Self::new(language, weight)?) + } +} + +impl From for LanguageProposal { + fn from(language: LanguageRange) -> Self { + Self { + language, + weight: None, + } + } +} + +impl PartialEq for LanguageProposal { + fn eq(&self, other: &LanguageRange) -> bool { + self.language == *other + } +} + +impl PartialEq for &LanguageProposal { + fn eq(&self, other: &LanguageRange) -> bool { + self.language == *other + } +} + +impl Deref for LanguageProposal { + type Target = LanguageRange; + fn deref(&self) -> &Self::Target { + &self.language + } +} + +impl DerefMut for LanguageProposal { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.language + } +} + +impl PartialOrd for LanguageProposal { + fn partial_cmp(&self, other: &Self) -> Option { + match (self.weight, other.weight) { + (Some(left), Some(right)) => left.partial_cmp(&right), + (Some(_), None) => Some(Ordering::Greater), + (None, Some(_)) => Some(Ordering::Less), + (None, None) => None, + } + } +} + +impl From for HeaderValue { + fn from(entry: LanguageProposal) -> HeaderValue { + let s = match entry.weight { + Some(weight) => format!("{};q={:.3}", entry.language, weight), + None => entry.language.to_string(), + }; + unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn smoke() { + let _ = LanguageProposal::new("en", Some(1.0)).unwrap(); + } + + #[test] + fn error_code_500() { + let err = LanguageProposal::new("en", Some(1.1)).unwrap_err(); + assert_eq!(err.status(), 500); + } +} \ No newline at end of file diff --git a/src/content/mod.rs b/src/content/mod.rs index aff9fb4b..fb5fd5b4 100644 --- a/src/content/mod.rs +++ b/src/content/mod.rs @@ -33,6 +33,7 @@ pub mod accept; pub mod accept_encoding; +pub mod accept_language; pub mod content_encoding; mod content_length; @@ -40,6 +41,7 @@ mod content_location; mod content_type; mod encoding; mod encoding_proposal; +mod language_range_proposal; mod media_type_proposal; #[doc(inline)] @@ -47,10 +49,13 @@ pub use accept::Accept; #[doc(inline)] pub use accept_encoding::AcceptEncoding; #[doc(inline)] +pub use accept_language::AcceptLanguage; +#[doc(inline)] pub use content_encoding::ContentEncoding; pub use content_length::ContentLength; pub use content_location::ContentLocation; pub use content_type::ContentType; pub use encoding::Encoding; pub use encoding_proposal::EncodingProposal; +pub use language_range_proposal::LanguageProposal; pub use media_type_proposal::MediaTypeProposal; diff --git a/src/language/mod.rs b/src/language/mod.rs index 87917abd..21b78001 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -7,7 +7,8 @@ mod parse; use crate::headers::HeaderValue; use std::{fmt::{self, Display}, borrow::Cow, str::FromStr}; -#[derive(Debug)] +/// An RFC 4647 language range. +#[derive(Debug, Clone, PartialEq)] pub struct LanguageRange { pub(crate) tags: Vec> } @@ -40,3 +41,9 @@ impl FromStr for LanguageRange { parse::parse(s) } } + +impl<'a> From<&'a str> for LanguageRange { + fn from(value: &'a str) -> Self { + Self::from_str(value).unwrap() + } +} \ No newline at end of file From 0b57fc34fc223ce490044512c531c2086127a14e Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 30 Jan 2022 17:46:31 -0500 Subject: [PATCH 3/7] Remove wildcard from LanguageRange --- src/language/parse.rs | 45 +++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/language/parse.rs b/src/language/parse.rs index 3e05d22a..d77d1ca4 100644 --- a/src/language/parse.rs +++ b/src/language/parse.rs @@ -16,44 +16,35 @@ fn split_tag(input: &str) -> Option<(&str, &str)> { // language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" // alphanum = ALPHA / DIGIT pub(crate) fn parse(input: &str) -> crate::Result { - let tags = if input == "*" { - vec![Cow::from(input.to_string())] - } else { - let mut tags = Vec::new(); + let mut tags = Vec::new(); + + let (tag, mut input) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; + crate::ensure!(!tag.is_empty(), "Language tag should not be empty"); + crate::ensure!( + tag.bytes() + .all(|b| (b'a'..=b'z').contains(&b) || (b'A'..=b'Z').contains(&b)), + "Language tag should be alpha" + ); + tags.push(Cow::from(tag.to_string())); - let (tag, mut input) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; + while !input.is_empty() { + let (tag, rest) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; crate::ensure!(!tag.is_empty(), "Language tag should not be empty"); crate::ensure!( - tag.bytes() - .all(|b| (b'a'..=b'z').contains(&b) || (b'A'..=b'Z').contains(&b)), - "Language tag should be alpha" + tag.bytes().all(|b| (b'a'..=b'z').contains(&b) + || (b'A'..=b'Z').contains(&b) + || (b'0'..=b'9').contains(&b)), + "Language tag should be alpha numeric" ); tags.push(Cow::from(tag.to_string())); - - while !input.is_empty() { - let (tag, rest) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?; - crate::ensure!(!tag.is_empty(), "Language tag should not be empty"); - crate::ensure!( - tag.bytes().all(|b| (b'a'..=b'z').contains(&b) - || (b'A'..=b'Z').contains(&b) - || (b'0'..=b'9').contains(&b)), - "Language tag should be alpha numeric" - ); - tags.push(Cow::from(tag.to_string())); - input = rest; - } - - tags - }; + input = rest; + } Ok(LanguageRange { tags }) } #[test] fn test() { - let range = parse("*").unwrap(); - assert_eq!(&range.tags, &["*"]); - let range = parse("en").unwrap(); assert_eq!(&range.tags, &["en"]); From 7438580bd305e0150f2e69e6b66fc4ca335a5ae3 Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 30 Jan 2022 18:00:19 -0500 Subject: [PATCH 4/7] Flush out AcceptLanguage and add some tests --- src/content/accept_language.rs | 152 +++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/src/content/accept_language.rs b/src/content/accept_language.rs index 378a67bd..62d47b68 100644 --- a/src/content/accept_language.rs +++ b/src/content/accept_language.rs @@ -4,6 +4,7 @@ use crate::content::LanguageProposal; use crate::headers::{Header, HeaderValue, Headers, ACCEPT_LANGUAGE}; use std::fmt::{self, Debug, Write}; +use std::slice; /// Client header advertising which languages the client is able to understand. pub struct AcceptLanguage { @@ -48,6 +49,35 @@ impl AcceptLanguage { Ok(Some(Self { wildcard, entries })) } + + /// Push a directive into the list of entries. + pub fn push(&mut self, prop: impl Into) { + self.entries.push(prop.into()) + } + + /// Returns `true` if a wildcard directive was passed. + pub fn wildcard(&self) -> bool { + self.wildcard + } + + /// Set the wildcard directive. + pub fn set_wildcard(&mut self, wildcard: bool) { + self.wildcard = wildcard + } + + /// An iterator visiting all entries. + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.entries.iter(), + } + } + + /// An iterator visiting all entries. + pub fn iter_mut(&mut self) -> IterMut<'_> { + IterMut { + inner: self.entries.iter_mut(), + } + } } impl Header for AcceptLanguage { @@ -77,6 +107,63 @@ impl Header for AcceptLanguage { } } +/// A borrowing iterator over entries in `AcceptLanguage`. +#[derive(Debug)] +pub struct IntoIter { + inner: std::vec::IntoIter, +} + +impl Iterator for IntoIter { + type Item = LanguageProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A lending iterator over entries in `AcceptLanguage`. +#[derive(Debug)] +pub struct Iter<'a> { + inner: slice::Iter<'a, LanguageProposal>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a LanguageProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A mutable iterator over entries in `AcceptLanguage`. +#[derive(Debug)] +pub struct IterMut<'a> { + inner: slice::IterMut<'a, LanguageProposal>, +} + +impl<'a> Iterator for IterMut<'a> { + type Item = &'a mut LanguageProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + impl Debug for AcceptLanguage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); @@ -86,3 +173,68 @@ impl Debug for AcceptLanguage { list.finish() } } + +impl IntoIterator for AcceptLanguage { + type Item = LanguageProposal; + type IntoIter = IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoIter { + inner: self.entries.into_iter(), + } + } +} + +impl<'a> IntoIterator for &'a AcceptLanguage { + type Item = &'a LanguageProposal; + type IntoIter = Iter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut AcceptLanguage { + type Item = &'a mut LanguageProposal; + type IntoIter = IterMut<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Response; + + #[test] + fn smoke() -> crate::Result<()> { + let lang = LanguageProposal::new("en-CA", Some(1.0)).unwrap(); + let mut accept = AcceptLanguage::new(); + accept.push(lang.clone()); + + let mut headers = Response::new(200); + accept.apply_header(&mut headers); + + let accept = AcceptLanguage::from_headers(headers)?.unwrap(); + assert_eq!(accept.iter().next().unwrap(), &lang); + Ok(()) + } + + #[test] + fn wildcard() -> crate::Result<()> { + let mut accept = AcceptLanguage::new(); + accept.set_wildcard(true); + + let mut headers = Response::new(200); + accept.apply_header(&mut headers); + + let accept = AcceptLanguage::from_headers(headers)?.unwrap(); + assert!(accept.wildcard()); + Ok(()) + } +} \ No newline at end of file From e361b11410015f51d492275fee6fea3d2b2c93ff Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 30 Jan 2022 18:04:00 -0500 Subject: [PATCH 5/7] Run cargo fmt --- src/content/accept_language.rs | 2 +- src/content/language_range_proposal.rs | 2 +- src/language/mod.rs | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/content/accept_language.rs b/src/content/accept_language.rs index 62d47b68..e6937db9 100644 --- a/src/content/accept_language.rs +++ b/src/content/accept_language.rs @@ -237,4 +237,4 @@ mod test { assert!(accept.wildcard()); Ok(()) } -} \ No newline at end of file +} diff --git a/src/content/language_range_proposal.rs b/src/content/language_range_proposal.rs index 3dc0bb49..1b4b85c7 100644 --- a/src/content/language_range_proposal.rs +++ b/src/content/language_range_proposal.rs @@ -122,4 +122,4 @@ mod test { let err = LanguageProposal::new("en", Some(1.1)).unwrap_err(); assert_eq!(err.status(), 500); } -} \ No newline at end of file +} diff --git a/src/language/mod.rs b/src/language/mod.rs index 21b78001..0b6e4816 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -1,16 +1,20 @@ //! RFC 4647 Language Ranges. -//! +//! //! [Read more](https://datatracker.ietf.org/doc/html/rfc4647) mod parse; use crate::headers::HeaderValue; -use std::{fmt::{self, Display}, borrow::Cow, str::FromStr}; +use std::{ + borrow::Cow, + fmt::{self, Display}, + str::FromStr, +}; /// An RFC 4647 language range. #[derive(Debug, Clone, PartialEq)] pub struct LanguageRange { - pub(crate) tags: Vec> + pub(crate) tags: Vec>, } impl Display for LanguageRange { @@ -46,4 +50,4 @@ impl<'a> From<&'a str> for LanguageRange { fn from(value: &'a str) -> Self { Self::from_str(value).unwrap() } -} \ No newline at end of file +} From efe6ea47f41d691d0d324eb178692910525ad955 Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 30 Jan 2022 18:38:15 -0500 Subject: [PATCH 6/7] Add iter and iter_mut to LanguageRange --- src/language/mod.rs | 123 +++++++++++++++++++++++++++++++++++++++++- src/language/parse.rs | 8 +-- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/src/language/mod.rs b/src/language/mod.rs index 0b6e4816..73a7d603 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -8,18 +8,35 @@ use crate::headers::HeaderValue; use std::{ borrow::Cow, fmt::{self, Display}, + slice, str::FromStr, }; /// An RFC 4647 language range. #[derive(Debug, Clone, PartialEq)] pub struct LanguageRange { - pub(crate) tags: Vec>, + pub(crate) subtags: Vec>, +} + +impl LanguageRange { + /// An iterator visiting all entries. + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.subtags.iter(), + } + } + + /// An iterator visiting all entries. + pub fn iter_mut(&mut self) -> IterMut<'_> { + IterMut { + inner: self.subtags.iter_mut(), + } + } } impl Display for LanguageRange { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut tags = self.tags.iter(); + let mut tags = self.subtags.iter(); if let Some(tag) = tags.next() { write!(f, "{}", tag)?; @@ -31,6 +48,95 @@ impl Display for LanguageRange { } } +/// A borrowing iterator over entries in `LanguageRange`. +#[derive(Debug)] +pub struct IntoIter { + inner: std::vec::IntoIter>, +} + +impl Iterator for IntoIter { + type Item = Cow<'static, str>; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A lending iterator over entries in `LanguageRange`. +#[derive(Debug)] +pub struct Iter<'a> { + inner: slice::Iter<'a, Cow<'static, str>>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a Cow<'static, str>; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A mutable iterator over entries in `LanguageRange`. +#[derive(Debug)] +pub struct IterMut<'a> { + inner: slice::IterMut<'a, Cow<'static, str>>, +} + +impl<'a> Iterator for IterMut<'a> { + type Item = &'a mut Cow<'static, str>; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl IntoIterator for LanguageRange { + type Item = Cow<'static, str>; + type IntoIter = IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoIter { + inner: self.subtags.into_iter(), + } + } +} + +impl<'a> IntoIterator for &'a LanguageRange { + type Item = &'a Cow<'static, str>; + type IntoIter = Iter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut LanguageRange { + type Item = &'a mut Cow<'static, str>; + type IntoIter = IterMut<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + impl From for HeaderValue { fn from(language: LanguageRange) -> Self { let s = language.to_string(); @@ -51,3 +157,16 @@ impl<'a> From<&'a str> for LanguageRange { Self::from_str(value).unwrap() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_iter() -> crate::Result<()> { + let range: LanguageRange = "en-CA".parse().unwrap(); + let subtags: Vec<_> = range.iter().collect(); + assert_eq!(&subtags, &["en", "CA"]); + Ok(()) + } +} \ No newline at end of file diff --git a/src/language/parse.rs b/src/language/parse.rs index d77d1ca4..78c5c7e1 100644 --- a/src/language/parse.rs +++ b/src/language/parse.rs @@ -40,20 +40,20 @@ pub(crate) fn parse(input: &str) -> crate::Result { input = rest; } - Ok(LanguageRange { tags }) + Ok(LanguageRange { subtags: tags }) } #[test] fn test() { let range = parse("en").unwrap(); - assert_eq!(&range.tags, &["en"]); + assert_eq!(&range.subtags, &["en"]); let range = parse("en-CA").unwrap(); - assert_eq!(&range.tags, &["en", "CA"]); + assert_eq!(&range.subtags, &["en", "CA"]); let range = parse("zh-Hant-CN-x-private1-private2").unwrap(); assert_eq!( - &range.tags, + &range.subtags, &["zh", "Hant", "CN", "x", "private1", "private2"] ); } From 35788aca08b018a2a681326b53b1f0cfec3daa86 Mon Sep 17 00:00:00 2001 From: Simon Gomizelj Date: Sun, 6 Feb 2022 16:15:57 -0500 Subject: [PATCH 7/7] Run cargo fmt --- src/language/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/mod.rs b/src/language/mod.rs index 73a7d603..45bcd045 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -169,4 +169,4 @@ mod test { assert_eq!(&subtags, &["en", "CA"]); Ok(()) } -} \ No newline at end of file +}