use std::env;
use std::fs::{self, File};
use std::io::{Write, BufReader};
use std::path::PathBuf;
use std::collections::HashMap;
use heck::{CamelCase, KebabCase};
use rio_api::{
parser::TriplesParser,
model::{
Triple,
Term,
NamedOrBlankNode,
NamedNode,
BlankNode,
Literal,
},
};
use rio_turtle::{self, TurtleParser, TurtleError};
use serde::{Serialize, Deserialize};
use serde_json;
static SCHEMA_LOCATION: &'static str = "./schema/om-2.0.ttl";
struct StringWriter {
string: String,
indent: usize,
}
impl StringWriter {
fn new() -> Self {
Self { string: String::from(""), indent: 0 }
}
fn write<T>(&mut self, val: T)
where T: Into<String>
{
self.string.push_str(val.into().as_str());
}
fn line<T>(&mut self, val: T)
where T: Into<String>
{
let indent: String = (0..(self.indent * 4)).map(|_| " ").collect::<Vec<_>>().concat();
self.write(&indent);
self.write(val);
self.nl();
}
fn nl(&mut self) {
self.write("\n");
}
fn inc_indent(&mut self) {
self.indent += 1;
}
fn dec_indent(&mut self) {
if self.indent > 0 { self.indent -= 1; }
}
fn to_string(self) -> String {
let Self { string: val, .. } = self;
val
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
enum DataType {
#[serde(rename = "bool")]
#[serde(alias = "http://www.w3.org/2001/XMLSchema#boolean")]
Boolean,
#[serde(rename = "f64")]
#[serde(alias = "http://www.w3.org/2001/XMLSchema#double")]
Double,
#[serde(alias = "http://www.w3.org/2001/XMLSchema#string")]
String,
#[serde(rename = "dtype::NumericUnion")]
#[serde(alias = "http://www.linkedmodel.org/schema/dtype#numericUnion")]
NumericUnion,
Literal(String),
RangeEnum(String),
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
enum NodeType {
#[serde(rename = "http://www.w3.org/2002/07/owl#Ontology")]
Ontology,
#[serde(rename = "http://www.w3.org/2002/07/owl#Class")]
StructOrEnum,
#[serde(rename = "http://www.w3.org/2002/07/owl#ObjectProperty")]
Field,
#[serde(rename = "http://www.w3.org/2002/07/owl#NamedIndividual")]
EnumVal,
#[serde(rename = "http://www.w3.org/2002/07/owl#DatatypeProperty")]
DataType,
Literal(String),
}
#[derive(Debug, PartialEq, Clone, Deserialize)]
enum Relationship {
#[serde(rename = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type")]
Type,
#[serde(rename = "http://www.w3.org/2000/01/rdf-schema#domain")]
Domain,
#[serde(rename = "http://www.w3.org/2000/01/rdf-schema#range")]
Range,
#[serde(rename = "http://www.w3.org/2000/01/rdf-schema#label")]
Label,
#[serde(rename = "http://www.w3.org/2000/01/rdf-schema#comment")]
Comment,
#[serde(rename = "http://www.w3.org/2000/01/rdf-schema#subClassOf")]
Subclass,
#[serde(rename = "http://www.w3.org/2003/06/sw-vocab-status/ns#term_status")]
Status,
#[serde(rename = "http://www.w3.org/2002/07/owl#unionOf")]
Union,
#[serde(rename = "http://www.w3.org/2002/07/owl#oneOf")]
OneOf,
#[serde(rename = "http://www.w3.org/2002/07/owl#equivalentClass")]
EquivalentClass,
#[serde(rename = "http://www.w3.org/1999/02/22-rdf-syntax-ns#first")]
First,
#[serde(rename = "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest")]
Rest,
#[serde(rename = "http://www.w3.org/2002/07/owl#propertyChainAxiom")]
PropertyChainAxiom,
#[serde(rename = "http://purl.org/dc/elements/1.1/creator")]
Creator,
#[serde(rename = "http://purl.org/dc/elements/1.1/date")]
Date,
#[serde(rename = "http://purl.org/dc/elements/1.1/identifier")]
Identifier,
#[serde(rename = "http://purl.org/dc/elements/1.1/title")]
Title,
#[serde(rename = "http://purl.org/ontology/bibo/uri")]
Uri,
Literal(String),
}
macro_rules! to_enum {
($enumty:ty, $val:expr) => {
match serde_json::from_str::<$enumty>(&format!(r#""{}""#, $val)) {
Ok(x) => x,
Err(_) => <$enumty>::Literal($val.into())
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize)]
struct Node {
id: Option<String>,
ty: Option<NodeType>,
label: Option<String>,
comment: Option<String>,
status: Option<String>,
domain: Vec<String>,
range: Vec<String>,
subclass: Vec<String>,
rel_pairs: Vec<(String, String)>,
subnodes: Vec<Box<Node>>,
custom: Option<(String, String)>,
}
impl Node {
fn new(id: &str) -> Self {
let mut node = Self::default();
node.id = Some(id.to_string());
node
}
fn id_noprefix(&self) -> String {
self.id.as_ref().unwrap().trim_start_matches("http://www.ontology-of-units-of-measure.org/resource/om-2/").to_string()
}
fn typename(&self) -> String {
let id_noprefix = self.id_noprefix();
if id_noprefix.starts_with("_") {
match id_noprefix.as_str() {
"_0-100" => "ZeroToOneHundred",
"_0-5" => "ZeroToFive",
"_1-0" => "OneDividedByZero",
"_1-10" => "OneToTen",
"_1-5" => "OneToFive",
"_1000ColonyFormingUnitPerMillilitre" => "OneThousandColonyFormingUnitPerMillilitre",
_ => panic!("Node.typename() -- unknown underscore-prefixed id encountered: {}", id_noprefix),
}.to_string()
} else if id_noprefix.is_ascii() {
id_noprefix.to_camel_case()
} else {
match id_noprefix.as_str() {
"röntgen" => "Rontgen",
_ => panic!("Node.typename() -- unknown non-ascii identifier encountered: {}", id_noprefix),
}.to_string()
}
}
fn ser_name(&self) -> String {
if let Some(label) = self.label.as_ref() {
label.to_string()
} else {
self.id_noprefix()
}
}
}
fn gen_schema() -> String {
let mut out = StringWriter::new();
let file = fs::File::open(SCHEMA_LOCATION).expect("error opening schema file");
let bufread = BufReader::new(file);
let mut nodemap: HashMap<String, Node> = HashMap::new();
let mut cur_node_id: String = "".to_string();
let mut cur_list_id: Option<String> = None;
let mut cur_list: Vec<String> = vec![];
TurtleParser::new(bufread, "file:vf.ttl").unwrap().parse_all(&mut |t| -> Result<(), TurtleError> {
let Triple { subject, predicate: predicate_named, object } = t;
let NamedNode { iri: predicate } = predicate_named;
let (id, blank): (String, bool) = match subject {
NamedOrBlankNode::NamedNode(NamedNode { iri }) => (iri.into(), false),
NamedOrBlankNode::BlankNode(BlankNode { id }) => (id.into(), true),
};
let blank_id: Option<String> = if id != "" && blank { Some(id.clone()) } else { None };
let (obj_id, obj_val, _obj_blank): (Option<String>, Option<String>, bool) = match object.clone() {
Term::Literal(Literal::Simple { value: string }) => (None, Some(string.into()), false),
Term::Literal(Literal::LanguageTaggedString { value: string, language: lang }) if lang == "en" => (None, Some(string.into()), false),
Term::NamedNode(NamedNode { iri }) => (Some(iri.into()), None, false),
Term::BlankNode(BlankNode { id }) => (Some(id.into()), None, true),
_ => return Ok(()),
};
if !blank {
cur_node_id = id.clone();
}
let cur_node = nodemap.entry(cur_node_id.clone()).or_insert(Node::new(&cur_node_id));
if cur_node.id == Some("http://www.ontology-of-units-of-measure.org/resource/om-2/".to_string()) {
return Ok(());
}
let rel = to_enum!(Relationship, predicate);
match rel {
Relationship::Type => {
let ty = to_enum!(NodeType, obj_id.as_ref().unwrap());
if cur_node.ty.is_some() {
cur_node.domain.push(obj_id.unwrap());
} else {
cur_node.ty = Some(ty);
}
}
Relationship::Domain => {
if obj_id.is_some() && obj_id == cur_list_id {
cur_node.domain = cur_list.clone();
cur_list = vec![];
} else if let Some(type_id) = obj_id {
cur_node.domain = vec![type_id];
}
}
Relationship::Range => {
if obj_id.is_some() && obj_id == cur_list_id {
cur_node.range = cur_list.clone();
cur_list = vec![];
} else if let Some(type_id) = obj_id {
cur_node.range = vec![type_id];
}
}
Relationship::Subclass => {
if obj_id.is_some() && obj_id == cur_list_id {
cur_node.subclass = cur_list.clone();
cur_list = vec![];
} else if let Some(type_id) = obj_id {
cur_node.subclass = vec![type_id];
}
}
Relationship::Label => { cur_node.label = obj_val; }
Relationship::Comment => { cur_node.comment = obj_val; }
Relationship::Status => { cur_node.status = obj_val; }
Relationship::Union | Relationship::OneOf | Relationship::EquivalentClass => {
cur_list_id = blank_id;
cur_list = vec![];
}
Relationship::First => { cur_list.push(obj_id.unwrap()); }
Relationship::Rest => {}
Relationship::PropertyChainAxiom => {}
Relationship::Creator => {}
Relationship::Date => {}
Relationship::Identifier => {}
Relationship::Title => {}
Relationship::Uri => {}
Relationship::Literal(val) => {
cur_node.rel_pairs.push((val, obj_id.or(obj_val).unwrap()));
}
}
Ok(())
}).expect("error parsing");
let unit_node = nodemap.get("http://www.ontology-of-units-of-measure.org/resource/om-2/Unit").unwrap().clone();
let mut units: Vec<Node> = Vec::new();
let unit_classes = vec![
"CompoundUnit",
"LengthUnit",
"PrefixedUnit",
"TemperatureUnit",
"Unit",
"UnitDivision",
"UnitExponentiation",
"UnitMultiplication",
];
for (_, node) in nodemap {
let mut node_saved = false;
for class in &unit_classes {
if node.domain.contains(&format!("http://www.ontology-of-units-of-measure.org/resource/om-2/{}", class)) {
units.push(node.clone());
node_saved = true;
break;
}
if node.ty == Some(NodeType::Literal(format!("http://www.ontology-of-units-of-measure.org/resource/om-2/{}", class))) {
units.push(node.clone());
node_saved = true;
break;
}
}
if node_saved { continue; }
}
units.sort_by_key(|x| x.typename());
out.line(format!("/// {}", unit_node.comment.as_ref().unwrap()));
out.line("///");
out.line(format!("/// ID: {}", unit_node.id.as_ref().unwrap()));
out.line("#[derive(Debug, PartialEq, Clone)]");
out.line(r#"#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))]"#);
out.line("pub enum Unit {");
out.inc_indent();
for unit in &units {
let typename = unit.typename();
let label = unit.ser_name();
let alias = label.to_kebab_case();
if let Some(comment) = unit.comment.as_ref() {
out.line(format!("/// {}", comment));
}
out.line(format!(r#"#[cfg_attr(feature = "with_serde", serde(rename = "{}"))]"#, label));
if alias != label {
out.line(format!(r#"#[cfg_attr(feature = "with_serde", serde(alias = "{}"))]"#, alias));
}
out.line(format!("{},", typename.to_camel_case()));
}
out.dec_indent();
out.line("}");
out.nl();
out.line("impl Unit {");
out.inc_indent();
out.line("pub fn label(&self) -> Option<String> {");
out.inc_indent();
out.line("match self {");
out.inc_indent();
for unit in &units {
let typename = unit.typename();
if let Some(label) = unit.label.as_ref() {
out.line(format!(r#"Unit::{} => Some(String::from(r#"{}"{})),"#, typename, label, "#"));
} else {
out.line(format!(r#"Unit::{} => None,"#, typename));
}
}
out.dec_indent();
out.line("}");
out.dec_indent();
out.line("}");
out.nl();
out.line("pub fn symbol(&self) -> Option<String> {");
out.inc_indent();
out.line("match self {");
out.inc_indent();
for unit in &units {
let typename = unit.typename();
let symbol = unit.rel_pairs.iter()
.filter(|x| x.0 == "http://www.ontology-of-units-of-measure.org/resource/om-2/symbol")
.map(|x| x.1.clone())
.collect::<Vec<_>>();
if symbol.len() > 0 {
out.line(format!(r#"Unit::{} => Some(String::from(r#"{}"{})),"#, typename, symbol[0], "#"));
} else {
out.line(format!(r#"Unit::{} => None,"#, typename));
}
}
out.dec_indent();
out.line("}");
out.dec_indent();
out.line("}");
out.dec_indent();
out.line("}");
out.nl();
out.line("#[cfg(test)]");
out.line(r#"#[cfg(feature = "with_serde")]"#);
out.line("mod test {");
out.inc_indent();
out.line("use super::*;");
out.line("use serde_json;");
out.line("#[test]");
out.line("fn serde() {");
out.inc_indent();
for i in 0..units.len() {
if i % 5 != 0 { continue; }
let unit = &units[i];
let typename = unit.typename().to_camel_case();
let label = unit.ser_name();
let alias = label.to_kebab_case();
out.line(format!(r#"assert_eq!(Unit::{}, serde_json::from_str(r#""{}""{}).unwrap());"#, typename, label, "#"));
if alias != label {
out.line(format!(r#"assert_eq!(Unit::{}, serde_json::from_str(r#""{}""{}).unwrap());"#, typename, alias, "#"));
}
out.line(format!(r#"assert_eq!(serde_json::to_string(&Unit::{}).unwrap(), r#""{}""{});"#, typename, label, "#"));
}
out.dec_indent();
out.line("}");
out.dec_indent();
out.line("}");
out.to_string()
}
fn print_header() -> String {
let mut header = String::new();
header.push_str(r#"#[cfg(feature = "with_serde")]"#);
header.push_str("use serde_derive::{Serialize, Deserialize};\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("om_gen.rs");
let mut f = File::create(&dest_path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
}
fn main() {
let header = print_header();
let contents = gen_schema();
save(format!("{}\n{}", header, contents));
}