use super::{Trigger, TriggerError};
use std::sync::mpsc::Sender;
use log::{debug, info};
use thiserror::Error;
use tiny_http::{Response, Server};
pub struct HttpTrigger {
http: String,
}
#[derive(Debug, Error)]
pub enum HttpError {
#[error("cannot start server on {0}")]
CantStartServer(String),
#[error("cannot trigger changes, receiver hang up")]
ReceiverHangup(#[from] std::sync::mpsc::SendError<Option<()>>),
#[error("failed to send response")]
FailedResponse(#[from] std::io::Error),
}
impl From<HttpError> for TriggerError {
fn from(val: HttpError) -> Self {
match val {
HttpError::CantStartServer(s) => TriggerError::Misconfigured(s),
HttpError::ReceiverHangup(s) => TriggerError::ReceiverHangup(s),
HttpError::FailedResponse(s) => TriggerError::FailedTrigger(s.to_string()),
}
}
}
impl HttpTrigger {
pub fn new(http: String) -> Self {
Self { http }
}
fn listen_inner(&self, tx: Sender<Option<()>>) -> Result<(), HttpError> {
let listener =
Server::http(&self.http).map_err(|_| HttpError::CantStartServer(self.http.clone()))?;
info!("Listening on {}...", self.http);
for request in listener.incoming_requests() {
debug!("Received request on {} {}", request.method(), request.url());
tx.send(Some(())).map_err(HttpError::from)?;
request.respond(Response::from_string("OK"))?;
}
Ok(())
}
}
impl Trigger for HttpTrigger {
fn listen(&self, tx: Sender<Option<()>>) -> Result<(), TriggerError> {
self.listen_inner(tx)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
error::Error,
sync::mpsc,
thread::{self, sleep},
time::Duration,
};
#[test]
fn it_should_be_created_from_http_url() {
let trigger = HttpTrigger::new(String::from("0.0.0.0:1234"));
assert_eq!("0.0.0.0:1234", &trigger.http);
}
#[test]
fn it_should_return_ok_on_every_request() -> Result<(), Box<dyn Error>> {
let trigger = HttpTrigger::new(String::from("0.0.0.0:10101"));
let (tx, rx) = mpsc::channel::<Option<()>>();
thread::spawn(move || {
let _ = trigger.listen_inner(tx);
});
sleep(Duration::from_millis(100));
let result = ureq::get("http://localhost:10101").call()?;
assert_eq!(200, result.status());
assert_eq!("OK", result.into_string()?);
let result = ureq::post("http://localhost:10101/trigger").call()?;
assert_eq!(200, result.status());
assert_eq!("OK", result.into_string()?);
let msg = rx.recv()?;
assert_eq!(Some(()), msg);
let msg = rx.recv()?;
assert_eq!(Some(()), msg);
Ok(())
}
#[test]
fn it_should_fail_if_http_url_invalid() {
let trigger = HttpTrigger::new(String::from("aaaaa"));
let (tx, _rx) = mpsc::channel::<Option<()>>();
let result = trigger.listen_inner(tx);
assert!(
matches!(result, Err(HttpError::CantStartServer(_))),
"{result:?} should be CantStartServer"
)
}
#[test]
fn it_should_fail_if_sending_fails() -> Result<(), Box<dyn Error>> {
let trigger = HttpTrigger::new(String::from("0.0.0.0:10102"));
let (tx, rx) = mpsc::channel::<Option<()>>();
thread::spawn(move || {
sleep(Duration::from_millis(200));
let _ = ureq::get("http://localhost:10102").call();
});
drop(rx);
let result = trigger.listen_inner(tx);
assert!(
matches!(result, Err(HttpError::ReceiverHangup(_))),
"{result:?} should be ReceiverHangup"
);
Ok(())
}
}