use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::collections::HashMap;
use heck::{SnakeCase, CamelCase};
use serde_derive::Deserialize;
use url::Url;
static SCHEMA_LOCATION: &'static str = "./schema/schemas/";
#[derive(Deserialize, Debug)]
enum SpecType {
#[serde(rename = "object")]
Object,
#[serde(rename = "array")]
Array,
#[serde(rename = "boolean")]
Boolean,
#[serde(rename = "integer")]
Integer,
#[serde(rename = "number")]
Number,
#[serde(rename = "string")]
String,
}
#[derive(Deserialize, Debug)]
enum Format {
#[serde(rename = "uri")]
Uri,
#[serde(rename = "date-time")]
DateTime,
}
#[derive(Deserialize, Debug)]
struct Property {
#[serde(rename = "type")]
ty: Option<SpecType>,
description: Option<String>,
#[serde(rename = "enum")]
enum_vals: Option<Vec<String>>,
format: Option<Format>,
#[serde(rename = "$ref")]
reftype: Option<String>,
items: Option<Box<Property>>,
}
#[derive(Deserialize, Debug)]
struct Schema {
#[serde(rename = "$id")]
id: String,
#[serde(rename = "type")]
ty: SpecType,
title: String,
description: Option<String>,
properties: HashMap<String, Property>,
required: Option<Vec<String>>,
}
struct PropSpec {
ty: String,
meta: Option<String>,
enumdef: Option<String>,
}
impl PropSpec {
fn new(ty: &str, meta: Option<String>, enumdef: Option<String>) -> Self {
Self { ty: ty.into(), meta, enumdef }
}
}
fn prop_to_type(classname: &str, name: &str, prop: &Property, indent: &str) -> PropSpec {
match prop.reftype {
Some(ref reftype) if reftype.ends_with(".json") => {
let url: Url = Url::parse(reftype).expect("prop_to_type() -- error parsing ref url");
let ty = url.path_segments().unwrap().last().unwrap().trim_end_matches(".json");
PropSpec::new(&format!("Box<{}::{}>", ty.to_snake_case(), ty), None, None)
}
Some(ref reftype) => {
panic!("prop_to_type() -- found a reftype that doesn't point to a JSON file: {}", reftype);
}
_ => {
match prop.ty.as_ref().unwrap() {
SpecType::Object => panic!("prop_to_type() -- `object` type not implemented for properties"),
SpecType::Array => {
let type_prop = prop.items.as_ref().expect("prop_to_type() -- `array` type is missing `items` sibling. curious.");
let PropSpec { ty, meta, enumdef } = prop_to_type(classname, name, type_prop, indent);
PropSpec::new(&format!("Vec<{}>", ty), meta.map(|x| format!("{}_vec", x)), enumdef)
}
SpecType::Boolean => PropSpec::new("bool", None, None),
SpecType::Integer => PropSpec::new("i64", None, None),
SpecType::Number => PropSpec::new("f64", None, None),
SpecType::String => {
if prop.enum_vals.is_some() {
let enumtype = name.to_camel_case();
let mut enum_out = String::new();
enum_out.push_str(&format!("{}#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]\n", indent));
enum_out.push_str(&format!("{}pub enum {} {{\n", indent, enumtype));
for enumval in prop.enum_vals.as_ref().unwrap() {
enum_out.push_str(&format!(r#"{} #[serde(rename="{}")]"#, indent, enumval));
enum_out.push_str("\n");
enum_out.push_str(&format!("{} {},\n", indent, enumval.to_camel_case()));
}
enum_out.push_str(&format!("{}}}", indent));
PropSpec::new(&enumtype, None, Some(enum_out))
} else {
match &prop.format {
Some(Format::Uri) => PropSpec::new("Url", Some("crate::ser::url".into()), None),
Some(Format::DateTime) => PropSpec::new("DateTime<Utc>", Some("crate::ser::datetime".into()), None),
None => PropSpec::new("String", None, None),
}
}
}
}
}
}
}
fn schema_to_class(schema: Schema) -> String {
let mut out = String::new();
let indent = " ";
macro_rules! line {
($contents:expr, $indent:expr) => {
out.push_str($indent);
out.push_str($contents);
out.push_str("\n");
};
($contents:expr) => { line!($contents, indent); }
}
let mut struct_out: Vec<String> = Vec::new();
let mut enum_out: Vec<String> = Vec::new();
let mut builder_out: Vec<String> = Vec::new();
let mut names = schema.properties.keys().map(|x| x.clone()).collect::<Vec<_>>();
names.sort();
for name in &names {
let name_snake = name.to_snake_case();
let prop = schema.properties.get(name).unwrap();
if let Some(ref desc) = prop.description {
struct_out.push(format!(" /// {}", desc));
}
let required = match schema.required {
Some(ref x) => x.contains(name),
None => false,
};
let PropSpec { ty: prop_type, meta, enumdef } = prop_to_type(&schema.title, &name, prop, indent);
if let Some(enumdef) = enumdef {
enum_out.push(format!("{}\n", enumdef));
}
let (prop_type, meta) = if required {
(prop_type, meta)
} else {
(
if prop_type.contains("Vec") {
prop_type
} else {
format!("Option<{}>", prop_type)
},
meta.map(|x| {
if x.ends_with("_vec") {
x
} else {
format!("{}_opt", x)
}
}),
)
};
if let Some(meta) = meta {
struct_out.push(format!(r#" #[serde(with="{}")]"#, meta));
}
if prop_type.contains("Option") {
struct_out.push(" #[builder(setter(into, strip_option), default)]".into());
}
struct_out.push(format!(" pub {}: {},", name_snake, prop_type));
let builder_line = if prop_type.contains("Option") {
format!("match {0} {{ Some(x) => builder.{0}(x), None => builder, }}", name_snake)
} else {
format!("builder.{0}({0})", name_snake)
};
builder_out.push(format!(" builder = {};", builder_line));
}
line!(&format!("pub mod {} {{", schema.title.to_snake_case()), "");
line!("use super::*;");
line!("");
if enum_out.len() > 0 {
line!(&enum_out.join("\n"), "");
}
if let Some(ref desc) = schema.description {
line!(&format!("/// {}", desc));
line!("///");
}
line!(&format!("/// ID: {}", schema.id));
line!("#[derive(Serialize, Deserialize, Debug, PartialEq, Builder, Clone)]");
line!(r#"#[builder(pattern = "owned")]"#);
line!(&format!("pub struct {} {{", schema.title));
for field in struct_out {
line!(&field);
}
line!("}");
line!("");
line!(&format!("impl {} {{", schema.title));
line!(&format!(" /// Turns {} into {}Builder", schema.title, schema.title));
line!(&format!(" pub fn into_builder(self) -> {}Builder {{", schema.title));
let fields = names.into_iter()
.map(|x| x.clone().to_snake_case())
.collect::<Vec<_>>()
.join(", ");
line!(&format!(" let {} {{ {} }} = self;", schema.title, fields));
line!(&format!(" let mut builder = {}Builder::default();", schema.title));
for buildfield in builder_out {
line!(&buildfield);
}
line!(" builder");
line!(" }");
line!("}");
line!("}", "");
line!("", "");
out
}
fn gen_schema() -> String {
let mut out = String::new();
let mut files = fs::read_dir(SCHEMA_LOCATION).expect("Error finding schema files")
.map(|f| f.unwrap().path())
.collect::<Vec<_>>();
files.sort();
for path in files {
let path_str = path.as_path().to_str().expect("cannot convert path to str ='(");
let name = path.as_path().file_stem().unwrap().to_str().unwrap();
if !path_str.ends_with(".json") { continue; }
let contents = fs::read_to_string(&path).expect("Error reading file");
let schema: Schema = match serde_json::from_str(&contents) {
Ok(x) => x,
Err(e) => panic!("error parsing schema: {}: {}", name, e),
};
let gen = schema_to_class(schema);
out.push_str(&gen);
}
out
}
fn gen_header() -> String {
let mut header = String::new();
header.push_str("use chrono::prelude::*;\n");
header.push_str("use derive_builder::Builder;\n");
header.push_str("use serde_derive::{Serialize, Deserialize};\n");
header.push_str("use url::Url;\n");
header
}
fn save(contents: String) {
let out_dir = env::var("OUT_DIR").unwrap();
let mut dest_path = PathBuf::from(&out_dir);
dest_path.push("vf_gen.rs");
let mut f = File::create(&dest_path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
}
fn main() {
let header = gen_header();
let contents = gen_schema();
save(format!("{}\n{}", header, contents));
}