1use futures::TryFuture;
6use htsget_axum::server::ticket::TicketServer;
7use htsget_config::config::Config;
8use htsget_config::package_info;
9use lambda_http::http::Uri;
10use lambda_http::request::{LambdaRequest, RequestContext};
11use lambda_http::{
12 Error, IntoResponse, LambdaEvent, Request, RequestExt, Service, TransformResponse, lambda_runtime,
13};
14use std::marker::PhantomData;
15use std::path::Path;
16use std::task::{Context, Poll};
17use tracing::debug;
18
19pub struct Adapter<'a, R, S> {
21 service: S,
22 _phantom_data: PhantomData<&'a R>,
23}
24
25impl<'a, R, S, E> From<S> for Adapter<'a, R, S>
26where
27 S: Service<Request, Response = R, Error = E>,
28 S::Future: Send + 'a,
29 R: IntoResponse,
30{
31 fn from(service: S) -> Self {
32 Adapter {
33 service,
34 _phantom_data: PhantomData,
35 }
36 }
37}
38
39impl<'a, R, S, E> Service<LambdaEvent<serde_json::Value>> for Adapter<'a, R, S>
40where
41 S: Service<Request, Response = R, Error = E>,
42 S::Future: Send + 'a,
43 R: IntoResponse,
44{
45 type Response = <TransformResponse<'a, R, E> as TryFuture>::Ok;
46 type Error = E;
47 type Future = TransformResponse<'a, R, Self::Error>;
48
49 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
50 self.service.poll_ready(cx)
51 }
52
53 fn call(&mut self, req: LambdaEvent<serde_json::Value>) -> Self::Future {
54 let original_request = req.payload.clone();
56
57 let lambda_request: LambdaRequest =
58 serde_json::from_value(req.payload).expect("invalid payload when deserializing to json");
59 let request_origin = lambda_request.request_origin();
60 let mut event: Request = lambda_request.into();
61
62 strip_stage_from_path(&mut event);
63
64 debug!(original_request = ?original_request, "original_request");
66 event.extensions_mut().insert(original_request);
67
68 let fut = Box::pin(self.service.call(event.with_lambda_context(req.context)));
69 TransformResponse::Request(request_origin, fut)
70 }
71}
72
73fn strip_stage_from_path(event: &mut Request) {
75 let stage = match event.request_context_ref() {
76 Some(RequestContext::ApiGatewayV1(context)) => context.stage.as_deref(),
77 Some(RequestContext::ApiGatewayV2(context)) => context.stage.as_deref(),
78 _ => None,
79 };
80
81 let Some(stage) = stage.filter(|stage| *stage != "$default") else {
82 return;
83 };
84
85 let stripped = match event.uri().path().strip_prefix(&format!("/{stage}")) {
86 Some("") => "/",
87 Some(rest) if rest.starts_with('/') => rest,
88 _ => return,
89 };
90
91 let path_and_query = match event.uri().query() {
92 Some(query) => format!("{stripped}?{query}"),
93 None => stripped.to_string(),
94 };
95
96 let mut parts = event.uri().clone().into_parts();
97 let path_and_query = path_and_query
98 .parse()
99 .expect("expected valid path and query from a valid uri");
100 parts.path_and_query = Some(path_and_query);
101 *event.uri_mut() = Uri::from_parts(parts).expect("expected valid parts from a valid uri");
102}
103
104pub async fn run_handler(path: &Path) -> Result<(), Error> {
106 let mut config = Config::from_path(path)?;
107 config.set_package_info(package_info!())?;
108 config.setup_tracing()?;
109
110 debug!(config = ?config, "config parsed");
111
112 let service_info = config.service_info().clone();
113 let cors = config.ticket_server().cors().clone();
114 let auth = config.ticket_server().auth().cloned();
115 let package_info = config.package_info().clone();
116 let router = TicketServer::router(
117 config.into_locations(),
118 service_info,
119 cors,
120 auth,
121 Some(package_info),
122 )?;
123
124 lambda_runtime::run(Adapter::from(router)).await
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use aws_lambda_events::apigw::{ApiGatewayProxyRequestContext, ApiGatewayV2httpRequestContext};
131 use lambda_http::Body;
132
133 fn v1_request(uri: &str, stage: Option<&str>) -> Request {
134 let mut context = ApiGatewayProxyRequestContext::default();
135 context.stage = stage.map(|stage| stage.to_string());
136 request(uri).with_request_context(RequestContext::ApiGatewayV1(context))
137 }
138
139 fn v2_request(uri: &str, stage: Option<&str>) -> Request {
140 let mut context = ApiGatewayV2httpRequestContext::default();
141 context.stage = stage.map(|stage| stage.to_string());
142 request(uri).with_request_context(RequestContext::ApiGatewayV2(context))
143 }
144
145 fn request(uri: &str) -> Request {
146 lambda_http::http::Request::builder()
147 .uri(uri)
148 .body(Body::Empty)
149 .unwrap()
150 }
151
152 fn strip(mut event: Request) -> String {
153 strip_stage_from_path(&mut event);
154 event.uri().to_string()
155 }
156
157 #[test]
158 fn strip_stage() {
159 assert_eq!(
160 strip(v1_request("/prod/reads/id", Some("prod"))),
161 "/reads/id"
162 );
163 assert_eq!(
164 strip(v2_request("/prod/reads/id", Some("prod"))),
165 "/reads/id"
166 );
167
168 assert_eq!(
169 strip(v1_request("/prod/reads/id?format=BAM", Some("prod"))),
170 "/reads/id?format=BAM"
171 );
172 assert_eq!(strip(v1_request("/prod", Some("prod"))), "/");
173 assert_eq!(strip(request("/prod/reads/id")), "/prod/reads/id");
174 assert_eq!(
175 strip(v1_request("/reads/id", Some("$default"))),
176 "/reads/id"
177 );
178 assert_eq!(strip(v1_request("/reads/id", None)), "/reads/id");
179 assert_eq!(
180 strip(v1_request("/production/reads/id", Some("prod"))),
181 "/production/reads/id"
182 );
183 assert_eq!(strip(v1_request("/reads/id", Some("prod"))), "/reads/id");
184 }
185}