jmespath 0.1.1

Rust implementation of JMESPath, a query language for JSON
Documentation
//! Generates compliance tests and benchmarks

extern crate serde_json;
extern crate slug;

use std::collections::BTreeMap;
use std::env;
use std::path::Path;
use std::fs::{self, File};
use std::io::{Read, Write};

use self::serde_json::Value;

pub fn main() {
    let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR not specified");
    let bench_path = Path::new(&out_dir).join("benches.rs");
    let mut bench_file = File::create(&bench_path).expect("Could not create file");
    let compliance_path = Path::new(&out_dir).join("compliance_tests.rs");
    let mut compliance_file = File::create(&compliance_path).expect("Could not create file");
    let suites = load_test_suites();

    for (suite_num, &(ref filename, ref suite)) in suites.iter().enumerate() {
        let suite_obj = suite.as_object().expect("Suite not object");
        let given = suite_obj.get("given").expect("No given value");
        let cases = suite_obj.get("cases").expect("No cases value");
        let short_filename = filename.replace(".json", "").replace("tests/compliance/", "");
        let given_string = serde_json::to_string(given).unwrap();
        for (case_num, case) in cases.as_array().expect("cases not array").iter().enumerate() {
            let case_obj = case.as_object().expect("case not object");
            if case_obj.get("bench").is_some() {
                generate_bench(&short_filename, suite_num, case_num, case_obj,
                    &given_string, &mut bench_file);
            } else {
                generate_test(&short_filename, suite_num, case_num, case_obj,
                    &given_string, &mut compliance_file);
            }
        }
    }
}

/// Load all tests suites found in the tests/compliance directory.
pub fn load_test_suites() -> Vec<(String, Value)> {
    let mut result = vec![];
    let files = fs::read_dir("tests/compliance").expect("Invalid directory: tests/compliance");
    for filename in files {
        let path = filename.expect("Invalid file").path();
        let file_path = path.to_str().expect("Could not to_str file").to_string();
        let mut f = File::open(path).expect("Unable to open file");
        let mut file_data = String::new();
        f.read_to_string(&mut file_data).expect("Could to read JSON to string");
        let mut suite_json: Value = serde_json::from_str(&file_data).expect("invalid JSON");
        let mut suites = suite_json.as_array_mut().expect("Test suite is not a JSON array");
        while let Some(suite) = suites.pop() {
            result.push((file_path.clone(), suite));
        }
    }
    result
}

/// Gets the expression from a test case with helpful error messages.
#[inline]
fn get_expr(case: &BTreeMap<String, Value>) -> &str {
    case.get("expression")
        .expect("No expression in case")
        .as_str()
        .expect("Could not convert case to string")
}

/// SLugifies a string for use as a function name.
/// Ensures that the generated slug is truncated if too long.
#[inline]
fn slugify(s: &str) -> String {
    let mut s = slug::slugify(s).replace("-", "_");
    if s.len() > 25 {
        s.truncate(25);
    }
    s
}

/// Generates a function name for a test suite's test case.
#[inline]
fn generate_fn_name(filename: &str,
                    suite_num: usize,
                    case_num: usize,
                    case: &BTreeMap<String, Value>) -> String {
    let expr = get_expr(case);
    // Use the comment as the fn description if it is present.
    let description = match case.get("comment") {
        Some(ref c) => c.as_str().expect("comment is not a string"),
        None => expr
    };
    format!("{}_{}_{}_{}", slugify(filename), suite_num, case_num, slugify(description))
}

/// Generates a benchmark to be run with cargo bench.
///
/// Each test case will generate a case for parsing and a case for both
/// parsing and interpreting.
fn generate_bench(filename: &str,
                  suite_num: usize,
                  case_num: usize,
                  case: &BTreeMap<String, Value>,
                  given_string: &str,
                  f: &mut File) {
    let expr = get_expr(case);
    let expr_string = expr.replace("\"", "\\\"");
    let fn_suffix = generate_fn_name(filename, suite_num, case_num, case);
    let bench_type = case.get("bench").unwrap().as_str().expect("bench is not a string");

    // Validate that the bench attribute is an expected type.
    if bench_type != "parse" && bench_type != "full" && bench_type != "interpret" {
        panic!("invalid bench type: {}", bench_type);
    }

    // Create the parsing benchmark if "parse" or "full"
    if bench_type == "parse" || bench_type == "full" {
        f.write_all(format!("\
#[bench]
fn {}_parse_lex(b: &mut Bencher) {{
    b.iter(|| {{ parse({:?}).ok() }});
}}

", fn_suffix, expr_string).as_bytes()).expect("Error writing parse benchmark");
    }

    // Create the interpreter benchmark if "interpret" or "full"
    if bench_type == "interpret" || bench_type == "full" {
        f.write_all(format!("\
#[bench]
fn {}_interpret(b: &mut Bencher) {{
    let data = Rcvar::new(Variable::from_json({:?}).expect(\"Invalid JSON given\"));
    let expr = compile({:?}).unwrap();
    b.iter(|| {{ expr.search(&data).ok() }});
}}

", fn_suffix, given_string, expr_string).as_bytes()).expect("Error writing interpret benchmark");
    }

    // Create the "full" benchmark if "full"
    if bench_type == "full" {
        f.write_all(format!("\
#[bench]
fn {}_full(b: &mut Bencher) {{
    let data = Rcvar::new(Variable::from_json({:?}).expect(\"Invalid JSON given\"));
    b.iter(|| {{ compile({:?}).unwrap().search(&data).ok() }});
}}

", fn_suffix, given_string, expr_string).as_bytes()).expect("Error writing interpret benchmark");
    }
}

/// Generates a benchmark for a test case.
fn generate_test(filename: &str,
                 suite_num: usize,
                 case_num: usize,
                 case: &BTreeMap<String, Value>,
                 given_string: &str,
                 f: &mut File) {
    let fn_suffix = generate_fn_name(filename, suite_num, case_num, case);
    let case_string = serde_json::to_string(case).expect("Could not encode case");

    f.write_all(format!("\
#[test]
fn test_{}() {{
    let case: TestCase = TestCase::from_str({:?}).unwrap();
    let data = Rcvar::new(Variable::from_json({:?}).unwrap());
    case.assert({:?}, data).unwrap();
}}

", fn_suffix, case_string, given_string, filename).as_bytes()).expect("Unable to write test");
}