#![doc = include_str!("../README.proj.md")]
#![cfg(engine)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
use std::{io::Cursor, path::Path};
use perseus::{
i18n::TranslationsManager,
path::PathMaybeWithLocale,
server::ServerOptions,
stores::MutableStore,
turbine::{ApiResponse as PerseusApiResponse, Turbine},
};
use rocket::{
fs::{FileServer, NamedFile},
get,
http::{Method, Status},
response::Responder,
route::{Handler, Outcome},
routes,
tokio::fs::File,
Build, Data, Request, Response, Rocket, Route, State,
};
#[derive(Debug)]
struct ApiResponse(PerseusApiResponse);
impl From<PerseusApiResponse> for ApiResponse {
fn from(val: PerseusApiResponse) -> Self {
Self(val)
}
}
impl<'r> Responder<'r, 'static> for ApiResponse {
fn respond_to(self, _request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> {
let mut resp_build = Response::build();
resp_build
.status(rocket::http::Status {
code: self.0.status.into(),
})
.sized_body(self.0.body.len(), Cursor::new(self.0.body));
for h in self.0.headers.iter() {
if let Ok(value) = h.1.to_str() {
resp_build.raw_header(h.0.to_string(), value.to_string());
}
}
resp_build.ok()
}
}
#[get("/bundle.js")]
async fn get_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.js_bundle).await
}
#[get("/bundle.wasm")]
async fn get_wasm_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_bundle).await
}
#[get("/bundle.wasm.js")]
async fn get_wasm_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_js_bundle).await
}
async fn perseus_locale<'r, M, T>(req: &'r Request<'_>, turbine: &Turbine<M, T>) -> Outcome<'r>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
match req.routed_segment(1) {
Some(locale) => Outcome::from(req, ApiResponse(turbine.get_translations(locale).await)),
_ => Outcome::Failure(Status::BadRequest),
}
}
async fn perseus_initial_load_handler<'r, M, T>(
req: &'r Request<'_>,
turbine: &Turbine<M, T>,
) -> Outcome<'r>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
let path = req.uri().path().to_string();
let mut http_req = rocket::http::hyper::Request::builder();
http_req = http_req.method("GET");
for h in req.headers().iter() {
http_req = http_req.header(h.name.to_string(), h.value.to_string());
}
match http_req.body(()) {
Ok(r) => Outcome::from(
req,
ApiResponse(turbine.get_initial_load(PathMaybeWithLocale(path), r).await),
),
_ => Outcome::Failure(Status::BadRequest),
}
}
async fn perseus_subsequent_load_handler<'r, M, T>(
req: &'r Request<'_>,
turbine: &Turbine<M, T>,
) -> Outcome<'r>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
let locale_opt = req.routed_segment(1);
let entity_name_opt = req
.query_value::<&str>("entity_name")
.and_then(|res| res.ok());
let was_incremental_match_opt = req
.query_value::<bool>("was_incremental_match")
.and_then(|res| res.ok());
let (locale, entity_name, was_incremental_match) =
match (locale_opt, entity_name_opt, was_incremental_match_opt) {
(Some(l), Some(e), Some(w)) => (l.to_string(), e.to_string(), w),
_ => return Outcome::Failure(Status::BadRequest),
};
let raw_path = req.routed_segments(2..).collect::<Vec<&str>>().join("/");
let mut http_req = rocket::http::hyper::Request::builder();
http_req = http_req.method("GET");
for h in req.headers().iter() {
http_req = http_req.header(h.name.to_string(), h.value.to_string());
}
match http_req.body(()) {
Ok(r) => Outcome::from(
req,
ApiResponse(
turbine
.get_subsequent_load(
perseus::path::PathWithoutLocale(raw_path),
locale,
entity_name,
was_incremental_match,
r,
)
.await,
),
),
_ => Outcome::Failure(Status::BadRequest),
}
}
#[derive(Clone)]
enum PerseusRouteKind<'a> {
Locale,
StaticAlias(&'a String),
IntialLoadHandler,
SubsequentLoadHandler,
}
#[derive(Clone)]
struct RocketHandlerWithTurbine<'a, M, T>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
turbine: &'a Turbine<M, T>,
perseus_route: PerseusRouteKind<'a>,
}
#[rocket::async_trait]
impl<M, T> Handler for RocketHandlerWithTurbine<'static, M, T>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
async fn handle<'r>(&self, req: &'r Request<'_>, _data: Data<'r>) -> Outcome<'r> {
match self.perseus_route {
PerseusRouteKind::Locale => perseus_locale(req, self.turbine).await,
PerseusRouteKind::StaticAlias(static_alias) => {
perseus_static_alias(req, static_alias).await
}
PerseusRouteKind::IntialLoadHandler => {
perseus_initial_load_handler(req, self.turbine).await
}
PerseusRouteKind::SubsequentLoadHandler => {
perseus_subsequent_load_handler(req, self.turbine).await
}
}
}
}
async fn perseus_static_alias<'r>(req: &'r Request<'_>, static_alias: &String) -> Outcome<'r> {
match File::open(static_alias).await {
Ok(file) => Outcome::from(req, file),
_ => Outcome::Failure(Status::NotFound),
}
}
pub async fn perseus_base_app<M, T>(
turbine: &'static Turbine<M, T>,
opts: ServerOptions,
) -> Rocket<Build>
where
M: MutableStore + 'static,
T: TranslationsManager + 'static,
{
let get_locale = Route::new(
Method::Get,
"/translations/<path..>",
RocketHandlerWithTurbine {
turbine,
perseus_route: PerseusRouteKind::Locale,
},
);
let get_initial_load_handler = Route::ranked(
100,
Method::Get,
"/<path..>",
RocketHandlerWithTurbine {
turbine,
perseus_route: PerseusRouteKind::IntialLoadHandler,
},
);
let get_subsequent_load_handler = Route::new(
Method::Get,
"/page/<path..>",
RocketHandlerWithTurbine {
turbine,
perseus_route: PerseusRouteKind::SubsequentLoadHandler,
},
);
let mut perseus_routes: Vec<Route> =
routes![get_js_bundle, get_wasm_js_bundle, get_wasm_bundle];
perseus_routes.append(&mut vec![get_locale, get_subsequent_load_handler]);
let mut app = rocket::build()
.manage(opts.clone())
.mount("/.perseus/", perseus_routes)
.mount("/", vec![get_initial_load_handler]);
if Path::new(&opts.snippets).exists() {
app = app.mount("/.perseus/snippets", FileServer::from(opts.snippets))
}
if turbine.static_dir.exists() {
app = app.mount("/.perseus/static", FileServer::from(&turbine.static_dir))
}
let mut static_aliases: Vec<Route> = vec![];
for (url, static_path) in turbine.static_aliases.iter() {
let route = Route::new(
Method::Get,
url,
RocketHandlerWithTurbine {
turbine,
perseus_route: PerseusRouteKind::StaticAlias(static_path),
},
);
static_aliases.push(route)
}
app = app.mount("/", static_aliases);
app
}
#[cfg(feature = "dflt-server")]
pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
turbine: &'static Turbine<M, T>,
opts: ServerOptions,
(host, port): (String, u16),
) {
let addr = host.parse().expect("Invalid address provided to bind to.");
let mut app = perseus_base_app(turbine, opts).await;
let config = rocket::Config {
port,
address: addr,
..Default::default()
};
app = app.configure(config);
if let Err(err) = app.launch().await {
eprintln!("Error lauching Rocket app: {}.", err);
}
}