poem-web / poem

A full-featured and easy-to-use web framework with the Rust programming language.
Apache License 2.0
3.47k stars 283 forks source link

Support Tera Templating and other features like high-level API, live reloading, others templating engines... #512

Open Galitan-dev opened 1 year ago

Galitan-dev commented 1 year ago

Support Tera Templating

Description

Adding a Middleware which injects a Tera instance to each request (like Data) to avoid using lazy_static as the current example does and adding a wrapper for tera::Result to avoid the redundant mapping.

Usage Example

#[handler]
fn hello(tera: Tera, Path(name): Path<String>) -> tera::Result {
    let mut context = Context::new();
    context.insert("name", &name);
    tera.render("index.html.tera", &context)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = Route::new()
        .at("/hello/:name", get(hello))
        .with(poem::TeraTemplating::from_glob("templates/**/*"));

    Server::new(TcpListener::bind("127.0.0.1:3000"))
        .run(app)
        .await
}

Future Improvement Ideas

Galitan-dev commented 1 year ago

I'm already planning to implement it, but I wanted to let you know so you can share your opinion. Also, writing an issue helps me make my thoughts clearer in my head 😅.

Galitan-dev commented 1 year ago

In case you want to monitor progress: https://github.com/Galitan-dev/poem/tree/feat/tera

Galitan-dev commented 1 year ago

The example is already 13 lines shorter!

use poem::{
    get, handler,
    listener::TcpListener,
    web::Path,
    Route, Server,
    EndpointExt,
    tera::{TeraTemplating, TeraTemplate, Tera, Context}
};

#[handler]
fn hello(Path(name): Path<String>, tera: Tera) -> TeraTemplate {
    let mut context = Context::new();
    context.insert("name", &name);
    tera.render("index.html.tera", &context)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    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
}
Christoph-AK commented 1 year ago

Something screams at me that someone should write a macro for the context tempvar, so the ergonomics are more like python flask and similar, like this:

    tera.render("index.html.tera", ctx!(name: name))
// pseudo, will not compile
   macro_rules ctx {
     ($($key: name: $value:token)) => {
    let mut context = Context::new();
    (context.insert("$key", &$value))+;
    context
  }
}

It's way to late in the evening and probably someone already thought of this, but that would probalby improve ergonomics.

Galitan-dev commented 1 year ago

Something screams at me that someone should write a macro for the context tempvar, so the ergonomics are more like python flask and similar, like this:

    tera.render("index.html.tera", ctx!(name: name))
// pseudo, will not compile
   macro_rules ctx {
     ($($key: name: $value:token)) => {
    let mut context = Context::new();
    (context.insert("$key", &$value))+;
    context
  }
}

It's way to late in the evening and probably someone already thought of this, but that would probalby improve ergonomics.

I will work around this is a great idea

Here's the result:

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

#[macro_export]
macro_rules! ctx {
    { $( $key:literal: $value:expr ),* } => {
        {
            let mut context = Context::new();
            $(
                context.insert($key, $value);
            )*
            context
        }
    };
}
Galitan-dev commented 1 year ago

The Tera I18N filter is ready!

<h1>{{ "welcome" | translate(name=name) }}</h1>
#[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::translate)
        .data(resources);

    Server::new(TcpListener::bind("127.0.0.1:3000"))
        .run(app)
        .await
}
nebneb0703 commented 1 year ago

So it seems I started writing my own Tera glue library at the same time lol, I only just came across this issue/PR.

My attempt is hosted on Gitlab: https://gitlab.com/nebneb0703/poem-tera

It isn't in a polished/mergeable state, for example it is missing docs and examples, but I thought it could be good to compare and pick out the best parts to submit to the final PR.

I wanted a similar experience to Rocket so my solution also includes a file watcher for debug builds.

Galitan-dev commented 1 year ago

So it seems I started writing my own Tera glue library at the same time lol, I only just came across this issue/PR.

My attempt is hosted on Gitlab: https://gitlab.com/nebneb0703/poem-tera

It isn't in a polished/mergeable state, for example it is missing docs and examples, but I thought it could be good to compare and pick out the best parts to submit to the final PR.

I wanted a similar experience to Rocket so my solution also includes a file watcher for debug builds.

Nice ! I knew that I couldn't be the only one to have this need. That's why I created this issue. I already opened a PR but we can open a second one to add your ideas. I will check your repository.

Galitan-dev commented 1 year ago

So it seems I started writing my own Tera glue library at the same time lol, I only just came across this issue/PR. My attempt is hosted on Gitlab: https://gitlab.com/nebneb0703/poem-tera It isn't in a polished/mergeable state, for example it is missing docs and examples, but I thought it could be good to compare and pick out the best parts to submit to the final PR. I wanted a similar experience to Rocket so my solution also includes a file watcher for debug builds.

Nice ! I knew that I couldn't be the only one to have this need. That's why I created this issue. I already opened a PR but we can open a second one to add your ideas. I will check your repository.

Great work! I didn't know we could have different codes depending on the target. I see, we ended up doing quite the same thing 😅. The main notable differences are:

So I suggest that we make a second PR, where we first add a "from_directory" constructor and your live reloading implementation. Second, we can try implementing the layer around tera I spoke about sooner basing on your Tera struct.

The issue I am currently facing is intercepting the handler's response while having the tera::Tera instance in order to render poem::Template. Any Ideas? Here's the current code that intercepts the handler's response:

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)
    }
}
nebneb0703 commented 1 year ago

When testing my crate I did this:

#[poem::handler]
async fn index(tera: Tera) -> impl IntoResponse {
    tera.render("index.html.tera", &Default::default()).await
        .map(IntoResponse::into_response)
        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
}

Obviously this is not an ideal solution to have the user do. Maybe we could newtype around Result<Html<String>>, for example struct Template and we can add methods to customise error behaviour, but with sensible defaults.

Galitan-dev commented 1 year ago

Yes, because the tera error are just structure we will need to format them ourself. And yes we some how need a Template struct containing the Tera instance or what else. Also we can change a bit the poem logic and add a middleware after the response if it doesnt still exist.

Galitan-dev commented 1 year ago

For those interested, we continued talking on Discord.