use crate::{
attribute::{AttrOption, Attribute},
datamodel::DataModel,
markdown::frontmatter::FrontMatter,
object::{Enumeration, Object},
};
use convert_case::{Case, Casing};
use reqwest::Url;
use std::{error::Error, path::Path};
use super::datatype::DataType;
static PROP_KEYS: [&str; 10] = [
"type", "format", "enum", "minimum", "maximum", "minItems", "maxItems", "title", "items",
"$ref",
];
pub fn parse_json_schema(path: &Path) -> Result<DataModel, Box<dyn Error>> {
let schema = read_json_schema(path).expect(
"Could not read the JSON schema file. Make sure the file is a valid JSON schema file.",
);
let name = schema
.get("title")
.expect("Could not find title in the JSON schema")
.as_str()
.expect("Title is not a string")
.to_string();
let mut model = DataModel::new(Some(name), None);
model.config = Some(FrontMatter::default());
let object = create_object(&schema);
model.objects.push(object);
let definitions = schema.get("definitions").unwrap();
for (key, value) in definitions.as_object().unwrap() {
let data_type = DataType::from_object(value);
match data_type {
DataType::Object { properties: _ } => {
let object = create_object(value);
model.objects.push(object);
}
DataType::Enum { values } => {
let enumeration = create_enum(key, &values);
model.enums.push(enumeration);
}
_ => {}
}
}
Ok(model)
}
fn read_json_schema(path: &Path) -> Result<serde_json::Value, serde_json::Error> {
let content = std::fs::read_to_string(path).expect("Could not read the JSON schema file");
serde_json::from_str(&content)
}
fn create_enum(name: &str, values: &[String]) -> Enumeration {
let mappings = values
.iter()
.map(|v| (create_enum_alias(v), v.to_string()))
.collect();
Enumeration {
name: name.to_string(),
mappings,
docstring: "".to_string(),
}
}
fn create_enum_alias(name: &str) -> String {
let name = if let Ok(url) = Url::parse(name) {
url_to_enum_alias(url)
} else {
remove_special_characters(name)
};
name.to_case(Case::Snake).to_uppercase()
}
fn remove_special_characters(input: &str) -> String {
input.chars().filter(|c| c.is_alphanumeric()).collect()
}
fn url_to_enum_alias(url: Url) -> String {
let host = url.host_str().unwrap_or("");
let path = url.path();
let host = host.strip_prefix("www.").unwrap_or(host);
let mut result = host.replace('.', "_");
result.push('_');
result.push_str(&path.replace('/', "_"));
result.trim_end_matches('_').to_string()
}
fn create_object(schema: &serde_json::Value) -> Object {
let name = schema
.get("title")
.expect("Could not find title in the JSON schema")
.as_str()
.expect("Title is not a string");
let properties = schema
.get("properties")
.expect("Could not find properties in the JSON schema")
.as_object()
.expect("Properties is not an object");
let mut object = Object::new(name.to_string(), None);
for (key, value) in properties {
let data_type = DataType::from_object(value);
let mut attribute = match data_type {
DataType::Object { properties } => process_object(key, &properties),
DataType::Array => process_array(key, value),
DataType::Enum { values: _ } => process_enum(key),
DataType::Reference { reference } => process_reference(key, reference),
_ => process_primitive(key, value),
};
for (key, value) in value.as_object().unwrap() {
if !PROP_KEYS.contains(&key.as_str()) {
attribute
.add_option(AttrOption::new(
key.to_string(),
value.as_str().unwrap().to_string(),
))
.expect("Failed to add option");
}
}
object.attributes.push(attribute);
}
object
}
fn process_array(name: &str, value: &serde_json::Value) -> Attribute {
let mut attribute = Attribute::new(name.to_string(), false);
attribute.is_array = true;
let items = value
.get("items")
.expect("Could not find items in the array");
let data_type = DataType::from_object(items);
attribute.dtypes = match data_type {
DataType::Reference { reference } => vec![reference],
_ => vec![data_type.to_string()],
};
attribute
}
fn process_primitive(name: &str, value: &serde_json::Value) -> Attribute {
let mut attribute = Attribute::new(name.to_string(), false);
let data_type = value
.get("type")
.expect("Could not find type in the property")
.as_str()
.expect("Type is not a string")
.to_string();
attribute.dtypes = vec![data_type];
attribute
}
fn process_reference(name: &str, reference: String) -> Attribute {
let mut attribute = Attribute::new(name.to_string(), false);
attribute.dtypes = vec![reference];
attribute
}
fn process_object(_name: &str, _value: &serde_json::Value) -> Attribute {
panic!("Nested object type is not supported yet");
}
fn process_enum(_name: &str) -> Attribute {
panic!("Property enums are currently only allowed as reference");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_schema() {
let path = Path::new("tests/data/expected_json_schema.json");
let model = parse_json_schema(path).unwrap();
assert_eq!(model.objects.len(), 2);
assert_eq!(model.enums.len(), 1);
let object = &model.objects[0];
assert_eq!(object.name, "Test");
assert_eq!(object.attributes.len(), 4);
let object = &model.objects[1];
assert_eq!(object.name, "Test2");
assert_eq!(object.attributes.len(), 2);
let enumeration = &model.enums[0];
assert_eq!(enumeration.name, "Ontology");
assert_eq!(enumeration.mappings.len(), 3);
}
}