1#![doc = include_str!("../README.proj.md")]
2#![cfg(engine)] #![deny(missing_docs)]
11#![deny(missing_debug_implementations)]
12
13use actix_files::{Files, NamedFile};
14use actix_web::{web, HttpRequest, HttpResponse, Responder};
15use perseus::turbine::ApiResponse as PerseusApiResponse;
16use perseus::{
17 http::StatusCode,
18 i18n::TranslationsManager,
19 path::*,
20 server::ServerOptions,
21 stores::MutableStore,
22 turbine::{SubsequentLoadQueryParams, Turbine},
23 Request,
24};
25
26pub fn convert_req(raw: &actix_web::HttpRequest) -> Result<Request, String> {
30 let mut builder = Request::builder();
31
32 for (name, val) in raw.headers() {
33 builder = builder.header(name, val);
34 }
35
36 builder
37 .uri(raw.uri())
38 .method(raw.method())
39 .version(raw.version())
40 .body(())
44 .map_err(|err| err.to_string())
45}
46
47#[derive(Debug)]
50struct ApiResponse(PerseusApiResponse);
51impl From<PerseusApiResponse> for ApiResponse {
52 fn from(val: PerseusApiResponse) -> Self {
53 Self(val)
54 }
55}
56impl Responder for ApiResponse {
57 type Body = String;
58 fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
59 let mut res = HttpResponse::build(self.0.status);
60 for header in self.0.headers {
61 res.insert_header((header.0.unwrap(), header.1));
64 }
65 res.message_body(self.0.body).unwrap()
67 }
68}
69
70pub async fn configurer<M: MutableStore + 'static, T: TranslationsManager + 'static>(
77 turbine: &'static Turbine<M, T>,
78 opts: ServerOptions,
79) -> impl FnOnce(&mut actix_web::web::ServiceConfig) {
80 move |cfg: &mut web::ServiceConfig| {
81 let snippets_dir = opts.snippets.clone();
82 cfg
83 .app_data(web::Data::new(opts))
84 .route("/.perseus/bundle.js", web::get().to(js_bundle))
86 .route("/.perseus/bundle.wasm", web::get().to(wasm_bundle))
87 .route("/.perseus/bundle.wasm.js", web::get().to(wasm_js_bundle))
88 .service(Files::new("/.perseus/snippets", &snippets_dir))
89 .route(
91 "/.perseus/translations/{locale}",
92 web::get().to(move |http_req: HttpRequest| async move {
93 let locale = http_req.match_info().query("locale");
94 ApiResponse(turbine.get_translations(locale).await)
95 }),
96 )
97 .route(
98 "/.perseus/page/{locale}/{filename:.*}",
100 web::get().to(move |http_req: HttpRequest, web::Query(query_params): web::Query<SubsequentLoadQueryParams>| async move {
101 let raw_path = http_req.match_info().query("filename").to_string();
102 let locale = http_req.match_info().query("locale");
103 let SubsequentLoadQueryParams { entity_name, was_incremental_match } = query_params;
104 let http_req = match convert_req(&http_req) {
105 Ok(req) => req,
106 Err(err) => return ApiResponse(PerseusApiResponse::err(StatusCode::BAD_REQUEST, &err))
107 };
108
109 ApiResponse(turbine.get_subsequent_load(
110 PathWithoutLocale(raw_path),
111 locale.to_string(),
112 entity_name,
113 was_incremental_match,
114 http_req
115 ).await)
116 }),
117 );
118 if turbine.static_dir.exists() {
120 cfg.service(Files::new("/.perseus/static", &turbine.static_dir));
121 }
122 for url in turbine.static_aliases.keys() {
123 cfg.route(
124 url,
125 web::get().to(|req| async { static_alias(turbine, req).await }),
126 );
127 }
128 cfg.route(
130 "{route:.*}",
131 web::get().to(move |http_req: HttpRequest| async move {
132 let raw_path = http_req.path().to_string();
133 let http_req = match convert_req(&http_req) {
134 Ok(req) => req,
135 Err(err) => {
136 return ApiResponse(PerseusApiResponse::err(StatusCode::BAD_REQUEST, &err))
137 }
138 };
139 ApiResponse(
140 turbine
141 .get_initial_load(PathMaybeWithLocale(raw_path), http_req)
142 .await,
143 )
144 }),
145 );
146 }
147}
148
149async fn js_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
151 NamedFile::open(&opts.js_bundle)
152}
153async fn wasm_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
154 NamedFile::open(&opts.wasm_bundle)
155}
156async fn wasm_js_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
157 NamedFile::open(&opts.wasm_js_bundle)
158}
159async fn static_alias<M: MutableStore, T: TranslationsManager>(
160 turbine: &'static Turbine<M, T>,
161 req: HttpRequest,
162) -> std::io::Result<NamedFile> {
163 let filename = turbine.static_aliases.get(req.path());
164 let filename = match filename {
165 Some(filename) => filename,
166 None => return Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
168 };
169 NamedFile::open(filename)
170}
171
172#[cfg(feature = "dflt-server")]
179pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
180 turbine: &'static Turbine<M, T>,
181 opts: ServerOptions,
182 (host, port): (String, u16),
183) {
184 use actix_web::{App, HttpServer};
185 use futures::executor::block_on;
186 HttpServer::new(move ||
188 App::new()
189 .configure(
190 block_on(
191 configurer(
192 turbine,
193 opts.clone(),
194 )
195 )
196 )
197 )
198 .bind((host, port))
199 .expect("Couldn't bind to given address. Maybe something is already running on the selected port?")
200 .run()
201 .await
202 .expect("Server failed.") }