1use super::{Trigger, TriggerError};
2use crate::context::Context;
3use log::{debug, info};
4use std::{collections::HashMap, sync::mpsc::Sender};
5use thiserror::Error;
6use tiny_http::{Response, Server};
7
8const TRIGGER_NAME: &str = "HTTP";
9
10pub struct HttpTrigger {
16 http: String,
17}
18
19#[derive(Debug, Error)]
21pub enum HttpError {
22 #[error("cannot start server on {0}")]
25 CantStartServer(String),
26 #[error("cannot trigger changes, receiver hang up")]
28 ReceiverHangup(#[from] std::sync::mpsc::SendError<Option<Context>>),
29 #[error("failed to send response")]
31 FailedResponse(#[from] std::io::Error),
32}
33
34impl From<HttpError> for TriggerError {
35 fn from(val: HttpError) -> Self {
36 match val {
37 HttpError::CantStartServer(s) => TriggerError::Misconfigured(s),
38 HttpError::ReceiverHangup(s) => TriggerError::ReceiverHangup(s),
39 HttpError::FailedResponse(s) => TriggerError::FailedTrigger(s.to_string()),
40 }
41 }
42}
43
44impl HttpTrigger {
45 pub fn new(http: String) -> Self {
48 Self { http }
49 }
50
51 fn listen_inner(&self, tx: Sender<Option<Context>>) -> Result<(), HttpError> {
52 let listener =
53 Server::http(&self.http).map_err(|_| HttpError::CantStartServer(self.http.clone()))?;
54 info!("Listening on {}...", self.http);
55 for request in listener.incoming_requests() {
56 debug!("Received request on {} {}", request.method(), request.url());
57
58 let context: Context = HashMap::from([
59 ("TRIGGER_NAME", TRIGGER_NAME.to_string()),
60 ("HTTP_METHOD", request.method().to_string()),
61 ("HTTP_URL", request.url().to_string()),
62 ]);
63 tx.send(Some(context)).map_err(HttpError::from)?;
64
65 request.respond(Response::from_string("OK"))?;
66 }
67 Ok(())
68 }
69}
70
71impl Trigger for HttpTrigger {
72 fn listen(&self, tx: Sender<Option<Context>>) -> Result<(), TriggerError> {
76 self.listen_inner(tx)?;
77
78 Ok(())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::{
86 error::Error,
87 sync::mpsc,
88 thread::{self, sleep},
89 time::Duration,
90 };
91
92 #[test]
93 fn it_should_be_created_from_http_url() {
94 let trigger = HttpTrigger::new(String::from("0.0.0.0:1234"));
95 assert_eq!("0.0.0.0:1234", &trigger.http);
96 }
97
98 #[test]
99 fn it_should_return_ok_on_every_request() -> Result<(), Box<dyn Error>> {
100 let trigger = HttpTrigger::new(String::from("0.0.0.0:10101"));
101 let (tx, rx) = mpsc::channel::<Option<Context>>();
102
103 thread::spawn(move || {
104 let _ = trigger.listen_inner(tx);
105 });
106
107 sleep(Duration::from_millis(100));
109
110 let result = ureq::get("http://localhost:10101").call()?;
111 assert_eq!(200, result.status());
112 assert_eq!("OK", result.into_body().read_to_string()?);
113
114 let result = ureq::post("http://localhost:10101/trigger").send_empty()?;
115 assert_eq!(200, result.status());
116 assert_eq!("OK", result.into_body().read_to_string()?);
117
118 let msg = rx.recv()?;
119 let context = msg.unwrap();
120 assert_eq!(TRIGGER_NAME, context.get("TRIGGER_NAME").unwrap());
121 assert_eq!("GET", context.get("HTTP_METHOD").unwrap());
122 assert_eq!("/", context.get("HTTP_URL").unwrap());
123
124 let msg = rx.recv()?;
125 let context = msg.unwrap();
126 assert_eq!(TRIGGER_NAME, context.get("TRIGGER_NAME").unwrap());
127 assert_eq!("POST", context.get("HTTP_METHOD").unwrap());
128 assert_eq!("/trigger", context.get("HTTP_URL").unwrap());
129
130 Ok(())
131 }
132
133 #[test]
134 fn it_should_fail_if_http_url_invalid() {
135 let trigger = HttpTrigger::new(String::from("aaaaa"));
136
137 let (tx, _rx) = mpsc::channel::<Option<Context>>();
138
139 let result = trigger.listen_inner(tx);
140 assert!(
141 matches!(result, Err(HttpError::CantStartServer(_))),
142 "{result:?} should be CantStartServer"
143 )
144 }
145
146 #[test]
147 fn it_should_fail_if_sending_fails() -> Result<(), Box<dyn Error>> {
148 let trigger = HttpTrigger::new(String::from("0.0.0.0:10102"));
149 let (tx, rx) = mpsc::channel::<Option<Context>>();
150
151 thread::spawn(move || {
152 sleep(Duration::from_millis(200));
154
155 let _ = ureq::get("http://localhost:10102").call();
156 });
157
158 drop(rx);
160
161 let result = trigger.listen_inner(tx);
162 assert!(
163 matches!(result, Err(HttpError::ReceiverHangup(_))),
164 "{result:?} should be ReceiverHangup"
165 );
166
167 Ok(())
168 }
169}