├── examples
├── templates
│ ├── tera
│ │ └── {name}.html
│ └── minijinja
│ │ └── hello.html
├── nested.rs
├── tera.rs
├── dynamic_template.rs
├── handlebars.rs
├── minijinja.rs
├── custom_engine.rs
├── minijinja-autoreload.rs
└── custom_key.rs
├── .gitignore
├── .github
├── dependabot.yml
└── workflows
│ ├── release.yml
│ └── test.yml
├── src
├── engine
│ ├── tera.rs
│ ├── handlebars.rs
│ ├── minijinja.rs
│ └── mod.rs
├── traits.rs
├── lib.rs
├── key.rs
└── render.rs
├── tests
├── key.rs
├── engine.rs
├── render.rs
└── error.rs
├── LICENSE
├── Makefile
├── README.md
└── Cargo.toml
/examples/templates/tera/{name}.html:
--------------------------------------------------------------------------------
1 |
Hello Folder!
2 | {{name}}
--------------------------------------------------------------------------------
/examples/templates/minijinja/hello.html:
--------------------------------------------------------------------------------
1 | Hello Minijinja!
{{name}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7 | Cargo.lock
8 |
9 | # These are backup files generated by rustfmt
10 | **/*.rs.bk
11 |
12 | # Added by cargo
13 |
14 | /target
15 | /Cargo.lock
16 |
17 | # Editors
18 | .idea
19 | *.iml
20 | .vscode
21 | .zed
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "cargo" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/src/engine/tera.rs:
--------------------------------------------------------------------------------
1 | use crate::TemplateEngine;
2 |
3 | use super::Engine;
4 |
5 | use axum::{http::StatusCode, response::IntoResponse};
6 | use tera::{Context, Tera};
7 | use thiserror::Error;
8 |
9 | impl TemplateEngine for Engine {
10 | type Error = TeraError;
11 |
12 | fn render(&self, key: &str, data: D) -> Result {
13 | let data = Context::from_serialize(data)?;
14 | let rendered = self.engine.render(key, &data)?;
15 |
16 | Ok(rendered)
17 | }
18 | }
19 |
20 | /// Error wrapper for [`tera::Error`]
21 | #[derive(Error, Debug)]
22 | pub enum TeraError {
23 | /// See [`tera::Error`]
24 | #[error(transparent)]
25 | RenderError(#[from] tera::Error),
26 | }
27 |
28 | impl IntoResponse for TeraError {
29 | fn into_response(self) -> axum::response::Response {
30 | (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release new version
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | workflow_dispatch:
7 | env:
8 | CARGO_TERM_COLOR: always
9 | jobs:
10 | publish_crate:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | toolchain:
15 | - stable
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions/cache@v3
19 | with:
20 | path: |
21 | ~/.cargo/bin/
22 | ~/.cargo/registry/index/
23 | ~/.cargo/registry/cache/
24 | ~/.cargo/git/db/
25 | # target/
26 | key: ${{ runner.os }}-cargo-${{ matrix.toolchain }} # -${{ hashFiles('**/Cargo.lock') }}
27 | - run:
28 | rustup update ${{ matrix.toolchain }} && rustup default ${{
29 | matrix.toolchain }}
30 | - run: cargo publish
31 | env:
32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_REGISTRY_TOKEN }}
33 |
--------------------------------------------------------------------------------
/src/engine/handlebars.rs:
--------------------------------------------------------------------------------
1 | use crate::TemplateEngine;
2 |
3 | use super::Engine;
4 |
5 | use axum::{http::StatusCode, response::IntoResponse};
6 | use handlebars::Handlebars;
7 | use thiserror::Error;
8 |
9 | impl TemplateEngine for Engine> {
10 | type Error = HandlebarsError;
11 |
12 | fn render(&self, key: &str, data: D) -> Result {
13 | let rendered = self.engine.render(key, &data)?;
14 |
15 | Ok(rendered)
16 | }
17 | }
18 |
19 | /// Error wrapper for [`handlebars::RenderError`]
20 | #[derive(Error, Debug)]
21 | pub enum HandlebarsError {
22 | /// See [`handlebars::RenderError`]
23 | #[error(transparent)]
24 | RenderError(#[from] handlebars::RenderError),
25 | }
26 |
27 | impl IntoResponse for HandlebarsError {
28 | fn into_response(self) -> axum::response::Response {
29 | (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/key.rs:
--------------------------------------------------------------------------------
1 | use axum::{body::Body, http::Request, routing::get, Router};
2 | use axum_template::Key;
3 | use rstest::*;
4 | use speculoos::prelude::*;
5 | use tower::util::ServiceExt;
6 |
7 | #[rstest]
8 | #[case("/", "/")]
9 | #[case("/{hello}", "/world")]
10 | #[case("/{hello}", "/guys")]
11 | #[trace]
12 | #[tokio::test]
13 | async fn key_extracts_from_request_route_path(
14 | #[case] route: &'static str,
15 | #[case] uri: &'static str,
16 | ) -> anyhow::Result<()> {
17 | let router: Router = Router::new().route(
18 | route,
19 | get(move |Key(key): Key| async move { assert_that!(key.as_str()).is_equal_to(route) }),
20 | );
21 |
22 | let _response = router
23 | .oneshot(Request::builder().uri(uri).body(Body::empty())?)
24 | .await?;
25 |
26 | Ok(())
27 | }
28 |
29 | #[rstest]
30 | #[trace]
31 | #[tokio::test]
32 | async fn key_impl_asref_str() {
33 | fn inner(_: impl AsRef) {}
34 | inner(Key("Some String".into()));
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Check changes
2 | on:
3 | push:
4 | branches:
5 | - "**"
6 | - "*"
7 | pull_request:
8 | branches:
9 | - "master"
10 | - "main"
11 | env:
12 | CARGO_TERM_COLOR: always
13 | jobs:
14 | make_ci:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | toolchain:
19 | - stable
20 | - "1.83"
21 | # - beta
22 | # - nightly
23 | steps:
24 | - uses: actions/checkout@v3
25 | - uses: actions/cache@v3
26 | with:
27 | path: |
28 | ~/.cargo/bin/
29 | ~/.cargo/registry/index/
30 | ~/.cargo/registry/cache/
31 | ~/.cargo/git/db/
32 | # target/
33 | key: ${{ runner.os }}-cargo-${{ matrix.toolchain }} # ${{ hashFiles('**/Cargo.lock') }}
34 | - run:
35 | rustup update ${{ matrix.toolchain }} && rustup default ${{
36 | matrix.toolchain }} && rustup component add clippy rustfmt
37 | - run: make ci
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Altair Bueno
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | CARGO = cargo
2 | CARGO_CCARGS =
3 | CARGO_EXAMPLES = custom_engine \
4 | custom_key \
5 | handlebars \
6 | minijinja \
7 | nested \
8 | tera
9 |
10 | ################################################################################
11 | # Main goals
12 | ci: CARGO_CCARGS = --all-features --verbose
13 | ci: test build lint
14 |
15 | test:
16 | $(CARGO) test $(CARGO_CCARGS)
17 |
18 | lint: lint/clippy lint/fmt
19 |
20 | fmt:
21 | $(CARGO) fmt
22 |
23 | build: build/example build/crate
24 | ################################################################################
25 | build/example: $(addprefix build/example/, $(CARGO_EXAMPLES))
26 |
27 | build/example/%:
28 | $(CARGO) build --example=$* $(CARGO_CCARGS)
29 |
30 | build/crate:
31 | $(CARGO) build $(CARGO_CCARGS)
32 |
33 | lint/clippy:
34 | $(CARGO) clippy $(CARGO_CCARGS)
35 |
36 | lint/fmt:
37 | $(CARGO) fmt --check
38 |
39 | ################################################################################
40 |
41 | .PHONY: test ci \
42 | $(filter build%, $(MAKECMDGOALS)) \
43 | $(filter lint%, $(MAKECMDGOALS))
44 |
45 |
--------------------------------------------------------------------------------
/src/traits.rs:
--------------------------------------------------------------------------------
1 | use axum::response::IntoResponse;
2 | use serde::Serialize;
3 |
4 | /// An abstraction over different templating engines
5 | ///
6 | /// # Implementing custom engines
7 | ///
8 | /// ```
9 | /// # use axum_template::TemplateEngine;
10 | /// # use serde::Serialize;
11 | /// # use std::convert::Infallible;
12 | ///
13 | /// #[derive(Debug)]
14 | /// pub struct CustomEngine;
15 | ///
16 | /// impl TemplateEngine for CustomEngine {
17 | /// type Error = Infallible;
18 | /// fn render(&self, key: &str, data: S) -> Result {
19 | /// /* Render your template and return the result */
20 | /// let result = "Hello world".into();
21 | /// Ok(result)
22 | /// }
23 | /// }
24 | /// ```
25 | ///
26 | /// > See the full working example [`custom_engine.rs`]
27 | ///
28 | /// [`custom_engine.rs`]: https://github.com/Altair-Bueno/axum-template/blob/main/examples/custom_engine.rs
29 | pub trait TemplateEngine {
30 | /// Error type returned if the engine is unable to process the data
31 | type Error: IntoResponse;
32 |
33 | /// Renders the template defined by the given key using the Serializable data
34 | fn render(&self, key: &str, data: S) -> Result;
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../README.md")]
2 | #![warn(
3 | clippy::all,
4 | clippy::dbg_macro,
5 | clippy::todo,
6 | clippy::empty_enum,
7 | clippy::enum_glob_use,
8 | clippy::mem_forget,
9 | clippy::unused_self,
10 | clippy::filter_map_next,
11 | clippy::needless_continue,
12 | clippy::needless_borrow,
13 | clippy::match_wildcard_for_single_variants,
14 | clippy::if_let_mutex,
15 | unexpected_cfgs,
16 | clippy::await_holding_lock,
17 | clippy::match_on_vec_items,
18 | clippy::imprecise_flops,
19 | clippy::suboptimal_flops,
20 | clippy::lossy_float_literal,
21 | clippy::rest_pat_in_fully_bound_structs,
22 | clippy::fn_params_excessive_bools,
23 | clippy::exit,
24 | clippy::inefficient_to_string,
25 | clippy::linkedlist,
26 | clippy::macro_use_imports,
27 | clippy::option_option,
28 | clippy::verbose_file_reads,
29 | clippy::unnested_or_patterns,
30 | clippy::str_to_string,
31 | rust_2018_idioms,
32 | future_incompatible,
33 | nonstandard_style,
34 | missing_debug_implementations,
35 | missing_docs,
36 | rustdoc::missing_doc_code_examples
37 | )]
38 | #![deny(unreachable_pub)]
39 | #![forbid(unsafe_code)]
40 |
41 | mod key;
42 | mod render;
43 | mod traits;
44 |
45 | pub mod engine;
46 |
47 | pub use key::Key;
48 | pub use render::{Render, RenderHtml};
49 | pub use traits::TemplateEngine;
50 |
--------------------------------------------------------------------------------
/tests/engine.rs:
--------------------------------------------------------------------------------
1 | #![allow(unused)]
2 | use std::marker::PhantomData;
3 |
4 | use axum::extract::FromRef;
5 | use axum::extract::FromRequest;
6 | use axum::extract::FromRequestParts;
7 | use axum_template::engine::Engine;
8 | use axum_template::TemplateEngine;
9 | use rstest::*;
10 |
11 | struct AssertImpl(pub E, PhantomData)
12 | where
13 | E: Send + Sync + TemplateEngine + FromRef;
14 |
15 | #[cfg(feature = "tera")]
16 | #[rstest]
17 | fn engine_teras_assert_impl() {
18 | AssertImpl(Engine::new(tera::Tera::default()), Default::default());
19 | }
20 |
21 | #[cfg(feature = "handlebars")]
22 | #[rstest]
23 | fn engine_handlebars_assert_impl() {
24 | let phantom: PhantomData<()> = Default::default();
25 | AssertImpl(
26 | Engine::new(handlebars::Handlebars::new()),
27 | Default::default(),
28 | );
29 | }
30 |
31 | #[cfg(feature = "minijinja")]
32 | #[rstest]
33 | fn engine_minijinja_assert_impl() {
34 | let phantom: PhantomData<()> = Default::default();
35 | AssertImpl(
36 | Engine::new(minijinja::Environment::new()),
37 | Default::default(),
38 | );
39 | }
40 |
41 | #[cfg(feature = "minijinja-autoreload")]
42 | #[rstest]
43 | fn engine_minijinja_autoreload_assert_impl() {
44 | let phantom: PhantomData<()> = Default::default();
45 | let jinja = minijinja_autoreload::AutoReloader::new(move |_| Ok(minijinja::Environment::new()));
46 | AssertImpl(Engine::new(jinja), Default::default());
47 | }
48 |
--------------------------------------------------------------------------------
/src/engine/minijinja.rs:
--------------------------------------------------------------------------------
1 | use crate::TemplateEngine;
2 |
3 | use super::Engine;
4 |
5 | use axum::{http::StatusCode, response::IntoResponse};
6 |
7 | #[cfg(feature = "minijinja")]
8 | use minijinja::Environment;
9 |
10 | #[cfg(feature = "minijinja-autoreload")]
11 | use minijinja_autoreload::AutoReloader;
12 |
13 | use thiserror::Error;
14 |
15 | #[cfg(feature = "minijinja")]
16 | impl TemplateEngine for Engine> {
17 | type Error = MinijinjaError;
18 |
19 | fn render(&self, key: &str, data: D) -> Result {
20 | let template = self.engine.get_template(key)?;
21 | let rendered = template.render(&data)?;
22 |
23 | Ok(rendered)
24 | }
25 | }
26 |
27 | #[cfg(feature = "minijinja-autoreload")]
28 | impl TemplateEngine for Engine {
29 | type Error = MinijinjaError;
30 |
31 | fn render(&self, key: &str, data: D) -> Result {
32 | let reloader = self.engine.acquire_env()?;
33 | let template = reloader.get_template(key)?;
34 | // let template = self.engine.get_template(key)?;
35 | let rendered = template.render(&data)?;
36 |
37 | Ok(rendered)
38 | }
39 | }
40 |
41 | /// Error wrapper for [`minijinja::Error`]
42 | #[derive(Error, Debug)]
43 | pub enum MinijinjaError {
44 | /// See [`minijinja::Error`]
45 | #[error(transparent)]
46 | RenderError(#[from] minijinja::Error),
47 | }
48 |
49 | impl IntoResponse for MinijinjaError {
50 | fn into_response(self) -> axum::response::Response {
51 | (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/render.rs:
--------------------------------------------------------------------------------
1 | use std::convert::Infallible;
2 |
3 | use axum::response::IntoResponse;
4 | use axum_template::{Render, RenderHtml, TemplateEngine};
5 | use rstest::*;
6 |
7 | #[derive(Debug, Clone, Default)]
8 | pub struct MockEngine;
9 |
10 | impl TemplateEngine for MockEngine {
11 | type Error = Infallible;
12 |
13 | fn render(&self, _: &str, _: S) -> Result {
14 | Ok("".to_owned())
15 | }
16 | }
17 |
18 | #[fixture]
19 | fn engine() -> MockEngine {
20 | MockEngine
21 | }
22 |
23 | #[rstest]
24 | #[trace]
25 | #[tokio::test]
26 | async fn render_responds_with_text_plain(
27 | engine: impl TemplateEngine,
28 | #[values("")] key: &str,
29 | #[values(())] data: (),
30 | ) -> anyhow::Result<()> {
31 | let response = Render(key, engine, data).into_response();
32 |
33 | let (parts, _) = response.into_parts();
34 | let content_type = parts
35 | .headers
36 | .get(axum::http::header::CONTENT_TYPE)
37 | .unwrap()
38 | .as_bytes();
39 |
40 | assert_eq!(content_type, b"text/plain; charset=utf-8");
41 | Ok(())
42 | }
43 |
44 | #[rstest]
45 | #[trace]
46 | #[tokio::test]
47 | async fn render_html_responds_with_text_html(
48 | engine: impl TemplateEngine,
49 | #[values("")] key: &str,
50 | #[values(())] data: (),
51 | ) -> anyhow::Result<()> {
52 | let response = RenderHtml(key, engine, data).into_response();
53 |
54 | let (parts, _) = response.into_parts();
55 | let content_type = parts
56 | .headers
57 | .get(axum::http::header::CONTENT_TYPE)
58 | .unwrap()
59 | .as_bytes();
60 |
61 | assert_eq!(content_type, b"text/html; charset=utf-8");
62 | Ok(())
63 | }
64 |
--------------------------------------------------------------------------------
/examples/nested.rs:
--------------------------------------------------------------------------------
1 | //! Showcases nested routers and the `Key` extractor
2 | //!
3 | //! Run the example using
4 | //!
5 | //! ```sh
6 | //! cargo run --example=nested --features=handlebars
7 | //! ```
8 |
9 | use std::net::Ipv4Addr;
10 |
11 | use axum::{
12 | extract::{FromRef, Path},
13 | response::IntoResponse,
14 | routing::get,
15 | serve, Router,
16 | };
17 | use axum_template::{engine::Engine, Key, RenderHtml};
18 | use handlebars::Handlebars;
19 | use serde::Serialize;
20 | use tokio::net::TcpListener;
21 |
22 | type AppEngine = Engine>;
23 |
24 | #[derive(Debug, Serialize)]
25 | pub struct Person {
26 | name: String,
27 | }
28 |
29 | async fn get_name(engine: AppEngine, Key(key): Key, Path(name): Path) -> impl IntoResponse {
30 | let person = Person { name };
31 |
32 | RenderHtml(key, engine, person)
33 | }
34 |
35 | #[derive(Clone, FromRef)]
36 | struct AppState {
37 | engine: AppEngine,
38 | }
39 |
40 | #[tokio::main]
41 | async fn main() {
42 | let mut hbs = Handlebars::new();
43 | hbs.register_template_string("/{name}", "Simple {{ name }}")
44 | .unwrap();
45 | hbs.register_template_string("/nested/{name}", "Nested {{name}}")
46 | .unwrap();
47 |
48 | let nested = Router::new().route("/{name}", get(get_name));
49 |
50 | let app = Router::new()
51 | .route("/{name}", get(get_name))
52 | .nest("/nested", nested)
53 | .with_state(AppState {
54 | engine: Engine::from(hbs),
55 | });
56 |
57 | println!("See example: http://127.0.0.1:8080/example");
58 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080))
59 | .await
60 | .unwrap();
61 | serve(listener, app.into_make_service()).await.unwrap();
62 | }
63 |
--------------------------------------------------------------------------------
/examples/tera.rs:
--------------------------------------------------------------------------------
1 | //! Simple usage of using `axum_template` with the `tera` crate
2 | //!
3 | //! Run the example using
4 | //!
5 | //! ```sh
6 | //! cargo run --example=tera --features=tera
7 | //! ```
8 | use std::net::Ipv4Addr;
9 |
10 | use axum::{
11 | extract::{FromRef, Path},
12 | response::IntoResponse,
13 | routing::get,
14 | serve, Router,
15 | };
16 | use axum_template::{engine::Engine, Key, RenderHtml};
17 | use serde::Serialize;
18 | use tera::Tera;
19 | use tokio::net::TcpListener;
20 |
21 | // Type alias for our engine. For this example, we are using Tera
22 | type AppEngine = Engine;
23 |
24 | #[derive(Debug, Serialize)]
25 | pub struct Person {
26 | name: String,
27 | }
28 |
29 | async fn get_name(
30 | // Obtain the engine
31 | engine: AppEngine,
32 | // Extract the key
33 | Key(key): Key,
34 | Path(name): Path,
35 | ) -> impl IntoResponse {
36 | let person = Person { name };
37 |
38 | RenderHtml(key, engine, person)
39 | }
40 |
41 | // Define your application shared state
42 | #[derive(Clone, FromRef)]
43 | struct AppState {
44 | engine: AppEngine,
45 | }
46 |
47 | #[tokio::main]
48 | async fn main() {
49 | // Set up the Tera engine with the same route paths as the Axum router
50 | let mut tera = Tera::default();
51 | tera.add_raw_template("/{name}", "Hello Tera!
{{name}}
")
52 | .unwrap();
53 |
54 | let app = Router::new()
55 | .route("/{name}", get(get_name))
56 | // Create the application state
57 | .with_state(AppState {
58 | engine: Engine::from(tera),
59 | });
60 |
61 | println!("See example: http://127.0.0.1:8080/example");
62 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080))
63 | .await
64 | .unwrap();
65 | serve(listener, app.into_make_service()).await.unwrap();
66 | }
67 |
--------------------------------------------------------------------------------
/tests/error.rs:
--------------------------------------------------------------------------------
1 | //! Regression tests for stack overflow on IntoResponse calls for error types
2 | //!
3 | //! See https://github.com/Altair-Bueno/axum-template/issues/8
4 |
5 | #![allow(unused)]
6 |
7 | use axum::response::IntoResponse;
8 | use axum_template::engine::Engine;
9 | use axum_template::Render;
10 | use rstest::*;
11 |
12 | #[cfg(feature = "tera")]
13 | #[rstest]
14 | #[trace]
15 | #[tokio::test]
16 | async fn tera_error_into_response_check_infinite_recursion() -> anyhow::Result<()> {
17 | let engine = tera::Tera::new("./*.nothing")?;
18 | let engine = Engine::new(engine);
19 | let data = ();
20 | _ = Render("", engine, data).into_response();
21 | Ok(())
22 | }
23 |
24 | #[cfg(feature = "handlebars")]
25 | #[rstest]
26 | #[trace]
27 | #[tokio::test]
28 | async fn handlebars_error_into_response_check_infinite_recursion() -> anyhow::Result<()> {
29 | let engine = handlebars::Handlebars::new();
30 | let engine = Engine::new(engine);
31 | let data = ();
32 | _ = Render("", engine, data).into_response();
33 | Ok(())
34 | }
35 |
36 | #[cfg(feature = "minijinja")]
37 | #[rstest]
38 | #[trace]
39 | #[tokio::test]
40 | async fn minijinja_error_into_response_check_infinite_recursion() -> anyhow::Result<()> {
41 | let engine = minijinja::Environment::new();
42 | let engine = Engine::new(engine);
43 | let data = ();
44 | _ = Render("", engine, data).into_response();
45 | Ok(())
46 | }
47 |
48 | #[cfg(feature = "minijinja-autoreload")]
49 | #[rstest]
50 | #[trace]
51 | #[tokio::test]
52 | async fn minijinja_autoreload_error_into_response_check_infinite_recursion() -> anyhow::Result<()> {
53 | let jinja = minijinja_autoreload::AutoReloader::new(move |_| Ok(minijinja::Environment::new()));
54 | let engine = Engine::new(jinja);
55 | let data = ();
56 | _ = Render("", engine, data).into_response();
57 | Ok(())
58 | }
59 |
--------------------------------------------------------------------------------
/examples/dynamic_template.rs:
--------------------------------------------------------------------------------
1 | //! Simple usage of using `axum_template` with the `handlebars` crate
2 | //!
3 | //! Run the example using
4 | //!
5 | //! ```sh
6 | //! cargo run --example=dynamic_template --features=handlebars
7 | //! ```
8 | use std::net::Ipv4Addr;
9 |
10 | use axum::{extract::FromRef, response::IntoResponse, routing::get, serve, Router};
11 | use axum_template::{engine::Engine, RenderHtml};
12 | use handlebars::Handlebars;
13 | use tokio::net::TcpListener;
14 |
15 | // Type alias for our engine. For this example, we are using Handlebars
16 | type AppEngine = Engine>;
17 |
18 | async fn get_luck(
19 | // Obtain the engine
20 | engine: AppEngine,
21 | ) -> impl IntoResponse {
22 | // Anything that can be coerced to &str can be used as Key.
23 | let key = if rand::random::() % 6 == 0 {
24 | "lucky"
25 | } else {
26 | "unlucky"
27 | };
28 | RenderHtml(key, engine, &())
29 | }
30 |
31 | // Define your application shared state
32 | #[derive(Clone, FromRef)]
33 | struct AppState {
34 | engine: AppEngine,
35 | }
36 |
37 | #[tokio::main]
38 | async fn main() {
39 | // Set up the Handlebars engine with the same route paths as the Axum router
40 | let mut hbs = Handlebars::new();
41 | hbs.register_template_string("lucky", "Winner winner chicken dinner
")
42 | .unwrap();
43 | hbs.register_template_string("unlucky", "Try again!
")
44 | .unwrap();
45 |
46 | let app = Router::new()
47 | .route("/example", get(get_luck))
48 | // Create the application state
49 | .with_state(AppState {
50 | engine: Engine::from(hbs),
51 | });
52 | println!("See example: http://127.0.0.1:8080/example");
53 |
54 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080))
55 | .await
56 | .unwrap();
57 | serve(listener, app.into_make_service()).await.unwrap();
58 | }
59 |
--------------------------------------------------------------------------------
/examples/handlebars.rs:
--------------------------------------------------------------------------------
1 | //! Simple usage of using `axum_template` with the `handlebars` crate
2 | //!
3 | //! Run the example using
4 | //!
5 | //! ```sh
6 | //! cargo run --example=handlebars --features=handlebars
7 | //! ```
8 | use std::net::Ipv4Addr;
9 |
10 | use axum::{
11 | extract::{FromRef, Path},
12 | response::IntoResponse,
13 | routing::get,
14 | serve, Router,
15 | };
16 | use axum_template::{engine::Engine, Key, RenderHtml};
17 | use handlebars::Handlebars;
18 | use serde::Serialize;
19 | use tokio::net::TcpListener;
20 |
21 | // Type alias for our engine. For this example, we are using Handlebars
22 | type AppEngine = Engine>;
23 |
24 | #[derive(Debug, Serialize)]
25 | pub struct Person {
26 | name: String,
27 | }
28 |
29 | async fn get_name(
30 | // Obtain the engine
31 | engine: AppEngine,
32 | // Extract the key
33 | Key(key): Key,
34 | Path(name): Path,
35 | ) -> impl IntoResponse {
36 | let person = Person { name };
37 |
38 | RenderHtml(key, engine, person)
39 | }
40 |
41 | // Define your application shared state
42 | #[derive(Clone, FromRef)]
43 | struct AppState {
44 | engine: AppEngine,
45 | }
46 |
47 | #[tokio::main]
48 | async fn main() {
49 | // Set up the Handlebars engine with the same route paths as the Axum router
50 | let mut hbs = Handlebars::new();
51 | hbs.register_template_string("/{name}", "Hello HandleBars!
{{name}}
")
52 | .unwrap();
53 |
54 | let app = Router::new()
55 | .route("/{name}", get(get_name))
56 | // Create the application state
57 | .with_state(AppState {
58 | engine: Engine::from(hbs),
59 | });
60 | println!("See example: http://127.0.0.1:8080/example");
61 |
62 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080))
63 | .await
64 | .unwrap();
65 | serve(listener, app.into_make_service()).await.unwrap();
66 | }
67 |
--------------------------------------------------------------------------------
/examples/minijinja.rs:
--------------------------------------------------------------------------------
1 | //! Simple usage of using `axum_template` with the `minijinja` crate
2 | //!
3 | //! Run the example using
4 | //!
5 | //! ```sh
6 | //! cargo run --example=minijinja --features=minijinja
7 | //! ```
8 | use std::net::Ipv4Addr;
9 |
10 | use axum::{
11 | extract::{FromRef, Path},
12 | response::IntoResponse,
13 | routing::get,
14 | serve, Router,
15 | };
16 | use axum_template::{engine::Engine, Key, RenderHtml};
17 | use minijinja::Environment;
18 | use serde::Serialize;
19 | use tokio::net::TcpListener;
20 |
21 | // Type alias for our engine. For this example, we are using Mini Jinja
22 | type AppEngine = Engine>;
23 |
24 | #[derive(Debug, Serialize)]
25 | pub struct Person {
26 | name: String,
27 | }
28 |
29 | async fn get_name(
30 | // Obtain the engine
31 | engine: AppEngine,
32 | // Extract the key
33 | Key(key): Key,
34 | Path(name): Path,
35 | ) -> impl IntoResponse {
36 | let person = Person { name };
37 |
38 | RenderHtml(key, engine, person)
39 | }
40 |
41 | // Define your application shared state
42 | #[derive(Clone, FromRef)]
43 | struct AppState {
44 | engine: AppEngine,
45 | }
46 |
47 | #[tokio::main]
48 | async fn main() {
49 | // Set up the `minijinja` engine with the same route paths as the Axum router
50 | let mut jinja = Environment::new();
51 | jinja
52 | .add_template("/{name}", "Hello Minijinja!
{{name}}
")
53 | .unwrap();
54 |
55 | let app = Router::new()
56 | .route("/{name}", get(get_name))
57 | // Create the application state
58 | .with_state(AppState {
59 | engine: Engine::from(jinja),
60 | });
61 |
62 | println!("See example: http://127.0.0.1:8080/example");
63 |
64 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080))
65 | .await
66 | .unwrap();
67 | serve(listener, app.into_make_service()).await.unwrap();
68 | }
69 |
--------------------------------------------------------------------------------
/src/key.rs:
--------------------------------------------------------------------------------
1 | use axum::{
2 | extract::{
3 | rejection::MatchedPathRejection, FromRequestParts, MatchedPath, OptionalFromRequestParts,
4 | },
5 | http::request::Parts,
6 | RequestPartsExt,
7 | };
8 |
9 | /// Extracts matched path of the request
10 | ///
11 | /// # Usage
12 | ///
13 | /// ```
14 | /// # use axum::{response::IntoResponse, Router, routing::get};
15 | /// # use axum_template::Key;
16 | /// async fn handler(
17 | /// Key(key): Key
18 | /// ) -> impl IntoResponse
19 | /// {
20 | /// key
21 | /// }
22 | ///
23 | /// let router: Router<()> = Router::new()
24 | /// // key == "/some/route"
25 | /// .route("/some/route", get(handler))
26 | /// // key == "/{dynamic}"
27 | /// .route("/{dynamic}", get(handler));
28 | /// ```
29 | ///
30 | /// # Additional resources
31 | ///
32 | /// - [`MatchedPath`]
33 | /// - Example: [`custom_key.rs`]
34 | ///
35 | /// [`MatchedPath`]: axum::extract::MatchedPath
36 | /// [`custom_key.rs`]: https://github.com/Altair-Bueno/axum-template/blob/main/examples/custom_key.rs
37 | #[derive(Debug, Clone, PartialEq, Eq)]
38 | pub struct Key(pub String);
39 |
40 | impl FromRequestParts for Key
41 | where
42 | S: Send + Sync,
43 | {
44 | type Rejection = MatchedPathRejection;
45 |
46 | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result {
47 | let path = parts.extract::().await?.as_str().to_owned();
48 | Ok(Key(path))
49 | }
50 | }
51 |
52 | impl OptionalFromRequestParts for Key
53 | where
54 | S: Send + Sync,
55 | {
56 | type Rejection = >::Rejection;
57 |
58 | async fn from_request_parts(
59 | parts: &mut Parts,
60 | state: &S,
61 | ) -> Result