1use http_body_util::{BodyExt, Full};
2use hyper::{HeaderMap, Request, Response, StatusCode, Uri};
3use sms_core::{Headers, InboundRegistry};
4use sms_web_generic::{HeaderConverter, ResponseConverter, WebhookProcessor};
5use std::convert::Infallible;
6
7type HyperServiceFuture = std::pin::Pin<
8 Box<dyn std::future::Future<Output = Result<Response<Full<bytes::Bytes>>, Infallible>> + Send>,
9>;
10
11#[derive(Clone)]
12pub struct AppState {
13 pub registry: InboundRegistry,
14}
15
16pub struct HyperHeaderConverter;
18
19impl HeaderConverter for HyperHeaderConverter {
20 type HeaderType = HeaderMap;
21
22 fn to_generic_headers(headers: &Self::HeaderType) -> Headers {
23 headers
24 .iter()
25 .map(|(k, v)| {
26 (
27 k.as_str().to_string(),
28 v.to_str().unwrap_or_default().to_string(),
29 )
30 })
31 .collect()
32 }
33}
34
35pub struct HyperResponseConverter;
37
38impl ResponseConverter for HyperResponseConverter {
39 type ResponseType = Response<Full<bytes::Bytes>>;
40
41 fn from_webhook_response(response: sms_core::WebhookResponse) -> Self::ResponseType {
42 let status = StatusCode::from_u16(response.status.as_u16())
43 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
44
45 Response::builder()
46 .status(status)
47 .header("content-type", response.content_type)
48 .body(Full::new(bytes::Bytes::from(response.body)))
49 .unwrap()
50 }
51}
52
53fn extract_provider_from_path(uri: &Uri) -> Option<String> {
55 let path = uri.path();
56 path.strip_prefix("/webhooks/")
58 .map(|stripped| stripped.to_string())
59}
60
61pub async fn handle_webhook(
63 req: Request<hyper::body::Incoming>,
64 state: AppState,
65) -> Result<Response<Full<bytes::Bytes>>, Infallible> {
66 let provider = match extract_provider_from_path(req.uri()) {
68 Some(p) => p,
69 None => {
70 return Ok(Response::builder()
71 .status(StatusCode::NOT_FOUND)
72 .header("content-type", "application/json")
73 .body(Full::new(bytes::Bytes::from(r#"{"error":"Invalid path"}"#)))
74 .unwrap());
75 }
76 };
77
78 let generic_headers = HyperHeaderConverter::to_generic_headers(req.headers());
80
81 let body_bytes = match req.collect().await {
83 Ok(collected) => collected.to_bytes(),
84 Err(_) => {
85 return Ok(Response::builder()
86 .status(StatusCode::BAD_REQUEST)
87 .header("content-type", "application/json")
88 .body(Full::new(bytes::Bytes::from(
89 r#"{"error":"Failed to read body"}"#,
90 )))
91 .unwrap());
92 }
93 };
94
95 let processor = WebhookProcessor::new(state.registry);
96 let response = processor.process_webhook(&provider, generic_headers, &body_bytes);
97 Ok(HyperResponseConverter::from_webhook_response(response))
98}
99
100pub fn make_service(
102 state: AppState,
103) -> impl Fn(Request<hyper::body::Incoming>) -> HyperServiceFuture + Clone {
104 move |req| {
105 let state = state.clone();
106 Box::pin(handle_webhook(req, state))
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn extract_provider_works() {
116 let uri = "/webhooks/plivo".parse::<Uri>().unwrap();
117 assert_eq!(extract_provider_from_path(&uri), Some("plivo".to_string()));
118
119 let uri = "/other/path".parse::<Uri>().unwrap();
120 assert_eq!(extract_provider_from_path(&uri), None);
121 }
122
123 #[tokio::test]
124 async fn hyper_service_compiles() {
125 let registry = InboundRegistry::new();
126 let state = AppState { registry };
127 let _service = make_service(state);
128 }
129}