webmachine_rust/
content_negotiation.rs

1//! The `content_negotiation` module deals with handling media types, languages, charsets and
2//! encodings as per https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.
3
4use std::cmp::Ordering;
5
6use itertools::Itertools;
7
8use crate::Resource;
9use crate::context::{WebmachineContext, WebmachineRequest};
10use crate::headers::HeaderValue;
11
12/// Enum to represent a match with media types
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum MediaTypeMatch {
15    /// Full match
16    Full,
17    /// Match where the sub-type was a wild card
18    SubStar,
19    /// Full wild card match (type and sub-type)
20    Star,
21    /// Does not match
22    None
23}
24
25impl MediaTypeMatch {
26  /// If the match is a full or partial match
27  pub fn is_match(&self) -> bool {
28    match self {
29      MediaTypeMatch::None => false,
30      _ => true
31    }
32  }
33}
34
35/// Struct to represent a media type
36#[derive(Debug, Clone, PartialEq)]
37pub struct MediaType {
38    /// Main type of the media type
39    pub main: String,
40    /// Sub type of the media type
41    pub sub: String,
42    /// Weight associated with the media type
43    pub weight: f32
44}
45
46impl MediaType {
47    /// Parse a string into a MediaType struct
48    pub fn parse_string(media_type: &str) -> MediaType {
49      let types: Vec<&str> = media_type.splitn(2, '/').collect_vec();
50      if types.is_empty() || types[0].is_empty() {
51        MediaType {
52          main: "*".to_string(),
53          sub: "*".to_string(),
54          weight: 1.0
55        }
56      } else {
57        MediaType {
58          main: types[0].to_string(),
59          sub: if types.len() == 1 || types[1].is_empty() { "*".to_string() } else { types[1].to_string() },
60          weight: 1.0
61        }
62      }
63    }
64
65    /// Adds a quality weight to the media type
66    pub fn with_weight(&self, weight: &String) -> MediaType {
67        MediaType {
68            main: self.main.clone(),
69            sub: self.sub.clone(),
70            weight: weight.parse().unwrap_or(1.0)
71        }
72    }
73
74    /// Returns a weighting for this media type
75    pub fn weight(&self) -> (f32, u8) {
76        if self.main == "*" && self.sub == "*" {
77            (self.weight, 2)
78        } else if self.sub == "*" {
79            (self.weight, 1)
80        } else {
81            (self.weight, 0)
82        }
83    }
84
85    /// If this media type matches the other media type
86    pub fn matches(&self, other: &MediaType) -> MediaTypeMatch {
87      if other.main == "*" {
88        if other.sub == "*" || self.sub == other.sub {
89          MediaTypeMatch::Star
90        } else {
91          MediaTypeMatch::None
92        }
93      } else if self.main == other.main && other.sub == "*" {
94          MediaTypeMatch::SubStar
95      } else if self.main == other.main && self.sub == other.sub {
96          MediaTypeMatch::Full
97      } else {
98          MediaTypeMatch::None
99      }
100    }
101
102    /// Converts this media type into a string
103    pub fn to_string(&self) -> String {
104        format!("{}/{}", self.main, self.sub)
105    }
106}
107
108impl HeaderValue {
109  /// Converts the header value into a media type
110  pub fn as_media_type(&self) -> MediaType {
111    if self.params.contains_key("q") {
112      MediaType::parse_string(&self.value).with_weight(self.params.get("q").unwrap())
113    } else {
114      MediaType::parse_string(&self.value)
115    }
116  }
117}
118
119/// Sorts the list of media types by their weights
120pub fn sort_media_types(media_types: &Vec<HeaderValue>) -> Vec<HeaderValue> {
121  media_types.into_iter().cloned().sorted_by(|a, b| {
122    let media_a = a.as_media_type().weight();
123    let media_b = b.as_media_type().weight();
124    let order = media_a.0.partial_cmp(&media_b.0).unwrap_or(Ordering::Greater);
125    if order == Ordering::Equal {
126      Ord::cmp(&media_a.1, &media_b.1)
127    } else {
128      order.reverse()
129    }
130  }).collect()
131}
132
133/// Determines if the media types produced by the resource matches the acceptable media types
134/// provided by the client. Returns the match if there is one.
135pub fn matching_content_type(resource: &(dyn Resource + Send + Sync), request: &WebmachineRequest) -> Option<String> {
136  if request.has_accept_header() {
137    let acceptable_media_types = sort_media_types(&request.accept());
138    resource.produces().iter()
139      .cartesian_product(acceptable_media_types.iter())
140      .map(|(produced, acceptable)| {
141        let acceptable_media_type = acceptable.as_media_type();
142        let produced_media_type =  MediaType::parse_string(produced);
143        (produced_media_type.clone(), acceptable_media_type.clone(), produced_media_type.matches(&acceptable_media_type))
144      })
145      .sorted_by(|a, b| Ord::cmp(&a.2, &b.2))
146      .filter(|val| val.2 != MediaTypeMatch::None)
147      .next()
148      .map(|result| result.0.to_string())
149  } else {
150    resource.produces().first().map(|s| s.to_string())
151  }
152}
153
154/// Determines if the media type accepted by the resource matches the media type
155/// provided by the client. Returns the match if there is one.
156pub fn acceptable_content_type(
157  resource: &(dyn Resource + Send + Sync),
158  context: &mut WebmachineContext
159) -> bool {
160  let ct = context.request.content_type().as_media_type();
161  resource.acceptable_content_types(context)
162    .iter()
163    .any(|acceptable_ct| {
164      ct.matches(&MediaType::parse_string(acceptable_ct)).is_match()
165    })
166}
167
168/// Struct to represent a media language
169#[derive(Debug, Clone, PartialEq)]
170pub struct MediaLanguage {
171    /// Main type of the media language
172    pub main: String,
173    /// Sub type of the media language
174    pub sub: String,
175    /// Weight associated with the media language
176    pub weight: f32
177}
178
179impl MediaLanguage {
180  /// Parse a string into a MediaLanguage struct
181  pub fn parse_string(language: &str) -> MediaLanguage {
182    let types: Vec<&str> = language.splitn(2, '-').collect_vec();
183    if types.is_empty() || types[0].is_empty() {
184      MediaLanguage {
185        main: "*".to_string(),
186        sub: "".to_string(),
187        weight: 1.0
188      }
189    } else {
190      MediaLanguage {
191        main: types[0].to_string(),
192        sub: if types.len() == 1 || types[1].is_empty() { "".to_string() } else { types[1].to_string() },
193        weight: 1.0
194      }
195    }
196  }
197
198  /// Adds a quality weight to the media language
199  pub fn with_weight(&self, weight: &str) -> MediaLanguage {
200    MediaLanguage {
201      main: self.main.clone(),
202      sub: self.sub.clone(),
203      weight: weight.parse().unwrap_or(1.0)
204    }
205  }
206
207  /// If this media language matches the other media language
208  pub fn matches(&self, other: &MediaLanguage) -> bool {
209    if other.main == "*" || (self.main == other.main && self.sub == other.sub) {
210      true
211    } else {
212      let check = format!("{}-", self.to_string());
213      other.to_string().starts_with(&check)
214    }
215  }
216
217  /// Converts this media language into a string
218  pub fn to_string(&self) -> String {
219    if self.sub.is_empty() {
220      self.main.clone()
221    } else {
222      format!("{}-{}", self.main, self.sub)
223    }
224  }
225}
226
227impl HeaderValue {
228  /// Converts the header value into a media type
229  pub fn as_media_language(&self) -> MediaLanguage {
230    if self.params.contains_key("q") {
231      MediaLanguage::parse_string(&self.value).with_weight(self.params.get("q").unwrap())
232    } else {
233      MediaLanguage::parse_string(&self.value)
234    }
235  }
236}
237
238/// Sorts the list of media types by weighting
239pub fn sort_media_languages(media_languages: &Vec<HeaderValue>) -> Vec<MediaLanguage> {
240    media_languages.iter()
241        .cloned()
242        .map(|lang| lang.as_media_language())
243        .filter(|lang| lang.weight > 0.0)
244        .sorted_by(|a, b| {
245            let weight_a = a.weight;
246            let weight_b = b.weight;
247            weight_b.partial_cmp(&weight_a).unwrap_or(Ordering::Greater)
248        })
249      .collect()
250}
251
252/// Determines if the languages produced by the resource matches the acceptable languages
253/// provided by the client. Returns the match if there is one.
254pub fn matching_language(resource: &(dyn Resource + Send + Sync), request: &WebmachineRequest) -> Option<String> {
255  if request.has_accept_language_header() && !request.accept_language().is_empty() {
256    let acceptable_languages = sort_media_languages(&request.accept_language());
257    if resource.languages_provided().is_empty() {
258      acceptable_languages.first().map(|lang| lang.to_string())
259    } else {
260      acceptable_languages.iter()
261        .cartesian_product(resource.languages_provided().iter())
262        .map(|(acceptable_language, produced_language)| {
263          let produced_language = MediaLanguage::parse_string(produced_language);
264          (produced_language.clone(), produced_language.matches(&acceptable_language))
265        })
266        .find(|val| val.1)
267        .map(|result| result.0.to_string())
268    }
269  } else if resource.languages_provided().is_empty() {
270    Some("*".to_string())
271  } else {
272    resource.languages_provided().first().map(|s| s.to_string())
273  }
274}
275
276/// Struct to represent a character set
277#[derive(Debug, Clone, PartialEq)]
278pub struct Charset {
279    /// Charset code
280    pub charset: String,
281    /// Weight associated with the charset
282    pub weight: f32
283}
284
285impl Charset {
286  /// Parse a string into a Charset struct
287  pub fn parse_string(charset: &str) -> Charset {
288    Charset {
289      charset: charset.to_string(),
290      weight: 1.0
291    }
292  }
293
294  /// Adds a quality weight to the charset
295  pub fn with_weight(&self, weight: &str) -> Charset {
296    Charset {
297      charset: self.charset.clone(),
298      weight: weight.parse().unwrap_or(1.0)
299    }
300  }
301
302  /// If this media charset matches the other media charset
303  pub fn matches(&self, other: &Charset) -> bool {
304    other.charset == "*" || (self.charset.to_uppercase() == other.charset.to_uppercase())
305  }
306
307  /// Converts this charset into a string
308  pub fn to_string(&self) -> String {
309      self.charset.clone()
310  }
311}
312
313impl HeaderValue {
314  /// Converts the header value into a media type
315  pub fn as_charset(&self) -> Charset {
316    if self.params.contains_key("q") {
317      Charset::parse_string(&self.value).with_weight(self.params.get("q").unwrap())
318    } else {
319      Charset::parse_string(&self.value)
320    }
321  }
322}
323
324/// Sorts the list of charsets by weighting as per https://tools.ietf.org/html/rfc2616#section-14.2.
325/// Note that ISO-8859-1 is added as a default with a weighting of 1 if not all ready supplied.
326pub fn sort_media_charsets(charsets: &Vec<HeaderValue>) -> Vec<Charset> {
327    let mut charsets = charsets.clone();
328    if charsets.iter().find(|cs| cs.value == "*" || cs.value.to_uppercase() == "ISO-8859-1").is_none() {
329        charsets.push(h!("ISO-8859-1"));
330    }
331    charsets.into_iter()
332        .map(|cs| cs.as_charset())
333        .filter(|cs| cs.weight > 0.0)
334        .sorted_by(|a, b| {
335            let weight_a = a.weight;
336            let weight_b = b.weight;
337            weight_b.partial_cmp(&weight_a).unwrap_or(Ordering::Greater)
338        })
339      .collect()
340}
341
342/// Determines if the charsets produced by the resource matches the acceptable charsets
343/// provided by the client. Returns the match if there is one.
344pub fn matching_charset(resource: &(dyn Resource + Send + Sync), request: &WebmachineRequest) -> Option<String> {
345  if request.has_accept_charset_header() && !request.accept_charset().is_empty() {
346    let acceptable_charsets = sort_media_charsets(&request.accept_charset());
347    if resource.charsets_provided().is_empty() {
348      acceptable_charsets.first().map(|cs| cs.to_string())
349    } else {
350      acceptable_charsets.iter()
351        .cartesian_product(resource.charsets_provided().iter())
352        .map(|(acceptable_charset, provided_charset)| {
353            let provided_charset = Charset::parse_string(provided_charset);
354            (provided_charset.clone(), provided_charset.matches(&acceptable_charset))
355        })
356        .find(|val| val.1)
357        .map(|result| result.0.to_string())
358    }
359  } else if resource.charsets_provided().is_empty() {
360    Some("ISO-8859-1".to_string())
361  } else {
362    resource.charsets_provided().first().map(|s| s.to_string())
363  }
364}
365
366/// Struct to represent an encoding
367#[derive(Debug, Clone, PartialEq)]
368pub struct Encoding {
369    /// Encoding string
370    pub encoding: String,
371    /// Weight associated with the encoding
372    pub weight: f32
373}
374
375impl Encoding {
376  /// Parse a string into a Charset struct
377  pub fn parse_string(encoding: &str) -> Encoding {
378    Encoding {
379      encoding: encoding.to_string(),
380      weight: 1.0
381    }
382  }
383
384  /// Adds a quality weight to the charset
385  pub fn with_weight(&self, weight: &str) -> Encoding {
386    Encoding {
387      encoding: self.encoding.to_string(),
388      weight: weight.parse().unwrap_or(1.0)
389    }
390  }
391
392  /// If this encoding matches the other encoding
393  pub fn matches(&self, other: &Encoding) -> bool {
394    other.encoding == "*" || (self.encoding.to_lowercase() == other.encoding.to_lowercase())
395  }
396
397  /// Converts this encoding into a string
398  pub fn to_string(&self) -> String {
399        self.encoding.clone()
400    }
401}
402
403impl HeaderValue {
404  /// Converts the header value into a media type
405  pub fn as_encoding(&self) -> Encoding {
406    if self.params.contains_key("q") {
407      Encoding::parse_string(&self.value).with_weight(self.params.get("q").unwrap())
408    } else {
409      Encoding::parse_string(&self.value)
410    }
411  }
412}
413
414/// Sorts the list of encodings by weighting as per https://tools.ietf.org/html/rfc2616#section-14.3.
415/// Note that identity encoding is awlays added with a weight of 1 if not already present.
416pub fn sort_encodings(encodings: &Vec<HeaderValue>) -> Vec<Encoding> {
417    let mut encodings = encodings.clone();
418    if encodings.iter().find(|e| e.value == "*" || e.value.to_lowercase() == "identity").is_none() {
419        encodings.push(h!("identity"));
420    }
421    encodings.into_iter()
422        .map(|encoding| encoding.as_encoding())
423        .filter(|encoding| encoding.weight > 0.0)
424        .sorted_by(|a, b| {
425            let weight_a = a.weight;
426            let weight_b = b.weight;
427            weight_b.partial_cmp(&weight_a).unwrap_or(Ordering::Greater)
428        })
429      .collect()
430}
431
432/// Determines if the encodings supported by the resource matches the acceptable encodings
433/// provided by the client. Returns the match if there is one.
434pub fn matching_encoding(resource: &(dyn Resource + Send + Sync), request: &WebmachineRequest) -> Option<String> {
435  let identity = Encoding::parse_string("identity");
436  if request.has_accept_encoding_header() {
437    let acceptable_encodings = sort_encodings(&request.accept_encoding());
438    if resource.encodings_provided().is_empty() {
439      if acceptable_encodings.contains(&identity) {
440        Some("identity".to_string())
441      } else {
442        None
443      }
444    } else {
445      acceptable_encodings.iter()
446        .cartesian_product(resource.encodings_provided().iter())
447        .map(|(acceptable_encoding, provided_encoding)| {
448          let provided_encoding = Encoding::parse_string(provided_encoding);
449          (provided_encoding.clone(), provided_encoding.matches(&acceptable_encoding))
450        })
451        .find(|val| val.1)
452        .map(|result| { result.0.to_string() })
453    }
454  } else if resource.encodings_provided().is_empty() {
455    Some("identity".to_string())
456  } else {
457    resource.encodings_provided().first().map(|s| s.to_string())
458  }
459}