use std::cell::RefCell;
use std::fs::File;
use std::io::{BufReader, Read};
use std::rc::Rc;
use std::{convert::Infallible, net::SocketAddr};
use hyper::server::conn::AddrStream;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use commandline_macros::*;
use hair::*;
use binexpr::*;
#[derive(Command,Debug,Default)]
#[description("Static webserver that serves files and uses hair templates (.tpl) to serve .page request (using .json as data source)")]
#[program("hairprototyping")] #[related("md2h", 1)]
struct Options {
#[flag("-p", "--port", "number")]
port: Option<u16>,
#[flag("-m", "--maintemplate", "file")]
main_template_file: Option<String>,
#[positional("path",1,1)]
dir: String,
}
struct State {
options: Options,
counter: usize,
}
fn read_file_to_string(filename: &str) -> std::io::Result<String> {
let f = File::open(filename)?;
let mut reader = BufReader::new(f);
let mut bytes = String::new();
reader.read_to_string(&mut bytes)?;
Ok(bytes)
}
fn read_file_to_bytes(filename: &str) -> std::io::Result<Vec<u8>> {
let f = File::open(filename)?;
let mut reader = BufReader::new(f);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
Ok(bytes)
}
fn lookup_content_type(extension: &str) -> &'static str {
match extension {
"html" => "text/html",
"css" => "text/style",
"js" => "text/javascript",
"jpeg" => "image/jpeg",
"jpg" => "image/jpeg",
"png" => "image/png",
"svg" => "image/svg+xml",
"json" => "application/json",
_ => "text/plain",
}
}
fn parse_template<'a, 'c>(dir: &str, base: &str, scope: &'a mut MemoryScope<'c>) -> Result<(BytecodeVec, DecodedValue<'c>), Response<Body>> where 'c: 'a {
let filename_template = format!("{}/{}.tpl", dir, base);
let child_template = read_file_to_string(&filename_template);
if let Err(err) = child_template {
return Err(Response::builder().status(404).body(Body::from(format!("could not find {}: {}", filename_template, err))).unwrap());
}
let child_template = child_template.unwrap();
let child = hair_compile_html(&child_template, &filename_template, None, 0);
if let Err(err) = child {
return Err(Response::builder().status(500).body(Body::from(format!("hair compile error in {}:\n{}", filename_template, err))).unwrap());
}
let child = child.unwrap();
let filename_value = format!("{}/{}.json", dir, base);
let mut value : DecodedValue<'c> = value!({
});
if let Ok(value_contents) = read_file_to_string(&filename_value) {
let value_contents : &'c str = scope.copy_str(&value_contents);
match DecodedValue::parse_json(value_contents, scope) {
Ok(v) => value = v,
Err(err) => {
return Err(Response::builder().status(500).body(Body::from(format!("error parsing json file {}:\n{}", filename_value, binexpr_compile_error_format(value_contents, &err).1))).unwrap());
},
}
}
Ok((child, value))
}
async fn handle(req: Request<Body>, state: Rc<RefCell<State>>) -> Result<Response<Body>, Infallible> {
let mut state = state.borrow_mut();
state.counter += 1;
let mut allocator = MemoryPool::new();
let mut scope = allocator.clear();
let mut filename = req.uri().path();
if filename.ends_with("/") {
filename = write!(&mut scope, "{}index.page", filename);
}
let extension = filename.rsplit_once('.').map(|(_,ext)| ext).unwrap_or("");
if let Some(base) = filename.strip_suffix(".page") {
let (child, child_value) = match parse_template(&state.options.dir, base, &mut scope) {
Ok(v) => v,
Err(err) => return Ok(err),
};
if let Some(main_template_filename) = &state.options.main_template_file {
let mut main_template_filename = main_template_filename.clone();
if let Some(prefix) = main_template_filename.strip_suffix(".tpl") {
main_template_filename = prefix.to_string();
}
let (main_template, mut main_value) = match parse_template(&state.options.dir, &main_template_filename, &mut scope) {
Ok(v) => v,
Err(err) => return Ok(err),
};
if let DecodedValue::Object(obj) = &mut main_value {
obj.insert(key_str("bodytemplate"), (&child).into());
obj.insert(key_str("bodyvalue"), child_value);
} else {
return Ok(Response::builder().status(500).body(Body::from(format!("value for main template should be an object: {}{}.json", base, main_template_filename))).unwrap());
}
let output = hair_eval_html(main_template.to_ref(), main_value.to_vec(false).to_ref());
if let Err(err) = output {
return Ok(Response::builder().status(500).body(Body::from(format!("hair eval error in {}{}.tpl:\n{}", &state.options.dir, base, err))).unwrap());
}
let output = output.unwrap();
Ok(Response::builder().status(200).header("Content-Type", "text/html").body(output.into()).unwrap())
} else {
let output = hair_eval_html(child.to_ref(), child_value.to_vec(false).to_ref());
if let Err(err) = output {
return Ok(Response::builder().status(500).body(Body::from(format!("hair eval error in {}{}.tpl:\n{}", &state.options.dir, base, err))).unwrap());
}
let output = output.unwrap();
Ok(Response::builder().status(200).header("Content-Type", "text/html").body(output.into()).unwrap())
}
} else {
let filename = format!("{}{}", state.options.dir, filename);
let output = read_file_to_bytes(&filename);
match output {
Ok(output) => Ok(Response::builder().status(200).header("Content-Type", lookup_content_type(extension)).body(output.into()).unwrap()),
Err(err) => Ok(Response::builder().status(404).body(Body::from(format!("could not find {}: {}", filename, err))).unwrap()),
}
}
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
fn main() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build runtime");
let local = tokio::task::LocalSet::new();
local.block_on(&rt, run());
}
async fn run() {
let options : Options = commandline::parse_args();
let addr = SocketAddr::from(([0, 0, 0, 0], options.port.unwrap_or(3000)));
let state = Rc::new(RefCell::new(State{
options,
counter: 0,
}));
let make_svc = make_service_fn(move |_conn: &AddrStream| {
let state = state.clone();
async move {
Ok::<_, Infallible>(service_fn(move |req| {
handle(req, state.clone())
}))
}
});
let server = Server::bind(&addr).executor(LocalExec).serve(make_svc);
let server = server.with_graceful_shutdown(shutdown_signal());
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
#[derive(Clone, Copy, Debug)]
struct LocalExec;
impl<F> hyper::rt::Executor<F> for LocalExec
where
F: std::future::Future + 'static,
{
fn execute(&self, fut: F) {
tokio::task::spawn_local(fut);
}
}