1#![doc = include_str!("../README.proj.md")]
2
3#![cfg(engine)]
11#![deny(missing_docs)]
12#![deny(missing_debug_implementations)]
13
14use std::{io::Cursor, path::Path};
15
16use perseus::{
17 i18n::TranslationsManager,
18 path::PathMaybeWithLocale,
19 server::ServerOptions,
20 stores::MutableStore,
21 turbine::{ApiResponse as PerseusApiResponse, Turbine},
22};
23use rocket::{
24 fs::{FileServer, NamedFile},
25 get,
26 http::{Method, Status},
27 response::Responder,
28 route::{Handler, Outcome},
29 routes,
30 tokio::fs::File,
31 Build, Data, Request, Response, Rocket, Route, State,
32};
33
34#[derive(Debug)]
37struct ApiResponse(PerseusApiResponse);
38impl From<PerseusApiResponse> for ApiResponse {
39 fn from(val: PerseusApiResponse) -> Self {
40 Self(val)
41 }
42}
43impl<'r> Responder<'r, 'static> for ApiResponse {
44 fn respond_to(self, _request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> {
45 let mut resp_build = Response::build();
46 resp_build
47 .status(rocket::http::Status {
48 code: self.0.status.into(),
49 })
50 .sized_body(self.0.body.len(), Cursor::new(self.0.body));
51
52 for h in self.0.headers.iter() {
53 if let Ok(value) = h.1.to_str() {
56 resp_build.raw_header(h.0.to_string(), value.to_string());
57 }
58 }
59
60 resp_build.ok()
61 }
62}
63
64#[get("/bundle.js")]
67async fn get_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
68 NamedFile::open(&opts.js_bundle).await
69}
70
71#[get("/bundle.wasm")]
72async fn get_wasm_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
73 NamedFile::open(&opts.wasm_bundle).await
74}
75
76#[get("/bundle.wasm.js")]
77async fn get_wasm_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
78 NamedFile::open(&opts.wasm_js_bundle).await
79}
80
81async fn perseus_locale<'r, M, T>(req: &'r Request<'_>, turbine: &Turbine<M, T>) -> Outcome<'r>
84where
85 M: MutableStore + 'static,
86 T: TranslationsManager + 'static,
87{
88 match req.routed_segment(1) {
89 Some(locale) => Outcome::from(req, ApiResponse(turbine.get_translations(locale).await)),
90 _ => Outcome::Failure(Status::BadRequest),
91 }
92}
93
94async fn perseus_initial_load_handler<'r, M, T>(
95 req: &'r Request<'_>,
96 turbine: &Turbine<M, T>,
97) -> Outcome<'r>
98where
99 M: MutableStore + 'static,
100 T: TranslationsManager + 'static,
101{
102 let path = req.uri().path().to_string();
105
106 let mut http_req = rocket::http::hyper::Request::builder();
107 http_req = http_req.method("GET");
108 for h in req.headers().iter() {
109 http_req = http_req.header(h.name.to_string(), h.value.to_string());
110 }
111
112 match http_req.body(()) {
113 Ok(r) => Outcome::from(
114 req,
115 ApiResponse(turbine.get_initial_load(PathMaybeWithLocale(path), r).await),
116 ),
117 _ => Outcome::Failure(Status::BadRequest),
118 }
119}
120
121async fn perseus_subsequent_load_handler<'r, M, T>(
122 req: &'r Request<'_>,
123 turbine: &Turbine<M, T>,
124) -> Outcome<'r>
125where
126 M: MutableStore + 'static,
127 T: TranslationsManager + 'static,
128{
129 let locale_opt = req.routed_segment(1);
130 let entity_name_opt = req
131 .query_value::<&str>("entity_name")
132 .and_then(|res| res.ok());
133 let was_incremental_match_opt = req
134 .query_value::<bool>("was_incremental_match")
135 .and_then(|res| res.ok());
136
137 let (locale, entity_name, was_incremental_match) =
138 match (locale_opt, entity_name_opt, was_incremental_match_opt) {
139 (Some(l), Some(e), Some(w)) => (l.to_string(), e.to_string(), w),
140 _ => return Outcome::Failure(Status::BadRequest),
141 };
142
143 let raw_path = req.routed_segments(2..).collect::<Vec<&str>>().join("/");
144
145 let mut http_req = rocket::http::hyper::Request::builder();
146 http_req = http_req.method("GET");
147 for h in req.headers().iter() {
148 http_req = http_req.header(h.name.to_string(), h.value.to_string());
149 }
150
151 match http_req.body(()) {
152 Ok(r) => Outcome::from(
153 req,
154 ApiResponse(
155 turbine
156 .get_subsequent_load(
157 perseus::path::PathWithoutLocale(raw_path),
158 locale,
159 entity_name,
160 was_incremental_match,
161 r,
162 )
163 .await,
164 ),
165 ),
166 _ => Outcome::Failure(Status::BadRequest),
167 }
168}
169
170#[derive(Clone)]
173enum PerseusRouteKind<'a> {
174 Locale,
175 StaticAlias(&'a String),
176 IntialLoadHandler,
177 SubsequentLoadHandler,
178}
179
180#[derive(Clone)]
181struct RocketHandlerWithTurbine<'a, M, T>
182where
183 M: MutableStore + 'static,
184 T: TranslationsManager + 'static,
185{
186 turbine: &'a Turbine<M, T>,
187 perseus_route: PerseusRouteKind<'a>,
188}
189
190#[rocket::async_trait]
191impl<M, T> Handler for RocketHandlerWithTurbine<'static, M, T>
192where
193 M: MutableStore + 'static,
194 T: TranslationsManager + 'static,
195{
196 async fn handle<'r>(&self, req: &'r Request<'_>, _data: Data<'r>) -> Outcome<'r> {
197 match self.perseus_route {
198 PerseusRouteKind::Locale => perseus_locale(req, self.turbine).await,
199 PerseusRouteKind::StaticAlias(static_alias) => {
200 perseus_static_alias(req, static_alias).await
201 }
202 PerseusRouteKind::IntialLoadHandler => {
203 perseus_initial_load_handler(req, self.turbine).await
204 }
205 PerseusRouteKind::SubsequentLoadHandler => {
206 perseus_subsequent_load_handler(req, self.turbine).await
207 }
208 }
209 }
210}
211
212async fn perseus_static_alias<'r>(req: &'r Request<'_>, static_alias: &String) -> Outcome<'r> {
213 match File::open(static_alias).await {
214 Ok(file) => Outcome::from(req, file),
215 _ => Outcome::Failure(Status::NotFound),
216 }
217}
218
219pub async fn perseus_base_app<M, T>(
225 turbine: &'static Turbine<M, T>,
226 opts: ServerOptions,
227) -> Rocket<Build>
228where
229 M: MutableStore + 'static,
230 T: TranslationsManager + 'static,
231{
232 let get_locale = Route::new(
233 Method::Get,
234 "/translations/<path..>",
235 RocketHandlerWithTurbine {
236 turbine,
237 perseus_route: PerseusRouteKind::Locale,
238 },
239 );
240
241 let get_initial_load_handler = Route::ranked(
245 100,
246 Method::Get,
247 "/<path..>",
248 RocketHandlerWithTurbine {
249 turbine,
250 perseus_route: PerseusRouteKind::IntialLoadHandler,
251 },
252 );
253
254 let get_subsequent_load_handler = Route::new(
255 Method::Get,
256 "/page/<path..>",
257 RocketHandlerWithTurbine {
258 turbine,
259 perseus_route: PerseusRouteKind::SubsequentLoadHandler,
260 },
261 );
262
263 let mut perseus_routes: Vec<Route> =
264 routes![get_js_bundle, get_wasm_js_bundle, get_wasm_bundle];
265 perseus_routes.append(&mut vec![get_locale, get_subsequent_load_handler]);
266
267 let mut app = rocket::build()
268 .manage(opts.clone())
269 .mount("/.perseus/", perseus_routes)
270 .mount("/", vec![get_initial_load_handler]);
271
272 if Path::new(&opts.snippets).exists() {
273 app = app.mount("/.perseus/snippets", FileServer::from(opts.snippets))
274 }
275
276 if turbine.static_dir.exists() {
277 app = app.mount("/.perseus/static", FileServer::from(&turbine.static_dir))
278 }
279
280 let mut static_aliases: Vec<Route> = vec![];
281
282 for (url, static_path) in turbine.static_aliases.iter() {
283 let route = Route::new(
284 Method::Get,
285 url,
286 RocketHandlerWithTurbine {
287 turbine,
288 perseus_route: PerseusRouteKind::StaticAlias(static_path),
289 },
290 );
291 static_aliases.push(route)
292 }
293
294 app = app.mount("/", static_aliases);
295
296 app
297}
298
299#[cfg(feature = "dflt-server")]
305pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
306 turbine: &'static Turbine<M, T>,
307 opts: ServerOptions,
308 (host, port): (String, u16),
309) {
310 let addr = host.parse().expect("Invalid address provided to bind to.");
311
312 let mut app = perseus_base_app(turbine, opts).await;
313
314 let config = rocket::Config {
315 port,
316 address: addr,
317 ..Default::default()
318 };
319 app = app.configure(config);
320
321 if let Err(err) = app.launch().await {
322 eprintln!("Error lauching Rocket app: {}.", err);
323 }
324}