1#![doc = include_str!("../README.proj.md")]
2#![cfg(engine)] #![deny(missing_docs)]
11#![deny(missing_debug_implementations)]
12
13use axum::{
14 body::Body,
15 extract::{Path, Query},
16 http::{Request, StatusCode},
17 response::{IntoResponse, Response},
18 routing::{get, get_service},
19 Router,
20};
21use perseus::turbine::ApiResponse as PerseusApiResponse;
22use perseus::{
23 i18n::TranslationsManager,
24 path::*,
25 server::ServerOptions,
26 stores::MutableStore,
27 turbine::{SubsequentLoadQueryParams, Turbine},
28};
29use tower_http::services::{ServeDir, ServeFile};
30
31#[derive(Debug)]
39struct ApiResponse(PerseusApiResponse);
40impl From<PerseusApiResponse> for ApiResponse {
41 fn from(val: PerseusApiResponse) -> Self {
42 Self(val)
43 }
44}
45impl IntoResponse for ApiResponse {
46 fn into_response(self) -> Response {
47 (self.0.status, self.0.headers, self.0.body).into_response()
49 }
50}
51
52pub async fn get_router<M: MutableStore + 'static, T: TranslationsManager + 'static>(
57 turbine: &'static Turbine<M, T>,
58 opts: ServerOptions,
59) -> Router {
60 let router = Router::new()
61 .route(
63 "/.perseus/bundle.js",
64 get_service(ServeFile::new(opts.js_bundle.clone())).handle_error(handle_fs_error),
65 )
66 .route(
67 "/.perseus/bundle.wasm",
68 get_service(ServeFile::new(opts.wasm_bundle.clone())).handle_error(handle_fs_error),
69 )
70 .route(
71 "/.perseus/bundle.wasm.js",
72 get_service(ServeFile::new(opts.wasm_js_bundle.clone())).handle_error(handle_fs_error),
73 )
74 .nest_service(
75 "/.perseus/snippets",
76 get_service(ServeDir::new(opts.snippets)).handle_error(handle_fs_error),
77 );
78 let mut router = router
80 .route(
81 "/.perseus/translations/:locale",
82 get(move |Path(locale): Path<String>| async move {
83 ApiResponse(turbine.get_translations(&locale).await)
84 }),
85 )
86 .route(
87 "/.perseus/page/:locale/*tail",
88 get(
89 move |Path(path_parts): Path<Vec<String>>,
90 Query(SubsequentLoadQueryParams {
91 entity_name,
92 was_incremental_match,
93 }): Query<SubsequentLoadQueryParams>,
94 http_req: Request<Body>| async move {
95 let locale = &path_parts[0];
97 let raw_path = path_parts[1..]
98 .iter()
99 .map(|x| x.as_str())
100 .collect::<Vec<&str>>()
101 .join("/");
102 let req = Request::from_parts(http_req.into_parts().0, ());
104
105 ApiResponse(
106 turbine
107 .get_subsequent_load(
108 PathWithoutLocale(raw_path),
109 locale.to_string(),
110 entity_name,
111 was_incremental_match,
112 req,
113 )
114 .await,
115 )
116 },
117 ),
118 );
119 if turbine.static_dir.exists() {
121 router = router.nest_service(
122 "/.perseus/static",
123 get_service(ServeDir::new(&turbine.static_dir)).handle_error(handle_fs_error),
124 )
125 }
126 for (url, static_path) in turbine.static_aliases.iter() {
127 router = router.route(
128 url, get_service(ServeFile::new(static_path)).handle_error(handle_fs_error),
130 );
131 }
132 router.fallback_service(get(move |http_req: Request<Body>| async move {
134 let path = http_req.uri().path().to_string();
137 let http_req = Request::from_parts(http_req.into_parts().0, ());
138
139 ApiResponse(
140 turbine
141 .get_initial_load(PathMaybeWithLocale(path), http_req)
142 .await,
143 )
144 }))
145}
146
147async fn handle_fs_error(_err: std::io::Error) -> impl IntoResponse {
149 dbg!("Error!");
150 (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't serve file.")
151}
152
153#[cfg(feature = "dflt-server")]
159pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
160 turbine: &'static Turbine<M, T>,
161 opts: ServerOptions,
162 (host, port): (String, u16),
163) {
164 use std::net::SocketAddr;
165
166 let addr: SocketAddr = format!("{}:{}", host, port)
167 .parse()
168 .expect("Invalid address provided to bind to.");
169 let app = get_router(turbine, opts).await;
170 axum::Server::bind(&addr)
171 .serve(app.into_make_service())
172 .await
173 .unwrap();
174}