#![allow(dead_code)] use convert_case::{Case, Casing}; use error_rules::*; use std::io::Write; #[derive(Debug, Error)] enum Error { #[error_from] Reqwest(reqwest::Error), #[error_from] Json(json::Error), #[error_from] Io(std::io::Error), } enum PlotlyType { Angle, Any, Boolean, Color, Colorlist, ColorScale, DataArray, Enumerated, Flaglist, InfoArray, Integer, Number, String, SubplotId, } struct RustType { plotly_type: PlotlyType, subtype: Option, } impl RustType { fn new(s: &str, subtype: Option) -> Self { let plotly_type = match s { "angle" => PlotlyType::Angle, "any" => PlotlyType::Any, "boolean" => PlotlyType::Boolean, "color" => PlotlyType::Color, "colorlist" => PlotlyType::Colorlist, "colorscale" => PlotlyType::ColorScale, "data_array" => PlotlyType::DataArray, "enumerated" => PlotlyType::Enumerated, "flaglist" => PlotlyType::Flaglist, "info_array" => PlotlyType::InfoArray, "integer" => PlotlyType::Integer, "number" => PlotlyType::Number, "string" => PlotlyType::String, "subplotid" => PlotlyType::SubplotId, _ => panic!("unsupported type: {}", s), }; Self { plotly_type, subtype, } } pub fn num_lifetimes(&self) -> usize { match self.plotly_type { // strings PlotlyType::Color | PlotlyType::String | PlotlyType::SubplotId => 1, PlotlyType::Colorlist => 1, PlotlyType::ColorScale => 1, PlotlyType::DataArray => { if let Some(subtype) = &self.subtype { // TODO: this is a hack if subtype.starts_with("&") { 2 } else { 1 } } else { 1 } } PlotlyType::InfoArray => 1, _ => 0, } } pub fn num_generics(&self) -> usize { 0 } pub fn to_str(&self, lifetimes: Option<&[String]>, generics: Option<&[String]>) -> String { let mut v: Vec = Vec::new(); let num_lifetimes = self.num_lifetimes(); let num_generics = self.num_generics(); if let Some(s) = &lifetimes { assert_eq!(num_lifetimes, s.len()); } else { assert_eq!(num_lifetimes, 0); } if let Some(s) = &generics { assert_eq!(num_generics, s.len()); } else { assert_eq!(num_generics, 0); } match self.plotly_type { // strings PlotlyType::Color | PlotlyType::String | PlotlyType::SubplotId => { let lifetimes = lifetimes.unwrap(); write!(&mut v, "&{} str", lifetimes[0]).unwrap(); } PlotlyType::Enumerated => { write!(&mut v, "{}", self.subtype.as_ref().unwrap()).unwrap(); } PlotlyType::Flaglist => { write!(&mut v, "{}", self.subtype.as_ref().unwrap()).unwrap(); } PlotlyType::Colorlist => { let lifetimes = lifetimes.unwrap(); write!(&mut v, "&{} [&{} str]", lifetimes[0], lifetimes[0]).unwrap(); } PlotlyType::Angle => { write!(&mut v, "crate::Angle").unwrap(); } PlotlyType::Any => { write!(&mut v, "crate::Any").unwrap(); } PlotlyType::Boolean => write!(&mut v, "bool").unwrap(), PlotlyType::ColorScale => { let lifetimes = lifetimes.unwrap(); write!(&mut v, "crate::ColorScale<{}>", lifetimes[0]).unwrap(); } PlotlyType::DataArray => { let lifetimes = lifetimes.unwrap(); let elemtype = if let Some(subtype) = &self.subtype { subtype } else { "f64" }; write!(&mut v, "&{} [{}]", lifetimes[0], elemtype).unwrap(); } PlotlyType::InfoArray => { let lifetimes = lifetimes.unwrap(); write!(&mut v, "&{} crate::InfoArray", lifetimes[0]).unwrap(); } PlotlyType::Integer => { write!(&mut v, "u64").unwrap(); } PlotlyType::Number => { write!(&mut v, "f64").unwrap(); } } String::from_utf8(v).unwrap() } } fn make_lt_and_g( attrname: &str, num_lifetimes: usize, num_generics: usize, ) -> (Vec, Vec) { let mut type_lifetimes = Vec::with_capacity(num_lifetimes); let mut type_generics = Vec::with_capacity(num_generics); for _ in 0..type_lifetimes.capacity() { type_lifetimes.push("'a".to_string()); } for i in 0..type_generics.capacity() { type_generics.push(format!("{}G{}", attrname.to_case(Case::UpperFlat), i)); } (type_lifetimes, type_generics) } fn get_dataarray_type(attrname: &str) -> Option { match attrname { "ticktext" => Some("&'a str".to_string()), "categoryarray" => Some("usize".to_string()), "ids" => Some("&'a str".to_string()), _ => None, } } fn str2enum(s: &str) -> String { let s = match s { "e" => "SmallE", "E" => "BigE", "<=" => "LE", ">=" => "GE", "=" => "EQ", ">" => "GT", "<" => "LT", _ => s, }; let s = s.replace("+", "And"); let s = s.replace("|", "Or"); let s = s.replace("!", "Not"); let s = s.replace("=", "Equal"); let s = s.replace("/", "Slash"); let s = s.replace("\\", "Backslash"); let s = s.replace("^", "Caret"); let s = s.replace("[", "OSqBr"); let s = s.replace("]", "CSqBr"); let s = s.replace("(", "OBr"); let s = s.replace(")", "CBr"); let s = s.replace("{", "OCrlBr"); let s = s.replace("}", "CCrlBr"); let s = s.replace("?", "QM"); let s = s.replace("$", "Dollar"); s.to_case(Case::Pascal) } fn escape_str(s: &str) -> String { s.replace("\\", "\\\\").replace("\"", "\\\"") } fn enum_impl_start( mut w: W, subtypename: &str, add_lifetime: bool, ) -> Result<(), Error> { let ltstr = if add_lifetime { "<'a>" } else { "" }; writeln!(&mut w, "impl{} {}{} {{", ltstr, subtypename, ltstr)?; Ok(()) } fn enum_impl_end(mut w: W) -> Result<(), Error> { writeln!(&mut w, "}}")?; Ok(()) } fn impl_serialize_start( mut w: W, subtypename: &str, add_lifetime: bool, ) -> Result<(), Error> { let ltstr = if add_lifetime { "<'a>" } else { "" }; writeln!( &mut w, "impl{} serde::Serialize for {}{} {{", ltstr, subtypename, ltstr )?; writeln!(&mut w, " fn serialize(&self, serializer: S) -> Result {{")?; Ok(()) } fn impl_serialize_end(mut w: W) -> Result<(), Error> { writeln!(&mut w, " }}")?; writeln!(&mut w, "}}")?; Ok(()) } fn gen_enum( mut modcode: W1, mut serimpl: W2, values: &json::JsonValue, ) -> Result where W1: std::io::Write, W2: std::io::Write, { let mut nmembers = 0; for val in values.members() { let mut handle_str = |namejs: &str| -> Result, Error> { if namejs.len() == 0 { return Ok(None); } let namerust = str2enum(namejs); writeln!( &mut serimpl, " Self::{} => serializer.serialize_str(\"{}\"),", namerust, escape_str(namejs) )?; Ok(Some(namerust)) }; let namerust = match val { json::JsonValue::Short(v) => handle_str(v)?, json::JsonValue::String(v) => handle_str(v)?, json::JsonValue::Boolean(v) => { let namejs = if *v { "true" } else { "false" }.to_string(); let namerust = if *v { "True" } else { "False" }.to_string(); writeln!( &mut serimpl, " Self::{} => serializer.serialize_bool({}),", namerust, namejs )?; Some(namerust) } json::JsonValue::Number(n) => { let (positive, mantissa, exponent) = n.as_parts(); assert_eq!(exponent, 0); let sign = if positive { "" } else { "Neg" }; let namerust = format!("Num{}{}", sign, mantissa); writeln!( &mut serimpl, " Self::{} => serializer.serialize_u64({}),", namerust, mantissa )?; Some(namerust) } _ => panic!("unsupported enum vale {:?}", val), }; if let Some(namerust) = namerust { writeln!(&mut modcode, " {},", namerust)?; nmembers += 1; } } Ok(nmembers) } fn handle_enumerated( attrname: &str, attr: &json::JsonValue, mut modcode: W, ) -> Result where W: std::io::Write, { let subtypename = attrname.to_case(Case::Pascal); let mut serimpl: Vec = Vec::new(); impl_serialize_start(&mut serimpl, &subtypename, false)?; writeln!(&mut serimpl, " match self {{")?; writeln!(&mut modcode, "pub enum {} {{", subtypename)?; gen_enum(&mut modcode, &mut serimpl, &attr["values"])?; writeln!(&mut modcode, "}}")?; writeln!(&mut serimpl, " }}")?; impl_serialize_end(&mut serimpl)?; modcode.write(&serimpl)?; Ok(subtypename) } fn roundup(a: usize, b: usize) -> usize { (a + (b - 1)) & !(b - 1) } fn handle_flaglist( attrname: &str, attr: &json::JsonValue, mut modcode: W, ) -> Result where W: std::io::Write, { let subtypename = attrname.to_case(Case::Pascal); let subtypeflagsname = format!("{}Flags", subtypename); let flags = match &attr["flags"] { json::JsonValue::Array(v) => v, _ => panic!("flags is not an array"), }; let nbits = flags.len(); let nbytes = roundup(nbits, 8) / 8; writeln!(&mut modcode, "#[derive(Default)]")?; writeln!( &mut modcode, "pub struct {} ([u8; {}]);", subtypeflagsname, nbytes )?; enum_impl_start(&mut modcode, &subtypeflagsname, false)?; let mut serimpl: Vec = Vec::new(); impl_serialize_start(&mut serimpl, &subtypeflagsname, false)?; writeln!(&mut serimpl, " #[allow(unused_mut)]")?; writeln!(&mut serimpl, " let mut v: Vec<&str> = Vec::new();")?; for (bitabs, flag) in flags.iter().enumerate() { let namejs = flag.as_str().unwrap(); let byte = bitabs / 8; let bitrel = bitabs % 8; let namejs = match namejs { "final" => "r#final", _ => namejs, }; let fnname = namejs.to_case(Case::Snake); writeln!( &mut serimpl, " if (self.0[{}] >> {}) & 0x1 == 1 {{", byte, bitrel )?; writeln!( &mut serimpl, " v.push(\"{}\");", escape_str(&namejs) )?; writeln!(&mut serimpl, " }}")?; writeln!( &mut modcode, " pub fn {}(&mut self, v: bool) -> &mut Self {{", fnname )?; writeln!( &mut modcode, " if v {{ self.0[{}] |= 1 << {}; }}", byte, bitrel )?; writeln!( &mut modcode, " else {{ self.0[{}] &= !(1 << {}); }}", byte, bitrel )?; writeln!(&mut modcode, " self")?; writeln!(&mut modcode, " }}")?; } writeln!( &mut serimpl, " serializer.serialize_str(&v.join(\"+\"))" )?; impl_serialize_end(&mut serimpl)?; enum_impl_end(&mut modcode)?; modcode.write(&serimpl)?; let mut serimpl: Vec = Vec::new(); impl_serialize_start(&mut serimpl, &subtypename, false)?; writeln!(&mut serimpl, " match self {{")?; writeln!(&mut modcode, "pub enum {} {{", subtypename)?; writeln!(&mut modcode, " Flags({}),", subtypeflagsname)?; let nextras = gen_enum(&mut modcode, &mut serimpl, &attr["extras"])?; writeln!(&mut modcode, "}}")?; writeln!( &mut serimpl, " Self::Flags(v) => v.serialize(serializer)," )?; writeln!(&mut serimpl, " }}")?; impl_serialize_end(&mut serimpl)?; modcode.write(&serimpl)?; writeln!(&mut modcode, "impl Default for {} {{", subtypename)?; writeln!(&mut modcode, " fn default() -> Self {{")?; writeln!( &mut modcode, " Self::Flags({}::default())", subtypeflagsname )?; writeln!(&mut modcode, " }}")?; writeln!(&mut modcode, "}}")?; enum_impl_start(&mut modcode, &subtypename, false)?; writeln!( &mut modcode, " pub fn flags(&mut self) -> &mut {} {{", subtypeflagsname )?; writeln!( &mut modcode, " *self = Self::Flags({}::default());", subtypeflagsname )?; writeln!(&mut modcode, " match self {{")?; writeln!(&mut modcode, " Self::Flags(v) => v,")?; if nextras > 0 { writeln!(&mut modcode, " _ => unreachable!(),")?; } writeln!(&mut modcode, " }}")?; writeln!(&mut modcode, " }}")?; writeln!( &mut modcode, " pub fn set(&mut self, v: {}) {{", subtypename )?; writeln!(&mut modcode, " *self = v;")?; writeln!(&mut modcode, " }}")?; enum_impl_end(&mut modcode)?; Ok(subtypename) } fn gen_struct( f: &mut F, modname: Option<&str>, structname: &str, description: Option<&str>, attrs: &json::JsonValue, ) -> Result<(usize, usize), Error> { let mut code: Vec = Vec::new(); let mut modcode: Vec = Vec::new(); let mut fields: Vec = Vec::new(); let mut lifetimes: Vec = Vec::new(); let mut generics: Vec = Vec::new(); let namespace = if let Some(modname) = &modname { format!("{}::", modname) } else { "".to_string() }; for (attrname_js, attr) in attrs.entries() { if attrname_js == "box" { continue; } match attrname_js { "customdata" | "type" | "_deprecated" | "editType" | "description" | "impliedEdits" | "_isSubplotObj" | "_arrayAttrRegexps" => continue, "role" => { assert_eq!(attr.as_str().unwrap(), "object"); break; } _ => (), } let attrname = attrname_js.to_case(Case::Snake); if let Some(description) = attr["description"].as_str() { if description.contains("deprecated!") { continue; } } let (use_isempty, typestr) = if let Some(valtype) = attr["valType"].as_str() { let (subtypename, use_isempty) = match valtype { "enumerated" => ( Some(format!( "{}{}", namespace, handle_enumerated(&attrname, attr, &mut modcode)? )), false, ), "flaglist" => ( Some(format!( "{}{}", namespace, handle_flaglist(&attrname, attr, &mut modcode)? )), true, ), "data_array" => (get_dataarray_type(&attrname), false), _ => (None, false), }; let rusttype = RustType::new(valtype, subtypename); let (type_lifetimes, type_generics) = make_lt_and_g(&attrname, rusttype.num_lifetimes(), rusttype.num_generics()); if type_lifetimes.len() > 0 && lifetimes.len() == 0 { lifetimes.push("'a".to_string()); } generics.extend_from_slice(&type_generics); let typestr = rusttype.to_str(Some(&type_lifetimes), Some(&type_generics)); (use_isempty, typestr) } else if let Some(role) = attr["role"].as_str() { if role != "object" { panic!("role {} is not supported", role); } if attr.has_key("items") { assert_eq!(attr["items"].len(), 1); // TODO: array support continue; } let substructname = attrname.to_case(Case::Pascal); let (ss_num_lifetimes, ss_num_generics) = gen_struct( &mut modcode, Some(&attrname), &substructname, attr["description"].as_str(), attr, )?; let (type_lifetimes, type_generics) = make_lt_and_g(&attrname, ss_num_lifetimes, ss_num_generics); let params_joined = [&type_lifetimes[..], &type_generics[..]] .concat() .join(", "); if type_lifetimes.len() > 0 && lifetimes.len() == 0 { lifetimes.push("'a".to_string()); } generics.extend_from_slice(&type_generics); let typestr = format!("{}{}<{}>", namespace, substructname, params_joined); (true, typestr) } else { panic!( "unsupported member: {}/{} = {:?}", structname, attrname_js, attr ); }; if let Some(description) = attr["description"].as_str() { writeln!(&mut code, " /// {}", description)?; writeln!(&mut code, " ///")?; } if attr.has_key("dflt") { writeln!(&mut code, " /// default: `{}`", attr["dflt"])?; } if use_isempty { writeln!(&mut fields, " #[serde(rename = \"{}\")]", attrname_js)?; writeln!( &mut fields, " #[serde(skip_serializing_if = \"crate::IsEmpty::is_empty\")]" )?; writeln!( &mut fields, " {}: crate::IsEmpty<{}>,", attrname, typestr )?; writeln!( &mut code, " pub fn {}(&mut self) -> &mut {} {{", attrname, typestr )?; writeln!(&mut code, " self.{}.is_empty = false;", attrname)?; writeln!(&mut code, " &mut self.{}.data", attrname)?; writeln!(&mut code, " }}")?; } else { writeln!(&mut fields, " #[serde(rename = \"{}\")]", attrname_js)?; writeln!( &mut fields, " #[serde(skip_serializing_if = \"Option::is_none\")]" )?; writeln!(&mut fields, " {}: Option<{}>,", attrname, typestr)?; writeln!( &mut code, " pub fn {}(&mut self, {}: {}) -> &mut Self {{", attrname, attrname, typestr )?; writeln!(&mut code, " self.{} = Some({});", attrname, attrname)?; writeln!(&mut code, " self")?; writeln!(&mut code, " }}")?; } } let params_joined = [&lifetimes[..], &generics[..]].concat().join(", "); if modname.is_none() { writeln!(f, "#[allow(unused_imports)]")?; writeln!(f, "use serde::Serialize;")?; } writeln!(f)?; if let Some(description) = description { writeln!(f, "/// {}", description)?; } writeln!(f, "#[derive(Default, Serialize)]")?; writeln!(f, "pub struct {}<{}> {{", structname, params_joined)?; f.write(&fields)?; writeln!(f, "}}")?; writeln!(f)?; writeln!( f, "impl<{}> {}<{}> {{", params_joined, structname, params_joined )?; f.write(&code)?; writeln!(f, "}}")?; if modcode.len() > 0 { if let Some(modname) = &modname { writeln!(f, "pub mod {} {{", modname)?; writeln!(f, "#[allow(unused_imports)]")?; writeln!(f, "use serde::Serialize;")?; } f.write(&modcode)?; if modname.is_some() { writeln!(f, "}}")?; } } Ok((lifetimes.len(), generics.len())) } fn main() -> Result<(), Error> { let pkg_version = env!("CARGO_PKG_VERSION"); let plotly_version = pkg_version.splitn(2, "-").next().unwrap(); let schema_url = format!( "https://raw.githubusercontent.com/plotly/plotly.js/v{}/dist/plot-schema.json", plotly_version ); let local_schema_path = std::path::Path::new("plot-schema.json"); let schema = if local_schema_path.exists() { std::fs::read_to_string(local_schema_path)? } else { reqwest::blocking::get(&schema_url)?.text()? }; let schema = json::parse(&schema)?; let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); let traces_path = out_path.join("traces"); if traces_path.exists() { std::fs::remove_dir_all(&traces_path)?; } std::fs::create_dir(&traces_path)?; let transforms_path = out_path.join("transforms"); if transforms_path.exists() { std::fs::remove_dir_all(&transforms_path)?; } std::fs::create_dir(&transforms_path)?; let mut f_traces_mod = std::fs::File::create(traces_path.join("mod.rs"))?; for (mut tracename, trace) in schema["traces"].entries() { if tracename == "box" { tracename = "box_"; } let structname = tracename.to_case(Case::Pascal); writeln!(&mut f_traces_mod, "pub mod {};", tracename)?; let mut f = std::fs::File::create(traces_path.join(format!("{}.rs", tracename)))?; gen_struct( &mut f, None, &structname, trace["meta"]["description"].as_str(), &trace["attributes"], )?; } let mut f_transforms_mod = std::fs::File::create(transforms_path.join("mod.rs"))?; for (transformname, transform) in schema["transforms"].entries() { let structname = transformname.to_case(Case::Pascal); writeln!(&mut f_transforms_mod, "pub mod {};", transformname)?; let mut f = std::fs::File::create(transforms_path.join(format!("{}.rs", transformname)))?; gen_struct(&mut f, None, &structname, None, &transform["attributes"])?; } let mut f = std::fs::File::create(out_path.join("config.rs"))?; gen_struct(&mut f, None, "Config", None, &schema["config"])?; let mut f = std::fs::File::create(out_path.join("layout.rs"))?; gen_struct( &mut f, None, "Layout", None, &schema["layout"]["layoutAttributes"], )?; let mut f = std::fs::File::create(out_path.join("animation.rs"))?; gen_struct(&mut f, None, "Animation", None, &schema["animation"])?; let mut f_mod = std::fs::File::create(out_path.join("mod.rs"))?; writeln!(&mut f_mod, "pub mod traces;")?; writeln!(&mut f_mod, "pub mod transforms;")?; writeln!(&mut f_mod, "pub mod layout;")?; writeln!(&mut f_mod, "pub mod config;")?; writeln!(&mut f_mod, "pub mod animation;")?; writeln!( &mut f_mod, "/// https CDN URL to the plotly version that was used to build this crate" )?; writeln!( &mut f_mod, "pub static URL_CDN: &'static str = \"https://cdn.plot.ly/plotly-{}.min.js\";", plotly_version )?; println!("cargo:rerun-if-changed=build.rs"); Ok(()) }