Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/tera: Support Tera Templating #515

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
717c3d3
build: added tera feature definitions
Galitan-dev Feb 17, 2023
f197387
feat: tera templating middleware, endpoint and extractor
Galitan-dev Feb 17, 2023
65d0d1a
feat: wrap tera::Result
Galitan-dev Feb 17, 2023
dc5b54c
docs: add a tera templating handler example in tera module doc
Galitan-dev Feb 17, 2023
51a9e8d
docs: add tera feature in root doc
Galitan-dev Feb 17, 2023
37b8567
build: expose tera::Tera & tera::Context from poem::tera
Galitan-dev Feb 17, 2023
d937558
feat: ctx macro
Galitan-dev Feb 18, 2023
ccb5c9e
docs: use poem::tera::Tera
Galitan-dev Feb 18, 2023
08cfc13
fix: custom tera templating
Galitan-dev Feb 18, 2023
d3e52cb
style: move tera::endpoint in tera::middleware
Galitan-dev Feb 18, 2023
0745620
feat: tera i18n "translate" filter
Galitan-dev Feb 18, 2023
eeadb51
docs: tera transformers
Galitan-dev Feb 18, 2023
b34e6d0
style: ran cargo fmt
Galitan-dev Feb 18, 2023
d9d7165
style: ran cargo fmt
Galitan-dev Feb 18, 2023
552333b
fix: merge FromRequest and FromRequestSync
Galitan-dev Feb 18, 2023
d46fbba
docs: replace no_run by no_compile
Galitan-dev Feb 18, 2023
fead508
fix: use FromRequest in tera transformers
Galitan-dev Feb 18, 2023
41813a8
fix: update openapi to match new FromRequest trait
Galitan-dev Feb 18, 2023
f686047
style: ran cargo fmt
Galitan-dev Feb 18, 2023
ce73a80
fix: use tracing
Galitan-dev Feb 19, 2023
786f4c6
Merge from master to feat/tera
nebneb0703 Mar 23, 2023
d83f398
Fix doctests
nebneb0703 Mar 23, 2023
0051af4
Make template rendering engine-agnostic
nebneb0703 Mar 23, 2023
7691415
Add live reloading of templates
nebneb0703 Mar 23, 2023
69b0ed6
Fix visibility of LiveReloading
nebneb0703 Mar 24, 2023
7e9232c
Merge branch 'poem-web:master' into feat/tera
Galitan-dev Mar 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/poem/tera-i18n/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions examples/poem/tera-i18n/resources/en-US/simple.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello-world = Hello world!
welcome = welcome { $name }!
2 changes: 2 additions & 0 deletions examples/poem/tera-i18n/resources/fr/simple.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello-world = Bonjour le monde!
welcome = Bienvenue { $name }!
2 changes: 2 additions & 0 deletions examples/poem/tera-i18n/resources/zh-CN/simple.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello-world = 你好世界!
welcome = 欢迎 { $name }!
37 changes: 37 additions & 0 deletions examples/poem/tera-i18n/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use poem::{
ctx, get, handler,
i18n::I18NResources,
listener::TcpListener,
tera::{filters, Tera, TeraTemplate, TeraTemplating},
web::Path,
EndpointExt, Route, Server,
};

#[handler]
fn index(tera: Tera) -> TeraTemplate {
tera.render("index.html.tera", &ctx! {})
}

#[handler]
fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate {
tera.render("hello.html.tera", &ctx! { "name": &name })
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let resources = I18NResources::builder()
.add_path("resources")
.build()
.unwrap();

let app = Route::new()
.at("/", get(index))
.at("/hello/:name", get(hello))
.with(TeraTemplating::from_glob("templates/**/*"))
.using(filters::i18n::translate)
.data(resources);

Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(app)
.await
}
1 change: 1 addition & 0 deletions examples/poem/tera-i18n/templates/hello.html.tera
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>{{ "welcome" | translate(name=name) }}</h1>
1 change: 1 addition & 0 deletions examples/poem/tera-i18n/templates/index.html.tera
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>{{ "hello-world" | translate }}</h1>
3 changes: 1 addition & 2 deletions examples/poem/tera-templating/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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"
36 changes: 10 additions & 26 deletions examples/poem/tera-templating/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
use once_cell::sync::Lazy;
use poem::{
error::InternalServerError,
get, handler,
ctx, get, handler,
listener::TcpListener,
web::{Html, Path},
Route, Server,
tera::{Tera, TeraTemplate, TeraTemplating},
web::Path,
EndpointExt, Route, Server,
};
use tera::{Context, Tera};

static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
let mut tera = match Tera::new("templates/**/*") {
Ok(t) => t,
Err(e) => {
println!("Parsing error(s): {e}");
::std::process::exit(1);
}
};
tera.autoescape_on(vec![".html", ".sql"]);
tera
});

#[handler]
fn hello(Path(name): Path<String>) -> Result<Html<String>, poem::Error> {
let mut context = Context::new();
context.insert("name", &name);
TEMPLATES
.render("index.html.tera", &context)
.map_err(InternalServerError)
.map(Html)
fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate {
tera.render("index.html.tera", &ctx! { "name": &name })
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let app = Route::new().at("/hello/:name", get(hello));
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
Expand Down
3 changes: 2 additions & 1 deletion poem-openapi/src/base.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
collections::HashMap,
fmt::{self, Debug, Display},
marker::Send,
ops::Deref,
};

Expand Down Expand Up @@ -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 = ();
Expand Down
2 changes: 2 additions & 0 deletions poem/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand Down
5 changes: 3 additions & 2 deletions poem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion poem/src/i18n/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl Locale {

#[async_trait::async_trait]
impl<'a> FromRequest<'a> for Locale {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
fn from_request_sync(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
let resources = req
.extensions()
.get::<I18NResources>()
Expand Down
4 changes: 4 additions & 0 deletions poem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -274,6 +275,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 = "test")]
#[cfg_attr(docsrs, doc(cfg(feature = "test")))]
pub mod test;
Expand Down
135 changes: 135 additions & 0 deletions poem/src/tera/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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_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}");
Galitan-dev marked this conversation as resolved.
Show resolved Hide resolved
::std::process::exit(1);
}
};

Self { 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<E: Endpoint> Middleware<E> for TeraTemplatingMiddleware {
type Output = TeraTemplatingEndpoint<E>;

fn transform(&self, inner: E) -> Self::Output {
Self::Output {
tera: self.tera.clone(),
inner,
transformers: Vec::new(),
}
}
}

/// Tera Templating Endpoint
pub struct TeraTemplatingEndpoint<E> {
tera: Tera,
inner: E,
transformers: Vec<fn(&mut Tera, &mut Request)>,
}

#[async_trait::async_trait]
impl<E: Endpoint> Endpoint for TeraTemplatingEndpoint<E> {
type Output = E::Output;

async fn call(&self, mut req: Request) -> Result<Self::Output> {
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
}
}

#[async_trait::async_trait]
impl<'a> FromRequest<'a> for Tera {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
let tera = req
.extensions()
.get::<Tera>()
.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<String>;

impl IntoResult<Html<String>> for TeraTemplatingResult {
fn into_result(self) -> Result<Html<String>> {
if let Err(err) = &self {
println!("{err:?}");
}

self.map_err(InternalServerError).map(Html)
}
}

impl<E: Endpoint> TeraTemplatingEndpoint<E> {
/// 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
}
}