use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::collections::HashMap;
use std::time::Duration;
use ::Method;
use ::Status;
use regex::Regex;
pub struct Resource {
uri: String,
uri_regex: Regex,
params: Arc<Mutex<URIParameters>>,
status_code: Arc<Mutex<Status>>,
custom_status_code: Arc<Mutex<Option<String>>>,
headers: Arc<Mutex<HashMap<String, String>>>,
body: Arc<Mutex<Option<&'static str>>>,
body_builder: Arc<Mutex<Option<BodyBuilder>>>,
method: Arc<Mutex<Method>>,
delay: Arc<Mutex<Option<Duration>>>,
request_count: Arc<Mutex<u32>>,
is_stream: Arc<AtomicBool>,
stream_listeners: Arc<Mutex<Vec<mpsc::Sender<String>>>>
}
struct URIParameters {
path: Vec<String>,
query: HashMap<String, String>
}
type BodyBuilder = Box<dyn Fn(RequestParameters) -> String + Send>;
impl Resource {
pub(crate) fn new(uri: &str) -> Resource {
let (uri_regex, params) = create_uri_regex(uri);
Resource {
uri: String::from(uri),
uri_regex,
params: Arc::new(Mutex::new(params)),
status_code: Arc::new(Mutex::new(Status::OK)),
custom_status_code: Arc::new(Mutex::new(None)),
headers: Arc::new(Mutex::new(HashMap::new())),
body: Arc::new(Mutex::new(None)),
body_builder: Arc::new(Mutex::new(None)),
method: Arc::new(Mutex::new(Method::GET)),
delay: Arc::new(Mutex::new(None)),
request_count: Arc::new(Mutex::new(0)),
is_stream: Arc::new(AtomicBool::new(false)),
stream_listeners: Arc::new(Mutex::new(vec!()))
}
}
pub fn status(&self, status_code: Status) -> &Resource {
if let Ok(mut status) = self.status_code.lock() {
*status = status_code;
}
if let Ok(mut custom_status) = self.custom_status_code.lock() {
*custom_status = None;
}
self
}
fn get_status_description(&self) -> String {
match *(self.custom_status_code.lock().unwrap()) {
Some(ref custom_status) => custom_status.clone(),
None => self.status_code.lock().unwrap().description().to_string()
}
}
pub fn custom_status(&self, status_code: u16, description: &str) -> &Resource {
if let Ok(mut status) = self.custom_status_code.lock() {
*status = Some(format!("{} {}", status_code, description));
}
self
}
pub fn header(&self, header_name: &str, header_value: &str) -> &Resource {
let mut headers = self.headers.lock().unwrap();
headers.insert(String::from(header_name), String::from(header_value));
self
}
fn get_headers(&self) -> String {
let headers = self.headers.lock().unwrap();
headers.iter().fold(String::new(), | headers, (name, value) | {
headers + &format!("{}: {}\r\n", name, value)
})
}
pub fn query(&self, name: &str, value: &str) -> &Resource {
let mut params = self.params.lock().unwrap();
params.query.insert(String::from(name), String::from(value));
self
}
pub fn body(&self, content: &'static str) -> &Resource {
if self.body_builder.lock().unwrap().is_some() {
panic!("You can't define 'body' when 'body_fn' is already defined");
}
if let Ok(mut body) = self.body.lock() {
*body = Some(content);
}
self
}
pub fn body_fn(&self, builder: impl Fn(RequestParameters) -> String + Send + 'static) -> &Resource {
if self.body.lock().unwrap().is_some() {
panic!("You can't define 'body_fn' when 'body' is already defined");
}
if let Ok(mut body_builder) = self.body_builder.lock() {
*body_builder = Some(Box::new(builder));
}
self
}
pub fn method(&self, method: Method) -> &Resource {
if let Ok(mut m) = self.method.lock() {
*m = method;
}
self
}
pub(crate) fn get_method(&self) -> Method {
(*self.method.lock().unwrap()).clone()
}
pub fn delay(&self, delay: Duration) -> &Resource {
if let Ok(mut d) = self.delay.lock() {
*d = Some(delay);
}
self
}
pub(crate) fn get_delay(&self) -> Option<Duration> {
*self.delay.lock().unwrap()
}
pub fn stream(&self) -> &Resource {
self.is_stream.store(true, Ordering::Relaxed);
self
}
pub(crate) fn is_stream(&self) -> bool {
self.is_stream.load(Ordering::Relaxed)
}
fn create_body(&self, uri: &str) -> String {
let params = self.extract_params_from_uri(uri);
if let Some(body_builder) = &*self.body_builder.lock().unwrap() {
return body_builder(params);
}
match *self.body.lock().unwrap() {
Some(body) => {
let mut body = body.to_string();
for (name, value) in ¶ms.path {
let key = format!("{{path.{}}}", name);
body = body.replace(&key, value);
}
for (name, value) in ¶ms.query {
let key = format!("{{query.{}}}", name);
body = body.replace(&key, value);
}
body
},
None => {
String::from("")
}
}
}
fn extract_params_from_uri(&self, uri: &str) -> RequestParameters {
RequestParameters { path: self.extra_path_params(uri), query: extract_query_params(uri) }
}
fn extra_path_params(&self, uri: &str) -> HashMap<String, String> {
let mut params = HashMap::new();
if let Some(values) = self.uri_regex.captures(uri) {
for param in &self.params.lock().unwrap().path {
if let Some(value) = values.name(param) {
params.insert(String::from(param), String::from(value.as_str()));
}
}
}
params
}
pub(crate) fn build_response(&self, uri: &str) -> String {
format!("HTTP/1.1 {}\r\n{}\r\n{}",
self.get_status_description(),
self.get_headers(),
self.create_body(uri)
)
}
pub(crate) fn increment_request_count(&self) {
*(self.request_count.lock().unwrap()) += 1;
}
pub fn send(&self, data: &str) -> &Resource {
if let Ok(mut listeners) = self.stream_listeners.lock() {
let mut invalid_listeners = vec!();
for (i, listener) in listeners.iter().enumerate() {
if listener.send(String::from(data)).is_err() {
invalid_listeners.push(i);
}
}
for i in invalid_listeners.iter() {
listeners.remove(*i);
}
}
self
}
pub fn send_line(&self, data: &str) -> &Resource {
self.send(&format!("{}\n", data))
}
pub fn close_open_connections(&self) {
if let Ok(mut listeners) = self.stream_listeners.lock() {
listeners.clear();
}
}
pub fn open_connections_count(&self) -> usize {
let listeners = self.stream_listeners.lock().unwrap();
listeners.len()
}
pub fn stream_receiver(&self) -> mpsc::Receiver<String> {
let (tx, rx) = mpsc::channel();
if let Ok(mut listeners) = self.stream_listeners.lock() {
listeners.push(tx);
}
rx
}
pub fn request_count(&self) -> u32 {
*(self.request_count.lock().unwrap())
}
pub(crate) fn matches_uri(&self, uri: &str) -> bool {
self.uri_regex.is_match(uri) && self.matches_query_parameters(uri)
}
fn matches_query_parameters(&self, uri: &str) -> bool {
let query_params = extract_query_params(uri);
for (expected_key, expected_value) in &self.params.lock().unwrap().query {
if let Some(value) = query_params.get(expected_key) {
if expected_value != value && expected_value != "*" {
return false;
}
} else {
return false;
}
}
true
}
}
impl Clone for Resource {
fn clone(&self) -> Self {
Resource {
uri: self.uri.clone(),
uri_regex: self.uri_regex.clone(),
params: self.params.clone(),
status_code: self.status_code.clone(),
custom_status_code: self.custom_status_code.clone(),
headers: self.headers.clone(),
body: self.body.clone(),
body_builder: self.body_builder.clone(),
method: self.method.clone(),
delay: self.delay.clone(),
request_count: self.request_count.clone(),
is_stream: self.is_stream.clone(),
stream_listeners: self.stream_listeners.clone()
}
}
}
pub struct RequestParameters {
pub path: HashMap<String, String>,
pub query: HashMap<String, String>
}
fn create_uri_regex(uri: &str) -> (Regex, URIParameters) {
let re = Regex::new(r"\{(?P<p>([A-z|0-9|_])+)\}").unwrap();
let query_regex = Regex::new(r"\?.*").unwrap();
let params: Vec<String> = re.captures_iter(uri).filter_map(|cap| {
match cap.name("p") {
Some(p) => Some(String::from(p.as_str())),
None => None
}
}).collect();
let query_params = extract_query_params(uri);
let pattern = query_regex.replace(uri, "");
let pattern = re.replace_all(&pattern, r"(?P<$p>[^//|/?]+)");
(Regex::new(&pattern).unwrap(), URIParameters { path: params, query: query_params})
}
fn extract_query_params(uri: &str) -> HashMap<String, String> {
let query_regex = Regex::new(r"((?P<qk>[^&]+)=(?P<qv>[^&]+))*").unwrap();
let path_regex = Regex::new(r".*\?").unwrap();
let only_query_parameters = path_regex.replace(uri, "");
query_regex.captures_iter(&only_query_parameters).filter_map(|cap| {
if let Some(query_key) = cap.name("qk") {
let query_value = match cap.name("qv") {
Some(v) => String::from(v.as_str()),
None => String::from("")
};
return Some((String::from(query_key.as_str()), query_value));
}
None
}).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn should_convert_to_response_string() {
let resource = Resource::new("/");
resource.status(Status::NotFound);
assert_eq!(resource.build_response("/"), "HTTP/1.1 404 Not Found\r\n\r\n");
}
#[test]
fn should_convert_to_response_with_body() {
let resource = Resource::new("/");
resource.status(Status::Accepted).body("hello!");
assert_eq!(resource.build_response("/"), "HTTP/1.1 202 Accepted\r\n\r\nhello!");
}
#[test]
fn should_allows_custom_status() {
let resource = Resource::new("/");
resource.custom_status(666, "The Number Of The Beast").body("hello!");
assert_eq!(resource.build_response("/"), "HTTP/1.1 666 The Number Of The Beast\r\n\r\nhello!");
}
#[test]
fn should_overwrite_custom_status_with_status() {
let resource = Resource::new("/");
resource.custom_status(666, "The Number Of The Beast").status(Status::Forbidden).body("hello!");
assert_eq!(resource.build_response("/"), "HTTP/1.1 403 Forbidden\r\n\r\nhello!");
}
#[test]
fn should_add_headers() {
let resource = Resource::new("/");
resource
.header("Content-Type", "application/json")
.body("hello!");
assert_eq!(resource.build_response("/"), "HTTP/1.1 200 Ok\r\nContent-Type: application/json\r\n\r\nhello!");
}
#[test]
fn should_append_headers() {
let resource = Resource::new("/");
resource
.header("Content-Type", "application/json")
.header("Connection", "Keep-Alive")
.body("hello!");
let response = resource.build_response("/");
assert!(response.contains("Content-Type: application/json\r\n"));
assert!(response.contains("Connection: Keep-Alive\r\n"));
}
#[test]
fn should_increment_request_count() {
let resource = Resource::new("/");
resource.body("hello!");
resource.increment_request_count();
resource.increment_request_count();
resource.increment_request_count();
assert_eq!(resource.request_count(), 3);
}
#[test]
fn clones_should_share_same_state() {
let resource = Resource::new("/");
let dolly = resource.clone();
resource.increment_request_count();
dolly.increment_request_count();
assert_eq!(resource.request_count(), dolly.request_count());
assert_eq!(resource.request_count(), 2);
}
#[test]
fn should_set_as_stream() {
let resource = Resource::new("/");
resource.stream().status(Status::Accepted);
assert!(resource.is_stream());
}
#[test]
fn should_notify_data() {
let resource = Resource::new("/");
let receiver = resource.stream_receiver();
resource.send("some data").send("some data");
assert_eq!(receiver.recv().unwrap(), "some data");
assert_eq!(receiver.recv().unwrap(), "some data");
}
#[test]
fn should_close_connections() {
let resource = Resource::new("/");
let res = resource.clone();
let receiver = resource.stream_receiver();
thread::spawn(move || {
res.send("some data");
res.send("some data");
res.close_open_connections();
});
let mut string = String::new();
for data in receiver.iter() {
string = string + &data;
}
assert_eq!(string, "some datasome data");
}
#[test]
fn should_return_number_of_connecteds_users() {
let resource = Resource::new("/");
let _receiver = resource.stream_receiver();
let _receiver_2 = resource.stream_receiver();
assert_eq!(resource.open_connections_count(), 2);
}
#[test]
fn should_decrease_count_when_receiver_dropped() {
let resource = Resource::new("/");
resource.stream_receiver();
resource.send("some data");
assert_eq!(resource.open_connections_count(), 0);
}
#[test]
fn should_send_data_with_line_break() {
let resource = Resource::new("/");
let receiver = resource.stream_receiver();
resource.send_line("some data").send_line("again");
assert_eq!(receiver.recv().unwrap(), "some data\n");
assert_eq!(receiver.recv().unwrap(), "again\n");
}
#[test]
fn should_set_delay() {
let resource = Resource::new("/");
resource.delay(Duration::from_millis(200));
assert_eq!(resource.get_delay(), Some(Duration::from_millis(200)));
}
#[test]
fn should_match_uri() {
let resource = Resource::new("/some-endpoint");
assert!(resource.matches_uri("/some-endpoint"));
}
#[test]
fn should_not_match_uri_when_uri_does_not_match() {
let resource = Resource::new("/some-endpoint");
assert!(!resource.matches_uri("/some-other-endpoint"));
}
#[test]
fn should_match_uri_with_path_params() {
let resource = Resource::new("/endpoint/{param1}/some/{param2}");
assert!(resource.matches_uri("/endpoint/123/some/abc"));
assert!(resource.matches_uri("/endpoint/123-345/some/abc"));
}
#[test]
fn should_not_match_uri_with_path_params_when_uri_does_not_match() {
let resource = Resource::new("/endpoint/{param1}/some/{param2}");
assert!(!resource.matches_uri("/endpoint/123/some/"));
}
#[test]
fn should_match_uri_with_query_params() {
let resource = Resource::new("/endpoint?userId=123");
assert!(resource.matches_uri("/endpoint?userId=123"));
}
#[test]
fn should_not_match_uri_with_wrong_query_parameter() {
let resource = Resource::new("/endpoint?userId=123");
assert!(!resource.matches_uri("/endpoint?userId=abc"));
}
#[test]
fn should_match_uri_with_multiple_query_params() {
let resource = Resource::new("/endpoint?userId=123&hello=abc");
assert!(resource.matches_uri("/endpoint?userId=123&hello=abc"));
}
#[test]
fn should_match_uri_with_wildcard_query_params() {
let resource = Resource::new("/endpoint?userId=123&collectionId=*");
assert!(resource.matches_uri("/endpoint?userId=123&collectionId=banana"));
}
#[test]
fn should_match_uri_with_query_params_in_different_order() {
let resource = Resource::new("/endpoint?hello=abc&userId=123");
assert!(resource.matches_uri("/endpoint?userId=123&hello=abc"));
}
#[test]
fn should_not_match_uri_when_one_query_param_is_wrong() {
let resource = Resource::new("/endpoint?userId=123&hello=abc");
assert!(!resource.matches_uri("/endpoint?userId=123&hello=bbc"));
}
#[test]
fn should_match_uri_with_query_params_defined_through_method() {
let resource = Resource::new("/endpoint");
resource.query("hello", "abc").query("userId", "123");
assert!(resource.matches_uri("/endpoint?userId=123&hello=abc"));
}
#[test]
fn should_match_uri_with_wildcard_query_params_defined_through_method() {
let resource = Resource::new("/endpoint");
resource.query("hello", "*");
assert!(resource.matches_uri("/endpoint?hello=1234"));
}
#[test]
fn should_build_response() {
let resource = Resource::new("/");
resource.status(Status::NotFound);
assert_eq!(resource.build_response("/"), "HTTP/1.1 404 Not Found\r\n\r\n");
}
#[test]
fn should_build_response_with_body() {
let resource = Resource::new("/");
resource.status(Status::Accepted).body("hello!");
assert_eq!(resource.build_response("/"), "HTTP/1.1 202 Accepted\r\n\r\nhello!");
}
#[test]
fn should_build_response_with_path_parameters() {
let resource = Resource::new("/endpoint/{param1}/{param2}");
resource.status(Status::Accepted).body("Hello: {path.param2} {path.param1}");
assert_eq!(resource.build_response("/endpoint/123/abc"), "HTTP/1.1 202 Accepted\r\n\r\nHello: abc 123");
}
#[test]
fn should_build_response_with_query_parameters() {
let resource = Resource::new("/endpoint/{param1}?param2=111");
resource.status(Status::Accepted).body("Hello: {query.param2} {path.param1}");
assert_eq!(resource.build_response("/endpoint/123?param2=111"), "HTTP/1.1 202 Accepted\r\n\r\nHello: 111 123");
}
#[test]
fn should_build_response_with_wildcard_query_parameters() {
let resource = Resource::new("/endpoint/{param1}?param2=111¶m3=*");
resource.status(Status::Accepted).body("Hello: {query.param3}");
assert_eq!(resource.build_response("/endpoint/123?param2=111¶m3=banana"), "HTTP/1.1 202 Accepted\r\n\r\nHello: banana");
}
#[test]
fn should_build_response_using_body_fn() {
let resource = Resource::new("/endpoint/{param1}/{param2}");
resource.status(Status::Accepted).body_fn(|params| {
format!("Hello: {} {}", params.path.get("param2").unwrap(), params.path.get("param1").unwrap())
});
assert_eq!(resource.build_response("/endpoint/123/abc"), "HTTP/1.1 202 Accepted\r\n\r\nHello: abc 123");
}
#[test]
#[should_panic(expected = "You can't define 'body_fn' when 'body' is already defined")]
fn should_fail_when_trying_to_define_body_fn_after_defining_body() {
let resource = Resource::new("/endpoint/{param1}/{param2}");
resource.body("some body");
resource.body_fn(|_params| String::from(""));
}
#[test]
#[should_panic(expected = "You can't define 'body' when 'body_fn' is already defined")]
fn should_fail_when_trying_to_define_body_after_defining_body_fn() {
let resource = Resource::new("/endpoint/{param1}/{param2}");
resource.body_fn(|_params| String::from(""));
resource.body("some body");
}
}