hairy 0.1.0

Compiled text templates (not unlike Mustache and Handlebars), with support for expressions and custom functions inside such expressions.
Documentation
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")] // can have an argument, outputs man-page + shell completion
#[related("md2h", 1)]
struct Options {
    /// Port number
    #[flag("-p", "--port", "number")]
    port: Option<u16>,
    #[flag("-m", "--maintemplate", "file")]
    /// Load main template from specified file, expect the main template to have `{{call **bodytemplate with bodyvalue}}` to print the body. Relative to path given.
    main_template_file: Option<String>,
    /// directory to serve
    #[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!({
        // "bodytemplate": child,
    });
    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() {
    // Wait for the CTRL+C 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);
    }
}