Skip to main content

sms_web_rocket/
lib.rs

1//! # SMS Web Rocket
2//!
3//! [Rocket](https://rocket.rs/) web framework integration for smskit webhook
4//! processing.
5
6use rocket::{http::Status, Request, State};
7use sms_core::{Headers, InboundRegistry};
8use sms_web_generic::{ResponseConverter, WebhookProcessor};
9
10/// Shared application state holding the provider registry.
11#[derive(Clone)]
12pub struct AppState {
13    pub registry: InboundRegistry,
14}
15
16/// Raw body data extractor for Rocket.
17#[derive(Debug)]
18pub struct RawBody(pub Vec<u8>);
19
20#[rocket::async_trait]
21impl<'r> rocket::data::FromData<'r> for RawBody {
22    type Error = Box<dyn std::error::Error + Send + Sync>;
23
24    async fn from_data(
25        _req: &'r Request<'_>,
26        data: rocket::Data<'r>,
27    ) -> rocket::data::Outcome<'r, Self> {
28        use rocket::data::ToByteUnit;
29
30        match data.open(2.megabytes()).into_bytes().await {
31            Ok(bytes) if bytes.is_complete() => {
32                rocket::data::Outcome::Success(RawBody(bytes.into_inner()))
33            }
34            Ok(_) => rocket::data::Outcome::Error((
35                Status::PayloadTooLarge,
36                Box::new(std::io::Error::other("Body too large")),
37            )),
38            Err(e) => rocket::data::Outcome::Error((Status::BadRequest, Box::new(e))),
39        }
40    }
41}
42
43/// Request guard that extracts HTTP headers into smskit's generic
44/// [`Headers`](sms_core::Headers) format.
45pub struct ExtractedHeaders(pub Headers);
46
47#[rocket::async_trait]
48impl<'r> rocket::request::FromRequest<'r> for ExtractedHeaders {
49    type Error = std::convert::Infallible;
50
51    async fn from_request(req: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
52        let headers: Headers = req
53            .headers()
54            .iter()
55            .map(|h| (h.name().to_string(), h.value().to_string()))
56            .collect();
57        rocket::request::Outcome::Success(ExtractedHeaders(headers))
58    }
59}
60
61/// Rocket-specific response converter.
62pub struct RocketResponseConverter;
63
64impl ResponseConverter for RocketResponseConverter {
65    type ResponseType = (Status, (rocket::http::ContentType, String));
66
67    fn from_webhook_response(response: sms_core::WebhookResponse) -> Self::ResponseType {
68        let status = match response.status.as_u16() {
69            200 => Status::Ok,
70            400 => Status::BadRequest,
71            401 => Status::Unauthorized,
72            404 => Status::NotFound,
73            _ => Status::InternalServerError,
74        };
75
76        let content_type = match response.content_type.as_str() {
77            "application/json" => rocket::http::ContentType::JSON,
78            _ => rocket::http::ContentType::Plain,
79        };
80
81        (status, (content_type, response.body))
82    }
83}
84
85/// Unified webhook handler for Rocket.
86///
87/// Extracts the provider name from the URL path, reads headers via a request
88/// guard, and delegates to the [`WebhookProcessor`].
89#[rocket::post("/webhooks/<provider>", data = "<body>")]
90pub fn unified_webhook(
91    provider: String,
92    body: RawBody,
93    extracted: ExtractedHeaders,
94    state: &State<AppState>,
95) -> (Status, (rocket::http::ContentType, String)) {
96    let processor = WebhookProcessor::new(state.registry.clone());
97    let response = processor.process_webhook(&provider, extracted.0, &body.0);
98    RocketResponseConverter::from_webhook_response(response)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn rocket_types_compile() {
107        let registry = InboundRegistry::new();
108        let _state = AppState { registry };
109    }
110
111    #[test]
112    fn response_converter_maps_status_codes() {
113        let resp = sms_core::WebhookResponse {
114            status: sms_core::HttpStatus::Ok,
115            body: "{}".into(),
116            content_type: "application/json".into(),
117        };
118        let (status, (ct, body)) = RocketResponseConverter::from_webhook_response(resp);
119        assert_eq!(status, Status::Ok);
120        assert_eq!(ct, rocket::http::ContentType::JSON);
121        assert_eq!(body, "{}");
122    }
123
124    #[test]
125    fn response_converter_handles_error_status() {
126        let resp = sms_core::WebhookResponse::error(sms_core::HttpStatus::NotFound, "not found");
127        let (status, _) = RocketResponseConverter::from_webhook_response(resp);
128        assert_eq!(status, Status::NotFound);
129    }
130}