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