webmachine_rust/
context.rs

1//! The `context` module encapsulates the context of the environment that the webmachine is
2//! executing in. Basically wraps the request and response.
3
4use std::collections::{BTreeMap, HashMap};
5use std::fmt::Display;
6
7use bytes::Bytes;
8use chrono::{DateTime, FixedOffset};
9use maplit::hashmap;
10
11use crate::headers::HeaderValue;
12
13/// Request that the state machine is executing against
14#[derive(Debug, Clone, PartialEq)]
15pub struct WebmachineRequest {
16  /// Path of the request relative to the resource
17  pub request_path: String,
18  /// Resource base path
19  pub base_path: String,
20  /// Request method
21  pub method: String,
22  /// Request headers
23  pub headers: HashMap<String, Vec<HeaderValue>>,
24  /// Request body
25  pub body: Option<Bytes>,
26  /// Query parameters
27  pub query: HashMap<String, Vec<String>>
28}
29
30impl Default for WebmachineRequest {
31  /// Creates a default request (GET /)
32  fn default() -> WebmachineRequest {
33    WebmachineRequest {
34      request_path: "/".to_string(),
35      base_path: "/".to_string(),
36      method: "GET".to_string(),
37      headers: HashMap::new(),
38      body: None,
39      query: HashMap::new()
40    }
41  }
42}
43
44impl WebmachineRequest {
45    /// returns the content type of the request, based on the content type header. Defaults to
46    /// 'application/json' if there is no header.
47    pub fn content_type(&self) -> String {
48      match self.headers.keys().find(|k| k.to_uppercase() == "CONTENT-TYPE") {
49        Some(header) => match self.headers.get(header).unwrap().first() {
50          Some(value) => value.clone().value,
51          None => "application/json".to_string()
52        },
53        None => "application/json".to_string()
54      }
55    }
56
57    /// If the request is a put or post
58    pub fn is_put_or_post(&self) -> bool {
59        ["PUT", "POST"].contains(&self.method.to_uppercase().as_str())
60    }
61
62    /// If the request is a get or head request
63    pub fn is_get_or_head(&self) -> bool {
64        ["GET", "HEAD"].contains(&self.method.to_uppercase().as_str())
65    }
66
67    /// If the request is a get
68    pub fn is_get(&self) -> bool {
69        self.method.to_uppercase() == "GET"
70    }
71
72    /// If the request is an options
73    pub fn is_options(&self) -> bool {
74        self.method.to_uppercase() == "OPTIONS"
75    }
76
77    /// If the request is a put
78    pub fn is_put(&self) -> bool {
79        self.method.to_uppercase() == "PUT"
80    }
81
82    /// If the request is a post
83    pub fn is_post(&self) -> bool {
84        self.method.to_uppercase() == "POST"
85    }
86
87    /// If the request is a delete
88    pub fn is_delete(&self) -> bool {
89        self.method.to_uppercase() == "DELETE"
90    }
91
92    /// If an Accept header exists
93    pub fn has_accept_header(&self) -> bool {
94        self.has_header("ACCEPT")
95    }
96
97    /// Returns the acceptable media types from the Accept header
98    pub fn accept(&self) -> Vec<HeaderValue> {
99        self.find_header("ACCEPT")
100    }
101
102    /// If an Accept-Language header exists
103    pub fn has_accept_language_header(&self) -> bool {
104        self.has_header("ACCEPT-LANGUAGE")
105    }
106
107    /// Returns the acceptable languages from the Accept-Language header
108    pub fn accept_language(&self) -> Vec<HeaderValue> {
109        self.find_header("ACCEPT-LANGUAGE")
110    }
111
112    /// If an Accept-Charset header exists
113    pub fn has_accept_charset_header(&self) -> bool {
114        self.has_header("ACCEPT-CHARSET")
115    }
116
117    /// Returns the acceptable charsets from the Accept-Charset header
118    pub fn accept_charset(&self) -> Vec<HeaderValue> {
119        self.find_header("ACCEPT-CHARSET")
120    }
121
122    /// If an Accept-Encoding header exists
123    pub fn has_accept_encoding_header(&self) -> bool {
124        self.has_header("ACCEPT-ENCODING")
125    }
126
127    /// Returns the acceptable encodings from the Accept-Encoding header
128    pub fn accept_encoding(&self) -> Vec<HeaderValue> {
129        self.find_header("ACCEPT-ENCODING")
130    }
131
132    /// If the request has the provided header
133    pub fn has_header(&self, header: &str) -> bool {
134      self.headers.keys().find(|k| k.to_uppercase() == header.to_uppercase()).is_some()
135    }
136
137    /// Returns the list of values for the provided request header. If the header is not present,
138    /// or has no value, and empty vector is returned.
139    pub fn find_header(&self, header: &str) -> Vec<HeaderValue> {
140        match self.headers.keys().find(|k| k.to_uppercase() == header.to_uppercase()) {
141            Some(header) => self.headers.get(header).unwrap().clone(),
142            None => Vec::new()
143        }
144    }
145
146    /// If the header has a matching value
147    pub fn has_header_value(&self, header: &str, value: &str) -> bool {
148        match self.headers.keys().find(|k| k.to_uppercase() == header.to_uppercase()) {
149            Some(header) => match self.headers.get(header).unwrap().iter().find(|val| *val == value) {
150                Some(_) => true,
151                None => false
152            },
153            None => false
154        }
155    }
156}
157
158/// Response that is generated as a result of the webmachine execution
159#[derive(Debug, Clone, PartialEq)]
160pub struct WebmachineResponse {
161    /// status code to return
162    pub status: u16,
163    /// headers to return
164    pub headers: BTreeMap<String, Vec<HeaderValue>>,
165    /// Response Body
166    pub body: Option<Bytes>
167}
168
169impl WebmachineResponse {
170    /// Creates a default response (200 OK)
171    pub fn default() -> WebmachineResponse {
172        WebmachineResponse {
173            status: 200,
174            headers: BTreeMap::new(),
175            body: None
176        }
177    }
178
179    /// If the response has the provided header
180    pub fn has_header(&self, header: &str) -> bool {
181      self.headers.keys().find(|k| k.to_uppercase() == header.to_uppercase()).is_some()
182    }
183
184    /// Adds the header values to the headers
185    pub fn add_header(&mut self, header: &str, values: Vec<HeaderValue>) {
186      self.headers.insert(header.to_string(), values);
187    }
188
189    /// Adds the headers from a HashMap to the headers
190    pub fn add_headers(&mut self, headers: HashMap<String, Vec<String>>) {
191      for (k, v) in headers {
192        self.headers.insert(k, v.iter().map(HeaderValue::basic).collect());
193      }
194    }
195
196    /// Adds standard CORS headers to the response
197    pub fn add_cors_headers(&mut self, allowed_methods: &Vec<String>) {
198      let cors_headers = WebmachineResponse::cors_headers(allowed_methods);
199      for (k, v) in cors_headers {
200        self.add_header(k.as_str(), v.iter().map(HeaderValue::basic).collect());
201      }
202    }
203
204    /// Returns a HashMap of standard CORS headers
205    pub fn cors_headers(allowed_methods: &Vec<String>) -> HashMap<String, Vec<String>> {
206      hashmap!{
207        "Access-Control-Allow-Origin".to_string() => vec!["*".to_string()],
208        "Access-Control-Allow-Methods".to_string() => allowed_methods.clone(),
209        "Access-Control-Allow-Headers".to_string() => vec!["Content-Type".to_string()]
210      }
211    }
212
213    /// If the response has a body
214    pub fn has_body(&self) -> bool {
215        match &self.body {
216            &None => false,
217            &Some(ref body) => !body.is_empty()
218        }
219    }
220}
221
222/// Values that can be stored as metadata
223#[derive(Debug, Clone, PartialEq)]
224pub enum MetaDataValue {
225  /// No Value,
226  Empty,
227  /// String Value
228  String(String),
229  /// Unsigned integer
230  UInteger(u64),
231  /// Signed integer
232  Integer(i64)
233}
234
235impl Display for MetaDataValue {
236  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237    match self {
238      MetaDataValue::String(s) => write!(f, "{}", s.as_str()),
239      MetaDataValue::UInteger(u) => write!(f, "{}", *u),
240      MetaDataValue::Integer(i) => write!(f, "{}", *i),
241      MetaDataValue::Empty => Ok(())
242    }
243  }
244}
245
246impl Default for MetaDataValue {
247  fn default() -> Self {
248    MetaDataValue::Empty
249  }
250}
251
252impl Default for &MetaDataValue {
253  fn default() -> Self {
254    &MetaDataValue::Empty
255  }
256}
257
258impl From<String> for MetaDataValue {
259  fn from(value: String) -> Self {
260    MetaDataValue::String(value)
261  }
262}
263
264impl From<&String> for MetaDataValue {
265  fn from(value: &String) -> Self {
266    MetaDataValue::String(value.clone())
267  }
268}
269
270impl From<&str> for MetaDataValue {
271  fn from(value: &str) -> Self {
272    MetaDataValue::String(value.to_string())
273  }
274}
275
276impl From<u16> for MetaDataValue {
277  fn from(value: u16) -> Self {
278    MetaDataValue::UInteger(value as u64)
279  }
280}
281
282impl From<i16> for MetaDataValue {
283  fn from(value: i16) -> Self {
284    MetaDataValue::Integer(value as i64)
285  }
286}
287
288impl From<u64> for MetaDataValue {
289  fn from(value: u64) -> Self {
290    MetaDataValue::UInteger(value)
291  }
292}
293
294impl From<i64> for MetaDataValue {
295  fn from(value: i64) -> Self {
296    MetaDataValue::Integer(value)
297  }
298}
299
300/// Main context struct that holds the request and response.
301#[derive(Debug, Clone, PartialEq)]
302pub struct WebmachineContext {
303  /// Request that the webmachine is executing against
304  pub request: WebmachineRequest,
305  /// Response that is the result of the execution
306  pub response: WebmachineResponse,
307  /// selected media type after content negotiation
308  pub selected_media_type: Option<String>,
309  /// selected language after content negotiation
310  pub selected_language: Option<String>,
311  /// selected charset after content negotiation
312  pub selected_charset: Option<String>,
313  /// selected encoding after content negotiation
314  pub selected_encoding: Option<String>,
315  /// parsed date and time from the If-Unmodified-Since header
316  pub if_unmodified_since: Option<DateTime<FixedOffset>>,
317  /// parsed date and time from the If-Modified-Since header
318  pub if_modified_since: Option<DateTime<FixedOffset>>,
319  /// If the response should be a redirect
320  pub redirect: bool,
321  /// If a new resource was created
322  pub new_resource: bool,
323  /// General store of metadata. You can use this to store attributes as the webmachine executes.
324  pub metadata: HashMap<String, MetaDataValue>
325}
326
327impl Default for WebmachineContext {
328  /// Creates a default context
329  fn default() -> WebmachineContext {
330    WebmachineContext {
331      request: WebmachineRequest::default(),
332      response: WebmachineResponse::default(),
333      selected_media_type: None,
334      selected_language: None,
335      selected_charset: None,
336      selected_encoding: None,
337      if_unmodified_since: None,
338      if_modified_since: None,
339      redirect: false,
340      new_resource: false,
341      metadata: HashMap::new()
342    }
343  }
344}
345
346#[cfg(test)]
347mod tests {
348  use expectest::prelude::*;
349
350  use crate::headers::*;
351
352  use super::*;
353
354  #[test]
355  fn request_does_not_have_header_test() {
356      let request = WebmachineRequest {
357          .. WebmachineRequest::default()
358      };
359      expect!(request.has_header("Vary")).to(be_false());
360      expect!(request.has_header_value("Vary", "*")).to(be_false());
361  }
362
363  #[test]
364  fn request_with_empty_header_test() {
365      let request = WebmachineRequest {
366          headers: hashmap!{ "HeaderA".to_string() => Vec::new() },
367          .. WebmachineRequest::default()
368      };
369      expect!(request.has_header("HeaderA")).to(be_true());
370      expect!(request.has_header_value("HeaderA", "*")).to(be_false());
371  }
372
373  #[test]
374  fn request_with_header_single_value_test() {
375      let request = WebmachineRequest {
376          headers: hashmap!{ "HeaderA".to_string() => vec![h!("*")] },
377          .. WebmachineRequest::default()
378      };
379      expect!(request.has_header("HeaderA")).to(be_true());
380      expect!(request.has_header_value("HeaderA", "*")).to(be_true());
381      expect!(request.has_header_value("HeaderA", "other")).to(be_false());
382  }
383
384  #[test]
385  fn request_with_header_multiple_value_test() {
386      let request = WebmachineRequest {
387          headers: hashmap!{ "HeaderA".to_string() => vec![h!("*"), h!("other")]},
388          .. WebmachineRequest::default()
389      };
390      expect!(request.has_header("HeaderA")).to(be_true());
391      expect!(request.has_header_value("HeaderA", "*")).to(be_true());
392      expect!(request.has_header_value("HeaderA", "other")).to(be_true());
393      expect!(request.has_header_value("HeaderA", "other2")).to(be_false());
394  }
395}