Skip to content

Commit

Permalink
Merge pull request #94 from kaj/more-tide
Browse files Browse the repository at this point in the history
Tide feature and improved example
  • Loading branch information
kaj authored Aug 7, 2020
2 parents 15d1cbe + 07f5f1c commit 406e69b
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ sass = ["rsass"]
mime02 = []
mime03 = ["mime"]
warp02 = ["mime03"]
http-types = []
tide013 = ["http-types"]

[dependencies]
base64 = "^0.12.0"
Expand Down
7 changes: 4 additions & 3 deletions examples/tide/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[package]
name = "tide"
name = "ructe-tide"
version = "0.4.0"
authors = ["Rasmus Kaj <[email protected]>"]
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"
3 changes: 3 additions & 0 deletions examples/tide/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
110 changes: 99 additions & 11 deletions examples/tide/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,112 @@
// 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);
app.listen(addr).await?;

Ok(())
}

/// Handler for a page in the web site.
async fn frontpage(_req: Request<()>) -> Result<Response, Error> {
// 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<Response, Error> {
let path = req.param::<String>("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<Response, Error> {
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<Mime>, not AsRef<Mime>
.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<Box<dyn Future<Output = Result<Response, Error>> + 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"));
108 changes: 108 additions & 0 deletions examples/tide/src/ructe_tide.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
/// let content = "something";
/// let other = 17;
/// let mut result = Response::new(200);
/// result.render(|o| page(o, content, other))?;
/// # Ok(())
/// # }
/// ```
fn render<Call>(&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<Call>(&mut self, call: Call) -> std::io::Result<()>
where
Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>;
Expand All @@ -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<Call>(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<Call>(self, call: Call) -> tide::ResponseBuilder
where
Call: FnOnce(&mut dyn std::io::Write) -> std::io::Result<()>;
}

impl RenderBuilder for tide::ResponseBuilder {
fn render<Call>(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<Call>(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)
}
}
1 change: 1 addition & 0 deletions examples/tide/statics/btfl.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/tide/statics/cloud.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/tide/statics/grass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/tide/statics/squirrel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions examples/tide/style.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions examples/tide/templates/error.rs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use super::statics::*;
@use crate::footer;
@use tide::StatusCode;

@(code: StatusCode, message: &str)

<!doctype html>
<html lang="en" class="error">
<head>
<title>Error @(u16::from(code)): @code.canonical_reason()</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" href="/static/@style_css.name"/>
</head>
<body>
<main>
<h1>@code.canonical_reason()</h1>

<p>@message</p>
<p>We are sorry about this.
In a real application, this would mention the incident having
been logged, and giving contact details for further reporting.</p>
</main>
@:footer()
</body>
</html>
Loading

0 comments on commit 406e69b

Please sign in to comment.