use anyhow::Result;
use std::collections::HashMap;
use tensorlogic_adapters::{DomainInfo, PredicateInfo, SymbolTable};
use tensorlogic_ir::{TLExpr, Term};
#[derive(Debug, Clone)]
pub struct GraphQLType {
pub name: String,
pub fields: Vec<GraphQLField>,
pub interfaces: Vec<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GraphQLField {
pub name: String,
pub field_type: String,
pub is_required: bool,
pub is_list: bool,
pub arguments: Vec<GraphQLArgument>,
pub directives: Vec<GraphQLDirective>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GraphQLArgument {
pub name: String,
pub arg_type: String,
pub is_required: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GraphQLDirective {
pub name: String,
pub arguments: HashMap<String, DirectiveValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DirectiveValue {
String(String),
Int(i64),
Float(f64),
Boolean(bool),
List(Vec<DirectiveValue>),
}
pub struct GraphQLConverter {
types: HashMap<String, GraphQLType>,
}
impl GraphQLConverter {
pub fn new() -> Self {
GraphQLConverter {
types: HashMap::new(),
}
}
pub fn parse_schema(&mut self, schema: &str) -> Result<SymbolTable> {
self.types.clear();
let lines: Vec<&str> = schema.lines().map(|l| l.trim()).collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.is_empty() || line.starts_with('#') {
i += 1;
continue;
}
if line.starts_with("type ") {
let type_def = self.parse_type_definition(&lines, &mut i)?;
self.types.insert(type_def.name.clone(), type_def);
} else {
i += 1;
}
}
self.to_symbol_table()
}
fn parse_type_definition(&self, lines: &[&str], index: &mut usize) -> Result<GraphQLType> {
let line = lines[*index];
let type_line = line
.strip_prefix("type ")
.ok_or_else(|| anyhow::anyhow!("Invalid type definition"))?;
let mut parts = type_line.split('{');
let type_header = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing type header"))?
.trim();
let (name, interfaces) = if type_header.contains("implements") {
let mut impl_parts = type_header.split("implements");
let name = impl_parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing type name"))?
.trim()
.to_string();
let interfaces_str = impl_parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing interfaces"))?
.trim();
let interfaces = interfaces_str
.split('&')
.map(|s| s.trim().to_string())
.collect();
(name, interfaces)
} else {
(type_header.to_string(), Vec::new())
};
let mut fields = Vec::new();
*index += 1;
while *index < lines.len() {
let field_line = lines[*index].trim();
if field_line == "}" {
*index += 1;
break;
}
if field_line.is_empty() || field_line.starts_with('#') {
*index += 1;
continue;
}
if let Some(field) = self.parse_field(field_line)? {
fields.push(field);
}
*index += 1;
}
Ok(GraphQLType {
name,
fields,
interfaces,
description: None,
})
}
fn parse_field(&self, line: &str) -> Result<Option<GraphQLField>> {
let field_line = if line.find(':').is_some() {
line
} else {
return Ok(None); };
let (field_part, type_part) = field_line
.split_once(':')
.expect("colon presence validated above");
let field_part = field_part.trim();
let mut type_part = type_part.trim();
let directives = self.parse_directives(type_part);
if let Some(at_pos) = type_part.find('@') {
type_part = type_part[..at_pos].trim();
}
let (field_name, arguments) = if field_part.contains('(') {
let name_end = field_part
.find('(')
.expect("parenthesis presence validated by contains check");
let field_name = field_part[..name_end].trim();
let args_str = field_part[name_end + 1..]
.strip_suffix(')')
.unwrap_or("")
.trim();
let arguments = if args_str.is_empty() {
Vec::new()
} else {
self.parse_arguments(args_str)?
};
(field_name, arguments)
} else {
(field_part, Vec::new())
};
let (field_type, is_required, is_list) = self.parse_type(type_part);
Ok(Some(GraphQLField {
name: field_name.to_string(),
field_type,
is_required,
is_list,
arguments,
directives,
description: None,
}))
}
fn parse_arguments(&self, args_str: &str) -> Result<Vec<GraphQLArgument>> {
let mut arguments = Vec::new();
for arg in args_str.split(',') {
let arg = arg.trim();
if arg.is_empty() {
continue;
}
if let Some((name, type_str)) = arg.split_once(':') {
let name = name.trim().to_string();
let (arg_type, is_required, _is_list) = self.parse_type(type_str.trim());
arguments.push(GraphQLArgument {
name,
arg_type,
is_required,
});
}
}
Ok(arguments)
}
fn parse_type(&self, type_str: &str) -> (String, bool, bool) {
let type_str = type_str.trim();
let is_required = type_str.ends_with('!');
let type_str = type_str.trim_end_matches('!');
let is_list = type_str.starts_with('[') && type_str.ends_with(']');
let type_str = if is_list {
type_str
.strip_prefix('[')
.expect("is_list guarantees '[' prefix")
.strip_suffix(']')
.expect("is_list guarantees ']' suffix")
.trim_end_matches('!')
} else {
type_str
};
(type_str.to_string(), is_required, is_list)
}
pub fn to_symbol_table(&self) -> Result<SymbolTable> {
let mut table = SymbolTable::new();
for scalar in &["String", "Int", "Float", "Boolean", "ID", "Value"] {
let domain = DomainInfo::new(*scalar, 1000); table.add_domain(domain)?;
}
for (type_name, type_def) in &self.types {
if matches!(type_name.as_str(), "Query" | "Mutation" | "Subscription") {
continue;
}
let mut domain = DomainInfo::new(type_name, 100);
if let Some(desc) = &type_def.description {
domain = domain.with_description(desc);
}
table.add_domain(domain)?;
}
for type_def in self.types.values() {
if matches!(
type_def.name.as_str(),
"Query" | "Mutation" | "Subscription"
) {
continue;
}
for field in &type_def.fields {
let predicate_name = format!("{}_{}", type_def.name, field.name);
let mut arg_domains = vec![type_def.name.clone()];
if self.types.contains_key(&field.field_type)
|| self.is_scalar_type(&field.field_type)
{
arg_domains.push(field.field_type.clone());
} else {
arg_domains.push("Value".to_string()); }
let mut predicate = PredicateInfo::new(&predicate_name, arg_domains);
if let Some(desc) = &field.description {
predicate = predicate.with_description(desc);
}
table.add_predicate(predicate)?;
}
}
Ok(table)
}
fn is_scalar_type(&self, type_name: &str) -> bool {
matches!(
type_name,
"String" | "Int" | "Float" | "Boolean" | "ID" | "Value"
)
}
fn parse_directives(&self, line: &str) -> Vec<GraphQLDirective> {
let mut directives = Vec::new();
let mut current_pos = 0;
while let Some(at_pos) = line[current_pos..].find('@') {
let abs_pos = current_pos + at_pos;
let remaining = &line[abs_pos + 1..];
let name_end = remaining
.find(|c: char| c == '(' || c.is_whitespace() || c == '@')
.unwrap_or(remaining.len());
let directive_name = remaining[..name_end].trim().to_string();
let mut arguments = HashMap::new();
if remaining.len() > name_end && remaining.chars().nth(name_end) == Some('(') {
if let Some(close_paren) = remaining.find(')') {
let args_str = &remaining[name_end + 1..close_paren];
arguments = self.parse_directive_arguments(args_str);
current_pos = abs_pos + 1 + close_paren + 1;
} else {
current_pos = abs_pos + 1 + name_end;
}
} else {
current_pos = abs_pos + 1 + name_end;
}
directives.push(GraphQLDirective {
name: directive_name,
arguments,
});
}
directives
}
fn parse_directive_arguments(&self, args_str: &str) -> HashMap<String, DirectiveValue> {
let mut arguments = HashMap::new();
for arg in args_str.split(',') {
let arg = arg.trim();
if arg.is_empty() {
continue;
}
if let Some((name, value_str)) = arg.split_once(':') {
let name = name.trim().to_string();
let value_str = value_str.trim();
if let Some(value) = self.parse_directive_value(value_str) {
arguments.insert(name, value);
}
}
}
arguments
}
fn parse_directive_value(&self, value_str: &str) -> Option<DirectiveValue> {
Self::parse_directive_value_impl(value_str)
}
fn parse_directive_value_impl(value_str: &str) -> Option<DirectiveValue> {
let value_str = value_str.trim();
if value_str.starts_with('"') && value_str.ends_with('"') {
let s = value_str[1..value_str.len() - 1].to_string();
return Some(DirectiveValue::String(s));
}
if value_str == "true" {
return Some(DirectiveValue::Boolean(true));
}
if value_str == "false" {
return Some(DirectiveValue::Boolean(false));
}
if let Ok(i) = value_str.parse::<i64>() {
return Some(DirectiveValue::Int(i));
}
if let Ok(f) = value_str.parse::<f64>() {
return Some(DirectiveValue::Float(f));
}
if value_str.starts_with('[') && value_str.ends_with(']') {
let inner = &value_str[1..value_str.len() - 1];
let items: Vec<DirectiveValue> = inner
.split(',')
.filter_map(|s| Self::parse_directive_value_impl(s.trim()))
.collect();
return Some(DirectiveValue::List(items));
}
None
}
pub fn directives_to_constraints(&self, type_name: &str, field: &GraphQLField) -> Vec<TLExpr> {
let mut constraints = Vec::new();
let field_var = format!("{}_{}", type_name, field.name);
for directive in &field.directives {
match directive.name.as_str() {
"constraint" => {
constraints.extend(self.constraint_directive_to_expr(&field_var, directive));
}
"range" => {
constraints.extend(self.range_directive_to_expr(&field_var, directive));
}
"length" => {
constraints.extend(self.length_directive_to_expr(&field_var, directive));
}
"pattern" => {
constraints.extend(self.pattern_directive_to_expr(&field_var, directive));
}
"uniqueItems" => {
constraints.push(self.unique_items_directive_to_expr(&field_var));
}
_ => {} }
}
constraints
}
fn constraint_directive_to_expr(
&self,
field_var: &str,
directive: &GraphQLDirective,
) -> Vec<TLExpr> {
let mut constraints = Vec::new();
if let Some(DirectiveValue::Int(min_len)) = directive.arguments.get("minLength") {
let expr = TLExpr::pred(
"minLength",
vec![Term::var(field_var), Term::constant(min_len.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Int(max_len)) = directive.arguments.get("maxLength") {
let expr = TLExpr::pred(
"maxLength",
vec![Term::var(field_var), Term::constant(max_len.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
let expr = TLExpr::pred(
"greaterOrEqual",
vec![Term::var(field_var), Term::constant(min.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Float(min)) = directive.arguments.get("min") {
let expr = TLExpr::pred(
"greaterOrEqual",
vec![Term::var(field_var), Term::constant(min.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
let expr = TLExpr::pred(
"lessOrEqual",
vec![Term::var(field_var), Term::constant(max.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Float(max)) = directive.arguments.get("max") {
let expr = TLExpr::pred(
"lessOrEqual",
vec![Term::var(field_var), Term::constant(max.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::String(pattern)) = directive.arguments.get("pattern") {
let expr = TLExpr::pred(
"matches",
vec![Term::var(field_var), Term::constant(pattern)],
);
constraints.push(expr);
}
if let Some(DirectiveValue::String(format)) = directive.arguments.get("format") {
let expr = TLExpr::pred(
format!("isValid{}", capitalize_first(format)),
vec![Term::var(field_var)],
);
constraints.push(expr);
}
constraints
}
fn range_directive_to_expr(
&self,
field_var: &str,
directive: &GraphQLDirective,
) -> Vec<TLExpr> {
let mut constraints = Vec::new();
if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
let expr = TLExpr::pred(
"greaterOrEqual",
vec![Term::var(field_var), Term::constant(min.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
let expr = TLExpr::pred(
"lessOrEqual",
vec![Term::var(field_var), Term::constant(max.to_string())],
);
constraints.push(expr);
}
constraints
}
fn length_directive_to_expr(
&self,
field_var: &str,
directive: &GraphQLDirective,
) -> Vec<TLExpr> {
let mut constraints = Vec::new();
if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
let expr = TLExpr::pred(
"minLength",
vec![Term::var(field_var), Term::constant(min.to_string())],
);
constraints.push(expr);
}
if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
let expr = TLExpr::pred(
"maxLength",
vec![Term::var(field_var), Term::constant(max.to_string())],
);
constraints.push(expr);
}
constraints
}
fn pattern_directive_to_expr(
&self,
field_var: &str,
directive: &GraphQLDirective,
) -> Vec<TLExpr> {
if let Some(DirectiveValue::String(pattern)) = directive.arguments.get("value") {
vec![TLExpr::pred(
"matches",
vec![Term::var(field_var), Term::constant(pattern)],
)]
} else {
vec![]
}
}
fn unique_items_directive_to_expr(&self, field_var: &str) -> TLExpr {
TLExpr::pred("allUnique", vec![Term::var(field_var)])
}
pub fn get_constraints(&self, type_name: &str) -> Vec<TLExpr> {
let mut constraints = Vec::new();
if let Some(type_def) = self.types.get(type_name) {
for field in &type_def.fields {
constraints.extend(self.directives_to_constraints(type_name, field));
}
}
constraints
}
pub fn types(&self) -> &HashMap<String, GraphQLType> {
&self.types
}
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
impl Default for GraphQLConverter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graphql_converter_basic() {
let converter = GraphQLConverter::new();
assert!(converter.types().is_empty());
}
#[test]
fn test_parse_simple_type() {
let schema = r#"
type Person {
name: String!
age: Int
}
"#;
let mut converter = GraphQLConverter::new();
let result = converter.parse_schema(schema);
assert!(result.is_ok());
assert_eq!(converter.types().len(), 1);
assert!(converter.types().contains_key("Person"));
}
#[test]
fn test_parse_type_with_fields() {
let schema = r#"
type User {
id: ID!
name: String!
email: String
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
let user_type = converter.types().get("User").expect("unwrap");
assert_eq!(user_type.fields.len(), 3);
let id_field = &user_type.fields[0];
assert_eq!(id_field.name, "id");
assert_eq!(id_field.field_type, "ID");
assert!(id_field.is_required);
}
#[test]
fn test_parse_type_with_list() {
let schema = r#"
type Post {
tags: [String!]
comments: [Comment]
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
let post_type = converter.types().get("Post").expect("unwrap");
assert_eq!(post_type.fields.len(), 2);
let tags_field = &post_type.fields[0];
assert!(tags_field.is_list);
}
#[test]
fn test_to_symbol_table() {
let schema = r#"
type Person {
name: String!
age: Int
}
"#;
let mut converter = GraphQLConverter::new();
let table = converter.parse_schema(schema).expect("unwrap");
assert!(table.domains.contains_key("Person"));
assert!(table.predicates.contains_key("Person_name"));
assert!(table.predicates.contains_key("Person_age"));
}
#[test]
fn test_parse_multiple_types() {
let schema = r#"
type Author {
name: String!
}
type Book {
title: String!
author: Author!
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
assert_eq!(converter.types().len(), 2);
assert!(converter.types().contains_key("Author"));
assert!(converter.types().contains_key("Book"));
}
#[test]
fn test_skip_special_types() {
let schema = r#"
type Query {
user(id: ID!): User
}
type User {
name: String!
}
"#;
let mut converter = GraphQLConverter::new();
let table = converter.parse_schema(schema).expect("unwrap");
assert!(!table.domains.contains_key("Query"));
assert!(table.domains.contains_key("User"));
}
#[test]
fn test_parse_directive_simple() {
let converter = GraphQLConverter::new();
let directives = converter.parse_directives("name: String! @deprecated");
assert_eq!(directives.len(), 1);
assert_eq!(directives[0].name, "deprecated");
assert!(directives[0].arguments.is_empty());
}
#[test]
fn test_parse_directive_with_arguments() {
let converter = GraphQLConverter::new();
let directives = converter.parse_directives(r#"age: Int @constraint(min: 0, max: 120)"#);
assert_eq!(directives.len(), 1);
assert_eq!(directives[0].name, "constraint");
assert_eq!(directives[0].arguments.len(), 2);
assert_eq!(
directives[0].arguments.get("min"),
Some(&DirectiveValue::Int(0))
);
assert_eq!(
directives[0].arguments.get("max"),
Some(&DirectiveValue::Int(120))
);
}
#[test]
fn test_parse_directive_string_argument() {
let converter = GraphQLConverter::new();
let directives =
converter.parse_directives(r#"email: String @constraint(pattern: "^[a-z]+$")"#);
assert_eq!(directives.len(), 1);
assert_eq!(
directives[0].arguments.get("pattern"),
Some(&DirectiveValue::String("^[a-z]+$".to_string()))
);
}
#[test]
fn test_parse_multiple_directives() {
let converter = GraphQLConverter::new();
let directives = converter
.parse_directives(r#"name: String @length(min: 3, max: 50) @pattern(value: "[a-z]+")"#);
assert_eq!(directives.len(), 2);
assert_eq!(directives[0].name, "length");
assert_eq!(directives[1].name, "pattern");
}
#[test]
fn test_field_with_directive_parsing() {
let schema = r#"
type User {
age: Int @constraint(min: 0, max: 120)
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
let user_type = converter.types().get("User").expect("unwrap");
assert_eq!(user_type.fields.len(), 1);
let age_field = &user_type.fields[0];
assert_eq!(age_field.directives.len(), 1);
assert_eq!(age_field.directives[0].name, "constraint");
}
#[test]
fn test_constraint_directive_to_expr() {
let converter = GraphQLConverter::new();
let mut directive_args = HashMap::new();
directive_args.insert("min".to_string(), DirectiveValue::Int(0));
directive_args.insert("max".to_string(), DirectiveValue::Int(120));
let directive = GraphQLDirective {
name: "constraint".to_string(),
arguments: directive_args,
};
let constraints = converter.constraint_directive_to_expr("User_age", &directive);
assert_eq!(constraints.len(), 2);
let expr_strs: Vec<String> = constraints.iter().map(|e| format!("{:?}", e)).collect();
assert!(expr_strs
.iter()
.any(|s| s.contains("greaterOrEqual") && s.contains("User_age")));
assert!(expr_strs
.iter()
.any(|s| s.contains("lessOrEqual") && s.contains("User_age")));
}
#[test]
fn test_length_directive_to_expr() {
let converter = GraphQLConverter::new();
let mut directive_args = HashMap::new();
directive_args.insert("min".to_string(), DirectiveValue::Int(3));
directive_args.insert("max".to_string(), DirectiveValue::Int(50));
let directive = GraphQLDirective {
name: "length".to_string(),
arguments: directive_args,
};
let constraints = converter.length_directive_to_expr("User_name", &directive);
assert_eq!(constraints.len(), 2);
let expr_strs: Vec<String> = constraints.iter().map(|e| format!("{:?}", e)).collect();
assert!(expr_strs
.iter()
.any(|s| s.contains("minLength") && s.contains("User_name")));
assert!(expr_strs
.iter()
.any(|s| s.contains("maxLength") && s.contains("User_name")));
}
#[test]
fn test_pattern_directive_to_expr() {
let converter = GraphQLConverter::new();
let mut directive_args = HashMap::new();
directive_args.insert(
"value".to_string(),
DirectiveValue::String("^[a-z]+$".to_string()),
);
let directive = GraphQLDirective {
name: "pattern".to_string(),
arguments: directive_args,
};
let constraints = converter.pattern_directive_to_expr("User_username", &directive);
assert_eq!(constraints.len(), 1);
let expr_str = format!("{:?}", constraints[0]);
assert!(expr_str.contains("matches"));
assert!(expr_str.contains("User_username"));
assert!(expr_str.contains("^[a-z]+$"));
}
#[test]
fn test_range_directive_to_expr() {
let converter = GraphQLConverter::new();
let mut directive_args = HashMap::new();
directive_args.insert("min".to_string(), DirectiveValue::Int(18));
directive_args.insert("max".to_string(), DirectiveValue::Int(65));
let directive = GraphQLDirective {
name: "range".to_string(),
arguments: directive_args,
};
let constraints = converter.range_directive_to_expr("User_age", &directive);
assert_eq!(constraints.len(), 2);
}
#[test]
fn test_unique_items_directive() {
let converter = GraphQLConverter::new();
let expr = converter.unique_items_directive_to_expr("User_tags");
let expr_str = format!("{:?}", expr);
assert!(expr_str.contains("allUnique"));
assert!(expr_str.contains("User_tags"));
}
#[test]
fn test_get_constraints_for_type() {
let schema = r#"
type Product {
price: Float @constraint(min: 0.0, max: 10000.0)
name: String @length(min: 1, max: 100)
sku: String @pattern(value: "^[A-Z0-9]+$")
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
let constraints = converter.get_constraints("Product");
assert!(constraints.len() >= 5); }
#[test]
fn test_format_directive_to_expr() {
let converter = GraphQLConverter::new();
let mut directive_args = HashMap::new();
directive_args.insert(
"format".to_string(),
DirectiveValue::String("email".to_string()),
);
let directive = GraphQLDirective {
name: "constraint".to_string(),
arguments: directive_args,
};
let constraints = converter.constraint_directive_to_expr("User_email", &directive);
assert_eq!(constraints.len(), 1);
let expr_str = format!("{:?}", constraints[0]);
assert!(expr_str.contains("isValidEmail"));
assert!(expr_str.contains("User_email"));
}
#[test]
fn test_directive_value_parsing_boolean() {
let converter = GraphQLConverter::new();
assert_eq!(
converter.parse_directive_value("true"),
Some(DirectiveValue::Boolean(true))
);
assert_eq!(
converter.parse_directive_value("false"),
Some(DirectiveValue::Boolean(false))
);
}
#[test]
fn test_directive_value_parsing_numbers() {
let converter = GraphQLConverter::new();
assert_eq!(
converter.parse_directive_value("42"),
Some(DirectiveValue::Int(42))
);
assert_eq!(
converter.parse_directive_value("2.5"),
Some(DirectiveValue::Float(2.5))
);
}
#[test]
fn test_directive_value_parsing_list() {
let converter = GraphQLConverter::new();
if let Some(DirectiveValue::List(items)) = converter.parse_directive_value("[1, 2, 3]") {
assert_eq!(items.len(), 3);
assert_eq!(items[0], DirectiveValue::Int(1));
assert_eq!(items[1], DirectiveValue::Int(2));
assert_eq!(items[2], DirectiveValue::Int(3));
} else {
panic!("Expected List");
}
}
#[test]
fn test_complex_directive_scenario() {
let schema = r#"
type User {
username: String! @constraint(minLength: 3, maxLength: 20, pattern: "^[a-z0-9_]+$")
age: Int! @range(min: 13, max: 120)
email: String! @constraint(format: "email")
tags: [String!] @uniqueItems
}
"#;
let mut converter = GraphQLConverter::new();
converter.parse_schema(schema).expect("unwrap");
let user_type = converter.types().get("User").expect("unwrap");
let username_field = &user_type
.fields
.iter()
.find(|f| f.name == "username")
.expect("unwrap");
assert!(!username_field.directives.is_empty());
let constraints = converter.get_constraints("User");
assert!(constraints.len() >= 7); }
#[test]
fn test_capitalize_first() {
assert_eq!(capitalize_first("email"), "Email");
assert_eq!(capitalize_first("url"), "Url");
assert_eq!(capitalize_first(""), "");
assert_eq!(capitalize_first("A"), "A");
}
}