This commit is contained in:
2023-12-03 15:46:05 +01:00
parent a5e0857285
commit 2b133a9f3f
13 changed files with 779 additions and 152 deletions

View File

@@ -4,6 +4,7 @@ use ovlach_tera::entity::lang_entity;
use rocket::{*, fairing::AdHoc};
use rocket_dyn_templates::Template;
use ::serde::Deserialize;
use tools::tera::advanced_filter;
pub mod routes;
pub mod services;
@@ -21,13 +22,19 @@ pub struct CVBackendConfig {
cv_backend_path: String,
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct DefaultPerson {
default_person_name: String,
}
pub fn rocket_builder() -> Rocket<Build> {
pub fn rocket_builder() -> Rocket<Build> {
let rocket = rocket::build();
//let figment = rocket.figment();
// extract the entire config any `Deserialize` value
//let config: PresentationConfig = figment.extract().expect("config");
rocket.attach(
Template::try_custom(|engines| {
engines.tera.register_filter("static", static_filter);
@@ -39,6 +46,7 @@ pub fn rocket_builder() -> Rocket<Build> {
engines.tera.register_filter("lang_entity", lang_entity);
engines.tera.register_filter("format_date", get_year); // deprecated
engines.tera.register_filter("get_year", get_year);
engines.tera.register_filter("advanced_filter", advanced_filter);
Ok(())
})
).attach(
@@ -46,9 +54,11 @@ pub fn rocket_builder() -> Rocket<Build> {
).attach(
AdHoc::config::<CVBackendConfig>()
).attach(
tools::rocket::RequestTimer
AdHoc::config::<DefaultPerson>()
).attach(
nanobyte_opentelemetry::rocket::TracingFairing::ignite()
).mount("/", routes![
routes::root::index,
routes::root::index_without_lang
])
}
}

View File

@@ -1,8 +1,14 @@
use nanobyte_opentelemetry::{install_panic_handler, default_filter_layer, LogLevel};
use ovlach_frontend::rocket_builder;
use rocket::launch;
#[launch]
fn rocket() -> _ {
rocket_builder()
}
#[rocket::main]
async fn main() {
install_panic_handler();
let _opentelemetry = nanobyte_opentelemetry::init_telemetry(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
&std::env::var("OTLP_ENDPOINT").unwrap_or("".to_string()),
Some(default_filter_layer(LogLevel::DebugWithoutRs))
);
let _ = rocket_builder().launch().await;
}

View File

@@ -1,35 +1,45 @@
use std::{thread::sleep, time::Duration};
use log::error;
use ovlach_data::cv::cv::CV;
use rocket::{get, State, response::Redirect, http::Status};
use nanobyte_opentelemetry::rocket::{OtelReqwestClient, TracingSpan};
use nanobyte_tera::l18n::LanguageDescription;
use ovlach_data::cv::data::CV;
use rocket::{get, State, response::Redirect, http::Status, futures::executor::enter};
use rocket_dyn_templates::Template;
use serde::Serialize;
use crate::{PresentationConfig, services::cv::fetch_cv_data_from_backend, CVBackendConfig, tools::rocket::RequestLanguage};
use crate::{PresentationConfig, services::cv::fetch_cv_data_from_backend, CVBackendConfig, tools::rocket::RequestLanguage, DefaultPerson};
#[derive(Serialize, Debug)]
struct RootPage {
static_host: String,
cv: CV,
download_cv_url: String,
lang: String,
lang: LanguageDescription,
}
#[get("/<language>")]
pub async fn index(presentation_config: &State<PresentationConfig>, cv_config: &State<CVBackendConfig>, language: RequestLanguage) -> Result<Template, Status> {
let context = match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone()).await {
pub async fn index(presentation_config: &State<PresentationConfig>, cv_config: &State<CVBackendConfig>, language: RequestLanguage, client: OtelReqwestClient,
default_person: &State<DefaultPerson>, span: TracingSpan) -> Result<Template, Status> {
let span = span.0.enter();
let context = match fetch_cv_data_from_backend(&cv_config.cv_backend_path, &default_person.default_person_name, &client.0).await {
Ok(cv) => RootPage {
static_host: presentation_config.static_route.clone(),
cv,
download_cv_url: "FIXME!".to_string(),
lang: language.language,
lang: LanguageDescription::new(&language.language.as_str(), "ovlach_frontend"),
},
Err(e) => {
error!("Can't fetch CV data from backend {:?}", e);
drop(span);
return Err(Status::InternalServerError)
}
};
Ok(Template::render("default", &context))
let result = Ok(Template::render("default", &context));
drop(span);
result
}

View File

@@ -1,4 +1,6 @@
use ovlach_data::cv::cv::CV;
use ovlach_data::cv::data::CV;
use reqwest::Client;
use tracing::{debug, instrument};
#[derive(Debug)]
@@ -12,10 +14,14 @@ impl From<reqwest::Error> for FetchError {
}
}
pub async fn fetch_cv_data_from_backend(backend_host: String) -> Result<CV, FetchError> {
let resp = reqwest::get(format!("{}/{}", backend_host, "/api/cv/ovlach"))
#[instrument]
pub async fn fetch_cv_data_from_backend(backend_host: &String, person_name: &String, client: &Client) -> Result<CV, FetchError> {
let url = format!("{}/{}/{}", backend_host, "/api/v1/cv", person_name);
debug!("Fetching CV data from backend: {}", url);
let resp = client
.get(url).send()
.await?
.json::<CV>()
.await?;
Ok(resp)
}
}

View File

@@ -1,2 +1,3 @@
pub mod rocket;
pub mod lang;
pub(crate) mod rocket;
pub(crate) mod lang;
pub(crate) mod tera;

View File

@@ -27,57 +27,3 @@ impl<'r> FromParam<'r> for RequestLanguage {
}
}
}
/// Fairing for timing requests.
pub struct RequestTimer;
/// Value stored in request-local state.
#[derive(Copy, Clone)]
struct TimerStart(Option<SystemTime>);
#[rocket::async_trait]
impl Fairing for RequestTimer {
fn info(&self) -> Info {
Info {
name: "Request Timer",
kind: Kind::Request | Kind::Response
}
}
/// Stores the start time of the request in request-local state.
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
// Store a `TimerStart` instead of directly storing a `SystemTime`
// to ensure that this usage doesn't conflict with anything else
// that might store a `SystemTime` in request-local cache.
request.local_cache(|| TimerStart(Some(SystemTime::now())));
}
/// Adds a header to the response indicating how long the server took to
/// process the request.
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
let start_time = req.local_cache(|| TimerStart(None));
if let Some(Ok(duration)) = start_time.0.map(|st| st.elapsed()) {
let ms = duration.as_secs() * 1000 + duration.subsec_millis() as u64;
res.set_raw_header("X-Response-Time", format!("{} ms", ms));
info!("Response time: {} ms", ms);
}
}
}
/// Request guard used to retrieve the start time of a request.
#[derive(Copy, Clone)]
pub struct StartTime(pub SystemTime);
// Allows a route to access the time a request was initiated.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for StartTime {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, ()> {
match *request.local_cache(|| TimerStart(None)) {
TimerStart(Some(time)) => request::Outcome::Success(StartTime(time)),
TimerStart(None) => request::Outcome::Error((Status::InternalServerError, ())),
}
}
}

