use serde_json::{to_string, Number, Value}; use std::collections::HashSet; use std::env; use std::fs::read_to_string; use std::fs::File; use std::io::Write; use std::path::Path; use std::str::FromStr; use yaml_rust::{Yaml, YamlLoader}; fn main() { // request to be re-run whenever ../specification.yml changes let dev_path = std::path::Path::new("../specification.yml"); let crate_release_path = std::path::Path::new("specification.yml"); let spec_path = if dev_path.exists() { dev_path } else { crate_release_path }; println!("cargo:rerun-if-changed={}", spec_path.to_string_lossy()); // read ../specification.yml, falling back to a copy in this directory // for builds from a crate let spec = read_to_string(spec_path).unwrap(); let spec = YamlLoader::load_from_str(&spec).unwrap(); let out_dir = env::var("OUT_DIR").unwrap(); let test_path = Path::new(&out_dir).join("test_spec.rs"); let mut test_file = File::create(&test_path).unwrap(); writeln!(test_file, "use serde_json::Value;").unwrap(); writeln!(test_file, "use json_e::{{render, use_test_now}};").unwrap(); let mut section = String::from("unknown"); let section_key = Yaml::String("section".into()); let title_key = Yaml::String("title".into()); let context_key = Yaml::String("context".into()); let template_key = Yaml::String("template".into()); let result_key = Yaml::String("result".into()); let error_key = Yaml::String("error".into()); let mut test_names = HashSet::new(); for item in spec { if let Yaml::Hash(ref h) = item { // update the section name if this is a new section if let Some(v) = h.get(§ion_key) { section = String::from(v.as_str().unwrap()); continue; } let title = h.get(&title_key).unwrap().as_str().unwrap(); let context = h.get(&context_key).unwrap(); let template = h.get(&template_key).unwrap(); let result = h.get(&result_key); let error = h.get(&error_key); write_test( &mut test_file, &mut test_names, §ion, title, context, template, result, error, ); } else { panic!("YAML sub-document is not an object: {:?}", item); } } } /// Convert the given Yaml value to a serde_json::Value fn to_json(y: &Yaml) -> Value { match y { &Yaml::Real(ref v) => Value::Number(Number::from_str(v).unwrap()), &Yaml::Integer(v) => Value::Number(v.into()), &Yaml::String(ref v) => Value::String(v.into()), &Yaml::Boolean(ref v) => Value::Bool(*v), &Yaml::Array(ref v) => Value::Array(v.iter().map(|y| to_json(y)).collect()), &Yaml::Hash(ref v) => Value::Object( v.iter() .map(|(k, v)| (k.as_str().unwrap().to_string(), to_json(v))) .collect(), ), &Yaml::Null => Value::Null, &Yaml::Alias(_) => todo!(), &Yaml::BadValue => todo!(), } } /// Convert the given Yaml value to a JSON string fn to_json_str(y: &Yaml) -> String { to_string(&to_json(y)).unwrap() } fn write_test( test_file: &mut File, test_names: &mut HashSet, section: &str, title: &str, context: &Yaml, template: &Yaml, result: Option<&Yaml>, error: Option<&Yaml>, ) { // invent a unique test name let mut test_name: String = format!("{}_{}", section, title) .chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) .collect(); while test_names.contains(&test_name) { test_name.push('_'); } test_names.insert(test_name.clone()); let context = to_json_str(context); let template = to_json_str(template); write!( test_file, r##" #[test] fn {test_name}() {{ use_test_now(); println!("{{}} - {{}}", {section:?}, {title:?}); let context: Value = serde_json::from_str(r#"{context}"#).unwrap(); let template: Value = serde_json::from_str(r#"{template}"#).unwrap(); "##, test_name = test_name, section = section, title = title, context = context, template = template ) .unwrap(); if let Some(result) = result { let result = to_json_str(result); write!( test_file, r##" let result: Value = serde_json::from_str(r#"{result}"#).unwrap(); assert_eq!(render(&template, &context).unwrap(), result); }} "##, result = result ) .unwrap(); } else if let Some(_error) = error { // TODO: check that the error message matches write!( test_file, r##" assert!(render(&template, &context).is_err()); }} "## ) .unwrap(); } else { panic!("test case with neither result not error!"); } }