use crate::codegen::graphql::spec_parser::{
GraphQLEnumValue, GraphQLField, GraphQLInputField, GraphQLSchema, TypeKind,
};
pub struct SdlBuilder<'a> {
schema: &'a GraphQLSchema,
}
impl<'a> SdlBuilder<'a> {
pub const fn new(schema: &'a GraphQLSchema) -> Self {
Self { schema }
}
pub fn build(&self) -> String {
let mut sdl = String::new();
sdl.push_str(&self.format_directives());
sdl.push_str(&self.format_queries());
sdl.push_str(&self.format_mutations());
sdl.push_str(&self.format_subscriptions());
sdl.push_str(&self.format_types());
sdl.trim_end().to_string()
}
fn format_directives(&self) -> String {
let mut result = String::new();
for directive in &self.schema.directives {
if let Some(desc) = &directive.description {
result.push_str("\"\"\"");
result.push_str(desc);
result.push_str("\"\"\"\n");
}
result.push_str("directive @");
result.push_str(&directive.name);
if !directive.arguments.is_empty() {
result.push('(');
for (i, arg) in directive.arguments.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(&arg.name);
result.push_str(": ");
result.push_str(&self.format_gql_type(
&arg.type_name,
arg.is_nullable,
arg.is_list,
arg.list_item_nullable,
));
if let Some(default) = &arg.default_value {
result.push_str(" = ");
result.push_str(default);
}
}
result.push(')');
}
if !directive.locations.is_empty() {
result.push_str(" on ");
result.push_str(&directive.locations.join(" | "));
}
result.push_str("\n\n");
}
result
}
fn format_queries(&self) -> String {
let mut result = String::new();
if !self.schema.queries.is_empty() {
result.push_str("type Query {\n");
for field in &self.schema.queries {
result.push_str(&self.format_field(field));
}
result.push_str("}\n\n");
}
result
}
fn format_mutations(&self) -> String {
let mut result = String::new();
if !self.schema.mutations.is_empty() {
result.push_str("type Mutation {\n");
for field in &self.schema.mutations {
result.push_str(&self.format_field(field));
}
result.push_str("}\n\n");
}
result
}
fn format_subscriptions(&self) -> String {
let mut result = String::new();
if !self.schema.subscriptions.is_empty() {
result.push_str("type Subscription {\n");
for field in &self.schema.subscriptions {
result.push_str(&self.format_field(field));
}
result.push_str("}\n\n");
}
result
}
fn format_types(&self) -> String {
let mut result = String::new();
for (type_name, type_def) in &self.schema.types {
if matches!(
type_name.as_str(),
"String" | "Int" | "Float" | "Boolean" | "ID" | "DateTime" | "Date" | "Time" | "JSON" | "Upload"
) {
continue;
}
if let Some(desc) = &type_def.description {
result.push_str("\"\"\"");
result.push_str(desc);
result.push_str("\"\"\"\n");
}
match type_def.kind {
TypeKind::Object => {
result.push_str("type ");
result.push_str(&type_def.name);
result.push_str(" {\n");
for field in &type_def.fields {
result.push_str(&self.format_field(field));
}
result.push_str("}\n\n");
}
TypeKind::InputObject => {
result.push_str(&self.format_input_objects_single(&type_def.name, &type_def.input_fields));
}
TypeKind::Enum => {
result.push_str(&self.format_enums_single(&type_def.name, &type_def.enum_values));
}
TypeKind::Scalar => {
result.push_str("scalar ");
result.push_str(&type_def.name);
result.push_str("\n\n");
}
TypeKind::Union => {
result.push_str("union ");
result.push_str(&type_def.name);
result.push_str(" = ");
result.push_str(&type_def.possible_types.join(" | "));
result.push_str("\n\n");
}
TypeKind::Interface => {
result.push_str("interface ");
result.push_str(&type_def.name);
result.push_str(" {\n");
for field in &type_def.fields {
result.push_str(&self.format_field(field));
}
result.push_str("}\n\n");
}
_ => {}
}
}
result
}
#[allow(dead_code)]
fn format_enums(&self) -> String {
let mut result = String::new();
for (type_name, type_def) in &self.schema.types {
if matches!(
type_name.as_str(),
"String" | "Int" | "Float" | "Boolean" | "ID" | "DateTime" | "Date" | "Time" | "JSON" | "Upload"
) {
continue;
}
if type_def.kind == TypeKind::Enum {
result.push_str(&self.format_enums_single(&type_def.name, &type_def.enum_values));
}
}
result
}
fn format_enums_single(&self, name: &str, values: &[GraphQLEnumValue]) -> String {
let mut result = String::new();
result.push_str("enum ");
result.push_str(name);
result.push_str(" {\n");
for value in values {
if let Some(desc) = &value.description {
result.push_str(" \"\"\"");
result.push_str(desc);
result.push_str("\"\"\"\n");
}
result.push_str(" ");
result.push_str(&value.name);
if value.is_deprecated {
if let Some(reason) = &value.deprecation_reason {
result.push_str(" @deprecated(reason: \"");
result.push_str(&reason.replace('"', "\\\""));
result.push_str("\")");
} else {
result.push_str(" @deprecated");
}
}
result.push('\n');
}
result.push_str("}\n\n");
result
}
#[allow(dead_code)]
fn format_input_objects(&self) -> String {
let mut result = String::new();
for (type_name, type_def) in &self.schema.types {
if matches!(
type_name.as_str(),
"String" | "Int" | "Float" | "Boolean" | "ID" | "DateTime" | "Date" | "Time" | "JSON" | "Upload"
) {
continue;
}
if type_def.kind == TypeKind::InputObject {
result.push_str(&self.format_input_objects_single(&type_def.name, &type_def.input_fields));
}
}
result
}
fn format_input_objects_single(&self, name: &str, fields: &[GraphQLInputField]) -> String {
let mut result = String::new();
result.push_str("input ");
result.push_str(name);
result.push_str(" {\n");
for field in fields {
if let Some(desc) = &field.description {
result.push_str(" \"\"\"");
result.push_str(desc);
result.push_str("\"\"\"\n");
}
result.push_str(" ");
result.push_str(&field.name);
result.push_str(": ");
result.push_str(&self.format_gql_type(
&field.type_name,
field.is_nullable,
field.is_list,
field.list_item_nullable,
));
if let Some(default) = &field.default_value {
result.push_str(" = ");
result.push_str(default);
}
result.push('\n');
}
result.push_str("}\n\n");
result
}
#[allow(dead_code)]
fn format_unions(&self) -> String {
let mut result = String::new();
for (type_name, type_def) in &self.schema.types {
if matches!(
type_name.as_str(),
"String" | "Int" | "Float" | "Boolean" | "ID" | "DateTime" | "Date" | "Time" | "JSON" | "Upload"
) {
continue;
}
if type_def.kind == TypeKind::Union {
result.push_str("union ");
result.push_str(&type_def.name);
result.push_str(" = ");
result.push_str(&type_def.possible_types.join(" | "));
result.push_str("\n\n");
}
}
result
}
fn format_field(&self, field: &GraphQLField) -> String {
let mut result = String::new();
if let Some(desc) = &field.description {
result.push_str(" \"\"\"");
result.push_str(desc);
result.push_str("\"\"\"\n");
}
result.push_str(" ");
result.push_str(&field.name);
if !field.arguments.is_empty() {
result.push('(');
for (i, arg) in field.arguments.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(&arg.name);
result.push_str(": ");
result.push_str(&self.format_gql_type(
&arg.type_name,
arg.is_nullable,
arg.is_list,
arg.list_item_nullable,
));
if let Some(default) = &arg.default_value {
result.push_str(" = ");
result.push_str(default);
}
}
result.push(')');
}
result.push_str(": ");
result.push_str(&self.format_gql_type(
&field.type_name,
field.is_nullable,
field.is_list,
field.list_item_nullable,
));
if let Some(reason) = &field.deprecation_reason {
result.push_str(" @deprecated(reason: \"");
result.push_str(&reason.replace('"', "\\\""));
result.push_str("\")");
}
result.push('\n');
result
}
fn format_gql_type(&self, type_name: &str, is_nullable: bool, is_list: bool, list_item_nullable: bool) -> String {
let clean_type = type_name.trim_matches(|c| c == '!' || c == '[' || c == ']');
let mut result = if is_list {
if list_item_nullable {
format!("[{clean_type}]")
} else {
format!("[{clean_type}!]")
}
} else {
clean_type.to_string()
};
if !is_nullable {
result.push('!');
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_format_gql_type_non_nullable() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", false, false, false), "String!");
assert_eq!(builder.format_gql_type("User", false, false, false), "User!");
}
#[test]
fn test_format_gql_type_nullable() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", true, false, false), "String");
assert_eq!(builder.format_gql_type("User", true, false, false), "User");
}
#[test]
fn test_format_gql_type_non_nullable_list_non_nullable_items() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", false, true, false), "[String!]!");
}
#[test]
fn test_format_gql_type_non_nullable_list_nullable_items() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", false, true, true), "[String]!");
}
#[test]
fn test_format_gql_type_nullable_list_non_nullable_items() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", true, true, false), "[String!]");
}
#[test]
fn test_format_gql_type_nullable_list_nullable_items() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String", true, true, true), "[String]");
}
#[test]
fn test_format_gql_type_strips_existing_notation() {
let schema = GraphQLSchema {
types: HashMap::new(),
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
description: None,
};
let builder = SdlBuilder::new(&schema);
assert_eq!(builder.format_gql_type("String!", false, false, false), "String!");
assert_eq!(builder.format_gql_type("[String!]!", false, true, false), "[String!]!");
assert_eq!(builder.format_gql_type("String!", true, false, false), "String");
}
}