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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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>
4 changes: 2 additions & 2 deletions examples/poem/tera-templating/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true

[dependencies]
poem.workspace = true
poem = { workspace = true, features = ["tera"] }
tokio = { workspace = true, features = ["full"] }
tera = "1.17.1"
once_cell = "1.17.0"
tracing-subscriber.workspace = true
42 changes: 16 additions & 26 deletions examples/poem/tera-templating/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
use once_cell::sync::Lazy;
use poem::{
error::InternalServerError,
get, handler,
ctx, get, handler,
listener::TcpListener,
web::{Html, Path},
Route, Server,
tera::{Tera, TeraTemplate, TeraTemplating},
web::Path,
EndpointExt, Route, Server,
};
use tera::{Context, Tera};

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

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

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let app = Route::new().at("/hello/:name", get(hello));
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "poem=debug");
}
tracing_subscriber::fmt::init();

let app = Route::new()
.at("/hello/:name", get(hello))
.with(TeraTemplating::from_glob("templates/**/*"))
.with_live_reloading();

Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(app)
.await
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
5 changes: 5 additions & 0 deletions poem/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ acme-base = [
embed = ["rust-embed", "hex", "mime_guess"]
xml = ["quick-xml"]
yaml = ["serde_yaml"]
templates = []
live_reloading = ["dep:notify"]
tera = ["dep:tera", "templates"]

[dependencies]
poem-derive.workspace = true
Expand Down Expand Up @@ -161,6 +164,8 @@ hex = { version = "0.4", optional = true }
quick-xml = { workspace = true, optional = true }
serde_yaml = { workspace = true, optional = true }
tokio-stream = { workspace = true, optional = true }
tera = { version = "1.17.1", optional = true }
notify = { version = "5.1", optional = true }

# Feature optional dependencies
anyhow = { version = "1.0.0", optional = true }
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 @@ -76,7 +76,8 @@ which are disabled by default:
| tokio-metrics | Integrate with [`tokio-metrics`](https://crates.io/crates/tokio-metrics) crate. |
| embed | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. |
| xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. |
| yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. |
| yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. |
| tera | Support for [`tera`](https://crates.io/crates/tera) templating. |

## Safety

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 @@ -255,6 +255,7 @@
//! | embed | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. |
//! | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. |
//! | yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. |
//! | tera | Support for [`tera`](https://crates.io/crates/tera) templating. |

#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
Expand All @@ -276,6 +277,9 @@ pub mod middleware;
#[cfg(feature = "session")]
#[cfg_attr(docsrs, doc(cfg(feature = "session")))]
pub mod session;
#[cfg(feature = "templates")]
#[cfg_attr(docsrs, doc(cfg(feature = "templates")))]
pub mod templates;
#[cfg(feature = "test")]
#[cfg_attr(docsrs, doc(cfg(feature = "test")))]
pub mod test;
Expand Down
77 changes: 77 additions & 0 deletions poem/src/templates/live_reloading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use notify::{
Watcher as WatcherTrait,
Event, EventKind,
RecursiveMode
};

use std::sync::{
Arc, atomic::{ AtomicBool, Ordering },
};

pub(crate) struct Watcher {
pub(crate) needs_reload: Arc<AtomicBool>,
pub(crate) _path: String,
_watcher: Option<Arc<dyn WatcherTrait + Send + Sync + 'static>>
}

impl Watcher {
pub(crate) fn new(path: String) -> Self {
let needs_reload = Arc::new(AtomicBool::new(false));
let needs_reload_cloned = needs_reload.clone();

let watcher = notify::recommended_watcher(move |event| match event {
Ok(Event {
kind:
EventKind::Create(_)
| EventKind::Modify(_)
| EventKind::Remove(_),
..
}) => {
needs_reload.store(true, Ordering::Relaxed);
tracing::debug!("Sent reload request");
},
Err(e) => {
// Ignore errors for now and just output them.
// todo: make panic?
tracing::debug!("Watcher error: {e:?}");
},
_ => {},
});

let watcher = watcher
.map(|mut w| w
.watch(std::path::Path::new(&path), RecursiveMode::Recursive)
.map(|_| w)
);

let watcher = match watcher {
Ok(Ok(w)) => {
tracing::info!("Watching templates directory `{path}` for changes.");

Some(Arc::new(w) as Arc<dyn WatcherTrait + Send + Sync>)
}
Err(e) | Ok(Err(e)) => {
tracing::error!("Failed to start watcher: {e}");
tracing::debug!("Watcher error: {e:?}");

None
},
};

Self {
needs_reload: needs_reload_cloned,
_path: path,
_watcher: watcher,
}
}

pub(crate) fn needs_reload(&self) -> bool {
self.needs_reload.swap(false, Ordering::Relaxed)
}
}

pub enum LiveReloading {
Enabled(String),
Debug(String),
Disabled
}
14 changes: 14 additions & 0 deletions poem/src/templates/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mod template; pub use template::Template;



#[cfg(feature = "templates")]
#[cfg_attr(docsrs, doc(cfg(feature = "templates")))]
pub mod tera;

#[cfg(feature = "live_reloading")]
#[cfg_attr(docsrs, doc(cfg(feature = "live_reloading")))]
mod live_reloading;

#[cfg(feature = "live_reloading")]
pub use live_reloading::LiveReloading;
56 changes: 56 additions & 0 deletions poem/src/templates/template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::{
IntoResponse, Response,
http::StatusCode,
};

/// An engine-agnostic template to be rendered.
///
/// This response type requires a templating engine middleware
/// to work correctly. Missing the middleware will return
/// `500 Internal Server Error`.
///
/// ```
/// use poem::{
/// templates::Template,
/// ctx, handler,
/// web::Path,
/// };
///
/// #[handler]
/// fn hello(Path(name): Path<String>) -> Template {
/// Template::render("index.html.tera", &ctx! { "name": &name })
/// }
/// ```
pub struct Template<C> {
/// Path to the template.
pub name: String,
/// Template context. This is used
/// by engines for additional data.
pub context: C,
}

impl<C> Template<C> {
/// Renders the template.
pub fn render(name: impl Into<String>, context: C) -> Self {
Self {
name: name.into(),
context,
}
}
}

impl<C: Send + Sync + 'static> IntoResponse for Template<C> {
fn into_response(self) -> Response {
// At this stage, we respond with an internal server error,
// as we have not yet built the template.
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)

// We add this as an extension so that it can be
// accessed by the endpoint to actually render
// the template.
.extension(self)

.finish()
}
}
Loading