twig 0.1.0

Twig templating engine for Rust; work in progress.
Documentation
extern crate twig;
extern crate serde_json;
extern crate difference;
extern crate term;
#[macro_use] extern crate log;
extern crate env_logger;

use std::collections::HashMap;
use std::io::{self, Read, BufReader, BufRead};
use std::env;
use std::io::Write;
use difference::Difference;
use std::fs::{self, DirEntry, File};
use std::path::Path;

use twig::environment::{ Environment, Config };
use twig::loader::ArrayLoader;
use twig::Engine;

#[test]
fn fixtures() {
    env_logger::init().unwrap();

    let errors = visit_fixtures(&env::current_dir().unwrap().join("tests").join("fixtures"), &|entry| {
        let f = match File::open(entry.path()) {
            Ok(f) => f,
            Err(e) => panic!("error opening fixture file {:?}, {:?}", entry.path(), e),
        };
        let fixture = match Fixture::new(f) {
            Err(e) => panic!("invalid test {:?}", e),
            Ok(f) => f,
        };

        let message = match fixture.message.clone() {
            Some(m) => m,
            None => panic!("fixture {:?} must have a message", entry.path()),
        };
        print_fixture_start(&message).unwrap();

        let mut twig = Engine::new(ArrayLoader::new(
            vec![("index.twig".into(), fixture.template.expect("fixture must contain main template"))]
                .into_iter()
                .chain(fixture.templates.into_iter())
                .collect()
        ), match fixture.config {
            Some(config) => Environment::new(Config::from_hashmap(
                match serde_json::from_str(&config) {
                    Ok(map) => map,
                    Err(e) => panic!("failed to deserialize template config: {:#?}", e),
                }
            )),
            _ => Environment::default(),
        });

        let data = match fixture.data {
            Some(data) => match serde_json::from_str::<HashMap<String, String>>(&data) {
                Ok(map) => map,
                Err(e) => panic!("failed to deserialize template data: {:#?}", e),
            },
            None => HashMap::new(),
        };

        let res = match twig.get("index.twig", data) {
            Ok(res) => res,
            Err(e) => panic!("\nerror executing template:\n    {:#?}\n", e),
        };

        let expected = fixture.expect.expect("fixture must have expect block");

        if res != expected {
            print_fixture_result(false).unwrap();

            let (_, changeset) = difference::diff(
                &res,
                &expected,
                "\n"
            );
            print_diff(&changeset).unwrap();

            Some((
                entry.path().to_string_lossy().into_owned(),
                message,
                changeset
            ))
            //assert_eq!(res, expected);
        } else {
            print_fixture_result(true).unwrap();
            None
        }
    }).unwrap();

    let num_errors = errors.len();
    if num_errors > 0 {
        for (file, name, changeset) in errors {
            println!("in {}", file);
            println!("testing {}", name);
            print_uncolored(&changeset);
        }
        //panic!("{} fixtures produced errors", num_errors);
    }
}

fn visit_fixtures(dir: &Path, cb: &Fn(&DirEntry) -> Option<(String, String, Vec<Difference>)>) -> io::Result<Vec<(String, String, Vec<Difference>)>> {
    let mut errors = Vec::new();
    if try!(fs::metadata(dir)).is_dir() {
        for entry in try!(fs::read_dir(dir)) {
            let entry = try!(entry);
            if try!(fs::metadata(entry.path())).is_dir() {
                for e in try!(visit_fixtures(&entry.path(), cb)) {
                    errors.push(e);
                }
            } else {
                if let Some(Some("test")) = entry.path().extension().map(|v| v.to_str()) {
                    if let Some(err) = cb(&entry) {
                        errors.push(err);
                    }
                }
            }
        }
    }
    Ok(errors)
}

#[derive(Debug)]
enum FixtureError {
    ExpectedBlockStart,
    IoError,
}

#[derive(Debug)]
enum TemplateName {
    Main,
    Other(String),
}

#[derive(Debug)]
enum ReadState {
    Message(String),
    Template((TemplateName, String)),
    Data(String),
    Config(String),
    Expect(String)
}

#[derive(Debug)]
struct Fixture {
    message: Option<String>,
    template: Option<String>,
    templates: Vec<(String, String)>,
    data: Option<String>,
    config: Option<String>,
    expect: Option<String>,
}

