Skip to content

Commit 60e7a18

Browse files
committed
Added the groundwork for tera
1 parent cbfd197 commit 60e7a18

File tree

7 files changed

+521
-13
lines changed

7 files changed

+521
-13
lines changed

Cargo.lock

Lines changed: 259 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ params = "0.8"
5252
staticfile = { version = "0.4", features = [ "cache" ] }
5353
tempfile = "3.1.0"
5454

55+
# Templating
56+
tera = { version = "1.3.0", features = ["builtins"] }
57+
58+
# Template hot-reloading
59+
arc-swap = "0.4.6"
60+
5561
[target.'cfg(not(windows))'.dependencies]
5662
libc = "0.2"
5763

src/bin/cratesfyi.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ enum CommandLine {
5252
StartWebServer {
5353
#[structopt(name = "SOCKET_ADDR", default_value = "0.0.0.0:3000")]
5454
socket_addr: String,
55+
56+
/// Reload templates when they're changed
57+
#[structopt(long = "reload")]
58+
reload: bool,
5559
},
5660

5761
/// Starts cratesfyi daemon
@@ -78,8 +82,11 @@ impl CommandLine {
7882
pub fn handle_args(self) {
7983
match self {
8084
Self::Build(build) => build.handle_args(),
81-
Self::StartWebServer { socket_addr } => {
82-
Server::start(Some(&socket_addr));
85+
Self::StartWebServer {
86+
socket_addr,
87+
reload,
88+
} => {
89+
Server::start(Some(&socket_addr), reload);
8390
}
8491
Self::Daemon { foreground } => cratesfyi::utils::start_daemon(!foreground),
8592
Self::Database { subcommand } => subcommand.handle_args(),

src/utils/daemon.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub fn start_daemon(background: bool) {
247247
// at least start web server
248248
info!("Starting web server");
249249

250-
crate::Server::start(None);
250+
crate::Server::start(None, false);
251251
}
252252

253253
fn opts() -> DocBuilderOptions {

src/web/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,12 @@ pub struct Server {
362362
}
363363

364364
impl Server {
365-
pub fn start(addr: Option<&str>) -> Self {
365+
pub fn start(addr: Option<&str>, reload_templates: bool) -> Self {
366+
page::TEMPLATE_DATA.poke().expect("This returns Ok(())");
367+
if reload_templates {
368+
page::TemplateData::start_template_reloading();
369+
}
370+
366371
let server = Self::start_inner(addr.unwrap_or(DEFAULT_BIND), Pool::new());
367372
info!("Running docs.rs web server on http://{}", server.addr());
368373
server

src/web/page.rs renamed to src/web/page/mod.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
use handlebars_iron::Template;
44
use iron::response::Response;
55
use iron::{status, IronResult, Set};
6-
use serde::ser::{Serialize, SerializeStruct, Serializer};
6+
use serde::{
7+
ser::{SerializeStruct, Serializer},
8+
Serialize,
9+
};
710
use serde_json::Value;
811
use std::collections::BTreeMap;
912

@@ -32,14 +35,6 @@ fn load_rustc_resource_suffix() -> Result<String, failure::Error> {
3235
failure::bail!("failed to parse the rustc version");
3336
}
3437

35-
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize)]
36-
pub(crate) struct GlobalAlert {
37-
pub(crate) url: &'static str,
38-
pub(crate) text: &'static str,
39-
pub(crate) css_class: &'static str,
40-
pub(crate) fa_icon: &'static str,
41-
}
42-
4338
#[derive(Debug, Clone, PartialEq, Eq)]
4439
pub struct Page<T: Serialize> {
4540
title: Option<String>,
@@ -268,3 +263,47 @@ mod tests {
268263
assert!(Url::parse(&safe).is_ok());
269264
}
270265
}
266+
267+
// --- Tera ---
268+
269+
mod templates;
270+
271+
pub(crate) use templates::TemplateData;
272+
273+
lazy_static::lazy_static! {
274+
/// Holds all data relevant to templating
275+
pub(crate) static ref TEMPLATE_DATA: TemplateData = TemplateData::new().expect("Failed to load template data");
276+
}
277+
278+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
279+
pub(crate) struct GlobalAlert {
280+
pub(crate) url: &'static str,
281+
pub(crate) text: &'static str,
282+
pub(crate) css_class: &'static str,
283+
pub(crate) fa_icon: &'static str,
284+
}
285+
286+
#[cfg(test)]
287+
mod tera_tests {
288+
use super::*;
289+
use serde_json::json;
290+
291+
#[test]
292+
fn serialize_global_alert() {
293+
let alert = GlobalAlert {
294+
url: "http://www.hasthelargehadroncolliderdestroyedtheworldyet.com/",
295+
text: "THE WORLD WILL SOON END",
296+
css_class: "THE END IS NEAR",
297+
fa_icon: "https://gph.is/1uOvmqR",
298+
};
299+
300+
let correct_json = json!({
301+
"url": "http://www.hasthelargehadroncolliderdestroyedtheworldyet.com/",
302+
"text": "THE WORLD WILL SOON END",
303+
"css_class": "THE END IS NEAR",
304+
"fa_icon": "https://gph.is/1uOvmqR"
305+
});
306+
307+
assert_eq!(correct_json, serde_json::to_value(&alert).unwrap());
308+
}
309+
}

src/web/page/templates.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use super::TEMPLATE_DATA;
2+
use crate::error::Result;
3+
use arc_swap::ArcSwap;
4+
use serde_json::Value;
5+
use std::collections::HashMap;
6+
use tera::{Result as TeraResult, Tera};
7+
8+
/// Holds all data relevant to templating
9+
pub(crate) struct TemplateData {
10+
/// The actual templates, stored in an `ArcSwap` so that they're hot-swappable
11+
// TODO: Conditional compilation so it's not always wrapped, the `ArcSwap` is unneeded overhead for prod
12+
pub templates: ArcSwap<Tera>,
13+
/// The current global alert, serialized into a json value
14+
global_alert: Value,
15+
/// The version of docs.rs, serialized into a json value
16+
docsrs_version: Value,
17+
/// The current resource suffix of rustc, serialized into a json value
18+
resource_suffix: Value,
19+
}
20+
21+
impl TemplateData {
22+
pub fn new() -> Result<Self> {
23+
log::trace!("Loading templates");
24+
25+
let data = Self {
26+
templates: ArcSwap::from_pointee(load_templates()?),
27+
global_alert: serde_json::to_value(crate::GLOBAL_ALERT)?,
28+
docsrs_version: Value::String(crate::BUILD_VERSION.to_owned()),
29+
resource_suffix: Value::String(load_rustc_resource_suffix().unwrap_or_else(|err| {
30+
log::error!("Failed to load rustc resource suffix: {:?}", err);
31+
String::from("???")
32+
})),
33+
};
34+
35+
log::trace!("Finished loading templates");
36+
37+
Ok(data)
38+
}
39+
40+
pub fn start_template_reloading() {
41+
use std::{sync::Arc, thread, time::Duration};
42+
43+
thread::spawn(|| loop {
44+
match load_templates() {
45+
Ok(templates) => {
46+
log::info!("Reloaded templates");
47+
TEMPLATE_DATA.templates.swap(Arc::new(templates));
48+
thread::sleep(Duration::from_secs(10));
49+
}
50+
51+
Err(err) => {
52+
log::info!("Error Loading Templates:\n{}", err);
53+
thread::sleep(Duration::from_secs(5));
54+
}
55+
}
56+
});
57+
}
58+
59+
/// Used to initialize a `TemplateData` instance in a `lazy_static`.
60+
/// Loading tera takes a second, so it's important that this is done before any
61+
/// requests start coming in
62+
pub fn poke(&self) -> Result<()> {
63+
Ok(())
64+
}
65+
}
66+
67+
// TODO: Is there a reason this isn't fatal? If the rustc version is incorrect (Or "???" as used by default), then
68+
// all pages will be served *really* weird because they'll lack all CSS
69+
fn load_rustc_resource_suffix() -> Result<String> {
70+
let conn = crate::db::connect_db()?;
71+
72+
let res = conn.query(
73+
"SELECT value FROM config WHERE name = 'rustc_version';",
74+
&[],
75+
)?;
76+
if res.is_empty() {
77+
failure::bail!("missing rustc version");
78+
}
79+
80+
if let Some(Ok(vers)) = res.get(0).get_opt::<_, Value>("value") {
81+
if let Some(vers_str) = vers.as_str() {
82+
return Ok(crate::utils::parse_rustc_version(vers_str)?);
83+
}
84+
}
85+
86+
failure::bail!("failed to parse the rustc version");
87+
}
88+
89+
pub(super) fn load_templates() -> TeraResult<Tera> {
90+
let mut tera = Tera::new("templates/**/*")?;
91+
92+
// Custom functions
93+
tera.register_function("global_alert", global_alert);
94+
tera.register_function("docsrs_version", docsrs_version);
95+
tera.register_function("rustc_resource_suffix", rustc_resource_suffix);
96+
97+
// Custom filters
98+
tera.register_filter("timeformat", timeformat);
99+
tera.register_filter("dbg", dbg);
100+
tera.register_filter("dedent", dedent);
101+
102+
Ok(tera)
103+
}
104+
105+
/// Returns an `Option<GlobalAlert>` in json form for templates
106+
fn global_alert(args: &HashMap<String, Value>) -> TeraResult<Value> {
107+
debug_assert!(args.is_empty(), "global_alert takes no args");
108+
109+
Ok(TEMPLATE_DATA.global_alert.clone())
110+
}
111+
112+
/// Returns the version of docs.rs, takes the `safe` parameter which can be `true` to get a url-safe version
113+
fn docsrs_version(args: &HashMap<String, Value>) -> TeraResult<Value> {
114+
debug_assert!(
115+
args.is_empty(),
116+
"docsrs_version only takes no args, to get a safe version use `docsrs_version() | slugify`",
117+
);
118+
119+
Ok(TEMPLATE_DATA.docsrs_version.clone())
120+
}
121+
122+
/// Returns the current rustc resource suffix
123+
fn rustc_resource_suffix(args: &HashMap<String, Value>) -> TeraResult<Value> {
124+
debug_assert!(args.is_empty(), "rustc_resource_suffix takes no args");
125+
126+
Ok(TEMPLATE_DATA.resource_suffix.clone())
127+
}
128+
129+
/// Prettily format a timestamp
130+
// TODO: This can be replaced by chrono
131+
fn timeformat(value: &Value, args: &HashMap<String, Value>) -> TeraResult<Value> {
132+
let fmt = if let Some(Value::Bool(true)) = args.get("relative") {
133+
let value = time::strptime(value.as_str().unwrap(), "%Y-%m-%dT%H:%M:%S%z").unwrap();
134+
135+
super::super::duration_to_str(value.to_timespec())
136+
} else {
137+
const TIMES: &[&str] = &["seconds", "minutes", "hours"];
138+
139+
let mut value = value.as_f64().unwrap();
140+
let mut chosen_time = &TIMES[0];
141+
142+
for time in &TIMES[1..] {
143+
if value / 60.0 >= 1.0 {
144+
chosen_time = time;
145+
value /= 60.0;
146+
} else {
147+
break;
148+
}
149+
}
150+
151+
// TODO: This formatting section can be optimized, two string allocations aren't needed
152+
let mut value = format!("{:.1}", value);
153+
if value.ends_with(".0") {
154+
value.truncate(value.len() - 2);
155+
}
156+
157+
format!("{} {}", value, chosen_time)
158+
};
159+
160+
Ok(Value::String(fmt))
161+
}
162+
163+
/// Print a tera value to stdout
164+
fn dbg(value: &Value, _args: &HashMap<String, Value>) -> TeraResult<Value> {
165+
println!("{:?}", value);
166+
167+
Ok(value.clone())
168+
}
169+
170+
/// Dedent a string by removing all leading whitespace
171+
fn dedent(value: &Value, _args: &HashMap<String, Value>) -> TeraResult<Value> {
172+
let string = value.as_str().expect("dedent takes a string");
173+
174+
Ok(Value::String(
175+
string
176+
.lines()
177+
.map(|l| l.trim_start())
178+
.collect::<Vec<&str>>()
179+
.join("\n"),
180+
))
181+
}
182+
183+
#[cfg(test)]
184+
mod tests {
185+
use super::*;
186+
187+
#[test]
188+
fn test_templates_are_valid() {
189+
let tera = load_templates().unwrap();
190+
tera.check_macro_files().unwrap();
191+
}
192+
}

0 commit comments

Comments
 (0)