diff options
-rw-r--r-- | .github/workflows/rust.yml | 13 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | askama/Cargo.toml | 1 | ||||
-rw-r--r-- | askama_axum/Cargo.toml | 23 | ||||
l--------- | askama_axum/LICENSE-APACHE | 1 | ||||
l--------- | askama_axum/LICENSE-MIT | 1 | ||||
-rw-r--r-- | askama_axum/README.md | 9 | ||||
-rw-r--r-- | askama_axum/src/lib.rs | 25 | ||||
-rw-r--r-- | askama_axum/templates/hello.html | 1 | ||||
-rw-r--r-- | askama_axum/tests/basic.rs | 35 | ||||
-rw-r--r-- | askama_derive/Cargo.toml | 1 | ||||
-rw-r--r-- | askama_derive/src/lib.rs | 1 | ||||
-rw-r--r-- | askama_shared/src/generator.rs | 22 | ||||
-rw-r--r-- | askama_shared/src/lib.rs | 1 | ||||
-rw-r--r-- | askama_shared/src/parser.rs | 35 | ||||
-rw-r--r-- | book/src/integrations.md | 12 | ||||
-rw-r--r-- | testing/templates/raw-ws.html | 2 | ||||
-rw-r--r-- | testing/tests/simple.rs | 10 |
19 files changed, 177 insertions, 19 deletions
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 398cb46..b62ae5d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,6 +48,19 @@ jobs: - run: cargo test --package askama_actix --all-targets - run: cargo clippy --package askama_actix --all-targets -- -D warnings + Axum: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + - run: cargo test --package askama_axum --all-targets + - run: cargo clippy --package askama_axum --all-targets -- -D warnings + Gotham: runs-on: ubuntu-latest steps: @@ -2,6 +2,7 @@ members = [ "askama", "askama_actix", + "askama_axum", "askama_gotham", "askama_derive", "askama_escape", @@ -26,7 +26,7 @@ in a for-profit context, please consider supporting my open source work on * Construct templates using a familiar, easy-to-use syntax * Benefit from the safety provided by Rust's type system * Template code is compiled into your crate for [optimal performance][benchmarks] -* Optional built-in support for Actix, Gotham, Iron, Rocket, tide, and warp web frameworks +* Optional built-in support for Actix, Axum, Gotham, Iron, Rocket, tide, and warp web frameworks * Debugging features to assist you in template development * Templates must be valid UTF-8 and produce UTF-8 when rendered * IDE support available in [JetBrains products](https://plugins.jetbrains.com/plugin/16591-askama-template-support) diff --git a/askama/Cargo.toml b/askama/Cargo.toml index 2e67892..f42e7d8 100644 --- a/askama/Cargo.toml +++ b/askama/Cargo.toml @@ -24,6 +24,7 @@ serde-json = ["askama_shared/json"] serde-yaml = ["askama_shared/yaml"] num-traits = ["askama_shared/num-traits"] with-actix-web = ["askama_derive/actix-web"] +with-axum = ["askama_derive/axum"] with-gotham = ["askama_derive/gotham"] with-iron = ["askama_derive/iron"] with-mendes = ["askama_derive/mendes"] diff --git a/askama_axum/Cargo.toml b/askama_axum/Cargo.toml new file mode 100644 index 0000000..7d3045d --- /dev/null +++ b/askama_axum/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "askama_axum" +version = "0.10.0" +authors = ["Michael Alyn Miller <malyn@strangeGizmo.com>"] +edition = "2018" +description = "Axum integration for Askama templates" +keywords = ["markup", "template", "jinja2", "html", "axum"] +categories = ["template-engine"] +homepage = "https://github.com/djc/askama" +repository = "https://github.com/djc/askama" +documentation = "https://docs.rs/askama" +license = "MIT OR Apache-2.0" +workspace = ".." +readme = "README.md" + +[dependencies] +askama = { version = "0.11.0-beta.1", path = "../askama", default-features = false, features = ["with-axum", "mime", "mime_guess"] } +axum = { version = "0.3", default-features = false } + +[dev-dependencies] +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1.0", features = ["full"] } +tower = { version = "0.4", features = ["util"] }
\ No newline at end of file diff --git a/askama_axum/LICENSE-APACHE b/askama_axum/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/askama_axum/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE
\ No newline at end of file diff --git a/askama_axum/LICENSE-MIT b/askama_axum/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/askama_axum/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT
\ No newline at end of file diff --git a/askama_axum/README.md b/askama_axum/README.md new file mode 100644 index 0000000..584dbd9 --- /dev/null +++ b/askama_axum/README.md @@ -0,0 +1,9 @@ +# askama_axum: Askama integration with Axum + +[![Documentation](https://docs.rs/askama_axum/badge.svg)](https://docs.rs/askama_axum/) +[![Latest version](https://img.shields.io/crates/v/askama_axum.svg)](https://crates.io/crates/askama_axum) +[![Build Status](https://github.com/djc/askama/workflows/CI/badge.svg)](https://github.com/djc/askama/actions?query=workflow%3ACI) +[![Chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/djc/askama) + +Integration of the [Askama](https://github.com/djc/askama) templating engine in +code building on the Axum web framework. diff --git a/askama_axum/src/lib.rs b/askama_axum/src/lib.rs new file mode 100644 index 0000000..0c21464 --- /dev/null +++ b/askama_axum/src/lib.rs @@ -0,0 +1,25 @@ +#![deny(elided_lifetimes_in_paths)] + +pub use askama::*; +use axum::{ + self, + body::{Bytes, Full}, + http::{Response, StatusCode}, +}; + +pub fn into_response<T: Template>(t: &T, ext: &str) -> axum::http::Response<Full<Bytes>> { + match t.render() { + Ok(body) => Response::builder() + .status(StatusCode::OK) + .header( + "content-type", + askama::mime::extension_to_mime_type(ext).to_string(), + ) + .body(body.into()) + .unwrap(), + Err(_) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(vec![].into()) + .unwrap(), + } +} diff --git a/askama_axum/templates/hello.html b/askama_axum/templates/hello.html new file mode 100644 index 0000000..8149be7 --- /dev/null +++ b/askama_axum/templates/hello.html @@ -0,0 +1 @@ +Hello, {{ name }}! diff --git a/askama_axum/tests/basic.rs b/askama_axum/tests/basic.rs new file mode 100644 index 0000000..acd53b7 --- /dev/null +++ b/askama_axum/tests/basic.rs @@ -0,0 +1,35 @@ +use askama::Template; +use axum::{ + body::Body, + http::{Request, StatusCode}, + routing::get, + Router, +}; +use tower::util::ServiceExt; + +#[derive(Template)] +#[template(path = "hello.html")] +struct HelloTemplate<'a> { + name: &'a str, +} + +async fn hello() -> HelloTemplate<'static> { + HelloTemplate { name: "world" } +} + +#[tokio::test] +async fn template_to_response() { + let app = Router::new().route("/", get(hello)); + + let res = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + + let headers = res.headers(); + assert_eq!(headers["Content-Type"], "text/html; charset=utf-8"); + + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(&body[..], b"Hello, world!"); +} diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml index a30aef4..60ad48d 100644 --- a/askama_derive/Cargo.toml +++ b/askama_derive/Cargo.toml @@ -16,6 +16,7 @@ proc-macro = true config = ["askama_shared/config"] actix-web = [] +axum = [] gotham = [] iron = [] mendes = [] diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index f2bee66..35070fe 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -111,6 +111,7 @@ fn find_used_templates( const INTEGRATIONS: Integrations = Integrations { actix: cfg!(feature = "actix-web"), + axum: cfg!(feature = "axum"), gotham: cfg!(feature = "gotham"), iron: cfg!(feature = "iron"), mendes: cfg!(feature = "mendes"), diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs index 78ada4b..cb395d4 100644 --- a/askama_shared/src/generator.rs +++ b/askama_shared/src/generator.rs @@ -96,6 +96,9 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { if self.integrations.actix { self.impl_actix_web_responder(&mut buf)?; } + if self.integrations.axum { + self.impl_axum_into_response(&mut buf)?; + } if self.integrations.gotham { self.impl_gotham_into_response(&mut buf)?; } @@ -222,6 +225,21 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { buf.writeln("}") } + // Implement Axum's `IntoResponse`. + fn impl_axum_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { + self.write_header(buf, "::axum::response::IntoResponse", None)?; + buf.writeln( + "type Body = ::axum::body::Full<::axum::body::Bytes>;\n\ + type BodyError = std::convert::Infallible;\n\ + fn into_response(self)\ + -> ::axum::http::Response<Self::Body> {", + )?; + let ext = self.input.extension().unwrap_or("txt"); + buf.writeln(&format!("::askama_axum::into_response(&self, {:?})", ext))?; + buf.writeln("}")?; + buf.writeln("}") + } + // Implement gotham's `IntoResponse`. fn impl_gotham_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { self.write_header(buf, "::askama_gotham::IntoResponse", None)?; @@ -439,9 +457,9 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { self.flush_ws(m.ws1); self.prepare_ws(m.ws2); } - Node::Raw(ws1, contents, ws2) => { + Node::Raw(ws1, lws, val, rws, ws2) => { self.handle_ws(ws1); - self.buf_writable.push(Writable::Lit(contents)); + self.visit_lit(lws, val, rws); self.handle_ws(ws2); } Node::Import(ws, _, _) => { diff --git a/askama_shared/src/lib.rs b/askama_shared/src/lib.rs index f217cfb..8d64e7b 100644 --- a/askama_shared/src/lib.rs +++ b/askama_shared/src/lib.rs @@ -274,6 +274,7 @@ pub fn get_template_source(tpl_path: &Path) -> std::result::Result<String, Compi #[derive(Clone, Copy, Debug)] pub struct Integrations { pub actix: bool, + pub axum: bool, pub gotham: bool, pub iron: bool, pub mendes: bool, diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs index 44902d0..535c7f2 100644 --- a/askama_shared/src/parser.rs +++ b/askama_shared/src/parser.rs @@ -4,7 +4,7 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{escaped, is_not, tag, take_till, take_until}; use nom::character::complete::{anychar, char, digit1}; -use nom::combinator::{complete, cut, eof, map, not, opt, recognize, value}; +use nom::combinator::{complete, consumed, cut, eof, map, not, opt, peek, recognize, value}; use nom::error::{Error, ErrorKind}; use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1}; use nom::sequence::{delimited, pair, preceded, terminated, tuple}; @@ -28,7 +28,7 @@ pub enum Node<'a> { Include(Ws, &'a str), Import(Ws, &'a str, &'a str), Macro(&'a str, Macro<'a>), - Raw(Ws, &'a str, Ws), + Raw(Ws, &'a str, &'a str, &'a str, Ws), Break(Ws), Continue(Ws), } @@ -1048,29 +1048,32 @@ fn block_macro<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> { } fn block_raw<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> { + let endraw = tuple(( + |i| tag_block_start(i, s), + opt(char('-')), + ws(tag("endraw")), + opt(char('-')), + peek(|i| tag_block_end(i, s)), + )); + let mut p = tuple(( opt(char('-')), ws(tag("raw")), cut(tuple(( opt(char('-')), |i| tag_block_end(i, s), - take_until("{% endraw %}"), - |i| tag_block_start(i, s), - opt(char('-')), - ws(tag("endraw")), - opt(char('-')), + consumed(skip_till(endraw)), ))), )); - let (i, (pws1, _, (nws1, _, contents, _, pws2, _, nws2))) = p(i)?; - Ok(( - i, - Node::Raw( - Ws(pws1.is_some(), nws1.is_some()), - contents, - Ws(pws2.is_some(), nws2.is_some()), - ), - )) + let (_, (pws1, _, (nws1, _, (contents, (i, (_, pws2, _, nws2, _)))))) = p(i)?; + let (lws, val, rws) = match split_ws_parts(contents) { + Node::Lit(lws, val, rws) => (lws, val, rws), + _ => unreachable!(), + }; + let ws1 = Ws(pws1.is_some(), nws1.is_some()); + let ws2 = Ws(pws2.is_some(), nws2.is_some()); + Ok((i, Node::Raw(ws1, lws, val, rws, ws2))) } fn break_statement<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> { diff --git a/book/src/integrations.md b/book/src/integrations.md index a972e4d..918b887 100644 --- a/book/src/integrations.md +++ b/book/src/integrations.md @@ -20,6 +20,18 @@ a value of that type in an Actix-web handler. See [the example](https://github.com/djc/askama/blob/main/askama_actix/tests/basic.rs) from the Askama test suite for more on how to integrate. +## Axum integration + +Enabling the `with-axum` feature appends an implementation of Axum's +`IntoResponse` trait for each template type. This makes it easy to trivially +return a value of that type in a Axum handler. See +[the example](https://github.com/djc/askama/blob/main/askama_axum/tests/basic.rs) +from the Askama test suite for more on how to integrate. + +In case of a run-time error occurring during templating, the response will be of the same +signature, with a status code of `500 Internal Server Error`, mime `*/*`, and an empty `Body`. +This preserves the response chain if any custom error handling needs to occur. + ## Gotham integration Enabling the `with-gotham` feature appends an implementation of Gotham's diff --git a/testing/templates/raw-ws.html b/testing/templates/raw-ws.html new file mode 100644 index 0000000..d590be7 --- /dev/null +++ b/testing/templates/raw-ws.html @@ -0,0 +1,2 @@ +<{% raw -%} {{hello}} {%- endraw %}> +< {%- raw %}{{bye}}{% endraw -%} > diff --git a/testing/tests/simple.rs b/testing/tests/simple.rs index c712900..e66485a 100644 --- a/testing/tests/simple.rs +++ b/testing/tests/simple.rs @@ -433,6 +433,16 @@ fn test_raw_complex() { ); } +#[derive(Template)] +#[template(path = "raw-ws.html")] +struct RawTemplateWs; + +#[test] +fn test_raw_ws() { + let template = RawTemplateWs; + assert_eq!(template.render().unwrap(), "<{{hello}}>\n<{{bye}}>"); +} + mod without_import_on_derive { #[derive(askama::Template)] #[template(source = "foo", ext = "txt")] |