const TEMPLATE_NAME_START: &'static str = "--TEMPLATE(";
const TEMPLATE_NAME_END: &'static str = ")--";

fn check_for_new_state(line: &str) -> Option<ReadState> {
    match &line[..] {
        "--TEST--" => Some(ReadState::Message(String::new())),
        "--DATA--" => Some(ReadState::Data(String::new())),
        "--CONFIG--" => Some(ReadState::Config(String::new())),
        "--EXPECT--" => Some(ReadState::Expect(String::new())),
        other => if other.starts_with("--TEMPLATE(") {
            let name = other[TEMPLATE_NAME_START.len()..other.len()-TEMPLATE_NAME_END.len()].to_string();
            Some(ReadState::Template((TemplateName::Other(name), String::new())))
        } else if other.starts_with("--TEMPLATE--") {
            Some(ReadState::Template((TemplateName::Main, String::new())))
        } else {
            None
        },
    }
}

impl Fixture {
    pub fn new<R: Read>(input: R) -> Result<Fixture, FixtureError> {
        let mut state = None;
        let mut fixture = Fixture {
            message: None,
            template: None,
            templates: Vec::new(),
            data: None,
            config: None,
            expect: None,
        };

        for maybe_line in BufReader::new(input).lines() {
            let line = match maybe_line {
                Ok(l) => l,
                Err(_) => return Err(FixtureError::IoError),
            };

            state = match state {
                None => {
                    Some(try!(check_for_new_state(&line).ok_or(FixtureError::ExpectedBlockStart)))
                },
                Some(mut old) => match check_for_new_state(&line) {
                    Some(new_state) => {
                        fixture.collect(old);
                        Some(new_state)
                    },
                    None => {
                        match old {
                            ReadState::Message(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); },
                            ReadState::Template((_, ref mut m)) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Data(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Config(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Expect(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                        };
                        Some(old)
                    }
                },
            }
        }

        if let Some(leftover_state) = state {
            fixture.collect(leftover_state);
        }

        Ok(fixture)
    }

    fn collect(&mut self, state: ReadState) {
        match state {
            ReadState::Message(m) => self.message = Some(m),
            ReadState::Template((TemplateName::Main, m)) => self.template = Some(m),
            ReadState::Template((TemplateName::Other(name), m)) => {
                self.templates.push((name, m));
            },
            ReadState::Data(m) => self.data = Some(m),
            ReadState::Config(m) => self.config = Some(m),
            ReadState::Expect(m) => self.expect = Some(m),
        }
    }
}

fn print_fixture_start(message: &str) -> io::Result<()> {
    let mut t = term::stdout().unwrap();
    try!(write!(t, "fixture "));
    try!(t.attr(term::Attr::Bold));
    try!(write!(t, "{}", message));
    try!(t.reset());
    try!(write!(t, " ... "));
    t.flush()
}

fn print_fixture_result(ok: bool) -> io::Result<()> {
    let mut t = term::stdout().unwrap();
    if ok {
        try!(t.fg(term::color::GREEN));
        try!(writeln!(t, "ok"));
    } else {
        try!(t.fg(term::color::RED));
        try!(writeln!(t, "ERROR!"));
    }
    try!(t.reset());
    t.flush()
}

fn print_diff(changeset: &Vec<Difference>) -> io::Result<()> {
    let mut t = term::stdout().unwrap();

    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                try!(t.reset());
                try!(writeln!(t, "  {}", x));
            },
            Difference::Add(ref x) => {
                for line in x.lines() {
                    try!(t.fg(term::color::GREEN));
                    try!(writeln!(t, "+ {}", line));
                }
            },
            Difference::Rem(ref x) => {
                for line in x.lines() {
                    try!(t.fg(term::color::RED));
                    try!(writeln!(t, "- {}", line));
                }
            }
        }
    }
    try!(t.reset());
    try!(writeln!(t, ""));
    t.flush()
}

fn print_uncolored(changeset: &Vec<Difference>) {
    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                println!("  {}", x);
            },
            Difference::Add(ref x) => {
                for line in x.lines() {
                    println!("+ {}", line);
                }
            },
            Difference::Rem(ref x) => {
                for line in x.lines() {
                    println!("- {}", line);
                }
            }
        }
    }
    println!("");
}