From 717c3d3a4aba28e19d64cf2dfd3d6b9688af3d75 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 22:20:34 +0100 Subject: [PATCH 01/25] build: added tera feature definitions --- poem/Cargo.toml | 2 ++ poem/README.md | 5 +++-- poem/src/lib.rs | 3 +++ poem/src/tera/mod.rs | 0 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 poem/src/tera/mod.rs diff --git a/poem/Cargo.toml b/poem/Cargo.toml index cfbb3ba17b..caa3884a9c 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -64,6 +64,7 @@ acme = [ embed = ["rust-embed", "hex", "mime_guess"] xml = ["quick-xml"] yaml = ["serde_yaml"] +tera = ["dep:tera"] [dependencies] poem-derive.workspace = true @@ -153,6 +154,7 @@ 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 } # Feature optional dependencies anyhow = { version = "1.0.0", optional = true } diff --git a/poem/README.md b/poem/README.md index 6ae329b58e..19d90f30ba 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 | @@ -75,7 +75,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/lib.rs b/poem/src/lib.rs index b305f68df8..1381e3605b 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -278,6 +278,9 @@ pub mod session; #[cfg_attr(docsrs, doc(cfg(feature = "test")))] pub mod test; pub mod web; +#[cfg(feature = "tera")] +#[cfg_attr(docsrs, doc(cfg(feature = "tera")))] +pub mod tera; #[doc(inline)] pub use http; diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs new file mode 100644 index 0000000000..e69de29bb2 From f197387a8b162fd034c8fdeb78b8904658474c7a Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 23:16:14 +0100 Subject: [PATCH 02/25] feat: tera templating middleware, endpoint and extractor --- examples/poem/tera-templating/Cargo.toml | 2 +- examples/poem/tera-templating/src/main.rs | 24 ++++-------- poem/src/tera/endpoint.rs | 33 +++++++++++++++++ poem/src/tera/middleware.rs | 45 +++++++++++++++++++++++ poem/src/tera/mod.rs | 17 +++++++++ 5 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 poem/src/tera/endpoint.rs create mode 100644 poem/src/tera/middleware.rs diff --git a/examples/poem/tera-templating/Cargo.toml b/examples/poem/tera-templating/Cargo.toml index 934aeeb3b1..727cf8bbd0 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" diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index b5ce0e3ac6..b0f0bc432e 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -1,30 +1,19 @@ -use once_cell::sync::Lazy; use poem::{ error::InternalServerError, get, handler, listener::TcpListener, web::{Html, Path}, Route, Server, + EndpointExt, + tera::TeraTemplating }; use tera::{Context, Tera}; -static TEMPLATES: Lazy = 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) -> Result, poem::Error> { +fn hello(Path(name): Path, tera: Tera) -> Result, poem::Error> { let mut context = Context::new(); context.insert("name", &name); - TEMPLATES + tera .render("index.html.tera", &context) .map_err(InternalServerError) .map(Html) @@ -32,7 +21,10 @@ fn hello(Path(name): Path) -> Result, poem::Error> { #[tokio::main] async fn main() -> Result<(), std::io::Error> { - let app = Route::new().at("/hello/:name", get(hello)); + let app = Route::new() + .at("/hello/:name", get(hello)) + .with(TeraTemplating::from_glob("templates/**/*")); + Server::new(TcpListener::bind("127.0.0.1:3000")) .run(app) .await diff --git a/poem/src/tera/endpoint.rs b/poem/src/tera/endpoint.rs new file mode 100644 index 0000000000..b378bfa052 --- /dev/null +++ b/poem/src/tera/endpoint.rs @@ -0,0 +1,33 @@ +use tera::Tera; + +use crate::{Endpoint, Request, Result, FromRequest, RequestBody}; + +/// Tera Templating Endpoint +pub struct TeraTemplatingEndpoint { + pub(super) tera: Tera, + pub(super) inner: E +} + +#[async_trait::async_trait] +impl Endpoint for TeraTemplatingEndpoint { + type Output = E::Output; + + async fn call(&self, mut req: Request) -> Result { + req.extensions_mut().insert(self.tera.clone()); + + self.inner.call(req).await + } +} + +#[async_trait::async_trait] +impl<'a> FromRequest<'a> for Tera { + async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { + let tera = req + .extensions() + .get::() + .expect("To use the `Tera` extractor, the `TeraTemplating` endpoit is required.") + .clone(); + + Ok(tera) + } +} \ No newline at end of file diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs new file mode 100644 index 0000000000..151111bc4d --- /dev/null +++ b/poem/src/tera/middleware.rs @@ -0,0 +1,45 @@ +use tera::Tera; + +use crate::{Endpoint, Middleware}; +use super::TeraTemplatingEndpoint; + +/// Tera Templating Middleware +pub struct TeraTemplatingMiddleware { + tera: Tera +} + +impl TeraTemplatingMiddleware { + + /// Create a new instance of TeraTemplating, containing all the parsed templates found in the glob + /// The errors are already handled. Use TeraTemplating::custom(tera: Tera) to modify tera settings. + /// + /// ```no_compile + /// use poem::tera::TeraTemplating; + /// + /// let templating = TeraTemplating::from_glob("templates/**/*"); + /// ``` + pub fn from_glob(glob: &str) -> Self { + let tera = match Tera::new(glob) { + Ok(t) => t, + Err(e) => { + println!("Parsing error(s): {e}"); + ::std::process::exit(1); + } + }; + + Self { + tera + } + } +} + +impl Middleware for TeraTemplatingMiddleware { + type Output = TeraTemplatingEndpoint; + + fn transform(&self, inner: E) -> Self::Output { + Self::Output { + tera: self.tera.clone(), + inner, + } + } +} \ No newline at end of file diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index e69de29bb2..07a429a722 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -0,0 +1,17 @@ +//! Tera Templating Support +//! +//! # Load templates from file system using a glob +//! +//! ```no_run +//! use poem::tera::TeraTemplating; +//! +//! let templating = TeraTemplating::from_glob("templates/**/*"); +//! ``` + +mod endpoint; +mod middleware; + +pub use self::{ + endpoint::TeraTemplatingEndpoint, + middleware::TeraTemplatingMiddleware as TeraTemplating +}; \ No newline at end of file From 65d0d1a3179a969a84db5976eaa381b8377f4b49 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 23:32:32 +0100 Subject: [PATCH 03/25] feat: wrap tera::Result --- examples/poem/tera-templating/src/main.rs | 12 ++++-------- poem/src/tera/endpoint.rs | 13 ++++++++++++- poem/src/tera/mod.rs | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index b0f0bc432e..e79d0b1e2c 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -1,22 +1,18 @@ use poem::{ - error::InternalServerError, get, handler, listener::TcpListener, - web::{Html, Path}, + web::Path, Route, Server, EndpointExt, - tera::TeraTemplating + tera::{TeraTemplating, TeraTemplate} }; use tera::{Context, Tera}; #[handler] -fn hello(Path(name): Path, tera: Tera) -> Result, poem::Error> { +fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { let mut context = Context::new(); context.insert("name", &name); - tera - .render("index.html.tera", &context) - .map_err(InternalServerError) - .map(Html) + tera.render("index.html.tera", &context) } #[tokio::main] diff --git a/poem/src/tera/endpoint.rs b/poem/src/tera/endpoint.rs index b378bfa052..fa7bd08b35 100644 --- a/poem/src/tera/endpoint.rs +++ b/poem/src/tera/endpoint.rs @@ -1,6 +1,6 @@ use tera::Tera; -use crate::{Endpoint, Request, Result, FromRequest, RequestBody}; +use crate::{Endpoint, error::IntoResult, Request, Result, FromRequest, RequestBody, error::InternalServerError, web::Html}; /// Tera Templating Endpoint pub struct TeraTemplatingEndpoint { @@ -30,4 +30,15 @@ impl<'a> FromRequest<'a> for Tera { Ok(tera) } +} + +/// Shortcut (or not) for a Tera Templating handler Response +pub type TeraTemplatingResult = tera::Result; + +impl IntoResult> for TeraTemplatingResult { + fn into_result(self) -> Result> { + self + .map_err(InternalServerError) + .map(Html) + } } \ No newline at end of file diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 07a429a722..404313462c 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -12,6 +12,6 @@ mod endpoint; mod middleware; pub use self::{ - endpoint::TeraTemplatingEndpoint, + endpoint::{TeraTemplatingEndpoint, TeraTemplatingResult as TeraTemplate}, middleware::TeraTemplatingMiddleware as TeraTemplating }; \ No newline at end of file From dc5b54c462885ccdda469e15cb7003f9b70df4ec Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 23:35:34 +0100 Subject: [PATCH 04/25] docs: add a tera templating handler example in tera module doc --- poem/src/tera/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 404313462c..8d68db0d2d 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -7,6 +7,20 @@ //! //! let templating = TeraTemplating::from_glob("templates/**/*"); //! ``` +//! +//! # Render a template inside an handler with some context vars +//! +//! ```no_run +//! use poem::{web::Path, tera::TeraTemplate}; +//! use tera::Tera; +//! +//! #[handler] +//! fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { +//! let mut context = Context::new(); +//! context.insert("name", &name); +//! tera.render("index.html.tera", &context) +//! } +//! ``` mod endpoint; mod middleware; From 51a9e8d21b0580fa67f816079233403c7f340db9 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 23:37:06 +0100 Subject: [PATCH 05/25] docs: add tera feature in root doc --- poem/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 1381e3605b..755bd2b2eb 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -254,6 +254,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")] From 37b85674d80e581a46d6fdb341498db619c3802c Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Fri, 17 Feb 2023 23:40:49 +0100 Subject: [PATCH 06/25] build: expose tera::Tera & tera::Context from poem::tera --- examples/poem/tera-templating/Cargo.toml | 1 - examples/poem/tera-templating/src/main.rs | 3 +-- poem/src/tera/mod.rs | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/poem/tera-templating/Cargo.toml b/examples/poem/tera-templating/Cargo.toml index 727cf8bbd0..22f813fce8 100644 --- a/examples/poem/tera-templating/Cargo.toml +++ b/examples/poem/tera-templating/Cargo.toml @@ -7,5 +7,4 @@ publish.workspace = true [dependencies] poem = { workspace = true, features = ["tera"] } tokio = { workspace = true, features = ["full"] } -tera = "1.17.1" once_cell = "1.17.0" diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index e79d0b1e2c..4fc63d95f5 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -4,9 +4,8 @@ use poem::{ web::Path, Route, Server, EndpointExt, - tera::{TeraTemplating, TeraTemplate} + tera::{TeraTemplating, TeraTemplate, Tera, Context} }; -use tera::{Context, Tera}; #[handler] fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 8d68db0d2d..0fc283a33c 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -25,6 +25,8 @@ mod endpoint; mod middleware; +pub use tera::{Tera, Context}; + pub use self::{ endpoint::{TeraTemplatingEndpoint, TeraTemplatingResult as TeraTemplate}, middleware::TeraTemplatingMiddleware as TeraTemplating From d937558386605026fbd06fc7e227ad13611fd6d9 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 09:26:42 +0100 Subject: [PATCH 07/25] feat: ctx macro --- examples/poem/tera-templating/src/main.rs | 8 +++--- poem/src/tera/mod.rs | 31 +++++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index 4fc63d95f5..56f16431e1 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -1,17 +1,15 @@ use poem::{ - get, handler, + ctx, get, handler, listener::TcpListener, web::Path, Route, Server, EndpointExt, - tera::{TeraTemplating, TeraTemplate, Tera, Context} + tera::{TeraTemplating, TeraTemplate, Tera} }; #[handler] fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { - let mut context = Context::new(); - context.insert("name", &name); - tera.render("index.html.tera", &context) + tera.render("index.html.tera", &ctx!{ "name": &name }) } #[tokio::main] diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 0fc283a33c..5f41971c50 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -11,14 +11,12 @@ //! # Render a template inside an handler with some context vars //! //! ```no_run -//! use poem::{web::Path, tera::TeraTemplate}; +//! use poem::{handler, ctx, web::Path, tera::TeraTemplate}; //! use tera::Tera; //! //! #[handler] //! fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { -//! let mut context = Context::new(); -//! context.insert("name", &name); -//! tera.render("index.html.tera", &context) +//! tera.render("index.html.tera", &ctx!{ "name": &name }) //! } //! ``` @@ -30,4 +28,27 @@ pub use tera::{Tera, Context}; pub use self::{ endpoint::{TeraTemplatingEndpoint, TeraTemplatingResult as TeraTemplate}, middleware::TeraTemplatingMiddleware as TeraTemplating -}; \ No newline at end of file +}; + +/// Macro for constructing a Tera Context +/// ```no_run +/// use poem::{handler, ctx, web::Path, tera::TeraTemplate}; +/// use tera::Tera; +/// +/// #[handler] +/// fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { +/// tera.render("index.html.tera", &ctx!{ "name": &name }) +/// } +/// ``` +#[macro_export] +macro_rules! ctx { + { $( $key:literal: $value:expr ),* } => { + { + let mut context = ::poem::tera::Context::new(); + $( + context.insert($key, $value); + )* + context + } + }; +} From ccb5c9e4c9d2b940a0fc955d791089b134473cf5 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 09:28:30 +0100 Subject: [PATCH 08/25] docs: use poem::tera::Tera --- poem/src/tera/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 5f41971c50..326f9148bc 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -11,8 +11,7 @@ //! # Render a template inside an handler with some context vars //! //! ```no_run -//! use poem::{handler, ctx, web::Path, tera::TeraTemplate}; -//! use tera::Tera; +//! use poem::{handler, ctx, web::Path, tera::{TeraTemplate, Tera}}; //! //! #[handler] //! fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { @@ -32,7 +31,7 @@ pub use self::{ /// Macro for constructing a Tera Context /// ```no_run -/// use poem::{handler, ctx, web::Path, tera::TeraTemplate}; +/// use poem::{handler, ctx, web::Path, tera::{TeraTemplate, Tera}}; /// use tera::Tera; /// /// #[handler] From 08cfc1371d10220268a6ca9be841ce5f53f56591 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 09:32:26 +0100 Subject: [PATCH 09/25] fix: custom tera templating --- poem/src/tera/middleware.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 151111bc4d..5444926ef9 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -31,6 +31,27 @@ impl TeraTemplatingMiddleware { tera } } + + /// Create a new instance of TeraTemplating, using the provided Tera instance + /// + /// ```no_compile + /// use poem::tera::{TeraTemplating, Tera}; + /// + /// 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"]); + /// let templating = TeraTemplating::custom(tera); + /// ``` + pub fn custom(tera: Tera) -> Self { + Self { + tera + } + } } impl Middleware for TeraTemplatingMiddleware { From d3e52cb188f883e16993bb12c0f81f6cefc83e34 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 09:38:56 +0100 Subject: [PATCH 10/25] style: move tera::endpoint in tera::middleware --- poem/src/tera/endpoint.rs | 44 ------------------------------------- poem/src/tera/middleware.rs | 44 +++++++++++++++++++++++++++++++++++-- poem/src/tera/mod.rs | 8 +++---- 3 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 poem/src/tera/endpoint.rs diff --git a/poem/src/tera/endpoint.rs b/poem/src/tera/endpoint.rs deleted file mode 100644 index fa7bd08b35..0000000000 --- a/poem/src/tera/endpoint.rs +++ /dev/null @@ -1,44 +0,0 @@ -use tera::Tera; - -use crate::{Endpoint, error::IntoResult, Request, Result, FromRequest, RequestBody, error::InternalServerError, web::Html}; - -/// Tera Templating Endpoint -pub struct TeraTemplatingEndpoint { - pub(super) tera: Tera, - pub(super) inner: E -} - -#[async_trait::async_trait] -impl Endpoint for TeraTemplatingEndpoint { - type Output = E::Output; - - async fn call(&self, mut req: Request) -> Result { - req.extensions_mut().insert(self.tera.clone()); - - self.inner.call(req).await - } -} - -#[async_trait::async_trait] -impl<'a> FromRequest<'a> for Tera { - async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { - let tera = req - .extensions() - .get::() - .expect("To use the `Tera` extractor, the `TeraTemplating` endpoit is required.") - .clone(); - - Ok(tera) - } -} - -/// Shortcut (or not) for a Tera Templating handler Response -pub type TeraTemplatingResult = tera::Result; - -impl IntoResult> for TeraTemplatingResult { - fn into_result(self) -> Result> { - self - .map_err(InternalServerError) - .map(Html) - } -} \ No newline at end of file diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 5444926ef9..ee35553d43 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -1,7 +1,6 @@ use tera::Tera; -use crate::{Endpoint, Middleware}; -use super::TeraTemplatingEndpoint; +use crate::{Endpoint, Middleware, error::IntoResult, Request, Result, FromRequest, RequestBody, error::InternalServerError, web::Html}; /// Tera Templating Middleware pub struct TeraTemplatingMiddleware { @@ -63,4 +62,45 @@ impl Middleware for TeraTemplatingMiddleware { inner, } } +} + +/// Tera Templating Endpoint +pub struct TeraTemplatingEndpoint { + pub(super) tera: Tera, + pub(super) inner: E +} + +#[async_trait::async_trait] +impl Endpoint for TeraTemplatingEndpoint { + type Output = E::Output; + + async fn call(&self, mut req: Request) -> Result { + req.extensions_mut().insert(self.tera.clone()); + + self.inner.call(req).await + } +} + +#[async_trait::async_trait] +impl<'a> FromRequest<'a> for Tera { + async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { + let tera = req + .extensions() + .get::() + .expect("To use the `Tera` extractor, the `TeraTemplating` endpoit is required.") + .clone(); + + Ok(tera) + } +} + +/// Shortcut (or not) for a Tera Templating handler Response +pub type TeraTemplatingResult = tera::Result; + +impl IntoResult> for TeraTemplatingResult { + fn into_result(self) -> Result> { + self + .map_err(InternalServerError) + .map(Html) + } } \ No newline at end of file diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 326f9148bc..a3b7446aa0 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -19,14 +19,14 @@ //! } //! ``` -mod endpoint; mod middleware; pub use tera::{Tera, Context}; -pub use self::{ - endpoint::{TeraTemplatingEndpoint, TeraTemplatingResult as TeraTemplate}, - middleware::TeraTemplatingMiddleware as TeraTemplating +pub use self::middleware::{ + TeraTemplatingEndpoint, + TeraTemplatingResult as TeraTemplate, + TeraTemplatingMiddleware as TeraTemplating }; /// Macro for constructing a Tera Context From 0745620dca9ea05dcd2ddc9b7bdc1e08460674a8 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 11:09:51 +0100 Subject: [PATCH 11/25] feat: tera i18n "translate" filter --- examples/poem/tera-i18n/Cargo.toml | 10 ++++ .../poem/tera-i18n/resources/en-US/simple.ftl | 2 + .../poem/tera-i18n/resources/fr/simple.ftl | 2 + .../poem/tera-i18n/resources/zh-CN/simple.ftl | 2 + examples/poem/tera-i18n/src/main.rs | 38 ++++++++++++++ .../poem/tera-i18n/templates/hello.html.tera | 1 + .../poem/tera-i18n/templates/index.html.tera | 1 + poem/src/i18n/locale.rs | 6 +-- poem/src/lib.rs | 8 +-- poem/src/tera/middleware.rs | 25 ++++++++-- poem/src/tera/mod.rs | 12 +++-- poem/src/tera/transformers.rs | 50 +++++++++++++++++++ poem/src/web/mod.rs | 27 ++++++++++ 13 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 examples/poem/tera-i18n/Cargo.toml create mode 100644 examples/poem/tera-i18n/resources/en-US/simple.ftl create mode 100644 examples/poem/tera-i18n/resources/fr/simple.ftl create mode 100644 examples/poem/tera-i18n/resources/zh-CN/simple.ftl create mode 100644 examples/poem/tera-i18n/src/main.rs create mode 100644 examples/poem/tera-i18n/templates/hello.html.tera create mode 100644 examples/poem/tera-i18n/templates/index.html.tera create mode 100644 poem/src/tera/transformers.rs 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..2cb389e4df --- /dev/null +++ b/examples/poem/tera-i18n/src/main.rs @@ -0,0 +1,38 @@ +use poem::{ + ctx, get, handler, + listener::TcpListener, + web::Path, + Route, Server, + EndpointExt, + tera::{TeraTemplating, TeraTemplate, Tera, filters}, + i18n::I18NResources +}; + +#[handler] +fn index(tera: Tera) -> TeraTemplate { + tera.render("index.html.tera", &ctx!{}) +} + +#[handler] +fn hello(Path(name): Path, 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::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 @@ +

