Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tide feature and improved example #94

Merged
merged 9 commits into from
Aug 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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