use crate::error::SammError;
use crate::metamodel::{Aspect, CharacteristicKind, ModelElement};
use std::collections::HashSet;
pub fn generate_graphql(aspect: &Aspect) -> Result<String, SammError> {
let mut schema = String::new();
let mut custom_scalars = HashSet::new();
let mut enum_types = Vec::new();
schema.push_str(&format!("# GraphQL schema for {}\n", aspect.name()));
schema.push_str("# Generated from SAMM Aspect Model\n");
schema.push_str(&format!("# URN: {}\n\n", aspect.metadata().urn));
for prop in aspect.properties() {
if let Some(char) = &prop.characteristic {
if let Some(dt) = &char.data_type {
if let Some(scalar) = get_custom_scalar_for_xsd(dt) {
custom_scalars.insert(scalar);
}
}
if let CharacteristicKind::Enumeration { values } = char.kind() {
let enum_name = to_pascal_case(&prop.name());
let enum_def = generate_enum_type(&enum_name, values);
enum_types.push(enum_def);
}
if let CharacteristicKind::State {
values,
default_value: _,
} = char.kind()
{
let enum_name = to_pascal_case(&prop.name());
let enum_def = generate_enum_type(&enum_name, values);
enum_types.push(enum_def);
}
}
}
for scalar in &custom_scalars {
schema.push_str(&format!("scalar {}\n", scalar));
}
if !custom_scalars.is_empty() {
schema.push('\n');
}
for enum_def in &enum_types {
schema.push_str(enum_def);
schema.push('\n');
}
let main_type = generate_main_type(aspect)?;
schema.push_str(&main_type);
schema.push('\n');
let query_type = generate_query_type(aspect)?;
schema.push_str(&query_type);
Ok(schema)
}
fn generate_enum_type(name: &str, values: &[String]) -> String {
let mut enum_def = String::new();
enum_def.push_str(&format!("enum {} {{\n", name));
for value in values {
let enum_value = value.to_uppercase().replace(['-', ' '], "_");
enum_def.push_str(&format!(" {}\n", enum_value));
}
enum_def.push_str("}\n");
enum_def
}
fn generate_main_type(aspect: &Aspect) -> Result<String, SammError> {
let type_name = to_pascal_case(&aspect.name());
let mut type_def = String::new();
if let Some(desc) = aspect.metadata().get_description("en") {
type_def.push_str(&format!("\"\"\"\n{}\n\"\"\"\n", desc));
}
type_def.push_str(&format!("type {} {{\n", type_name));
type_def.push_str(" \"\"\"Unique identifier\"\"\"\n");
type_def.push_str(" id: ID!\n\n");
for prop in aspect.properties() {
let field_name = to_camel_case(&prop.name());
let field_type = get_graphql_type(prop)?;
if let Some(desc) = prop.metadata().get_description("en") {
type_def.push_str(&format!(" \"\"\"\n {}\n \"\"\"\n", desc));
}
type_def.push_str(&format!(" {}: {}\n\n", field_name, field_type));
}
type_def.push_str(" \"\"\"Timestamp of creation\"\"\"\n");
type_def.push_str(" createdAt: DateTime!\n\n");
type_def.push_str(" \"\"\"Timestamp of last update\"\"\"\n");
type_def.push_str(" updatedAt: DateTime!\n");
type_def.push_str("}\n");
Ok(type_def)
}
fn generate_query_type(aspect: &Aspect) -> Result<String, SammError> {
let type_name = to_pascal_case(&aspect.name());
let query_name = to_camel_case(&aspect.name());
let query_name_plural = format!("{}s", query_name);
let mut query_def = String::new();
query_def.push_str("type Query {\n");
query_def.push_str(&format!(" \"\"\"Get a single {} by ID\"\"\"\n", type_name));
query_def.push_str(&format!(" {}(id: ID!): {}\n\n", query_name, type_name));
query_def.push_str(&format!(
" \"\"\"Get multiple {}s with pagination\"\"\"\n",
type_name
));
query_def.push_str(&format!(
" {}(limit: Int = 10, offset: Int = 0): [{}!]!\n",
query_name_plural, type_name
));
query_def.push_str("}\n");
Ok(query_def)
}
fn get_graphql_type(prop: &crate::metamodel::Property) -> Result<String, SammError> {
if let Some(char) = &prop.characteristic {
let base_type = match char.kind() {
CharacteristicKind::Enumeration { .. } | CharacteristicKind::State { .. } => {
to_pascal_case(&prop.name())
}
CharacteristicKind::Collection { .. } | CharacteristicKind::List { .. } => {
if let Some(dt) = &char.data_type {
let element_type = map_xsd_to_graphql(dt);
format!("[{}!]", element_type)
} else {
"[String!]".to_string()
}
}
CharacteristicKind::SingleEntity { entity_type } => {
let entity_name = entity_type.split('#').next_back().unwrap_or(entity_type);
to_pascal_case(entity_name)
}
CharacteristicKind::Set { .. } => {
if let Some(dt) = &char.data_type {
let element_type = map_xsd_to_graphql(dt);
format!("[{}!]", element_type)
} else {
"[String!]".to_string()
}
}
CharacteristicKind::SortedSet { .. } => {
if let Some(dt) = &char.data_type {
let element_type = map_xsd_to_graphql(dt);
format!("[{}!]", element_type)
} else {
"[String!]".to_string()
}
}
CharacteristicKind::TimeSeries { .. } => {
"[TimeSeriesValue!]".to_string()
}
_ => {
if let Some(dt) = &char.data_type {
map_xsd_to_graphql(dt)
} else {
"String".to_string()
}
}
};
if prop.optional {
Ok(base_type)
} else {
if base_type.starts_with('[') {
Ok(format!("{}!", base_type))
} else if base_type.ends_with('!') {
Ok(base_type)
} else {
Ok(format!("{}!", base_type))
}
}
} else {
if prop.optional {
Ok("String".to_string())
} else {
Ok("String!".to_string())
}
}
}
fn map_xsd_to_graphql(xsd_type: &str) -> String {
match xsd_type {
t if t.ends_with("string") => "String".to_string(),
t if t.ends_with("int") | t.ends_with("integer") => "Int".to_string(),
t if t.ends_with("long") => "Int".to_string(),
t if t.ends_with("short") | t.ends_with("byte") => "Int".to_string(),
t if t.ends_with("decimal") => "Float".to_string(),
t if t.ends_with("float") => "Float".to_string(),
t if t.ends_with("double") => "Float".to_string(),
t if t.ends_with("boolean") => "Boolean".to_string(),
t if t.ends_with("date") => "Date".to_string(),
t if t.ends_with("dateTime") | t.ends_with("dateTimeStamp") => "DateTime".to_string(),
t if t.ends_with("time") => "Time".to_string(),
t if t.ends_with("duration") => "Duration".to_string(),
t if t.ends_with("anyURI") => "String".to_string(),
t if t.ends_with("base64Binary") => "String".to_string(),
t if t.ends_with("hexBinary") => "String".to_string(),
_ => "String".to_string(),
}
}
fn get_custom_scalar_for_xsd(xsd_type: &str) -> Option<String> {
match xsd_type {
t if t.ends_with("date") => Some("Date".to_string()),
t if t.ends_with("dateTime") | t.ends_with("dateTimeStamp") => Some("DateTime".to_string()),
t if t.ends_with("time") => Some("Time".to_string()),
t if t.ends_with("duration") => Some("Duration".to_string()),
_ => None,
}
}
fn to_camel_case(s: &str) -> String {
let pascal = to_pascal_case(s);
if pascal.is_empty() {
return pascal;
}
let mut chars = pascal.chars();
let first = chars
.next()
.expect("iterator should have next element")
.to_lowercase()
.to_string();
format!("{}{}", first, chars.as_str())
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for ch in s.chars() {
if ch == '_' || ch == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(ch.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xsd_to_graphql_mapping() {
assert_eq!(
map_xsd_to_graphql("http://www.w3.org/2001/XMLSchema#string"),
"String"
);
assert_eq!(
map_xsd_to_graphql("http://www.w3.org/2001/XMLSchema#int"),
"Int"
);
assert_eq!(
map_xsd_to_graphql("http://www.w3.org/2001/XMLSchema#boolean"),
"Boolean"
);
assert_eq!(
map_xsd_to_graphql("http://www.w3.org/2001/XMLSchema#float"),
"Float"
);
assert_eq!(
map_xsd_to_graphql("http://www.w3.org/2001/XMLSchema#dateTime"),
"DateTime"
);
}
#[test]
fn test_custom_scalar_detection() {
assert_eq!(
get_custom_scalar_for_xsd("http://www.w3.org/2001/XMLSchema#dateTime"),
Some("DateTime".to_string())
);
assert_eq!(
get_custom_scalar_for_xsd("http://www.w3.org/2001/XMLSchema#date"),
Some("Date".to_string())
);
assert_eq!(
get_custom_scalar_for_xsd("http://www.w3.org/2001/XMLSchema#string"),
None
);
}
#[test]
fn test_case_conversion() {
assert_eq!(to_camel_case("MovementAspect"), "movementAspect");
assert_eq!(to_camel_case("current_speed"), "currentSpeed");
assert_eq!(to_pascal_case("movement_aspect"), "MovementAspect");
assert_eq!(to_pascal_case("currentSpeed"), "CurrentSpeed");
}
#[test]
fn test_enum_generation() {
let values = vec!["GREEN".to_string(), "YELLOW".to_string(), "RED".to_string()];
let enum_def = generate_enum_type("TrafficLight", &values);
assert!(enum_def.contains("enum TrafficLight"));
assert!(enum_def.contains("GREEN"));
assert!(enum_def.contains("YELLOW"));
assert!(enum_def.contains("RED"));
}
}