{{ "welcome" | translate(name=name) }}

\ 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 @@ +

{{ "hello-world" | translate }}

\ No newline at end of file diff --git a/poem/src/i18n/locale.rs b/poem/src/i18n/locale.rs index 584c26d77a..3bd5c4c718 100644 --- a/poem/src/i18n/locale.rs +++ b/poem/src/i18n/locale.rs @@ -7,7 +7,7 @@ use unic_langid::LanguageIdentifier; use crate::{ error::I18NError, i18n::{I18NArgs, I18NBundle, I18NResources}, - FromRequest, Request, RequestBody, Result, + FromRequestSync, Request, RequestBody, Result, }; type LanguageArray = SmallVec<[LanguageIdentifier; 8]>; @@ -85,8 +85,8 @@ impl Locale { } #[async_trait::async_trait] -impl<'a> FromRequest<'a> for Locale { - async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { +impl<'a> FromRequestSync<'a> for Locale { + fn from_request_sync(req: &'a Request, _body: &mut RequestBody) -> Result { let resources = req .extensions() .get::() diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 755bd2b2eb..7fd9f221e4 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -275,13 +275,13 @@ pub mod middleware; #[cfg(feature = "session")] #[cfg_attr(docsrs, doc(cfg(feature = "session")))] pub mod session; +#[cfg(feature = "tera")] +#[cfg_attr(docsrs, doc(cfg(feature = "tera")))] +pub mod tera; #[cfg(feature = "test")] #[cfg_attr(docsrs, doc(cfg(feature = "test")))] pub mod test; pub mod web; -#[cfg(feature = "tera")] -#[cfg_attr(docsrs, doc(cfg(feature = "tera")))] -pub mod tera; #[doc(inline)] pub use http; @@ -309,4 +309,4 @@ pub use route::{ }; #[cfg(feature = "server")] pub use server::Server; -pub use web::{FromRequest, IntoResponse, RequestBody}; +pub use web::{FromRequest, FromRequestSync, IntoResponse, RequestBody}; diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index ee35553d43..e145ee105b 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -60,14 +60,16 @@ impl Middleware for TeraTemplatingMiddleware { Self::Output { tera: self.tera.clone(), inner, + transformers: Vec::new() } } } /// Tera Templating Endpoint pub struct TeraTemplatingEndpoint { - pub(super) tera: Tera, - pub(super) inner: E + tera: Tera, + inner: E, + transformers: Vec } #[async_trait::async_trait] @@ -75,7 +77,13 @@ impl Endpoint for TeraTemplatingEndpoint { type Output = E::Output; async fn call(&self, mut req: Request) -> Result { - req.extensions_mut().insert(self.tera.clone()); + let mut tera = self.tera.clone(); + + for transformer in &self.transformers { + transformer(&mut tera, &mut req); + } + + req.extensions_mut().insert(tera); self.inner.call(req).await } @@ -99,8 +107,19 @@ pub type TeraTemplatingResult = tera::Result; impl IntoResult> for TeraTemplatingResult { fn into_result(self) -> Result> { + if let Err(err) = &self { + println!("{err:?}"); + } + self .map_err(InternalServerError) .map(Html) } +} + +impl TeraTemplatingEndpoint { + pub fn using(mut self, transformer: fn(&mut Tera, &mut Request)) -> Self { + self.transformers.push(transformer); + self + } } \ No newline at end of file diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index a3b7446aa0..1321b4fe89 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -20,13 +20,17 @@ //! ``` mod middleware; +mod transformers; pub use tera::{Tera, Context}; -pub use self::middleware::{ - TeraTemplatingEndpoint, - TeraTemplatingResult as TeraTemplate, - TeraTemplatingMiddleware as TeraTemplating +pub use self::{ + middleware::{ + TeraTemplatingEndpoint, + TeraTemplatingResult as TeraTemplate, + TeraTemplatingMiddleware as TeraTemplating, + }, + transformers::filters }; /// Macro for constructing a Tera Context diff --git a/poem/src/tera/transformers.rs b/poem/src/tera/transformers.rs new file mode 100644 index 0000000000..be39031fc6 --- /dev/null +++ b/poem/src/tera/transformers.rs @@ -0,0 +1,50 @@ +pub mod filters { + use std::{collections::HashMap, borrow::Cow}; + use tera::{self, Tera, Value, Filter}; + use fluent::{ + types::{FluentNumber, FluentNumberOptions}, + FluentValue, + }; + use crate::{Request, i18n::Locale, FromRequestSync}; + + pub struct TranslateFilter { + locale: Locale + } + + impl Filter for TranslateFilter { + fn filter(&self, id: &Value, args: &HashMap) -> tera::Result { + 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)) + } + } + + #[cfg(feature = "i18n")] + #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] + pub fn translate(tera: &mut Tera, req: &mut Request) { + tera.register_filter("translate", TranslateFilter { + locale: Locale::from_request_without_body_sync(req).unwrap() + }); + } + +} \ No newline at end of file diff --git a/poem/src/web/mod.rs b/poem/src/web/mod.rs index 144e7bd634..6efea4a608 100644 --- a/poem/src/web/mod.rs +++ b/poem/src/web/mod.rs @@ -328,6 +328,26 @@ pub trait FromRequest<'a>: Sized { } } +/// Represents an type that can be extract from requests synchronously. +/// See [crate::web::FromRequest] +pub trait FromRequestSync<'a>: Sized { + /// Extract from request head and body. + fn from_request_sync(req: &'a Request, body: &mut RequestBody) -> Result; + + /// Extract from request head. + /// + /// If you know that this type does not need to extract the body, then you + /// can just use it. + /// + /// For example [`Query`], [`Path`] they only extract the content from the + /// request head, using this method would be more convenient. + /// `String`,`Vec` they extract the body of the request, using this + /// method will cause `ReadBodyError` error. + fn from_request_without_body_sync(req: &'a Request) -> Result { + Self::from_request_sync(req, &mut Default::default()) + } +} + /// Represents a type that can convert into response. /// /// # Provided Implementations @@ -735,6 +755,13 @@ impl<'a> FromRequest<'a> for &'a Request { } } +#[async_trait::async_trait] +impl<'a, T> FromRequest<'a> for &'a T where &'a T: FromRequestSync<'a> { + async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { + Self::from_request_sync(req, body) + } +} + #[async_trait::async_trait] impl<'a> FromRequest<'a> for &'a Uri { async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { From eeadb51ffb6df857384db00e63f140b6652f6f42 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 14:17:46 +0100 Subject: [PATCH 12/25] docs: tera transformers --- examples/poem/tera-i18n/src/main.rs | 2 +- poem/src/tera/middleware.rs | 11 +++ poem/src/tera/transformers.rs | 119 ++++++++++++++++++---------- 3 files changed, 88 insertions(+), 44 deletions(-) diff --git a/examples/poem/tera-i18n/src/main.rs b/examples/poem/tera-i18n/src/main.rs index 2cb389e4df..7f89b240d7 100644 --- a/examples/poem/tera-i18n/src/main.rs +++ b/examples/poem/tera-i18n/src/main.rs @@ -29,7 +29,7 @@ async fn main() -> Result<(), std::io::Error> { .at("/", get(index)) .at("/hello/:name", get(hello)) .with(TeraTemplating::from_glob("templates/**/*")) - .using(filters::translate) + .using(filters::i18n::translate) .data(resources); Server::new(TcpListener::bind("127.0.0.1:3000")) diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index e145ee105b..1ac21037b1 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -118,6 +118,17 @@ impl IntoResult> for TeraTemplatingResult { } impl TeraTemplatingEndpoint { + + /// Add a transformer that apply changes to each tera instances (for instance, registering a dynamic filter) + /// before passing tera to request handlers + /// + /// ```no_compile + /// use poem::{Route, EndpointExt, tera::TeraTemplating}; + /// + /// let app = Route::new() + /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .using(|tera, req| println!("{tera:?}\n{req:?}")); + /// ``` pub fn using(mut self, transformer: fn(&mut Tera, &mut Request)) -> Self { self.transformers.push(transformer); self diff --git a/poem/src/tera/transformers.rs b/poem/src/tera/transformers.rs index be39031fc6..1bc726f76b 100644 --- a/poem/src/tera/transformers.rs +++ b/poem/src/tera/transformers.rs @@ -1,50 +1,83 @@ +/// Tera Templating built-in filters pub mod filters { - use std::{collections::HashMap, borrow::Cow}; - use tera::{self, Tera, Value, Filter}; - use fluent::{ - types::{FluentNumber, FluentNumberOptions}, - FluentValue, - }; - use crate::{Request, i18n::Locale, FromRequestSync}; + /// Tera Templating built-in i18n filters + #[cfg(feature = "i18n")] + #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] + pub mod i18n { + use std::{collections::HashMap, borrow::Cow}; + use tera::{self, Tera, Value, Filter}; + use fluent::{ + types::{FluentNumber, FluentNumberOptions}, + FluentValue, + }; + use crate::{Request, i18n::Locale, FromRequestSync}; - pub struct TranslateFilter { - locale: Locale - } + /// Tera Templating i18n filter + /// + /// ```no_compile + /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; + /// + /// let resources = I18NResources::builder() + /// .add_path("resources") + /// .build() + /// .unwrap(); + /// + /// let app = Route::new() + /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .using(filters::i18n::translate) + /// .data(resources); + /// ``` + pub struct TranslateFilter { + locale: Locale + } - impl Filter for TranslateFilter { - fn filter(&self, id: &Value, args: &HashMap) -> tera::Result { - 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, - }, - ); + impl Filter for TranslateFilter { + fn filter(&self, id: &Value, args: &HashMap) -> tera::Result { + 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) } - self.locale - .text_with_args(id.as_str().unwrap(), fluent_args) + .map(|str| Value::String(str)) + .map_err(|err| tera::Error::msg(err)) } - .map(|str| Value::String(str)) - .map_err(|err| tera::Error::msg(err)) + } + + /// Tera Templating built-in filters + /// + /// ```no_compile + /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; + /// + /// let resources = I18NResources::builder() + /// .add_path("resources") + /// .build() + /// .unwrap(); + /// + /// let app = Route::new() + /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .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() + }); } } - - #[cfg(feature = "i18n")] - #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] - pub fn translate(tera: &mut Tera, req: &mut Request) { - tera.register_filter("translate", TranslateFilter { - locale: Locale::from_request_without_body_sync(req).unwrap() - }); - } - -} \ No newline at end of file +} From b34e6d04b73a2f913201ff0a38a3f49cb708875e Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 14:33:34 +0100 Subject: [PATCH 13/25] style: ran cargo fmt --- poem/src/tera/middleware.rs | 47 +++++++++++++++++------------------ poem/src/tera/mod.rs | 27 ++++++++++++-------- poem/src/tera/transformers.rs | 33 +++++++++++++----------- poem/src/web/mod.rs | 5 +++- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 1ac21037b1..79de229763 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -1,20 +1,24 @@ use tera::Tera; -use crate::{Endpoint, Middleware, error::IntoResult, Request, Result, FromRequest, RequestBody, error::InternalServerError, web::Html}; +use crate::{ + error::{InternalServerError, IntoResult}, + web::Html, + Endpoint, FromRequest, Middleware, Request, RequestBody, Result, +}; /// Tera Templating Middleware pub struct TeraTemplatingMiddleware { - tera: Tera + tera: Tera, } impl TeraTemplatingMiddleware { - - /// Create a new instance of TeraTemplating, containing all the parsed templates found in the glob - /// The errors are already handled. Use TeraTemplating::custom(tera: Tera) to modify tera settings. + /// Create a new instance of TeraTemplating, containing all the parsed + /// templates found in the glob The errors are already handled. Use + /// TeraTemplating::custom(tera: Tera) to modify tera settings. /// /// ```no_compile /// use poem::tera::TeraTemplating; - /// + /// /// let templating = TeraTemplating::from_glob("templates/**/*"); /// ``` pub fn from_glob(glob: &str) -> Self { @@ -26,16 +30,15 @@ impl TeraTemplatingMiddleware { } }; - Self { - tera - } + Self { tera } } - /// Create a new instance of TeraTemplating, using the provided Tera instance + /// Create a new instance of TeraTemplating, using the provided Tera + /// instance /// /// ```no_compile /// use poem::tera::{TeraTemplating, Tera}; - /// + /// /// let mut tera = match Tera::new("templates/**/*") { /// Ok(t) => t, /// Err(e) => { @@ -47,9 +50,7 @@ impl TeraTemplatingMiddleware { /// let templating = TeraTemplating::custom(tera); /// ``` pub fn custom(tera: Tera) -> Self { - Self { - tera - } + Self { tera } } } @@ -60,7 +61,7 @@ impl Middleware for TeraTemplatingMiddleware { Self::Output { tera: self.tera.clone(), inner, - transformers: Vec::new() + transformers: Vec::new(), } } } @@ -69,7 +70,7 @@ impl Middleware for TeraTemplatingMiddleware { pub struct TeraTemplatingEndpoint { tera: Tera, inner: E, - transformers: Vec + transformers: Vec, } #[async_trait::async_trait] @@ -111,20 +112,18 @@ impl IntoResult> for TeraTemplatingResult { println!("{err:?}"); } - self - .map_err(InternalServerError) - .map(Html) + self.map_err(InternalServerError).map(Html) } } impl TeraTemplatingEndpoint { - - /// Add a transformer that apply changes to each tera instances (for instance, registering a dynamic filter) - /// before passing tera to request handlers + /// Add a transformer that apply changes to each tera instances (for + /// instance, registering a dynamic filter) before passing tera to + /// request handlers /// /// ```no_compile /// use poem::{Route, EndpointExt, tera::TeraTemplating}; - /// + /// /// let app = Route::new() /// .with(TeraTemplating::from_glob("templates/**/*")) /// .using(|tera, req| println!("{tera:?}\n{req:?}")); @@ -133,4 +132,4 @@ impl TeraTemplatingEndpoint { self.transformers.push(transformer); self } -} \ No newline at end of file +} diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index 1321b4fe89..a956c49b1f 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -7,40 +7,47 @@ //! //! let templating = TeraTemplating::from_glob("templates/**/*"); //! ``` -//! +//! //! # Render a template inside an handler with some context vars //! //! ```no_run -//! use poem::{handler, ctx, web::Path, tera::{TeraTemplate, Tera}}; +//! use poem::{ +//! ctx, handler, +//! tera::{Tera, TeraTemplate}, +//! web::Path, +//! }; //! //! #[handler] //! fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { -//! tera.render("index.html.tera", &ctx!{ "name": &name }) +//! tera.render("index.html.tera", &ctx! { "name": &name }) //! } //! ``` mod middleware; mod transformers; -pub use tera::{Tera, Context}; +pub use tera::{Context, Tera}; pub use self::{ middleware::{ - TeraTemplatingEndpoint, - TeraTemplatingResult as TeraTemplate, - TeraTemplatingMiddleware as TeraTemplating, + TeraTemplatingEndpoint, TeraTemplatingMiddleware as TeraTemplating, + TeraTemplatingResult as TeraTemplate, }, - transformers::filters + transformers::filters, }; /// Macro for constructing a Tera Context /// ```no_run -/// use poem::{handler, ctx, web::Path, tera::{TeraTemplate, Tera}}; +/// use poem::{ +/// ctx, handler, +/// tera::{Tera, TeraTemplate}, +/// web::Path, +/// }; /// use tera::Tera; /// /// #[handler] /// fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { -/// tera.render("index.html.tera", &ctx!{ "name": &name }) +/// tera.render("index.html.tera", &ctx! { "name": &name }) /// } /// ``` #[macro_export] diff --git a/poem/src/tera/transformers.rs b/poem/src/tera/transformers.rs index 1bc726f76b..718add2c13 100644 --- a/poem/src/tera/transformers.rs +++ b/poem/src/tera/transformers.rs @@ -4,31 +4,33 @@ pub mod filters { #[cfg(feature = "i18n")] #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] pub mod i18n { - use std::{collections::HashMap, borrow::Cow}; - use tera::{self, Tera, Value, Filter}; + use std::{borrow::Cow, collections::HashMap}; + use fluent::{ types::{FluentNumber, FluentNumberOptions}, FluentValue, }; - use crate::{Request, i18n::Locale, FromRequestSync}; + use tera::{self, Filter, Tera, Value}; + + use crate::{i18n::Locale, FromRequestSync, Request}; /// Tera Templating i18n filter - /// + /// /// ```no_compile /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; - /// + /// /// let resources = I18NResources::builder() /// .add_path("resources") /// .build() /// .unwrap(); - /// + /// /// let app = Route::new() /// .with(TeraTemplating::from_glob("templates/**/*")) /// .using(filters::i18n::translate) /// .data(resources); /// ``` pub struct TranslateFilter { - locale: Locale + locale: Locale, } impl Filter for TranslateFilter { @@ -58,26 +60,29 @@ pub mod filters { .map_err(|err| tera::Error::msg(err)) } } - + /// Tera Templating built-in filters - /// + /// /// ```no_compile /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; - /// + /// /// let resources = I18NResources::builder() /// .add_path("resources") /// .build() /// .unwrap(); - /// + /// /// let app = Route::new() /// .with(TeraTemplating::from_glob("templates/**/*")) /// .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() - }); + 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 6efea4a608..69a7c5d91f 100644 --- a/poem/src/web/mod.rs +++ b/poem/src/web/mod.rs @@ -756,7 +756,10 @@ impl<'a> FromRequest<'a> for &'a Request { } #[async_trait::async_trait] -impl<'a, T> FromRequest<'a> for &'a T where &'a T: FromRequestSync<'a> { +impl<'a, T> FromRequest<'a> for &'a T +where + &'a T: FromRequestSync<'a>, +{ async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { Self::from_request_sync(req, body) } From d9d7165f556bcb611c7da5de981860e37a7cdd07 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 14:36:10 +0100 Subject: [PATCH 14/25] style: ran cargo fmt --- examples/poem/tera-i18n/src/main.rs | 11 +++++------ examples/poem/tera-templating/src/main.rs | 7 +++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/poem/tera-i18n/src/main.rs b/examples/poem/tera-i18n/src/main.rs index 7f89b240d7..c6bced680c 100644 --- a/examples/poem/tera-i18n/src/main.rs +++ b/examples/poem/tera-i18n/src/main.rs @@ -1,21 +1,20 @@ use poem::{ ctx, get, handler, + i18n::I18NResources, listener::TcpListener, + tera::{filters, Tera, TeraTemplate, TeraTemplating}, web::Path, - Route, Server, - EndpointExt, - tera::{TeraTemplating, TeraTemplate, Tera, filters}, - i18n::I18NResources + EndpointExt, Route, Server, }; #[handler] fn index(tera: Tera) -> TeraTemplate { - tera.render("index.html.tera", &ctx!{}) + tera.render("index.html.tera", &ctx! {}) } #[handler] fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { - tera.render("hello.html.tera", &ctx!{ "name": &name }) + tera.render("hello.html.tera", &ctx! { "name": &name }) } #[tokio::main] diff --git a/examples/poem/tera-templating/src/main.rs b/examples/poem/tera-templating/src/main.rs index 56f16431e1..a810dcf5ac 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -1,15 +1,14 @@ use poem::{ ctx, get, handler, listener::TcpListener, + tera::{Tera, TeraTemplate, TeraTemplating}, web::Path, - Route, Server, - EndpointExt, - tera::{TeraTemplating, TeraTemplate, Tera} + EndpointExt, Route, Server, }; #[handler] fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { - tera.render("index.html.tera", &ctx!{ "name": &name }) + tera.render("index.html.tera", &ctx! { "name": &name }) } #[tokio::main] From 552333b9324d731832647c0f768327ee1a91d630 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 15:01:26 +0100 Subject: [PATCH 15/25] fix: merge FromRequest and FromRequestSync --- poem/src/i18n/locale.rs | 4 ++-- poem/src/lib.rs | 2 +- poem/src/web/mod.rs | 38 ++++++++++---------------------------- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/poem/src/i18n/locale.rs b/poem/src/i18n/locale.rs index 3bd5c4c718..ffda1c40f6 100644 --- a/poem/src/i18n/locale.rs +++ b/poem/src/i18n/locale.rs @@ -7,7 +7,7 @@ use unic_langid::LanguageIdentifier; use crate::{ error::I18NError, i18n::{I18NArgs, I18NBundle, I18NResources}, - FromRequestSync, Request, RequestBody, Result, + FromRequest, Request, RequestBody, Result, }; type LanguageArray = SmallVec<[LanguageIdentifier; 8]>; @@ -85,7 +85,7 @@ impl Locale { } #[async_trait::async_trait] -impl<'a> FromRequestSync<'a> for Locale { +impl<'a> FromRequest<'a> for Locale { fn from_request_sync(req: &'a Request, _body: &mut RequestBody) -> Result { let resources = req .extensions() diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 7fd9f221e4..ab5c16babb 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -309,4 +309,4 @@ pub use route::{ }; #[cfg(feature = "server")] pub use server::Server; -pub use web::{FromRequest, FromRequestSync, IntoResponse, RequestBody}; +pub use web::{FromRequest, IntoResponse, RequestBody}; diff --git a/poem/src/web/mod.rs b/poem/src/web/mod.rs index 69a7c5d91f..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; + async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { + Self::from_request_sync(req, body) + } /// Extract from request head. /// @@ -326,23 +328,13 @@ pub trait FromRequest<'a>: Sized { async fn from_request_without_body(req: &'a Request) -> Result { Self::from_request(req, &mut Default::default()).await } -} -/// Represents an type that can be extract from requests synchronously. -/// See [crate::web::FromRequest] -pub trait FromRequestSync<'a>: Sized { - /// Extract from request head and body. - fn from_request_sync(req: &'a Request, body: &mut RequestBody) -> Result; + /// Extract from request head and body synchronously. + fn from_request_sync(_req: &'a Request, _body: &mut RequestBody) -> Result { + panic!("Not implemented, please implement one of from_request and from_request_sync"); + } - /// Extract from request head. - /// - /// If you know that this type does not need to extract the body, then you - /// can just use it. - /// - /// For example [`Query`], [`Path`] they only extract the content from the - /// request head, using this method would be more convenient. - /// `String`,`Vec` they extract the body of the request, using this - /// method will cause `ReadBodyError` error. + /// Extract from request head synchronously. fn from_request_without_body_sync(req: &'a Request) -> Result { Self::from_request_sync(req, &mut Default::default()) } @@ -755,16 +747,6 @@ impl<'a> FromRequest<'a> for &'a Request { } } -#[async_trait::async_trait] -impl<'a, T> FromRequest<'a> for &'a T -where - &'a T: FromRequestSync<'a>, -{ - async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { - Self::from_request_sync(req, body) - } -} - #[async_trait::async_trait] impl<'a> FromRequest<'a> for &'a Uri { async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { @@ -837,14 +819,14 @@ impl<'a> FromRequest<'a> for &'a LocalAddr { } #[async_trait::async_trait] -impl<'a, T: FromRequest<'a>> FromRequest<'a> for Option { +impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Option { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { Ok(T::from_request(req, body).await.ok()) } } #[async_trait::async_trait] -impl<'a, T: FromRequest<'a>> FromRequest<'a> for Result { +impl<'a, T: FromRequest<'a> + Send> FromRequest<'a> for Result { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { Ok(T::from_request(req, body).await) } From d46fbba7b4310668e46cd2ae83fafb1e401f0297 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 15:05:51 +0100 Subject: [PATCH 16/25] docs: replace no_run by no_compile --- poem/src/tera/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index a956c49b1f..bbf1c8d230 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -2,7 +2,7 @@ //! //! # Load templates from file system using a glob //! -//! ```no_run +//! ```no_compile //! use poem::tera::TeraTemplating; //! //! let templating = TeraTemplating::from_glob("templates/**/*"); @@ -10,7 +10,7 @@ //! //! # Render a template inside an handler with some context vars //! -//! ```no_run +//! ```no_compile //! use poem::{ //! ctx, handler, //! tera::{Tera, TeraTemplate}, @@ -37,7 +37,7 @@ pub use self::{ }; /// Macro for constructing a Tera Context -/// ```no_run +/// ```no_compile /// use poem::{ /// ctx, handler, /// tera::{Tera, TeraTemplate}, From fead508cfe3396f3c7ba448c73c26605f6925e08 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 15:08:55 +0100 Subject: [PATCH 17/25] fix: use FromRequest in tera transformers --- poem/src/tera/transformers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poem/src/tera/transformers.rs b/poem/src/tera/transformers.rs index 718add2c13..6960568cbe 100644 --- a/poem/src/tera/transformers.rs +++ b/poem/src/tera/transformers.rs @@ -12,7 +12,7 @@ pub mod filters { }; use tera::{self, Filter, Tera, Value}; - use crate::{i18n::Locale, FromRequestSync, Request}; + use crate::{i18n::Locale, FromRequest, Request}; /// Tera Templating i18n filter /// From 41813a895f0ad220e4406753e6bc49c9310aad2a Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 15:17:13 +0100 Subject: [PATCH 18/25] fix: update openapi to match new FromRequest trait --- poem-openapi/src/base.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs index dd9a735f39..483d772262 100644 --- a/poem-openapi/src/base.rs +++ b/poem-openapi/src/base.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, fmt::{self, Debug, Display}, ops::Deref, + marker::Send }; use poem::{endpoint::BoxEndpoint, http::Method, Error, FromRequest, Request, RequestBody, Result}; @@ -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 = (); From f68604733be2bb0cbfb2cff17b3c423c3cfa1ac8 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sat, 18 Feb 2023 15:20:01 +0100 Subject: [PATCH 19/25] style: ran cargo fmt --- poem-openapi/src/base.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs index 483d772262..068b05ae5d 100644 --- a/poem-openapi/src/base.rs +++ b/poem-openapi/src/base.rs @@ -1,8 +1,8 @@ use std::{ collections::HashMap, fmt::{self, Debug, Display}, + marker::Send, ops::Deref, - marker::Send }; use poem::{endpoint::BoxEndpoint, http::Method, Error, FromRequest, Request, RequestBody, Result}; From ce73a80f35e3f14677db8b201390c8c5ae74d2d5 Mon Sep 17 00:00:00 2001 From: Galitan-dev Date: Sun, 19 Feb 2023 18:55:10 +0100 Subject: [PATCH 20/25] fix: use tracing --- poem/src/tera/middleware.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 79de229763..00a28f1e67 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -25,7 +25,8 @@ impl TeraTemplatingMiddleware { let tera = match Tera::new(glob) { Ok(t) => t, Err(e) => { - println!("Parsing error(s): {e}"); + tracing::error!("Failed to parse Tera template: {err}"); + tracing::debug!("Tera Parsing error: {err:?}"); ::std::process::exit(1); } }; @@ -109,7 +110,8 @@ pub type TeraTemplatingResult = tera::Result; impl IntoResult> for TeraTemplatingResult { fn into_result(self) -> Result> { if let Err(err) = &self { - println!("{err:?}"); + tracing::error!("Failed to render Tera template: {err}"); + tracing::debug!("Tera Rendering error: {err:?}"); } self.map_err(InternalServerError).map(Html) From 786f4c6ca334c89325c77f5432de1fca1ec6d53e Mon Sep 17 00:00:00 2001 From: ~Nebula~ Date: Thu, 23 Mar 2023 10:30:20 +0000 Subject: [PATCH 21/25] Merge from master to feat/tera --- .rustfmt.toml | 10 +++--- Cargo.toml | 10 +++--- examples/poem/tera-templating/Cargo.toml | 1 + examples/poem/tera-templating/src/main.rs | 8 ++++- poem-dbsession/Cargo.toml | 2 +- poem-derive/Cargo.toml | 2 +- poem-grpc-build/Cargo.toml | 2 +- poem-grpc/Cargo.toml | 2 +- poem-lambda/Cargo.toml | 2 +- poem-openapi-derive/Cargo.toml | 2 +- poem-openapi/Cargo.toml | 2 +- poem/CHANGELOG.md | 7 +++++ poem/Cargo.toml | 14 +++++++-- poem/README.md | 3 +- poem/src/lib.rs | 3 +- poem/src/listener/acme/client.rs | 14 ++++----- poem/src/listener/mod.rs | 10 +++--- poem/src/tera/middleware.rs | 38 ++++++++++++++++++++++- poem/src/web/sse/response.rs | 18 ++++++----- 19 files changed, 107 insertions(+), 43 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 88af0ae588..b2cfe19264 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,12 +1,12 @@ edition = "2021" newline_style = "unix" # comments -normalize_comments=true -wrap_comments=true -format_code_in_doc_comments=true +normalize_comments = true +#wrap_comments=true +format_code_in_doc_comments = true # imports -imports_granularity="Crate" -group_imports="StdExternalCrate" +imports_granularity = "Crate" +group_imports = "StdExternalCrate" # report #report_fixme="Unnumbered" #report_todo="Unnumbered" diff --git a/Cargo.toml b/Cargo.toml index d469adb1bc..d13f5cece8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ repository = "https://github.com/poem-web/poem" rust-version = "1.64" [workspace.dependencies] -poem = { path = "poem", version = "1.3.54", default-features = false } -poem-derive = { path = "poem-derive", version = "1.3.54" } +poem = { path = "poem", version = "1.3.55", default-features = false } +poem-derive = { path = "poem-derive", version = "1.3.55" } poem-openapi-derive = { path = "poem-openapi-derive", version = "2.0.25" } poem-grpc-build = { path = "poem-grpc-build", version = "0.2.18" } @@ -37,6 +37,8 @@ bytes = "1.1.0" futures-util = "0.3.17" tokio-stream = "0.1.8" serde_yaml = "0.9" -quick-xml = { version = "=0.26.0", features = ["serialize"] } # https://github.com/tafia/quick-xml/issues/540 +quick-xml = { version = "=0.26.0", features = [ + "serialize", +] } # https://github.com/tafia/quick-xml/issues/540 base64 = "0.21.0" -serde_urlencoded = "0.7.1" \ No newline at end of file +serde_urlencoded = "0.7.1" diff --git a/examples/poem/tera-templating/Cargo.toml b/examples/poem/tera-templating/Cargo.toml index 22f813fce8..fc992645ff 100644 --- a/examples/poem/tera-templating/Cargo.toml +++ b/examples/poem/tera-templating/Cargo.toml @@ -8,3 +8,4 @@ publish.workspace = true poem = { workspace = true, features = ["tera"] } tokio = { workspace = true, features = ["full"] } 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 a810dcf5ac..e188cf5da4 100644 --- a/examples/poem/tera-templating/src/main.rs +++ b/examples/poem/tera-templating/src/main.rs @@ -13,9 +13,15 @@ fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { #[tokio::main] async fn main() -> Result<(), std::io::Error> { + 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(TeraTemplating::from_glob("templates/**/*")) + .with_live_reloading(); Server::new(TcpListener::bind("127.0.0.1:3000")) .run(app) diff --git a/poem-dbsession/Cargo.toml b/poem-dbsession/Cargo.toml index b1bb46f8e0..9754377e1f 100644 --- a/poem-dbsession/Cargo.toml +++ b/poem-dbsession/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-dbsession" -version = "0.3.54" +version = "0.3.55" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-derive/Cargo.toml b/poem-derive/Cargo.toml index a3e5f8f0e5..98034bcb50 100644 --- a/poem-derive/Cargo.toml +++ b/poem-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-derive" -version = "1.3.54" +version = "1.3.55" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc-build/Cargo.toml b/poem-grpc-build/Cargo.toml index f1f1f858bb..f135d4f33e 100644 --- a/poem-grpc-build/Cargo.toml +++ b/poem-grpc-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc-build" -version = "0.2.18" +version = "0.2.19" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index a5e67e7229..7ba0a7f250 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc" -version = "0.2.18" +version = "0.2.19" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-lambda/Cargo.toml b/poem-lambda/Cargo.toml index bd33586977..eadc5ced3b 100644 --- a/poem-lambda/Cargo.toml +++ b/poem-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-lambda" -version = "1.3.54" +version = "1.3.55" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi-derive/Cargo.toml b/poem-openapi-derive/Cargo.toml index eacf71aebd..63c561c20e 100644 --- a/poem-openapi-derive/Cargo.toml +++ b/poem-openapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi-derive" -version = "2.0.25" +version = "2.0.26" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index c13278c214..4a7217fd9a 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi" -version = "2.0.25" +version = "2.0.26" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem/CHANGELOG.md b/poem/CHANGELOG.md index c8cbc57cd1..4789c71918 100644 --- a/poem/CHANGELOG.md +++ b/poem/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [1.3.55] 2023-02-18 + +- fix: export real error `RedisSessionError` for `redis-session` [#501](fix: export real error `RedisSessionError` for `redis-session`) +- Add From implementation for I18NArgs [#507](https://github.com/poem-web/poem/pull/507) +- fix errors when parse yaml & xml request [#498](https://github.com/poem-web/poem/pull/498) +- fix `SSE::keep_alive` caused the event stream to not terminate properly. + # [1.3.53] 2023-01-31 - fix: static_files percent encode filename [#495](https://github.com/poem-web/poem/pull/495) diff --git a/poem/Cargo.toml b/poem/Cargo.toml index caa3884a9c..e19d57b118 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem" -version = "1.3.54" +version = "1.3.55" authors.workspace = true edition.workspace = true license.workspace = true @@ -50,7 +50,10 @@ i18n = [ "unic-langid", "intl-memoizer", ] -acme = [ +acme = ["acme-native-roots"] +acme-native-roots = ["acme-base", "hyper-rustls/native-tokio"] +acme-webpki-roots = ["acme-base", "hyper-rustls/webpki-tokio"] +acme-base = [ "server", "hyper/client", "rustls", @@ -145,7 +148,12 @@ fluent-syntax = { version = "0.11.0", optional = true } unic-langid = { version = "0.9.0", optional = true, features = ["macros"] } intl-memoizer = { version = "0.5.1", optional = true } ring = { version = "0.16.20", optional = true } -hyper-rustls = { version = "0.23.0", optional = true } +hyper-rustls = { version = "0.23.0", optional = true, default-features = false, features = [ + "http1", + "http2", + "tls12", + "logging", +] } rcgen = { version = "0.10.0", optional = true } x509-parser = { version = "0.14.0", optional = true } tokio-metrics = { version = "0.1.0", optional = true } diff --git a/poem/README.md b/poem/README.md index 19d90f30ba..7978d66d03 100644 --- a/poem/README.md +++ b/poem/README.md @@ -71,7 +71,8 @@ which are disabled by default: | anyhow | Integrate with [`anyhow`](https://crates.io/crates/anyhow) crate. | | eyre06 | Integrate with version 0.6.x of the [`eyre`](https://crates.io/crates/eyre) crate. | | i18n | Support for internationalization | -| acme | Support for ACME(Automatic Certificate Management Environment) | +| acme-native-roots | Support for ACME(Automatic Certificate Management Environment) | +| acme-webpki-roots | Support for ACME using webpki TLS roots rather than native TLS roots | | 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. | diff --git a/poem/src/lib.rs b/poem/src/lib.rs index ab5c16babb..57c13b2d55 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -249,7 +249,8 @@ //! | anyhow | Integrate with the [`anyhow`](https://crates.io/crates/anyhow) crate. | //! | eyre06 | Integrate with version 0.6.x of the [`eyre`](https://crates.io/crates/eyre) crate. | //! | i18n | Support for internationalization | -//! | acme | Support for ACME(Automatic Certificate Management Environment) | +//! | acme-native-roots | Support for ACME(Automatic Certificate Management Environment) | +//! | acme-webpki-roots | Support for ACME using webpki TLS roots rather than native TLS roots | //! | tokio-metrics | Integrate with the [`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. | diff --git a/poem/src/listener/acme/client.rs b/poem/src/listener/acme/client.rs index 6ea6805d1d..3a643c95eb 100644 --- a/poem/src/listener/acme/client.rs +++ b/poem/src/listener/acme/client.rs @@ -35,13 +35,13 @@ impl AcmeClient { key_pair: Arc, contacts: Vec, ) -> IoResult { - let client = Client::builder().build( - HttpsConnectorBuilder::new() - .with_native_roots() - .https_or_http() - .enable_http1() - .build(), - ); + let client_builder = HttpsConnectorBuilder::new(); + #[cfg(feature = "acme-native-roots")] + let client_builder1 = client_builder.with_native_roots(); + #[cfg(all(feature = "acme-webpki-roots", not(feature = "acme-native-roots")))] + let client_builder1 = client_builder.with_webpki_roots(); + let client = + Client::builder().build(client_builder1.https_or_http().enable_http1().build()); let directory = get_directory(&client, directory_url).await?; Ok(Self { client, diff --git a/poem/src/listener/mod.rs b/poem/src/listener/mod.rs index 2efdae4d2f..06552939da 100644 --- a/poem/src/listener/mod.rs +++ b/poem/src/listener/mod.rs @@ -1,7 +1,7 @@ //! Commonly used listeners. -#[cfg(feature = "acme")] -#[cfg_attr(docsrs, doc(cfg(feature = "acme")))] +#[cfg(feature = "acme-base")] +#[cfg_attr(docsrs, doc(cfg(feature = "acme-base")))] pub mod acme; mod combined; #[cfg(any(feature = "native-tls", feature = "rustls", feature = "openssl-tls"))] @@ -29,7 +29,7 @@ use futures_util::{future::BoxFuture, FutureExt, TryFutureExt}; use http::uri::Scheme; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, Result as IoResult}; -#[cfg(feature = "acme")] +#[cfg(feature = "acme-base")] use self::acme::{AutoCert, AutoCertListener}; #[cfg(any(feature = "native-tls", feature = "rustls", feature = "openssl-tls"))] pub use self::handshake_stream::HandshakeStream; @@ -213,8 +213,8 @@ pub trait Listener: Send { /// .unwrap(), /// ); /// ``` - #[cfg(feature = "acme")] - #[cfg_attr(docsrs, doc(cfg(feature = "acme")))] + #[cfg(feature = "acme-base")] + #[cfg_attr(docsrs, doc(cfg(feature = "acme-base")))] #[must_use] fn acme(self, auto_cert: AutoCert) -> AutoCertListener where diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 00a28f1e67..11660f210a 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -34,6 +34,19 @@ impl TeraTemplatingMiddleware { Self { tera } } + /// Create a new instance of TeraTemplating, containing all the parsed + /// templates found in the directory. The errors are already handled. Use + /// TeraTemplating::custom(tera: Tera) to modify tera settings. + /// + /// ```no_compile + /// use poem::tera::TeraTemplating; + /// + /// let templating = TeraTemplating::from_glob("templates"); + /// ``` + pub fn from_directory(template_directory: &str) -> Self { + Self::from_glob(&format!("{template_directory}/**/*")); + } + /// Create a new instance of TeraTemplating, using the provided Tera /// instance /// @@ -55,6 +68,12 @@ impl TeraTemplatingMiddleware { } } +impl Default for TeraTemplatingMiddleware { + fn default() -> Self { + Self::from_directory("templates") + } +} + impl Middleware for TeraTemplatingMiddleware { type Output = TeraTemplatingEndpoint; @@ -85,7 +104,7 @@ impl Endpoint for TeraTemplatingEndpoint { transformer(&mut tera, &mut req); } - req.extensions_mut().insert(tera); + req.set_data(tera); self.inner.call(req).await } @@ -134,4 +153,21 @@ impl TeraTemplatingEndpoint { self.transformers.push(transformer); self } + + /// Enable live reloading only for debug mode (not for release) + /// + /// ```no_compile + /// use poem::{Route, EndpointExt, tera::TeraTemplating}; + /// + /// let app = Route::new() + /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .with_live_reloading(); + /// ``` + pub fn with_live_reloading(self) -> Self { + #[cfg(debug_assertions)] { + tracing::debug!("Live Reloading for Tera Templating is enabled"); + } + + self + } } diff --git a/poem/src/web/sse/response.rs b/poem/src/web/sse/response.rs index f1acbe1b3e..a9d7c68d17 100644 --- a/poem/src/web/sse/response.rs +++ b/poem/src/web/sse/response.rs @@ -1,3 +1,5 @@ +use std::task::Poll; + use bytes::Bytes; use futures_util::{stream::BoxStream, Stream, StreamExt}; use tokio::time::Duration; @@ -68,14 +70,14 @@ impl IntoResponse for SSE { .boxed(); if let Some(duration) = self.keep_alive { let comment = Bytes::from_static(b":\n\n"); - stream = futures_util::stream::select( - stream, - tokio_stream::wrappers::IntervalStream::new(tokio::time::interval_at( - tokio::time::Instant::now() + duration, - duration, - )) - .map(move |_| Ok(comment.clone())), - ) + let mut interval = + tokio::time::interval_at(tokio::time::Instant::now() + duration, duration); + stream = futures_util::stream::poll_fn(move |cx| { + if let Poll::Ready(msg) = stream.poll_next_unpin(cx) { + return Poll::Ready(msg); + } + interval.poll_tick(cx).map(|_| Some(Ok(comment.clone()))) + }) .boxed(); } From d83f398882ca1b66e87e2d01fe13da1c2b51475e Mon Sep 17 00:00:00 2001 From: ~Nebula~ Date: Thu, 23 Mar 2023 10:31:24 +0000 Subject: [PATCH 22/25] Fix doctests --- poem/src/tera/middleware.rs | 31 +++++++++++++------------------ poem/src/tera/mod.rs | 7 +++---- poem/src/tera/transformers.rs | 8 ++++---- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs index 11660f210a..646ea6c4c8 100644 --- a/poem/src/tera/middleware.rs +++ b/poem/src/tera/middleware.rs @@ -16,7 +16,7 @@ impl TeraTemplatingMiddleware { /// templates found in the glob The errors are already handled. Use /// TeraTemplating::custom(tera: Tera) to modify tera settings. /// - /// ```no_compile + /// ```no_run /// use poem::tera::TeraTemplating; /// /// let templating = TeraTemplating::from_glob("templates/**/*"); @@ -25,9 +25,9 @@ impl TeraTemplatingMiddleware { let tera = match Tera::new(glob) { Ok(t) => t, Err(e) => { - tracing::error!("Failed to parse Tera template: {err}"); - tracing::debug!("Tera Parsing error: {err:?}"); - ::std::process::exit(1); + // todo: move this up the stack via Result? + tracing::debug!("Tera Parsing error: {e:?}"); + panic!("Failed to parse Tera template: {e}"); } }; @@ -38,28 +38,23 @@ impl TeraTemplatingMiddleware { /// templates found in the directory. The errors are already handled. Use /// TeraTemplating::custom(tera: Tera) to modify tera settings. /// - /// ```no_compile + /// ```no_run /// use poem::tera::TeraTemplating; /// /// let templating = TeraTemplating::from_glob("templates"); /// ``` pub fn from_directory(template_directory: &str) -> Self { - Self::from_glob(&format!("{template_directory}/**/*")); + Self::from_glob(&format!("{template_directory}/**/*")) } /// Create a new instance of TeraTemplating, using the provided Tera /// instance /// - /// ```no_compile + /// ```no_run /// use poem::tera::{TeraTemplating, Tera}; /// - /// let mut tera = match Tera::new("templates/**/*") { - /// Ok(t) => t, - /// Err(e) => { - /// println!("Parsing error(s): {e}"); - /// ::std::process::exit(1); - /// } - /// }; + /// let mut tera = Tera::new("templates/**/*").expect("Failed to parse templates"); + /// /// tera.autoescape_on(vec![".html", ".sql"]); /// let templating = TeraTemplating::custom(tera); /// ``` @@ -142,7 +137,7 @@ impl TeraTemplatingEndpoint { /// instance, registering a dynamic filter) before passing tera to /// request handlers /// - /// ```no_compile + /// ```no_run /// use poem::{Route, EndpointExt, tera::TeraTemplating}; /// /// let app = Route::new() @@ -156,7 +151,7 @@ impl TeraTemplatingEndpoint { /// Enable live reloading only for debug mode (not for release) /// - /// ```no_compile + /// ```no_run /// use poem::{Route, EndpointExt, tera::TeraTemplating}; /// /// let app = Route::new() @@ -166,8 +161,8 @@ impl TeraTemplatingEndpoint { pub fn with_live_reloading(self) -> Self { #[cfg(debug_assertions)] { tracing::debug!("Live Reloading for Tera Templating is enabled"); - } - + } + self } } diff --git a/poem/src/tera/mod.rs b/poem/src/tera/mod.rs index bbf1c8d230..81082c834a 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/tera/mod.rs @@ -2,7 +2,7 @@ //! //! # Load templates from file system using a glob //! -//! ```no_compile +//! ```no_run //! use poem::tera::TeraTemplating; //! //! let templating = TeraTemplating::from_glob("templates/**/*"); @@ -10,7 +10,7 @@ //! //! # Render a template inside an handler with some context vars //! -//! ```no_compile +//! ``` //! use poem::{ //! ctx, handler, //! tera::{Tera, TeraTemplate}, @@ -37,13 +37,12 @@ pub use self::{ }; /// Macro for constructing a Tera Context -/// ```no_compile +/// ``` /// use poem::{ /// ctx, handler, /// tera::{Tera, TeraTemplate}, /// web::Path, /// }; -/// use tera::Tera; /// /// #[handler] /// fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { diff --git a/poem/src/tera/transformers.rs b/poem/src/tera/transformers.rs index 6960568cbe..664971f537 100644 --- a/poem/src/tera/transformers.rs +++ b/poem/src/tera/transformers.rs @@ -16,8 +16,8 @@ pub mod filters { /// Tera Templating i18n filter /// - /// ```no_compile - /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; + /// ```no_run + /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, filters}}; /// /// let resources = I18NResources::builder() /// .add_path("resources") @@ -63,8 +63,8 @@ pub mod filters { /// Tera Templating built-in filters /// - /// ```no_compile - /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, transformers::filters}}; + /// ```no_run + /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, filters}}; /// /// let resources = I18NResources::builder() /// .add_path("resources") From 0051af430c58b81fa92001b4485a620efb5d8905 Mon Sep 17 00:00:00 2001 From: ~Nebula~ Date: Thu, 23 Mar 2023 12:01:05 +0000 Subject: [PATCH 23/25] Make template rendering engine-agnostic Rename some tera types and fix docs. --- poem/Cargo.toml | 3 +- poem/src/lib.rs | 6 +- poem/src/templates/mod.rs | 5 + poem/src/templates/template.rs | 56 ++++++ poem/src/templates/tera/middleware.rs | 159 +++++++++++++++++ poem/src/{ => templates}/tera/mod.rs | 26 +-- poem/src/{ => templates}/tera/transformers.rs | 14 +- poem/src/tera/middleware.rs | 168 ------------------ 8 files changed, 245 insertions(+), 192 deletions(-) create mode 100644 poem/src/templates/mod.rs create mode 100644 poem/src/templates/template.rs create mode 100644 poem/src/templates/tera/middleware.rs rename poem/src/{ => templates}/tera/mod.rs (56%) rename poem/src/{ => templates}/tera/transformers.rs (89%) delete mode 100644 poem/src/tera/middleware.rs diff --git a/poem/Cargo.toml b/poem/Cargo.toml index e19d57b118..e1ca57c91b 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -67,7 +67,8 @@ acme-base = [ embed = ["rust-embed", "hex", "mime_guess"] xml = ["quick-xml"] yaml = ["serde_yaml"] -tera = ["dep:tera"] +templates = [] +tera = ["dep:tera", "templates"] [dependencies] poem-derive.workspace = true diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 57c13b2d55..dd82445179 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -276,9 +276,9 @@ pub mod middleware; #[cfg(feature = "session")] #[cfg_attr(docsrs, doc(cfg(feature = "session")))] pub mod session; -#[cfg(feature = "tera")] -#[cfg_attr(docsrs, doc(cfg(feature = "tera")))] -pub mod tera; +#[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/mod.rs b/poem/src/templates/mod.rs new file mode 100644 index 0000000000..86e5aa3936 --- /dev/null +++ b/poem/src/templates/mod.rs @@ -0,0 +1,5 @@ +mod template; pub use template::Template; + +#[cfg(feature = "templates")] +#[cfg_attr(docsrs, doc(cfg(feature = "templates")))] +pub mod tera; \ 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) -> Template { +/// Template::render("index.html.tera", &ctx! { "name": &name }) +/// } +/// ``` +pub struct Template { + /// Path to the template. + pub name: String, + /// Template context. This is used + /// by engines for additional data. + pub context: C, +} + +impl Template { + /// Renders the template. + pub fn render(name: impl Into, context: C) -> Self { + Self { + name: name.into(), + context, + } + } +} + +impl IntoResponse for Template { + 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..470f95c993 --- /dev/null +++ b/poem/src/templates/tera/middleware.rs @@ -0,0 +1,159 @@ +use tera::Tera; + +use crate::{ + templates::Template, + error::{InternalServerError, IntoResult}, + web::Html, + Endpoint, Middleware, Request, Result, + Response, IntoResponse, +}; + +/// Tera template with context. +pub type TeraTemplate = Template; + +/// 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 { + 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::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 Middleware for TeraEngine { + type Output = TeraEndpoint; + + fn transform(&self, inner: E) -> Self::Output { + Self::Output { + tera: self.tera.clone(), + inner, + transformers: Vec::new(), + } + } +} + +/// Tera templates endpoint. +pub struct TeraEndpoint { + tera: Tera, + inner: E, + transformers: Vec, +} + +#[async_trait::async_trait] +impl Endpoint for TeraEndpoint { + type Output = Response; + + async fn call(&self, mut req: Request) -> Result { + let mut tera = self.tera.clone(); + + for transformer in &self.transformers { + transformer(&mut tera, &mut req); + } + + let response = self.inner.call(req).await?.into_response(); + + match response.extensions().get::() { + 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: double check if we should always error here + tracing::error!("Missing template response"); + + response.into_result() + } + } + } +} + +impl TeraEndpoint { + /// 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(mut self, transformer: fn(&mut Tera, &mut Request)) -> Self { + self.transformers.push(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(true); + /// ``` + pub fn with_live_reloading(self, live_reloading: bool) -> Self { + tracing::debug!("Live Reloading for Tera templates is enabled"); + + todo!(); + + self + } +} diff --git a/poem/src/tera/mod.rs b/poem/src/templates/tera/mod.rs similarity index 56% rename from poem/src/tera/mod.rs rename to poem/src/templates/tera/mod.rs index 81082c834a..1ea191084f 100644 --- a/poem/src/tera/mod.rs +++ b/poem/src/templates/tera/mod.rs @@ -3,9 +3,9 @@ //! # Load templates from file system using a glob //! //! ```no_run -//! use poem::tera::TeraTemplating; +//! use poem::templates::tera::TeraEngine; //! -//! let templating = TeraTemplating::from_glob("templates/**/*"); +//! let tera = TeraEngine::default(); //! ``` //! //! # Render a template inside an handler with some context vars @@ -13,26 +13,23 @@ //! ``` //! use poem::{ //! ctx, handler, -//! tera::{Tera, TeraTemplate}, +//! templates::Template, //! web::Path, //! }; //! //! #[handler] -//! fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { -//! tera.render("index.html.tera", &ctx! { "name": &name }) +//! fn hello(Path(name): Path) -> Template<_> { +//! Template::render("index.html.tera", &ctx! { "name": &name }) //! } //! ``` mod middleware; mod transformers; -pub use tera::{Context, Tera}; +pub use tera::{ Context, Tera }; pub use self::{ - middleware::{ - TeraTemplatingEndpoint, TeraTemplatingMiddleware as TeraTemplating, - TeraTemplatingResult as TeraTemplate, - }, + middleware::{ TeraEndpoint, TeraEngine, TeraTemplate }, transformers::filters, }; @@ -40,15 +37,18 @@ pub use self::{ /// ``` /// use poem::{ /// ctx, handler, -/// tera::{Tera, TeraTemplate}, +/// templates::Template, /// web::Path, /// }; /// /// #[handler] -/// fn hello(Path(name): Path, tera: Tera) -> TeraTemplate { -/// tera.render("index.html.tera", &ctx! { "name": &name }) +/// fn hello(Path(name): Path) -> 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 ),* } => { diff --git a/poem/src/tera/transformers.rs b/poem/src/templates/tera/transformers.rs similarity index 89% rename from poem/src/tera/transformers.rs rename to poem/src/templates/tera/transformers.rs index 664971f537..71197e90dd 100644 --- a/poem/src/tera/transformers.rs +++ b/poem/src/templates/tera/transformers.rs @@ -1,6 +1,6 @@ -/// Tera Templating built-in filters +/// Tera templates built-in filters pub mod filters { - /// Tera Templating built-in i18n filters + /// Tera templates built-in i18n filters #[cfg(feature = "i18n")] #[cfg_attr(docsrs, doc(cfg(feature = "i18n")))] pub mod i18n { @@ -14,10 +14,10 @@ pub mod filters { use crate::{i18n::Locale, FromRequest, Request}; - /// Tera Templating i18n filter + /// Tera templates i18n filter /// /// ```no_run - /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, filters}}; + /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}}; /// /// let resources = I18NResources::builder() /// .add_path("resources") @@ -25,7 +25,7 @@ pub mod filters { /// .unwrap(); /// /// let app = Route::new() - /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .with(TeraEngine::default()) /// .using(filters::i18n::translate) /// .data(resources); /// ``` @@ -64,7 +64,7 @@ pub mod filters { /// Tera Templating built-in filters /// /// ```no_run - /// use poem::{Route, EndpointExt, i18n::I18NResources, tera::{TeraTemplating, filters}}; + /// use poem::{Route, EndpointExt, i18n::I18NResources, templates::tera::{TeraEngine, filters}}; /// /// let resources = I18NResources::builder() /// .add_path("resources") @@ -72,7 +72,7 @@ pub mod filters { /// .unwrap(); /// /// let app = Route::new() - /// .with(TeraTemplating::from_glob("templates/**/*")) + /// .with(TeraEngine::default()) /// .using(filters::i18n::translate) /// .data(resources); /// ``` diff --git a/poem/src/tera/middleware.rs b/poem/src/tera/middleware.rs deleted file mode 100644 index 646ea6c4c8..0000000000 --- a/poem/src/tera/middleware.rs +++ /dev/null @@ -1,168 +0,0 @@ -use tera::Tera; - -use crate::{ - error::{InternalServerError, IntoResult}, - web::Html, - Endpoint, FromRequest, Middleware, Request, RequestBody, Result, -}; - -/// Tera Templating Middleware -pub struct TeraTemplatingMiddleware { - tera: Tera, -} - -impl TeraTemplatingMiddleware { - /// Create a new instance of TeraTemplating, containing all the parsed - /// templates found in the glob The errors are already handled. Use - /// TeraTemplating::custom(tera: Tera) to modify tera settings. - /// - /// ```no_run - /// use poem::tera::TeraTemplating; - /// - /// let templating = TeraTemplating::from_glob("templates/**/*"); - /// ``` - pub fn from_glob(glob: &str) -> Self { - let tera = match Tera::new(glob) { - Ok(t) => t, - Err(e) => { - // todo: move this up the stack via Result? - tracing::debug!("Tera Parsing error: {e:?}"); - panic!("Failed to parse Tera template: {e}"); - } - }; - - Self { tera } - } - - /// Create a new instance of TeraTemplating, containing all the parsed - /// templates found in the directory. The errors are already handled. Use - /// TeraTemplating::custom(tera: Tera) to modify tera settings. - /// - /// ```no_run - /// use poem::tera::TeraTemplating; - /// - /// let templating = TeraTemplating::from_glob("templates"); - /// ``` - pub fn from_directory(template_directory: &str) -> Self { - Self::from_glob(&format!("{template_directory}/**/*")) - } - - /// Create a new instance of TeraTemplating, using the provided Tera - /// instance - /// - /// ```no_run - /// use poem::tera::{TeraTemplating, Tera}; - /// - /// let mut tera = Tera::new("templates/**/*").expect("Failed to parse templates"); - /// - /// tera.autoescape_on(vec![".html", ".sql"]); - /// let templating = TeraTemplating::custom(tera); - /// ``` - pub fn custom(tera: Tera) -> Self { - Self { tera } - } -} - -impl Default for TeraTemplatingMiddleware { - fn default() -> Self { - Self::from_directory("templates") - } -} - -impl Middleware for TeraTemplatingMiddleware { - type Output = TeraTemplatingEndpoint; - - fn transform(&self, inner: E) -> Self::Output { - Self::Output { - tera: self.tera.clone(), - inner, - transformers: Vec::new(), - } - } -} - -/// Tera Templating Endpoint -pub struct TeraTemplatingEndpoint { - tera: Tera, - inner: E, - transformers: Vec, -} - -#[async_trait::async_trait] -impl Endpoint for TeraTemplatingEndpoint { - type Output = E::Output; - - async fn call(&self, mut req: Request) -> Result { - let mut tera = self.tera.clone(); - - for transformer in &self.transformers { - transformer(&mut tera, &mut req); - } - - req.set_data(tera); - - self.inner.call(req).await - } -} - -#[async_trait::async_trait] -impl<'a> FromRequest<'a> for Tera { - async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { - let tera = req - .extensions() - .get::() - .expect("To use the `Tera` extractor, the `TeraTemplating` endpoit is required.") - .clone(); - - Ok(tera) - } -} - -/// Shortcut (or not) for a Tera Templating handler Response -pub type TeraTemplatingResult = tera::Result; - -impl IntoResult> for TeraTemplatingResult { - fn into_result(self) -> Result> { - if let Err(err) = &self { - tracing::error!("Failed to render Tera template: {err}"); - tracing::debug!("Tera Rendering error: {err:?}"); - } - - self.map_err(InternalServerError).map(Html) - } -} - -impl TeraTemplatingEndpoint { - /// 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, tera::TeraTemplating}; - /// - /// let app = Route::new() - /// .with(TeraTemplating::from_glob("templates/**/*")) - /// .using(|tera, req| println!("{tera:?}\n{req:?}")); - /// ``` - pub fn using(mut self, transformer: fn(&mut Tera, &mut Request)) -> Self { - self.transformers.push(transformer); - self - } - - /// Enable live reloading only for debug mode (not for release) - /// - /// ```no_run - /// use poem::{Route, EndpointExt, tera::TeraTemplating}; - /// - /// let app = Route::new() - /// .with(TeraTemplating::from_glob("templates/**/*")) - /// .with_live_reloading(); - /// ``` - pub fn with_live_reloading(self) -> Self { - #[cfg(debug_assertions)] { - tracing::debug!("Live Reloading for Tera Templating is enabled"); - } - - self - } -} From 76914155fd9611806321ad470a98c65a9d588844 Mon Sep 17 00:00:00 2001 From: ~Nebula~ Date: Thu, 23 Mar 2023 21:53:03 +0000 Subject: [PATCH 24/25] Add live reloading of templates --- poem/Cargo.toml | 2 + poem/src/templates/live_reloading.rs | 77 +++++++++++++++++++++++++ poem/src/templates/mod.rs | 6 +- poem/src/templates/tera/middleware.rs | 82 +++++++++++++++++++++++---- poem/src/templates/tera/mod.rs | 11 ++++ 5 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 poem/src/templates/live_reloading.rs diff --git a/poem/Cargo.toml b/poem/Cargo.toml index e1ca57c91b..7afd1130c0 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -68,6 +68,7 @@ embed = ["rust-embed", "hex", "mime_guess"] xml = ["quick-xml"] yaml = ["serde_yaml"] templates = [] +live_reloading = ["dep:notify"] tera = ["dep:tera", "templates"] [dependencies] @@ -164,6 +165,7 @@ 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/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, + pub(crate) _path: String, + _watcher: Option> +} + +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) + } + 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 index 86e5aa3936..1927d40d1f 100644 --- a/poem/src/templates/mod.rs +++ b/poem/src/templates/mod.rs @@ -2,4 +2,8 @@ mod template; pub use template::Template; #[cfg(feature = "templates")] #[cfg_attr(docsrs, doc(cfg(feature = "templates")))] -pub mod tera; \ No newline at end of file +pub mod tera; + +#[cfg(feature = "live_reloading")] +#[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))] +mod live_reloading; \ No newline at end of file diff --git a/poem/src/templates/tera/middleware.rs b/poem/src/templates/tera/middleware.rs index 470f95c993..3f98dd6a06 100644 --- a/poem/src/templates/tera/middleware.rs +++ b/poem/src/templates/tera/middleware.rs @@ -1,5 +1,7 @@ use tera::Tera; +use super::Flavor; + use crate::{ templates::Template, error::{InternalServerError, IntoResult}, @@ -8,6 +10,9 @@ use crate::{ Response, IntoResponse, }; +#[cfg(feature = "live_reloading")] +use crate::templates::live_reloading::{ Watcher, LiveReloading }; + /// Tera template with context. pub type TeraTemplate = Template; @@ -73,7 +78,7 @@ impl Middleware for TeraEngine { fn transform(&self, inner: E) -> Self::Output { Self::Output { - tera: self.tera.clone(), + tera: Flavor::Immutable(self.tera.clone()), inner, transformers: Vec::new(), } @@ -82,9 +87,9 @@ impl Middleware for TeraEngine { /// Tera templates endpoint. pub struct TeraEndpoint { - tera: Tera, + tera: Flavor, inner: E, - transformers: Vec, + transformers: Vec>, } #[async_trait::async_trait] @@ -92,7 +97,31 @@ impl Endpoint for TeraEndpoint { type Output = Response; async fn call(&self, mut req: Request) -> Result { - let mut tera = self.tera.clone(); + 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); @@ -134,8 +163,10 @@ impl TeraEndpoint { /// .with(TeraEngine::default()) /// .using(|tera, req| println!("{tera:?}\n{req:?}")); /// ``` - pub fn using(mut self, transformer: fn(&mut Tera, &mut Request)) -> Self { - self.transformers.push(transformer); + pub fn using(mut self, transformer: F) -> Self where + F: Fn(&mut Tera, &mut Request) + Send + Sync + 'static + { + self.transformers.push(Box::new(transformer)); self } @@ -147,12 +178,43 @@ impl TeraEndpoint { /// /// let app = Route::new() /// .with(TeraEngine::default()) - /// .with_live_reloading(true); + /// .with_live_reloading(LiveReloading::Disabled); /// ``` - pub fn with_live_reloading(self, live_reloading: bool) -> Self { - tracing::debug!("Live Reloading for Tera templates is enabled"); - todo!(); + #[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 index 1ea191084f..d851a94c8f 100644 --- a/poem/src/templates/tera/mod.rs +++ b/poem/src/templates/tera/mod.rs @@ -33,6 +33,17 @@ pub use self::{ transformers::filters, }; +enum Flavor { + Immutable(Tera), + + #[cfg(feature = "live_reloading")] + #[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))] + LiveReload { + tera: tokio::sync::RwLock, + watcher: super::live_reloading::Watcher + } +} + /// Macro for constructing a Tera Context /// ``` /// use poem::{ From 69b0ed6edc5959883f8cfa30d8b9a7af34f1a1d6 Mon Sep 17 00:00:00 2001 From: ~Nebula~ Date: Fri, 24 Mar 2023 13:34:23 +0000 Subject: [PATCH 25/25] Fix visibility of LiveReloading --- poem/src/templates/mod.rs | 7 ++++++- poem/src/templates/tera/middleware.rs | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/poem/src/templates/mod.rs b/poem/src/templates/mod.rs index 1927d40d1f..287a7a9537 100644 --- a/poem/src/templates/mod.rs +++ b/poem/src/templates/mod.rs @@ -1,9 +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; \ No newline at end of file +mod live_reloading; + +#[cfg(feature = "live_reloading")] +pub use live_reloading::LiveReloading; \ No newline at end of file diff --git a/poem/src/templates/tera/middleware.rs b/poem/src/templates/tera/middleware.rs index 3f98dd6a06..c346666b86 100644 --- a/poem/src/templates/tera/middleware.rs +++ b/poem/src/templates/tera/middleware.rs @@ -142,9 +142,7 @@ impl Endpoint for TeraEndpoint { .map_err(InternalServerError) }, None => { - // todo: double check if we should always error here - tracing::error!("Missing template response"); - + // todo: this destroys the type response.into_result() } }