diff --git a/Cargo.toml b/Cargo.toml index 83789d3..e87be8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ sass = ["rsass"] mime02 = [] mime03 = ["mime"] warp02 = ["mime03"] +http-types = [] +tide013 = ["http-types"] [dependencies] base64 = "^0.12.0" diff --git a/examples/tide/Cargo.toml b/examples/tide/Cargo.toml index 310356a..953e4c1 100644 --- a/examples/tide/Cargo.toml +++ b/examples/tide/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tide" +name = "ructe-tide" version = "0.4.0" authors = ["Rasmus Kaj "] edition = "2018" @@ -7,8 +7,9 @@ edition = "2018" build = "src/build.rs" [build-dependencies] -ructe = { path = "../.." } +ructe = { path = "../..", features = ["tide013", "sass"] } [dependencies] async-std = { version = "1.6.0", features = ["attributes"] } -tide = "0.10.0" +tide = "0.13.0" # Note: Feature flag for ructe matches this version +httpdate = "0.3.1" diff --git a/examples/tide/src/build.rs b/examples/tide/src/build.rs index d8df3d6..530c625 100644 --- a/examples/tide/src/build.rs +++ b/examples/tide/src/build.rs @@ -5,5 +5,8 @@ use ructe::{Ructe, RucteError}; fn main() -> Result<(), RucteError> { let mut ructe = Ructe::from_env()?; + let mut statics = ructe.statics()?; + statics.add_files("statics")?; + statics.add_sass_file("style.scss")?; ructe.compile_templates("templates") } diff --git a/examples/tide/src/main.rs b/examples/tide/src/main.rs index a1357b9..39cd3c6 100644 --- a/examples/tide/src/main.rs +++ b/examples/tide/src/main.rs @@ -1,20 +1,27 @@ -// And finally, include the generated code for templates and static files. -include!(concat!(env!("OUT_DIR"), "/templates.rs")); - +//! An example of how ructe can be used with the tide framework. mod ructe_tide; -use ructe_tide::Render; +use ructe_tide::{Render, RenderBuilder}; -use tide::{Response, StatusCode}; +use httpdate::fmt_http_date; +use std::future::Future; +use std::io::{self, Write}; +use std::pin::Pin; +use std::time::{Duration, SystemTime}; +use templates::statics::{cloud_svg, StaticFile}; +use tide::http::headers::EXPIRES; +use tide::http::Error; +use tide::{Next, Request, Response, StatusCode}; +/// Main entry point. +/// +/// Set up an app and start listening for requests. #[async_std::main] async fn main() -> Result<(), std::io::Error> { let mut app = tide::new(); - - app.at("/").get(|_| async { - let mut res = Response::new(StatusCode::Ok); - res.render_html(|o| Ok(templates::hello(o, "world")?))?; - Ok(res) - }); + app.with(handle_error); + app.at("/static/*path").get(static_file); + app.at("/favicon.ico").get(favicon); + app.at("/").get(frontpage); let addr = "127.0.0.1:3000"; println!("Starting server on http://{}/", addr); @@ -22,3 +29,84 @@ async fn main() -> Result<(), std::io::Error> { Ok(()) } + +/// Handler for a page in the web site. +async fn frontpage(_req: Request<()>) -> Result { + // A real site would probably have some business logic here. + Ok(Response::builder(StatusCode::Ok) + .render_html(|o| templates::page(o, &[("world", 5), ("tide", 7)])) + .build()) +} + +/// Handler for static files. +/// +/// Ructe provides the static files as constants, and the StaticFile +/// interface to get a file by url path. +async fn static_file(req: Request<()>) -> Result { + let path = req.param::("path")?; + StaticFile::get(&path) + .ok_or_else(|| Error::from_str(StatusCode::NotFound, "not found")) + .map(static_response) +} + +/// Specialized static file handler for the favicon +async fn favicon(_req: Request<()>) -> Result { + Ok(static_response(&cloud_svg)) +} + +/// Make a response from a StaticFile +/// +/// Helper for static_file and favicon. +fn static_response(data: &StaticFile) -> Response { + Response::builder(StatusCode::Ok) + .content_type(data.mime.clone()) // Takes Into, not AsRef + .header(EXPIRES, fmt_http_date(SystemTime::now() + 180 * DAY)) + .body(data.content) + .build() +} + +/// 24 hours. +const DAY: Duration = Duration::from_secs(24 * 60 * 60); + +/// This method can be used as a "template tag", i.e. a method that +/// can be called directly from a template. +fn footer(out: &mut dyn Write) -> io::Result<()> { + templates::footer( + out, + &[ + ("ructe", "https://crates.io/crates/ructe"), + ("tide", "https://crates.io/crates/tide"), + ], + ) +} + +/// A middleware to log errors and render a html error message. +/// +/// If the response has content, this function does not overwrite it. +fn handle_error<'a>( + request: Request<()>, + next: Next<'a, ()>, +) -> Pin> + Send + 'a>> { + Box::pin(async { + // I don't really like to create this string for every request, + // but when I see if there is an error, the request is consumed. + let rdesc = format!("{} {:?}", request.method(), request.url()); + let mut res = next.run(request).await; + let status = res.status(); + if status.is_client_error() || status.is_server_error() { + println!("Error {} on {}: {:?}", status, rdesc, res.error()); + if res.is_empty().unwrap_or(false) { + // Note: We are adding a body to an existing response, + // so the builder patern cannot be used here. + // The Render trait is provided for Response. + res.render_html(|o| { + templates::error(o, status, status.canonical_reason()) + })? + } + } + Ok(res) + }) +} + +// And finally, include the generated code for templates and static files. +include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/examples/tide/src/ructe_tide.rs b/examples/tide/src/ructe_tide.rs index edc9e23..1478e07 100644 --- a/examples/tide/src/ructe_tide.rs +++ b/examples/tide/src/ructe_tide.rs @@ -1,8 +1,47 @@ +//! These traits and impl may be included in the tide feature of ructe +//! in a future release. +//! +//! Comments welcome at +//! [kaj/ructe#79](https://github.com/kaj/ructe/issues/79). + +/// Add `render` and `render_html` methods to [`tide::Response`]. +/// +/// [`tide::Response`]: ../../tide/struct.Response.html pub trait Render { + /// Render a template to the body of self. + /// + /// The `Call` takes a `Write` target as only argument, other + /// arguments will typically be moved or borrowed into a closure + /// that is used as `call`. + /// + /// # Examples + /// + /// ``` + /// # use tide::Response; + /// # use ructe_tide::ructe_tide::Render; + /// # use std::io::{self, Write}; + /// # // Mock template: + /// # fn page(o: impl Write, c: &str, n: u8) -> io::Result<()> { + /// # Ok(()) + /// # } + /// # fn main() -> Result<(), Box> { + /// let content = "something"; + /// let other = 17; + /// let mut result = Response::new(200); + /// result.render(|o| page(o, content, other))?; + /// # Ok(()) + /// # } + /// ``` fn render(&mut self, call: Call) -> std::io::Result<()> where Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>; + /// Render a template to the html body of self. + /// + /// just like `render`, excep it also sets the content-type of the + /// respons to [`HTML`]. + /// + /// [`HTML`]: ../../tide/http/mime/constant.HTML.html fn render_html(&mut self, call: Call) -> std::io::Result<()> where Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>; @@ -28,3 +67,72 @@ impl Render for tide::Response { Ok(()) } } + +/// Add `render` and `render_html` methods to [`tide::ResponseBuilder`]. +/// +/// [`tide::Response`]: ../../tide/struct.ResponseBuilder.html +pub trait RenderBuilder { + /// Render a template to the body of self. + /// + /// The `Call` takes a `Write` target as only argument, other + /// arguments will typically be moved or borrowed into a closure + /// that is used as `call`. + /// + /// # Examples + /// + /// ``` + /// # use tide::Response; + /// # use ructe_tide::ructe_tide::RenderBuilder; + /// # use std::io::{self, Write}; + /// # // Mock template: + /// # fn page(o: impl Write, c: &str, n: u8) -> io::Result<()> { + /// # Ok(()) + /// # } + /// let content = "something"; + /// let other = 17; + /// Response::builder(200) + /// .render(|o| page(o, content, other)) + /// .build() + /// # ; + /// ``` + fn render(self, call: Call) -> tide::ResponseBuilder + where + Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>; + + /// Render a template to the html body of self. + /// + /// just like `render`, excep it also sets the content-type of the + /// respons to [`HTML`]. + /// + /// [`HTML`]: ../../tide/http/mime/constant.HTML.html + fn render_html(self, call: Call) -> tide::ResponseBuilder + where + Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>; +} + +impl RenderBuilder for tide::ResponseBuilder { + fn render(self, call: Call) -> tide::ResponseBuilder + where + Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>, + { + let mut buf = Vec::new(); + match call(&mut buf) { + Ok(()) => self.body(buf), + Err(e) => { + // NOTE: A tide::Response may contain an Error, but there + // seem to be no way of setting that in a ResponseBuilder, + // so I just log the error and return a builder for a + // generic internal server error. + tide::log::error!("Failed to render response: {}", e); + tide::Response::builder(500) + } + } + } + + fn render_html(self, call: Call) -> tide::ResponseBuilder + where + Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>, + { + self.content_type(tide::http::mime::HTML).render(call) + } +} diff --git a/examples/tide/statics/btfl.svg b/examples/tide/statics/btfl.svg new file mode 100644 index 0000000..dcf4c41 --- /dev/null +++ b/examples/tide/statics/btfl.svg @@ -0,0 +1 @@ + diff --git a/examples/tide/statics/cloud.svg b/examples/tide/statics/cloud.svg new file mode 100644 index 0000000..3984def --- /dev/null +++ b/examples/tide/statics/cloud.svg @@ -0,0 +1 @@ + diff --git a/examples/tide/statics/grass.svg b/examples/tide/statics/grass.svg new file mode 100644 index 0000000..a743517 --- /dev/null +++ b/examples/tide/statics/grass.svg @@ -0,0 +1 @@ + diff --git a/examples/tide/statics/squirrel.jpg b/examples/tide/statics/squirrel.jpg new file mode 100644 index 0000000..900f409 Binary files /dev/null and b/examples/tide/statics/squirrel.jpg differ diff --git a/examples/tide/style.scss b/examples/tide/style.scss new file mode 100644 index 0000000..fbcd696 --- /dev/null +++ b/examples/tide/style.scss @@ -0,0 +1,74 @@ +html, body { + margin: 0; + padding: 0; +} + +body { + background: left bottom / auto 9% repeat-x url(static-name(grass.svg)) fixed, + 82% 96% / 5vh auto no-repeat url(static-name(btfl.svg)) fixed, + center bottom / auto 10% repeat-x url(static-name(grass.svg)) fixed, + right bottom / auto 11% repeat-x url(static-name(grass.svg)) fixed, + 10% 90% / 8vh auto no-repeat url(static-name(btfl.svg)) fixed, + linear-gradient(#519dd2, #7ec0ec) fixed; +} + +main { + padding: 2ex 3ex; + margin: 8vh auto 1em; + max-width: 37em; + background: white; + border-radius: 1em; + position: relative; + + &:before, &:after { + content: url(static-name(cloud.svg)); + display: block; + position: absolute; + z-index: -1; + } + &:before { + width: 52%; + top: -7vh; + left: -7%; + } + &:after { + width: 30%; + top: -6vh; + right: -4%; + } +} + +footer { + background: #84ff5e; + border-radius: 1em 0 0; + bottom: 0; + padding: 0 1em; + position: fixed; + right: 0; +} + +h1 { + margin: 0 0 1ex; +} +figure { + float: right; + margin: 0 0 1ex 1em; +} +p { + margin: 0 0 1em; +} + +.error { + body { + background: left bottom / auto 9% repeat-x url(static-name(grass.svg)) fixed, + 82% 98% / 5vh auto no-repeat url(static-name(btfl.svg)) fixed, + 10% 97% / 6vh auto no-repeat url(static-name(btfl.svg)) fixed, + right bottom / auto 11% repeat-x url(static-name(grass.svg)) fixed, + linear-gradient(#89a, #568) fixed; + } + main { + background: linear-gradient(#fff, #fff, #eee, #ccc, #888) padding-box; + border: solid 1px rgba(white, 0.3); + border-width: 0 2px 2px 1px; + } +} diff --git a/examples/tide/templates/error.rs.html b/examples/tide/templates/error.rs.html new file mode 100644 index 0000000..218d546 --- /dev/null +++ b/examples/tide/templates/error.rs.html @@ -0,0 +1,25 @@ +@use super::statics::*; +@use crate::footer; +@use tide::StatusCode; + +@(code: StatusCode, message: &str) + + + + + Error @(u16::from(code)): @code.canonical_reason() + + + + +
+

@code.canonical_reason()

+ +

@message

+

We are sorry about this. + In a real application, this would mention the incident having + been logged, and giving contact details for further reporting.

+
+ @:footer() + + diff --git a/examples/tide/templates/footer.rs.html b/examples/tide/templates/footer.rs.html new file mode 100644 index 0000000..533dbe8 --- /dev/null +++ b/examples/tide/templates/footer.rs.html @@ -0,0 +1,14 @@ +@(frameworks: &[(&str, &str)]) + +
+ @if let Some(((last_name, last_href), prev)) = frameworks.split_last() { + Made with + @if let Some(((last_name, last_href), prev)) = prev.split_last() { + @for (name, href) in prev { + @name, + } + @last_name and + } + @last_name. + } +
diff --git a/examples/tide/templates/page.rs.html b/examples/tide/templates/page.rs.html new file mode 100644 index 0000000..c8ad365 --- /dev/null +++ b/examples/tide/templates/page.rs.html @@ -0,0 +1,36 @@ +@use super::statics::*; +@use crate::footer; + +@(paras: &[(&str, usize)]) + + + + + Example + + + + +
+

Example

+ +

This is a simple sample page, + to be served with warp. + It contains an image of a squirrel and not much more.

+ +
+ Squirrel! +
A squirrel
+
+ + @for (order, n) in paras { +

This is a @order paragraph, with @n repeats. + @for _ in 1..=*n { + This is a @order paragraph. + } + } +

+ @:footer() + + diff --git a/src/lib.rs b/src/lib.rs index 7e0da92..9199f04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,13 +90,21 @@ //! version 0.2.x of the [mime] crate. //! * `warp02` -- Provide an extension to [`Response::Builder`] to //! simplify template rendering in the [warp] framework, versions 0.2.x. +//! * `http-types` -- Static files know their mime types, compatible with +//! the [http-types] crate. +//! * `tide013` -- Support for the [tide] framework version 0.13.x. +//! Implies the `http-types` feature (but does not require a direct +//! http-types requirement, as that is reexported by tide). //! //! [`response::Builder`]: ../http/response/struct.Builder.html //! [mime]: https://crates.rs/crates/mime //! [warp]: https://crates.rs/crates/warp +//! [tide]: https://crates.rs/crates/tide +//! [http-types]: https://crates.rs/crates/http-types //! -//! The `mime02` and `mime03` features are mutually exclusive and -//! requires a dependency on a matching version of `mime`. +//! The `mime02`, `mime03`, and `http-types` features are mutually +//! exclusive and requires a dependency on a matching version of +//! `mime` or `http-types`. //! Any of them can be combined with the `sass` feature. //! //! ```toml diff --git a/src/staticfiles.rs b/src/staticfiles.rs index 683d4e5..39d43cc 100644 --- a/src/staticfiles.rs +++ b/src/staticfiles.rs @@ -208,6 +208,11 @@ impl StaticFiles { if cfg!(feature = "mime03") { src.write_all(b"extern crate mime;\nuse self::mime::Mime;\n\n")?; } + if cfg!(feature = "tide013") { + src.write_all(b"use tide::http::mime::{self, Mime};\n\n")?; + } else if cfg!(feature = "http-types") { + src.write_all(b"use http_types::mime::{self, Mime};\n\n")?; + } src.write_all( b"/// A static file has a name (so its url can be recognized) and the /// actual file contents. @@ -226,6 +231,9 @@ pub struct StaticFile { if cfg!(feature = "mime03") { src.write_all(b" pub mime: &'static Mime,\n")?; } + if cfg!(feature = "http-types") { + src.write_all(b" pub mime: &'static Mime,\n")?; + } src.write_all( b"} #[allow(dead_code)] @@ -588,12 +596,13 @@ fn checksum_slug(data: &[u8]) -> String { } #[cfg(not(feature = "mime02"))] #[cfg(not(feature = "mime03"))] +#[cfg(not(feature = "http-types"))] fn mime_arg(_: &str) -> String { "".to_string() } #[cfg(feature = "mime02")] fn mime_arg(suffix: &str) -> String { - format!("_mime: {:?},\n", mime_from_suffix(suffix)) + format!(" _mime: {:?},\n", mime_from_suffix(suffix)) } #[cfg(feature = "mime02")] @@ -614,9 +623,9 @@ fn mime_from_suffix(suffix: &str) -> &'static str { } } -#[cfg(feature = "mime03")] +#[cfg(any(feature = "mime03", feature = "http-types"))] fn mime_arg(suffix: &str) -> String { - format!("mime: &mime::{},\n", mime_from_suffix(suffix)) + format!(" mime: &mime::{},\n", mime_from_suffix(suffix)) } #[cfg(feature = "mime03")] @@ -636,3 +645,21 @@ fn mime_from_suffix(suffix: &str) -> &'static str { _ => "APPLICATION_OCTET_STREAM", } } + +#[cfg(feature = "http-types")] +fn mime_from_suffix(suffix: &str) -> &'static str { + match suffix.to_lowercase().as_ref() { + "css" => "CSS", + "html" | "htm" => "CSS", + "ico" => "ICO", + "jpg" | "jpeg" => "JPEG", + "js" | "jsonp" => "JAVASCRIPT", + "json" => "JSON", + "png" => "PNG", + "svg" => "SVG", + "txt" => "PLAIN", + "wasm" => "WASM", + "xml" => "XML", + _ => "mime::BYTE_STREAM", + } +}