1use rocket::{http::Status, Request, State};
7use sms_core::{Headers, InboundRegistry};
8use sms_web_generic::{ResponseConverter, WebhookProcessor};
9
10#[derive(Clone)]
12pub struct AppState {
13 pub registry: InboundRegistry,
14}
15
16#[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
43pub 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
61pub 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#[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}