1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! HTTP parts of a Request/Response interaction

use std::collections::HashMap;
use std::str::from_utf8;

use maplit::hashmap;

use crate::bodies::OptionalBody;
use crate::content_types::{ContentType, detect_content_type_from_string};
use crate::generators::{Generator, GeneratorCategory, Generators};
use crate::matchingrules::{Category, MatchingRules};
use crate::path_exp::DocPath;

/// Trait to specify an HTTP part of an interaction. It encapsulates the shared parts of a request
/// and response.
pub trait HttpPart {
  /// Returns the headers of the HTTP part.
  fn headers(&self) -> &Option<HashMap<String, Vec<String>>>;

  /// Returns the headers of the HTTP part in a mutable form.
  fn headers_mut(&mut self) -> &mut HashMap<String, Vec<String>>;

  /// Returns the body of the HTTP part.
  fn body(&self) -> &OptionalBody;

  /// Returns the matching rules of the HTTP part.
  fn matching_rules(&self) -> &MatchingRules;

  /// Returns the generators of the HTTP part.
  fn generators(&self) -> &Generators;

  /// Lookup up the content type for the part
  fn lookup_content_type(&self) -> Option<String>;

  /// Tries to detect the content type of the body by matching some regular expressions against
  /// the first 32 characters.
  fn detect_content_type(&self) -> Option<ContentType> {
    match *self.body() {
      OptionalBody::Present(ref body, _, _) => {
        let s: String = match from_utf8(body) {
          Ok(s) => s.to_string(),
          Err(_) => String::new()
        };
        detect_content_type_from_string(&s)
      },
      _ => None
    }
  }

  /// Determine the content type of the HTTP part. If a `Content-Type` header is present, the
  /// value of that header will be returned. Otherwise, the body will be inspected.
  fn content_type(&self) -> Option<ContentType> {
    let body = self.body();
    if body.has_content_type() {
      body.content_type()
    } else {
      match self.lookup_content_type() {
        Some(ref h) => match ContentType::parse(h.as_str()) {
          Ok(v) => Some(v),
          Err(_) => self.detect_content_type()
        },
        None => self.detect_content_type()
      }
    }
  }

  /// Checks if the HTTP Part has the given header
  fn has_header(&self, header_name: &str) -> bool {
    self.lookup_header_value(header_name).is_some()
  }

  /// Checks if the HTTP Part has the given header
  fn lookup_header_value(&self, header_name: &str) -> Option<String> {
    match *self.headers() {
      Some(ref h) => h.iter()
        .find(|kv| kv.0.to_lowercase() == header_name.to_lowercase())
        .map(|kv| kv.1.clone().join(", ")),
      None => None
    }
  }

  /// If the body is a textual type (non-binary)
  fn has_text_body(&self) -> bool {
    let body = self.body();
    let str_body = body.str_value();
    body.is_present() && !str_body.is_empty() && str_body.is_ascii()
  }

  /// Convenience method to add a header
  fn add_header(&mut self, key: &str, val: Vec<&str>) {
    let headers = self.headers_mut();
    headers.insert(key.to_string(), val.iter().map(|v| v.to_string()).collect());
  }

  /// Builds a map of generators from the generators and matching rules
  fn build_generators(&self, category: &GeneratorCategory) -> HashMap<DocPath, Generator> {
    let mut generators = hashmap!{};
    if let Some(generators_for_category) = self.generators().categories.get(category) {
      for (path, generator) in generators_for_category {
        generators.insert(path.clone(), generator.clone());
      }
    }
    let mr_category: Category = category.clone().into();
    if let Some(rules) = self.matching_rules().rules_for_category(mr_category) {
      for (path, generator) in rules.generators() {
        generators.insert(path.clone(), generator.clone());
      }
    }
    generators
  }
}

#[cfg(test)]
mod tests {
  use expectest::prelude::*;
  use maplit::hashmap;

  use crate::bodies::OptionalBody;
  use crate::http_parts::HttpPart;
  use crate::request::Request;

  #[test]
  fn http_part_has_header_test() {
    let request = Request { method: "GET".to_string(), path: "/".to_string(), query: None,
      headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json; charset=UTF-8".to_string()] }),
      body: OptionalBody::Missing, .. Request::default() };
    expect!(request.has_header("Content-Type")).to(be_true());
    expect!(request.lookup_header_value("Content-Type")).to(be_some().value("application/json; charset=UTF-8"));
  }
}