116
src/tools/tera.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::collections::HashMap;
use rocket_dyn_templates::tera::{try_get_value, Error, Value, to_value};
/// If the `value` is not passed, optionally discard all elements where the attribute is null. (include_null -> all, none, only)
pub fn advanced_filter(value: &Value, args: &HashMap<String, Value>) -> Result<Value, Error> {
let mut arr = try_get_value!("filter", "value", Vec<Value>, value);
if arr.is_empty() {
return Ok(arr.into());
}
let key = match args.get("attribute") {
Some(val) => try_get_value!("filter", "attribute", String, val),
None => return Err(Error::msg("The `filter` filter has to have an `attribute` argument")),
};
let null = match args.get("include_null") {
Some(val) => try_get_value!("filter", "attribute", String, val),
None => "none".to_string(),
};
let against_value = match args.get("value") {
Some(val) => Some(try_get_value!("filter", "value", String, val)),
None => None
};
if null == "only" && against_value.is_some() {
return Err(Error::msg("The `filter` filter cannot have both `include_null=only` and `value`"))
}
arr = arr
.into_iter()
.filter(|v| {
let tested_value = v.get(key.clone()).unwrap_or(&Value::Null);
if tested_value.is_null() {
return match null.as_str() {
"all" => true,
"none" => false,
"only" => true,
_ => false,
}
} else {
if null != "only" {
let val = tested_value.as_str();
match val {
Some(match_v) => {
match against_value.clone() {
Some(against_v) => match_v == against_v,
None => true,
}
}
None => true,
}
} else {
false
}
}
})
.collect::<Vec<_>>();
Ok(to_value(arr).unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
fn call_advanced_filter(data: &Vec<TestClass>, include_null: &str, value: Option<&str>) -> Result<Value, Error> {
let mut args = HashMap::new();
args.insert("attribute".to_string(), json!("against_value"));
args.insert("include_null".to_string(), json!(include_null));
if let Some(val) = value {
args.insert("value".to_string(), json!(val));
}
advanced_filter(&json!(data), &args)
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct TestClass {
against_value: Option<String>,
}
impl TestClass {
pub fn new(against_value: Option<String>) -> Self {
TestClass { against_value }
}
}
fn generate_test_class(first_data: Option<String>, second_data: Option<String>) -> Vec<TestClass> {
vec![TestClass::new(first_data), TestClass::new(second_data)]
}
#[test]
fn test_advanced_filter() {
let data = vec![TestClass::new(Some("foo".to_string())), TestClass::new(None)];
let result = call_advanced_filter(&data, "all", None).unwrap();
assert_eq!(result, json!(generate_test_class(Some("foo".to_string()), None)));
let result = call_advanced_filter(&data, "none", None).unwrap();
assert_eq!(result, json!(vec![TestClass::new(Some("foo".to_string()))]));
let mut vec: Vec<TestClass> = Vec::new();
vec.push(TestClass::new(None));
let result = call_advanced_filter(&data, "only", None).unwrap();
assert_eq!(result, json!(vec![TestClass::new(None)]));
// Test filtering strings
let result = call_advanced_filter(&data, "all", Some("foo")).unwrap();
assert_eq!(result, json!(vec![TestClass::new(Some("foo".to_string())), TestClass::new(None)]));
let result = call_advanced_filter(&data, "none", Some("bar")).unwrap();
let vec: Vec<TestClass> = Vec::new();
assert_eq!(result, json!(vec));
let result = call_advanced_filter(&data, "only", Some("zz")).is_err();
assert_eq!(result, true);
}
}