Skip to main content

sms_web_hyper/
lib.rs

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
16/// Hyper-specific header converter
17pub 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
35/// Hyper-specific response converter
36pub 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
53/// Extract provider from URI path
54fn extract_provider_from_path(uri: &Uri) -> Option<String> {
55    let path = uri.path();
56    // Expected format: /webhooks/{provider}
57    path.strip_prefix("/webhooks/")
58        .map(|stripped| stripped.to_string())
59}
60
61/// Unified webhook handler for raw Hyper
62pub async fn handle_webhook(
63    req: Request<hyper::body::Incoming>,
64    state: AppState,
65) -> Result<Response<Full<bytes::Bytes>>, Infallible> {
66    // Extract provider from path
67    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    // Get headers before consuming the request
79    let generic_headers = HyperHeaderConverter::to_generic_headers(req.headers());
80
81    // Read the body
82    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
100/// Helper function to create a Hyper service
101pub 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}