diff --git a/examples/poem/tera-i18n/Cargo.toml b/examples/poem/tera-i18n/Cargo.toml new file mode 100644 index 0000000000..8372564792 --- /dev/null +++ b/examples/poem/tera-i18n/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "example-tera-i18n" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +poem = { workspace = true, features = ["tera", "i18n"] } +tokio = { workspace = true, features = ["full"] } +once_cell = "1.17.0" diff --git a/examples/poem/tera-i18n/resources/en-US/simple.ftl b/examples/poem/tera-i18n/resources/en-US/simple.ftl new file mode 100644 index 0000000000..f1b4bf0077 --- /dev/null +++ b/examples/poem/tera-i18n/resources/en-US/simple.ftl @@ -0,0 +1,2 @@ +hello-world = Hello world! +welcome = welcome { $name }! diff --git a/examples/poem/tera-i18n/resources/fr/simple.ftl b/examples/poem/tera-i18n/resources/fr/simple.ftl new file mode 100644 index 0000000000..e300799877 --- /dev/null +++ b/examples/poem/tera-i18n/resources/fr/simple.ftl @@ -0,0 +1,2 @@ +hello-world = Bonjour le monde! +welcome = Bienvenue { $name }! diff --git a/examples/poem/tera-i18n/resources/zh-CN/simple.ftl b/examples/poem/tera-i18n/resources/zh-CN/simple.ftl new file mode 100644 index 0000000000..a9074f2d61 --- /dev/null +++ b/examples/poem/tera-i18n/resources/zh-CN/simple.ftl @@ -0,0 +1,2 @@ +hello-world = 你好世界! +welcome = 欢迎 { $name }! diff --git a/examples/poem/tera-i18n/src/main.rs b/examples/poem/tera-i18n/src/main.rs new file mode 100644 index 0000000000..c6bced680c --- /dev/null +++ b/examples/poem/tera-i18n/src/main.rs @@ -0,0 +1,37 @@ +use poem::{ + ctx, get, handler, + i18n::I18NResources, + listener::TcpListener, + tera::{filters, Tera, TeraTemplate, TeraTemplating}, + web::Path, + EndpointExt, Route, Server, +}; + +#[handler] +fn index(tera: Tera) -> TeraTemplate { + tera.render("index.html.tera", &ctx! {}) +} + +#[handler] +fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate { + tera.render("hello.html.tera", &ctx! { "name": &name }) +} + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + let resources = I18NResources::builder() + .add_path("resources") + .build() + .unwrap(); + + let app = Route::new() + .at("/", get(index)) + .at("/hello/:name", get(hello)) + .with(TeraTemplating::from_glob("templates/**/*")) + .using(filters::i18n::translate) + .data(resources); + + Server::new(TcpListener::bind("127.0.0.1:3000")) + .run(app) + .await +} diff --git a/examples/poem/tera-i18n/templates/hello.html.tera b/examples/poem/tera-i18n/templates/hello.html.tera new file mode 100644 index 0000000000..5f15c30608 --- /dev/null +++ b/examples/poem/tera-i18n/templates/hello.html.tera @@ -0,0 +1 @@ +<h1>{{ "welcome" | translate(name=name) }}</h1> \ No newline at end of file diff --git a/examples/poem/tera-i18n/templates/index.html.tera b/examples/poem/tera-i18n/templates/index.html.tera new file mode 100644 index 0000000000..27a75593ea --- /dev/null +++ b/examples/poem/tera-i18n/templates/index.html.tera @@ -0,0 +1 @@ +<h1>{{ "hello-world" | translate }}</h1> \ No newline at end of file diff --git a/examples/poem/tera-templating/Cargo.toml b/examples/poem/tera-templating/Cargo.toml index 934aeeb3b1..fc992645ff 100644 --- a/examples/poem/tera-templating/Cargo.toml +++ b/examples/poem/tera-templating/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true publish.workspace = true [dependencies] -poem.workspace = true +poem = { workspace = true, features = ["tera"] } tokio = { workspace = true, features = ["full"] } -tera = "1.17.1" once_cell = "1.17.0" +tracing-subscriber.workspace = true \ No newline at end of file diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index b5ce0e3ac6..e188cf5da4 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -1,38 +1,28 @@ -use once_cell::sync::Lazy; use poem::{ - error::InternalServerError, - get, handler, + ctx, get, handler, listener::TcpListener, - web::{Html, Path}, - Route, Server, + tera::{Tera, TeraTemplate, TeraTemplating}, + web::Path, + EndpointExt, Route, Server, }; -use tera::{Context, Tera}; - -static TEMPLATES: Lazy<Tera> = Lazy::new(|| { - let mut tera = match Tera::new("templates/**/*") { - Ok(t) => t, - Err(e) => { - println!("Parsing error(s): {e}"); - ::std::process::exit(1); - } - }; - tera.autoescape_on(vec![".html", ".sql"]); - tera -}); #[handler] -fn hello(Path(name): Path<String>) -> Result<Html<String>, poem::Error> { - let mut context = Context::new(); - context.insert("name", &name); - TEMPLATES - .render("index.html.tera", &context) - .map_err(InternalServerError) - .map(Html) +fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate { + tera.render("index.html.tera", &ctx! { "name": &name }) } #[tokio::main] async fn main() -> Result<(), std::io::Error> { - let app = Route::new().at("/hello/:name", get(hello)); + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "poem=debug"); + } + tracing_subscriber::fmt::init(); + + let app = Route::new() + .at("/hello/:name", get(hello)) + .with(TeraTemplating::from_glob("templates/**/*")) + .with_live_reloading(); + Server::new(TcpListener::bind("127.0.0.1:3000")) .run(app) .await diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs index dd9a735f39..068b05ae5d 100644 --- a/poem-openapi/src/base.rs +++ b/poem-openapi/src/base.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt::{self, Debug, Display}, + marker::Send, ops::Deref, }; @@ -198,7 +199,7 @@ pub trait ApiExtractor<'a>: Sized { } #[poem::async_trait] -impl<'a, T: FromRequest<'a>> ApiExtractor<'a> for T { +impl<'a, T: FromRequest<'a> + Send> ApiExtractor<'a> for T { const TYPE: ApiExtractorType = ApiExtractorType::PoemExtractor; type ParamType = (); diff --git a/poem/Cargo.toml b/poem/Cargo.toml index c9970230a2..1429eaabed 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -67,6 +67,9 @@ acme-base = [ embed = ["rust-embed", "hex", "mime_guess"] xml = ["quick-xml"] yaml = ["serde_yaml"] +templates = [] +live_reloading = ["dep:notify"] +tera = ["dep:tera", "templates"] [dependencies] poem-derive.workspace = true @@ -161,6 +164,8 @@ hex = { version = "0.4", optional = true } quick-xml = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } tokio-stream = { workspace = true, optional = true } +tera = { version = "1.17.1", optional = true } +notify = { version = "5.1", optional = true } # Feature optional dependencies anyhow = { version = "1.0.0", optional = true } diff --git a/poem/README.md b/poem/README.md index e4d35ef65b..7978d66d03 100644 --- a/poem/README.md +++ b/poem/README.md @@ -51,7 +51,7 @@ which are disabled by default: | Feature | Description | |---------------|-------------------------------------------------------------------------------------------| -| server | Server and listener APIs (enabled by default) | | +| server | Server and listener APIs (enabled by default) | | compression | Support decompress request body and compress response body | | cookie | Support for Cookie | | csrf | Support for Cross-Site Request Forgery (CSRF) protection | @@ -76,7 +76,8 @@ which are disabled by default: | tokio-metrics | Integrate with [`tokio-metrics`](https://crates.io/crates/tokio-metrics) crate. | | embed | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. | | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. | -| yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. | +| yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. | +| tera | Support for [`tera`](https://crates.io/crates/tera) templating. | ## Safety diff --git a/poem/src/i18n/locale.rs b/poem/src/i18n/locale.rs index 584c26d77a..ffda1c40f6 100644 --- a/poem/src/i18n/locale.rs +++ b/poem/src/i18n/locale.rs @@ -86,7 +86,7 @@ impl Locale { #[async_trait::async_trait] impl<'a> FromRequest<'a> for Locale { - async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> { + fn from_request_sync(req: &'a Request, _body: &mut RequestBody) -> Result<Self> { let resources = req .extensions() .get::<I18NResources>() diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 80103cef90..e715097ac8 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -255,6 +255,7 @@ //! | embed | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. | //! | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. | //! | yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. | +//! | tera | Support for [`tera`](https://crates.io/crates/tera) templating. | #![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")] #![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")] @@ -276,6 +277,9 @@ pub mod middleware; #[cfg(feature = "session")] #[cfg_attr(docsrs, doc(cfg(feature = "session")))] pub mod session; +#[cfg(feature = "templates")] +#[cfg_attr(docsrs, doc(cfg(feature = "templates")))] +pub mod templates; #[cfg(feature = "test")] #[cfg_attr(docsrs, doc(cfg(feature = "test")))] pub mod test; diff --git a/poem/src/templates/live_reloading.rs b/poem/src/templates/live_reloading.rs new file mode 100644 index 0000000000..4291cb6ac5 --- /dev/null +++ b/poem/src/templates/live_reloading.rs @@ -0,0 +1,77 @@ +use notify::{ + Watcher as WatcherTrait, + Event, EventKind, + RecursiveMode +}; + +use std::sync::{ + Arc, atomic::{ AtomicBool, Ordering }, +}; + +pub(crate) struct Watcher { + pub(crate) needs_reload: Arc<AtomicBool>, + pub(crate) _path: String, + _watcher: Option<Arc<dyn WatcherTrait + Send + Sync + 'static>> +} + +impl Watcher { + pub(crate) fn new(path: String) -> Self { + let needs_reload = Arc::new(AtomicBool::new(false)); + let needs_reload_cloned = needs_reload.clone(); + + let watcher = notify::recommended_watcher(move |event| match event { + Ok(Event { + kind: + EventKind::Create(_) + | EventKind::Modify(_) + | EventKind::Remove(_), + .. + }) => { + needs_reload.store(true, Ordering::Relaxed); + tracing::debug!("Sent reload request"); + }, + Err(e) => { + // Ignore errors for now and just output them. + // todo: make panic? + tracing::debug!("Watcher error: {e:?}"); + }, + _ => {}, + }); + + let watcher = watcher + .map(|mut w| w + .watch(std::path::Path::new(&path), RecursiveMode::Recursive) + .map(|_| w) + ); + + let watcher = match watcher { + Ok(Ok(w)) => { + tracing::info!("Watching templates directory `{path}` for changes."); + + Some(Arc::new(w) as Arc<dyn WatcherTrait + Send + Sync>) + } + Err(e) | Ok(Err(e)) => { + tracing::error!("Failed to start watcher: {e}"); + tracing::debug!("Watcher error: {e:?}"); + + None + }, + }; + + Self { + needs_reload: needs_reload_cloned, + _path: path, + _watcher: watcher, + } + } + + pub(crate) fn needs_reload(&self) -> bool { + self.needs_reload.swap(false, Ordering::Relaxed) + } +} + +pub enum LiveReloading { + Enabled(String), + Debug(String), + Disabled +} \ No newline at end of file diff --git a/poem/src/templates/mod.rs b/poem/src/templates/mod.rs new file mode 100644 index 0000000000..287a7a9537 --- /dev/null +++ b/poem/src/templates/mod.rs @@ -0,0 +1,14 @@ +mod template; pub use template::Template; + + + +#[cfg(feature = "templates")] +#[cfg_attr(docsrs, doc(cfg(feature = "templates")))] +pub mod tera; + +#[cfg(feature = "live_reloading")] +#[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))] +mod live_reloading; + +#[cfg(feature = "live_reloading")] +pub use live_reloading::LiveReloading; \ No newline at end of file diff --git a/poem/src/templates/template.rs b/poem/src/templates/template.rs new file mode 100644 index 0000000000..078ba969bb --- /dev/null +++ b/poem/src/templates/template.rs @@ -0,0 +1,56 @@ +use crate::{ + IntoResponse, Response, + http::StatusCode, +}; + +/// An engine-agnostic template to be rendered. +/// +/// This response type requires a templating engine middleware +/// to work correctly. Missing the middleware will return +/// `500 Internal Server Error`. +/// +/// ``` +/// use poem::{ +/// templates::Template, +/// ctx, handler, +/// web::Path, +/// }; +/// +/// #[handler] +/// fn hello(Path(name): Path<String>) -> Template { +/// Template::render("index.html.tera", &ctx! { "name": &name }) +/// } +/// ``` +pub struct Template<C> { + /// Path to the template. + pub name: String, + /// Template context. This is used + /// by engines for additional data. + pub context: C, +} + +impl<C> Template<C> { + /// Renders the template. + pub fn render(name: impl Into<String>, context: C) -> Self { + Self { + name: name.into(), + context, + } + } +} + +impl<C: Send + Sync + 'static> IntoResponse for Template<C> { + fn into_response(self) -> Response { + // At this stage, we respond with an internal server error, + // as we have not yet built the template. + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + + // We add this as an extension so that it can be + // accessed by the endpoint to actually render + // the template. + .extension(self) + + .finish() + } +} \ No newline at end of file diff --git a/poem/src/templates/tera/middleware.rs b/poem/src/templates/tera/middleware.rs new file mode 100644 index 0000000000..c346666b86 --- /dev/null +++ b/poem/src/templates/tera/middleware.rs @@ -0,0 +1,219 @@ +use tera::Tera; + +use super::Flavor; + +use crate::{ + templates::Template, + error::{InternalServerError, IntoResult}, + web::Html, + Endpoint, Middleware, Request, Result, + Response, IntoResponse, +}; + +#[cfg(feature = "live_reloading")] +use crate::templates::live_reloading::{ Watcher, LiveReloading }; + +/// Tera template with context. +pub type TeraTemplate = Template<tera::Context>; + +/// Tera templates middleware. +pub struct TeraEngine { + tera: Tera, +} + +impl TeraEngine { + /// Create a new instance of `TeraEngine`, containing all the parsed + /// templates found in the glob The errors are already handled. + /// + /// ```no_run + /// use poem::templates::tera::TeraEngine; + /// + /// let tera = TeraEngine::from_glob("templates/**/*") + /// .expect("Failed to load templates"); + /// ``` + pub fn from_glob(glob: &str) -> tera::Result<Self> { + Ok(Self { + tera: Tera::new(glob)? + }) + } + + /// Create a new instance of `TeraEngine`, containing all the parsed + /// templates found in the directory. + /// + /// ```no_run + /// use poem::templates::tera::TeraEngine; + /// + /// let tera = TeraEngine::from_directory("templates") + /// .expect("Failed to load templates"); + /// ``` + pub fn from_directory(template_directory: &str) -> tera::Result<Self> { + Self::from_glob(&format!("{template_directory}/**/*")) + } + + /// Create a new instance of `TeraEngine`, using a provided `Tera` + /// instance. + /// + /// ```no_run + /// use poem::templates::tera::{TeraEngine, Tera}; + /// + /// let mut tera = Tera::new("templates/**/*").expect("Failed to parse templates"); + /// + /// tera.autoescape_on(vec![".html", ".sql"]); + /// let engine = TeraEngine::custom(tera); + /// ``` + pub fn custom(tera: Tera) -> Self { + Self { tera } + } +} + +impl Default for TeraEngine { + fn default() -> Self { + Self::from_directory("templates") + .expect("Failed to load templates") + } +} + +impl<E: Endpoint> Middleware<E> for TeraEngine { + type Output = TeraEndpoint<E>; + + fn transform(&self, inner: E) -> Self::Output { + Self::Output { + tera: Flavor::Immutable(self.tera.clone()), + inner, + transformers: Vec::new(), + } + } +} + +/// Tera templates endpoint. +pub struct TeraEndpoint<E> { + tera: Flavor, + inner: E, + transformers: Vec<Box<dyn Fn(&mut Tera, &mut Request) + Send + Sync + 'static>>, +} + +#[async_trait::async_trait] +impl<E: Endpoint> Endpoint for TeraEndpoint<E> { + type Output = Response; + + async fn call(&self, mut req: Request) -> Result<Self::Output> { + let mut tera = match &self.tera { + Flavor::Immutable(t) => t.clone(), + + #[cfg(feature = "live_reloading")] + Flavor::LiveReload { tera, watcher } => { + let lock = if watcher.needs_reload() { + tracing::info!("Detected changes to templates, reloading..."); + + let mut lock = tera.write().await; + + if let Err(e) = lock.full_reload() { + tracing::error!("Failed to reload templates: {e}"); + tracing::debug!("Reload templates error: {e:?}"); + + return Err(InternalServerError(e)); + } + + lock.downgrade() + } else { + tera.read().await + }; + + lock.clone() + } + }; + + for transformer in &self.transformers { + transformer(&mut tera, &mut req); + } + + let response = self.inner.call(req).await?.into_response(); + + match response.extensions().get::<TeraTemplate>() { + Some(template) => { + let result = tera.render(&template.name, &template.context); + + if let Err(e) = &result { + tracing::debug!("Tera Rendering error: {e:?}"); + tracing::error!("Failed to render Tera template: {e}"); + } + + result.map(|s| Html(s).into_response()) + .map_err(InternalServerError) + }, + None => { + // todo: this destroys the type + response.into_result() + } + } + } +} + +impl<E: Endpoint> TeraEndpoint<E> { + /// Add a transformer that apply changes to each tera instances (for + /// instance, registering a dynamic filter) before passing tera to + /// request handlers + /// + /// ```no_run + /// use poem::{Route, EndpointExt, templates::tera::TeraEngine}; + /// + /// let app = Route::new() + /// .with(TeraEngine::default()) + /// .using(|tera, req| println!("{tera:?}\n{req:?}")); + /// ``` + pub fn using<F>(mut self, transformer: F) -> Self where + F: Fn(&mut Tera, &mut Request) + Send + Sync + 'static + { + self.transformers.push(Box::new(transformer)); + self + } + + /// Toggle live reloading. Defaults to enabled for debug and + /// disabled for release builds. + /// + /// ```no_run + /// use poem::{Route, EndpointExt, templates::tera::TeraEngine}; + /// + /// let app = Route::new() + /// .with(TeraEngine::default()) + /// .with_live_reloading(LiveReloading::Disabled); + /// ``` + + #[cfg(feature = "live_reloading")] + #[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))] + pub fn with_live_reloading(mut self, live_reloading: LiveReloading) -> Self { + self.tera = match (self.tera, live_reloading) { + #[cfg(debug_assertions)] + (Flavor::Immutable(tera), LiveReloading::Debug(path)) => { + tracing::debug!("Live reloading for Tera templates is enabled"); + + Flavor::LiveReload { tera: tokio::sync::RwLock::new(tera), watcher: Watcher::new(path) } + }, + + (Flavor::Immutable(tera), LiveReloading::Enabled(path)) => { + tracing::debug!("Live reloading for Tera templates is enabled"); + + Flavor::LiveReload { tera: tokio::sync::RwLock::new(tera), watcher: Watcher::new(path) } + }, + + #[cfg(not(debug_assertions))] + (Flavor::LiveReload { tera, .. }, LiveReloading::Debug(_)) => { + tracing::debug!("Live reloading for Tera templates is disabled"); + + Flavor::Immutable(tera.into_inner()) + }, + + (Flavor::LiveReload { tera, .. }, LiveReloading::Disabled) => { + tracing::debug!("Live reloading for Tera templates is disabled"); + + Flavor::Immutable(tera.into_inner()) + }, + + // todo: enable changing watch path + + (tera, _) => tera + }; + + self + } +} diff --git a/poem/src/templates/tera/mod.rs b/poem/src/templates/tera/mod.rs new file mode 100644 index 0000000000..d851a94c8f --- /dev/null +++ b/poem/src/templates/tera/mod.rs @@ -0,0 +1,74 @@ +//! Tera Templating Support +//! +//! # Load templates from file system using a glob +//! +//! ```no_run +//! use poem::templates::tera::TeraEngine; +//! +//! let tera = TeraEngine::default(); +//! ``` +//! +//! # Render a template inside an handler with some context vars +//! +//! ``` +//! use poem::{ +//! ctx, handler, +//! templates::Template, +//! web::Path, +//! }; +//! +//! #[handler] +//! fn hello(Path(name): Path<String>) -> Template<_> { +//! Template::render("index.html.tera", &ctx! { "name": &name }) +//! } +//! ``` + +mod middleware; +mod transformers; + +pub use tera::{ Context, Tera }; + +pub use self::{ + middleware::{ TeraEndpoint, TeraEngine, TeraTemplate }, + transformers::filters, +}; + +enum Flavor { + Immutable(Tera), + + #[cfg(feature = "live_reloading")] + #[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))] + LiveReload { + tera: tokio::sync::RwLock<Tera>, + watcher: super::live_reloading::Watcher + } +} + +/// Macro for constructing a Tera Context +/// ``` +/// use poem::{ +/// ctx, handler, +/// templates::Template, +/// web::Path, +/// }; +/// +/// #[handler] +/// fn hello(Path(name): Path<String>) -> Template<_> { +/// Template::render("index.html.tera", &ctx! { "name": &name }) +/// } +/// ``` + +// todo: create common macro with common context + +#[macro_export] +macro_rules! ctx { + { $( $key:literal: $value:expr ),* } => { + { + let mut context = ::poem::tera::Context::new(); + $( + context.insert($key, $value); + )* + context + } + }; +} diff --git a/poem/src/templates/tera/transformers.rs b/poem/src/templates/tera/transformers.rs new file mode 100644 index 0000000000..71197e90dd --- /dev/null +++ b/poem/src/templates/tera/transformers.rs @@ -0,0 +1,88 @@ +/// Tera templates built-in filters +pub mod filters { + /// Tera templates built-in i18n filters + #[cfg(feature = "i18n")] + #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] + pub mod i18n { + use std::{borrow::Cow, collections::HashMap}; + + use fluent::{ + types::{FluentNumber, FluentNumberOptions}, + FluentValue, + }; + use tera::{self, Filter, Tera, Value}; + + use crate::{i18n::Locale, FromRequest, Request}; + + /// Tera templates i18n filter + /// + /// ```no_run + /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}}; + /// + /// let resources = I18NResources::builder() + /// .add_path("resources") + /// .build() + /// .unwrap(); + /// + /// let app = Route::new() + /// .with(TeraEngine::default()) + /// .using(filters::i18n::translate) + /// .data(resources); + /// ``` + pub struct TranslateFilter { + locale: Locale, + } + + impl Filter for TranslateFilter { + fn filter(&self, id: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> { + if args.len() == 0 { + self.locale.text(id.as_str().unwrap()) + } else { + let mut fluent_args = HashMap::new(); + for (key, value) in args { + fluent_args.insert( + key.as_str(), + match value { + Value::Null => FluentValue::None, + Value::Number(val) => FluentValue::Number(FluentNumber::new( + val.as_f64().unwrap(), + FluentNumberOptions::default(), + )), + Value::String(val) => FluentValue::String(Cow::Owned(val.clone())), + _ => FluentValue::Error, + }, + ); + } + self.locale + .text_with_args(id.as_str().unwrap(), fluent_args) + } + .map(|str| Value::String(str)) + .map_err(|err| tera::Error::msg(err)) + } + } + + /// Tera Templating built-in filters + /// + /// ```no_run + /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}}; + /// + /// let resources = I18NResources::builder() + /// .add_path("resources") + /// .build() + /// .unwrap(); + /// + /// let app = Route::new() + /// .with(TeraEngine::default()) + /// .using(filters::i18n::translate) + /// .data(resources); + /// ``` + pub fn translate(tera: &mut Tera, req: &mut Request) { + tera.register_filter( + "translate", + TranslateFilter { + locale: Locale::from_request_without_body_sync(req).unwrap(), + }, + ); + } + } +} diff --git a/poem/src/web/mod.rs b/poem/src/web/mod.rs index 144e7bd634..cafda38aae 100644 --- a/poem/src/web/mod.rs +++ b/poem/src/web/mod.rs @@ -312,7 +312,9 @@ impl RequestBody { #[async_trait::async_trait] pub trait FromRequest<'a>: Sized { /// Extract from request head and body. - async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self>; + async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> { + Self::from_request_sync(req, body) + } /// Extract from request head. /// @@ -326,6 +328,16 @@ pub trait FromRequest<'a>: Sized { async fn from_request_without_body(req: &'a Request) -> Result<Self> { Self::from_request(req, &mut Default::default()).await } + + /// Extract from request head and body synchronously. + fn from_request_sync(_req: &'a Request, _body: &mut RequestBody) -> Result<Self> { + panic!("Not implemented, please implement one of from_request and from_request_sync"); + } + + /// Extract from request head synchronously. + fn from_request_without_body_sync(req: &'a Request) -> Result<Self> { + Self::from_request_sync(req, &mut Default::default()) + } } /// Represents a type that can convert into response. @@ -807,14 +819,14 @@ impl<'a> FromRequest<'a> for &'a LocalAddr { } #[async_trait::async_trait] -impl<'a, T: FromRequest<'a>> FromRequest<'a> for Option<T> { +impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Option<T> { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> { Ok(T::from_request(req, body).await.ok()) } } #[async_trait::async_trait] -impl<'a, T: FromRequest<'a>> FromRequest<'a> for Result<T> { +impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Result<T> { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> { Ok(T::from_request(req, body).await) }