use anyhow::{Context, Result, anyhow};
use graphql_parser::schema::{Document, ObjectType, TypeDefinition, parse_schema};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLSchema {
pub types: HashMap<String, GraphQLType>,
pub queries: Vec<GraphQLField>,
pub mutations: Vec<GraphQLField>,
pub subscriptions: Vec<GraphQLField>,
pub directives: Vec<GraphQLDirective>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLType {
pub name: String,
pub kind: TypeKind,
pub fields: Vec<GraphQLField>,
pub description: Option<String>,
pub possible_types: Vec<String>,
pub enum_values: Vec<GraphQLEnumValue>,
pub input_fields: Vec<GraphQLInputField>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum TypeKind {
Object,
Interface,
Union,
Enum,
InputObject,
Scalar,
List,
NonNull,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLField {
pub name: String,
pub type_name: String,
pub is_list: bool,
pub list_item_nullable: bool,
pub is_nullable: bool,
pub arguments: Vec<GraphQLArgument>,
pub description: Option<String>,
pub deprecation_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLArgument {
pub name: String,
pub type_name: String,
pub is_nullable: bool,
pub is_list: bool,
pub list_item_nullable: bool,
pub default_value: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLDirective {
pub name: String,
pub locations: Vec<String>,
pub arguments: Vec<GraphQLArgument>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLEnumValue {
pub name: String,
pub description: Option<String>,
pub is_deprecated: bool,
pub deprecation_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLInputField {
pub name: String,
pub type_name: String,
pub is_nullable: bool,
pub is_list: bool,
pub list_item_nullable: bool,
pub default_value: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionEnvelope {
#[serde(rename = "__schema")]
schema: Option<IntrospectionSchemaDoc>,
data: Option<IntrospectionData>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionData {
#[serde(rename = "__schema")]
schema: IntrospectionSchemaDoc,
}
#[derive(Debug, Deserialize)]
struct IntrospectionSchemaDoc {
description: Option<String>,
#[serde(rename = "queryType")]
query_type: Option<IntrospectionNamedTypeRef>,
#[serde(rename = "mutationType")]
mutation_type: Option<IntrospectionNamedTypeRef>,
#[serde(rename = "subscriptionType")]
subscription_type: Option<IntrospectionNamedTypeRef>,
types: Vec<IntrospectionTypeDef>,
directives: Vec<IntrospectionDirectiveDef>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionNamedTypeRef {
name: String,
}
#[derive(Debug, Deserialize)]
struct IntrospectionTypeDef {
kind: String,
name: Option<String>,
description: Option<String>,
fields: Option<Vec<IntrospectionFieldDef>>,
#[serde(rename = "inputFields")]
input_fields: Option<Vec<IntrospectionInputValueDef>>,
#[serde(rename = "enumValues")]
enum_values: Option<Vec<IntrospectionEnumValueDef>>,
#[serde(rename = "possibleTypes")]
possible_types: Option<Vec<IntrospectionNamedTypeRef>>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionFieldDef {
name: String,
description: Option<String>,
args: Vec<IntrospectionInputValueDef>,
#[serde(rename = "type")]
field_type: IntrospectionTypeRef,
#[serde(rename = "isDeprecated", default)]
is_deprecated: bool,
#[serde(rename = "deprecationReason")]
deprecation_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionInputValueDef {
name: String,
description: Option<String>,
#[serde(rename = "type")]
value_type: IntrospectionTypeRef,
#[serde(rename = "defaultValue")]
default_value: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionEnumValueDef {
name: String,
description: Option<String>,
#[serde(rename = "isDeprecated", default)]
is_deprecated: bool,
#[serde(rename = "deprecationReason")]
deprecation_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IntrospectionDirectiveDef {
name: String,
description: Option<String>,
locations: Vec<String>,
args: Vec<IntrospectionInputValueDef>,
}
#[derive(Debug, Clone, Deserialize)]
struct IntrospectionTypeRef {
kind: String,
name: Option<String>,
#[serde(rename = "ofType")]
of_type: Option<Box<IntrospectionTypeRef>>,
}
pub fn parse_graphql_sdl(path: &Path) -> Result<GraphQLSchema> {
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read GraphQL SDL file: {}", path.display()))?;
parse_graphql_sdl_string(&content).with_context(|| format!("Failed to parse GraphQL SDL from {}", path.display()))
}
pub fn parse_graphql_sdl_string(content: &str) -> Result<GraphQLSchema> {
let doc: Document<String> = parse_schema(content).map_err(|e| anyhow!("GraphQL parsing error: {e}"))?;
let mut schema = GraphQLSchema {
types: HashMap::new(),
queries: Vec::new(),
mutations: Vec::new(),
subscriptions: Vec::new(),
directives: Vec::new(),
description: None,
};
for directive_def in &doc.definitions {
if let graphql_parser::schema::Definition::DirectiveDefinition(dir_def) = directive_def {
let args = dir_def
.arguments
.iter()
.map(|arg| GraphQLArgument {
name: arg.name.clone(),
type_name: extract_bare_type_name(&arg.value_type),
is_nullable: is_nullable_type(&arg.value_type),
is_list: is_list_type(&arg.value_type),
list_item_nullable: extract_list_item_nullability(&arg.value_type),
default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
description: arg.description.clone(),
})
.collect();
schema.directives.push(GraphQLDirective {
name: dir_def.name.clone(),
locations: dir_def.locations.iter().map(|l| format!("{l:?}")).collect(),
arguments: args,
description: dir_def.description.clone(),
});
}
}
for definition in &doc.definitions {
if let graphql_parser::schema::Definition::TypeDefinition(type_def) = definition {
match type_def {
TypeDefinition::Object(obj) => {
let fields = extract_fields_from_object(obj);
let gql_type = GraphQLType {
name: obj.name.clone(),
kind: TypeKind::Object,
fields: fields.clone(),
description: obj.description.clone(),
possible_types: Vec::new(),
enum_values: Vec::new(),
input_fields: Vec::new(),
};
if obj.name == "Query" {
schema.queries = fields;
} else if obj.name == "Mutation" {
schema.mutations = fields;
} else if obj.name == "Subscription" {
schema.subscriptions = fields;
} else {
if schema.types.contains_key(&obj.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
obj.name
));
}
schema.types.insert(obj.name.clone(), gql_type);
}
}
TypeDefinition::Interface(interface) => {
if schema.types.contains_key(&interface.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
interface.name
));
}
let fields = extract_fields_from_interface(interface);
schema.types.insert(
interface.name.clone(),
GraphQLType {
name: interface.name.clone(),
kind: TypeKind::Interface,
fields,
description: interface.description.clone(),
possible_types: Vec::new(),
enum_values: Vec::new(),
input_fields: Vec::new(),
},
);
}
TypeDefinition::Union(union) => {
if schema.types.contains_key(&union.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
union.name
));
}
let possible_types = union.types.clone();
schema.types.insert(
union.name.clone(),
GraphQLType {
name: union.name.clone(),
kind: TypeKind::Union,
fields: Vec::new(),
description: union.description.clone(),
possible_types,
enum_values: Vec::new(),
input_fields: Vec::new(),
},
);
}
TypeDefinition::Enum(enum_type) => {
if schema.types.contains_key(&enum_type.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
enum_type.name
));
}
let enum_values = enum_type
.values
.iter()
.map(|v| GraphQLEnumValue {
name: v.name.clone(),
description: v.description.clone(),
is_deprecated: v.directives.iter().any(|d| d.name == "deprecated"),
deprecation_reason: extract_deprecation_reason(&v.directives),
})
.collect();
schema.types.insert(
enum_type.name.clone(),
GraphQLType {
name: enum_type.name.clone(),
kind: TypeKind::Enum,
fields: Vec::new(),
description: enum_type.description.clone(),
possible_types: Vec::new(),
enum_values,
input_fields: Vec::new(),
},
);
}
TypeDefinition::InputObject(input_obj) => {
if schema.types.contains_key(&input_obj.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
input_obj.name
));
}
let input_fields = input_obj
.fields
.iter()
.map(|f| GraphQLInputField {
name: f.name.clone(),
type_name: extract_bare_type_name(&f.value_type),
is_nullable: is_nullable_type(&f.value_type),
is_list: is_list_type(&f.value_type),
list_item_nullable: extract_list_item_nullability(&f.value_type),
default_value: f.default_value.as_ref().map(|v| format_default_value(v)),
description: f.description.clone(),
})
.collect();
schema.types.insert(
input_obj.name.clone(),
GraphQLType {
name: input_obj.name.clone(),
kind: TypeKind::InputObject,
fields: Vec::new(),
description: input_obj.description.clone(),
possible_types: Vec::new(),
enum_values: Vec::new(),
input_fields,
},
);
}
TypeDefinition::Scalar(scalar) => {
if schema.types.contains_key(&scalar.name) {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
scalar.name
));
}
schema.types.insert(
scalar.name.clone(),
GraphQLType {
name: scalar.name.clone(),
kind: TypeKind::Scalar,
fields: Vec::new(),
description: scalar.description.clone(),
possible_types: Vec::new(),
enum_values: Vec::new(),
input_fields: Vec::new(),
},
);
}
}
}
}
for definition in &doc.definitions {
if let graphql_parser::schema::Definition::TypeDefinition(TypeDefinition::Object(obj)) = definition {
for interface_name in &obj.implements_interfaces {
if let Some(interface) = schema.types.get_mut(interface_name)
&& interface.kind == TypeKind::Interface
&& !interface.possible_types.contains(&obj.name)
{
interface.possible_types.push(obj.name.clone());
}
}
}
}
if schema.types.is_empty() && schema.queries.is_empty() {
return Err(anyhow!("Empty GraphQL schema - no types or queries defined"));
}
if schema.queries.is_empty() {
return Err(anyhow!(
"Invalid GraphQL schema - Query type is required by the GraphQL specification.\n\
Add a Query type to your schema:\n\
type Query {{\n hello: String!\n}}"
));
}
Ok(schema)
}
pub fn parse_graphql_schema(path: &Path) -> Result<GraphQLSchema> {
let ext = path.extension().and_then(|s| s.to_str()).map(str::to_lowercase);
match ext.as_deref() {
Some("json") => {
let content = fs::read_to_string(path)?;
if let Ok(value) = serde_json::from_str::<Value>(&content)
&& (value.get("__schema").is_some() || value.get("data").is_some())
{
return parse_graphql_introspection_value(&value);
}
parse_graphql_sdl_string(&content)
}
Some("graphql" | "gql") => parse_graphql_sdl(path),
_ => {
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read file: {}", path.display()))?;
if content.trim().starts_with('{') {
let value: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse as JSON: {}", path.display()))?;
parse_graphql_introspection_value(&value)
} else {
parse_graphql_sdl_string(&content)
}
}
}
}
#[allow(dead_code)]
fn parse_graphql_introspection(path: &Path) -> Result<GraphQLSchema> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read GraphQL introspection file: {}", path.display()))?;
let value: Value =
serde_json::from_str(&content).with_context(|| format!("Failed to parse JSON from {}", path.display()))?;
parse_graphql_introspection_value(&value)
}
fn parse_graphql_introspection_value(_value: &Value) -> Result<GraphQLSchema> {
let envelope: IntrospectionEnvelope =
serde_json::from_value(_value.clone()).context("Failed to deserialize GraphQL introspection JSON")?;
let introspection = envelope
.schema
.or(envelope.data.map(|data| data.schema))
.ok_or_else(|| anyhow!("GraphQL introspection JSON must contain '__schema' or 'data.__schema'"))?;
let query_type_name = introspection.query_type.as_ref().map(|t| t.name.as_str());
let mutation_type_name = introspection.mutation_type.as_ref().map(|t| t.name.as_str());
let subscription_type_name = introspection.subscription_type.as_ref().map(|t| t.name.as_str());
let mut schema = GraphQLSchema {
types: HashMap::new(),
queries: Vec::new(),
mutations: Vec::new(),
subscriptions: Vec::new(),
directives: introspection
.directives
.into_iter()
.map(|directive| {
Ok(GraphQLDirective {
name: directive.name,
locations: directive.locations,
arguments: directive
.args
.into_iter()
.map(introspection_argument_to_graphql)
.collect::<Result<Vec<_>>>()?,
description: directive.description,
})
})
.collect::<Result<Vec<_>>>()?,
description: introspection.description,
};
for type_def in introspection.types {
let Some(name) = type_def.name.clone() else {
continue;
};
if name.starts_with("__") {
continue;
}
let kind = introspection_kind_to_type_kind(&type_def.kind)?;
let gql_type = GraphQLType {
name: name.clone(),
kind,
fields: type_def
.fields
.unwrap_or_default()
.into_iter()
.map(introspection_field_to_graphql)
.collect::<Result<Vec<_>>>()?,
description: type_def.description,
possible_types: type_def
.possible_types
.unwrap_or_default()
.into_iter()
.map(|possible| possible.name)
.collect(),
enum_values: type_def
.enum_values
.unwrap_or_default()
.into_iter()
.map(|value| GraphQLEnumValue {
name: value.name,
description: value.description,
is_deprecated: value.is_deprecated,
deprecation_reason: value.deprecation_reason,
})
.collect(),
input_fields: type_def
.input_fields
.unwrap_or_default()
.into_iter()
.map(introspection_input_field_to_graphql)
.collect::<Result<Vec<_>>>()?,
};
if query_type_name == Some(name.as_str()) {
schema.queries = gql_type.fields;
} else if mutation_type_name == Some(name.as_str()) {
schema.mutations = gql_type.fields;
} else if subscription_type_name == Some(name.as_str()) {
schema.subscriptions = gql_type.fields;
} else if schema.types.insert(name.clone(), gql_type).is_some() {
return Err(anyhow!(
"Duplicate type definition: '{}' is defined more than once in the schema",
name
));
}
}
if schema.types.is_empty() && schema.queries.is_empty() {
return Err(anyhow!("Empty GraphQL schema - no types or queries defined"));
}
if schema.queries.is_empty() {
return Err(anyhow!(
"Invalid GraphQL schema - Query type is required by the GraphQL specification.\n\
Add a Query type to your schema:\n\
type Query {{\n hello: String!\n}}"
));
}
Ok(schema)
}
fn introspection_kind_to_type_kind(kind: &str) -> Result<TypeKind> {
match kind {
"OBJECT" => Ok(TypeKind::Object),
"INTERFACE" => Ok(TypeKind::Interface),
"UNION" => Ok(TypeKind::Union),
"ENUM" => Ok(TypeKind::Enum),
"INPUT_OBJECT" => Ok(TypeKind::InputObject),
"SCALAR" => Ok(TypeKind::Scalar),
other => Err(anyhow!("Unsupported GraphQL introspection type kind: {other}")),
}
}
fn introspection_field_to_graphql(field: IntrospectionFieldDef) -> Result<GraphQLField> {
Ok(GraphQLField {
name: field.name,
type_name: introspection_bare_type_name(&field.field_type)?,
is_list: introspection_is_list_type(&field.field_type),
list_item_nullable: introspection_list_item_nullable(&field.field_type),
is_nullable: introspection_is_nullable_type(&field.field_type),
arguments: field
.args
.into_iter()
.map(introspection_argument_to_graphql)
.collect::<Result<Vec<_>>>()?,
description: field.description,
deprecation_reason: if field.is_deprecated {
field.deprecation_reason.or_else(|| Some("Deprecated".to_string()))
} else {
field.deprecation_reason
},
})
}
fn introspection_argument_to_graphql(arg: IntrospectionInputValueDef) -> Result<GraphQLArgument> {
Ok(GraphQLArgument {
name: arg.name,
type_name: introspection_bare_type_name(&arg.value_type)?,
is_nullable: introspection_is_nullable_type(&arg.value_type),
is_list: introspection_is_list_type(&arg.value_type),
list_item_nullable: introspection_list_item_nullable(&arg.value_type),
default_value: arg.default_value,
description: arg.description,
})
}
fn introspection_input_field_to_graphql(field: IntrospectionInputValueDef) -> Result<GraphQLInputField> {
Ok(GraphQLInputField {
name: field.name,
type_name: introspection_bare_type_name(&field.value_type)?,
is_nullable: introspection_is_nullable_type(&field.value_type),
is_list: introspection_is_list_type(&field.value_type),
list_item_nullable: introspection_list_item_nullable(&field.value_type),
default_value: field.default_value,
description: field.description,
})
}
fn introspection_bare_type_name(type_ref: &IntrospectionTypeRef) -> Result<String> {
if let Some(name) = &type_ref.name {
return Ok(name.clone());
}
if let Some(inner) = &type_ref.of_type {
return introspection_bare_type_name(inner);
}
Err(anyhow!(
"Invalid GraphQL introspection type reference: missing terminal named type"
))
}
fn introspection_is_nullable_type(type_ref: &IntrospectionTypeRef) -> bool {
type_ref.kind != "NON_NULL"
}
fn introspection_is_list_type(type_ref: &IntrospectionTypeRef) -> bool {
match type_ref.kind.as_str() {
"LIST" => true,
"NON_NULL" => type_ref.of_type.as_deref().is_some_and(introspection_is_list_type),
_ => false,
}
}
fn introspection_list_item_nullable(type_ref: &IntrospectionTypeRef) -> bool {
match type_ref.kind.as_str() {
"NON_NULL" => type_ref
.of_type
.as_deref()
.map_or(true, introspection_list_item_nullable),
"LIST" => type_ref.of_type.as_deref().map_or(true, introspection_is_nullable_type),
_ => true,
}
}
fn format_default_value(value: &graphql_parser::schema::Value<String>) -> String {
use graphql_parser::schema::Value;
match value {
Value::Int(i) => {
i.as_i64().map_or_else(|| format!("{i:?}"), |num| format!("{num}"))
}
Value::Float(f) => format!("{f}"),
Value::String(s) => {
format!("\"{}\"", s.replace('"', "\\\""))
}
Value::Boolean(b) => format!("{b}"),
Value::Null => "null".to_string(),
Value::Enum(e) => e.clone(),
Value::List(items) => {
let formatted: Vec<String> = items.iter().map(|v| format_default_value(v)).collect();
format!("[{}]", formatted.join(", "))
}
Value::Object(fields) => {
let formatted: Vec<String> = fields
.iter()
.map(|(k, v)| format!("{}: {}", k, format_default_value(v)))
.collect();
format!("{{{}}}", formatted.join(", "))
}
_ => format!("{value:?}"),
}
}
fn extract_deprecation_reason(directives: &[graphql_parser::schema::Directive<String>]) -> Option<String> {
directives.iter().find(|d| d.name == "deprecated").and_then(|d| {
d.arguments
.iter()
.find(|(arg_name, _)| arg_name == "reason")
.and_then(|(_, value)| match value {
graphql_parser::schema::Value::String(s) => Some(s.clone()),
_ => None,
})
.or_else(|| Some("Deprecated".to_string()))
})
}
fn extract_fields_from_object(obj: &ObjectType<String>) -> Vec<GraphQLField> {
obj.fields
.iter()
.map(|field| GraphQLField {
name: field.name.clone(),
type_name: extract_bare_type_name(&field.field_type),
is_list: is_list_type(&field.field_type),
list_item_nullable: extract_list_item_nullability(&field.field_type),
is_nullable: is_nullable_type(&field.field_type),
arguments: field
.arguments
.iter()
.map(|arg| GraphQLArgument {
name: arg.name.clone(),
type_name: extract_bare_type_name(&arg.value_type),
is_nullable: is_nullable_type(&arg.value_type),
is_list: is_list_type(&arg.value_type),
list_item_nullable: extract_list_item_nullability(&arg.value_type),
default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
description: arg.description.clone(),
})
.collect(),
description: field.description.clone(),
deprecation_reason: extract_deprecation_reason(&field.directives),
})
.collect()
}
fn extract_fields_from_interface(interface: &graphql_parser::schema::InterfaceType<String>) -> Vec<GraphQLField> {
interface
.fields
.iter()
.map(|field| GraphQLField {
name: field.name.clone(),
type_name: extract_bare_type_name(&field.field_type),
is_list: is_list_type(&field.field_type),
list_item_nullable: extract_list_item_nullability(&field.field_type),
is_nullable: is_nullable_type(&field.field_type),
arguments: field
.arguments
.iter()
.map(|arg| GraphQLArgument {
name: arg.name.clone(),
type_name: extract_bare_type_name(&arg.value_type),
is_nullable: is_nullable_type(&arg.value_type),
is_list: is_list_type(&arg.value_type),
list_item_nullable: extract_list_item_nullability(&arg.value_type),
default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
description: arg.description.clone(),
})
.collect(),
description: field.description.clone(),
deprecation_reason: extract_deprecation_reason(&field.directives),
})
.collect()
}
#[allow(dead_code)]
fn format_type(type_def: &graphql_parser::schema::Type<String>) -> String {
match type_def {
graphql_parser::schema::Type::NamedType(name) => name.clone(),
graphql_parser::schema::Type::ListType(inner) => format!("[{}]", format_type(inner)),
graphql_parser::schema::Type::NonNullType(inner) => format!("{}!", format_type(inner)),
}
}
fn extract_bare_type_name(type_def: &graphql_parser::schema::Type<String>) -> String {
match type_def {
graphql_parser::schema::Type::NamedType(name) => name.clone(),
graphql_parser::schema::Type::ListType(inner) => extract_bare_type_name(inner),
graphql_parser::schema::Type::NonNullType(inner) => extract_bare_type_name(inner),
}
}
const fn is_nullable_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
!matches!(type_def, graphql_parser::schema::Type::NonNullType(_))
}
fn is_list_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
match type_def {
graphql_parser::schema::Type::ListType(_) => true,
graphql_parser::schema::Type::NonNullType(inner) => is_list_type(inner),
_ => false,
}
}
fn extract_list_item_nullability(type_def: &graphql_parser::schema::Type<String>) -> bool {
match type_def {
graphql_parser::schema::Type::NonNullType(inner) => extract_list_item_nullability(inner),
graphql_parser::schema::Type::ListType(inner) => is_nullable_type(inner),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_parse_simple_sdl() {
let sdl = r#"
type Query {
hello: String!
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(!schema.queries.is_empty());
assert_eq!(schema.queries[0].name, "hello");
assert!(schema.types.contains_key("User"));
}
#[test]
fn test_parse_sdl_with_enum() {
let sdl = r#"
type Query {
users(status: UserStatus): [User!]!
}
enum UserStatus {
ACTIVE
INACTIVE
PENDING
}
type User {
id: ID!
status: UserStatus!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(schema.types.contains_key("UserStatus"));
let user_status = &schema.types["UserStatus"];
assert_eq!(user_status.kind, TypeKind::Enum);
assert_eq!(user_status.enum_values.len(), 3);
}
#[test]
fn test_parse_sdl_with_input_object() {
let sdl = r#"
type Query {
createUser(input: CreateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
type User {
id: ID!
name: String!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(schema.types.contains_key("CreateUserInput"));
let input = &schema.types["CreateUserInput"];
assert_eq!(input.kind, TypeKind::InputObject);
assert_eq!(input.input_fields.len(), 3);
}
#[test]
fn test_parse_sdl_with_interface() {
let sdl = r#"
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Query {
node(id: ID!): Node
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(schema.types.contains_key("Node"));
let node = &schema.types["Node"];
assert_eq!(node.kind, TypeKind::Interface);
}
#[test]
fn test_parse_sdl_with_union() {
let sdl = r#"
union SearchResult = User | Post
type Query {
search(query: String!): [SearchResult!]!
}
type User {
id: ID!
name: String!
}
type Post {
id: ID!
title: String!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(schema.types.contains_key("SearchResult"));
let union = &schema.types["SearchResult"];
assert_eq!(union.kind, TypeKind::Union);
assert_eq!(union.possible_types.len(), 2);
}
#[test]
fn test_parse_sdl_with_directives() {
let sdl = r#"
directive @auth(role: String!) on FIELD_DEFINITION
type Query {
adminUsers: [User!]! @auth(role: "admin")
}
type User {
id: ID!
name: String!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
assert!(!schema.directives.is_empty());
let auth_dir = schema
.directives
.iter()
.find(|d| d.name == "auth")
.expect("auth directive");
assert_eq!(auth_dir.arguments.len(), 1);
}
#[test]
fn test_nullable_and_list_detection() {
let sdl = r#"
type Query {
required: String!
nullable: String
list: [String!]!
nullableList: [String!]
listOfNullable: [String]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
let required = &schema.queries[0];
assert!(!required.is_nullable);
assert!(!required.is_list);
let nullable = schema.queries.iter().find(|f| f.name == "nullable").unwrap();
assert!(nullable.is_nullable);
assert!(!nullable.is_list);
let list = schema.queries.iter().find(|f| f.name == "list").unwrap();
assert!(!list.is_nullable);
assert!(list.is_list);
let nullable_list = schema.queries.iter().find(|f| f.name == "nullableList").unwrap();
assert!(nullable_list.is_nullable);
assert!(nullable_list.is_list);
}
#[test]
fn test_enum_deprecation_with_custom_reason() {
let sdl = r#"
enum Status {
ACTIVE
INACTIVE @deprecated(reason: "Use ARCHIVED instead")
PENDING @deprecated
}
type Query {
status: Status
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
let status_enum = &schema.types["Status"];
assert_eq!(status_enum.enum_values.len(), 3);
let active = &status_enum.enum_values[0];
assert_eq!(active.name, "ACTIVE");
assert!(!active.is_deprecated);
assert!(active.deprecation_reason.is_none());
let inactive = &status_enum.enum_values[1];
assert_eq!(inactive.name, "INACTIVE");
assert!(inactive.is_deprecated);
assert_eq!(inactive.deprecation_reason, Some("Use ARCHIVED instead".to_string()));
let pending = &status_enum.enum_values[2];
assert_eq!(pending.name, "PENDING");
assert!(pending.is_deprecated);
assert_eq!(pending.deprecation_reason, Some("Deprecated".to_string()));
}
#[test]
fn test_field_deprecation_with_custom_reason() {
let sdl = r#"
type User {
id: ID!
name: String!
email: String @deprecated(reason: "Use emailAddress instead")
oldField: String @deprecated
}
type Query {
user(id: ID!): User
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
let user_type = &schema.types["User"];
assert_eq!(user_type.fields.len(), 4);
let id_field = &user_type.fields[0];
assert_eq!(id_field.name, "id");
assert!(id_field.deprecation_reason.is_none());
let name_field = &user_type.fields[1];
assert_eq!(name_field.name, "name");
assert!(name_field.deprecation_reason.is_none());
let email_field = &user_type.fields[2];
assert_eq!(email_field.name, "email");
assert_eq!(
email_field.deprecation_reason,
Some("Use emailAddress instead".to_string())
);
let old_field = &user_type.fields[3];
assert_eq!(old_field.name, "oldField");
assert_eq!(old_field.deprecation_reason, Some("Deprecated".to_string()));
}
#[test]
fn test_interface_field_deprecation() {
let sdl = r#"
interface Node {
id: ID!
createdAt: String @deprecated(reason: "Use timestamp instead")
}
type Query {
node(id: ID!): Node
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
let node_interface = &schema.types["Node"];
assert_eq!(node_interface.fields.len(), 2);
let id_field = &node_interface.fields[0];
assert_eq!(id_field.name, "id");
assert!(id_field.deprecation_reason.is_none());
let created_at_field = &node_interface.fields[1];
assert_eq!(created_at_field.name, "createdAt");
assert_eq!(
created_at_field.deprecation_reason,
Some("Use timestamp instead".to_string())
);
}
#[test]
fn test_list_item_nullability_detection() {
let sdl = r#"
type Query {
listOfNullableStrings: [String]
listOfNonNullStrings: [String!]
nonNullListOfNullableStrings: [String]!
nonNullListOfNonNullStrings: [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
let list_nullable = schema
.queries
.iter()
.find(|f| f.name == "listOfNullableStrings")
.unwrap();
assert!(list_nullable.is_nullable);
assert!(list_nullable.is_list);
assert!(list_nullable.list_item_nullable);
let list_non_null = schema
.queries
.iter()
.find(|f| f.name == "listOfNonNullStrings")
.unwrap();
assert!(list_non_null.is_nullable);
assert!(list_non_null.is_list);
assert!(!list_non_null.list_item_nullable);
let non_null_list_nullable = schema
.queries
.iter()
.find(|f| f.name == "nonNullListOfNullableStrings")
.unwrap();
assert!(!non_null_list_nullable.is_nullable);
assert!(non_null_list_nullable.is_list);
assert!(non_null_list_nullable.list_item_nullable);
let non_null_list_non_null = schema
.queries
.iter()
.find(|f| f.name == "nonNullListOfNonNullStrings")
.unwrap();
assert!(!non_null_list_non_null.is_nullable);
assert!(non_null_list_non_null.is_list);
assert!(!non_null_list_non_null.list_item_nullable);
}
#[test]
fn test_empty_schema_rejected() {
let sdl = r#"
directive @example on FIELD_DEFINITION
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Empty GraphQL schema"));
assert!(error_msg.contains("no types or queries defined"));
}
#[test]
fn test_schema_without_query_rejected() {
let sdl = r#"
type Mutation {
createUser(name: String!): User!
}
type User {
id: ID!
name: String!
}
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Query type is required"));
assert!(error_msg.contains("GraphQL specification"));
}
#[test]
fn test_duplicate_type_definition_rejected() {
let sdl = r#"
type Query {
hello: String!
}
type User {
id: ID!
name: String!
}
type User {
id: ID!
email: String!
}
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("User"));
assert!(error_msg.contains("defined more than once"));
}
#[test]
fn test_duplicate_enum_definition_rejected() {
let sdl = r#"
enum Status {
ACTIVE
INACTIVE
}
type Query {
status: Status!
}
enum Status {
PENDING
ARCHIVED
}
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("Status"));
}
#[test]
fn test_duplicate_scalar_definition_rejected() {
let sdl = r#"
scalar DateTime
type Query {
now: DateTime!
}
scalar DateTime
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("DateTime"));
}
#[test]
fn test_duplicate_interface_definition_rejected() {
let sdl = r#"
interface Node {
id: ID!
}
type Query {
node(id: ID!): Node
}
interface Node {
id: ID!
createdAt: String!
}
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("Node"));
}
#[test]
fn test_duplicate_input_object_definition_rejected() {
let sdl = r#"
input UserInput {
name: String!
}
type Query {
createUser(input: UserInput!): String!
}
input UserInput {
email: String!
}
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("UserInput"));
}
#[test]
fn test_duplicate_union_definition_rejected() {
let sdl = r#"
union SearchResult = User | Post
type Query {
search(query: String!): SearchResult!
}
type User {
id: ID!
}
type Post {
id: ID!
}
union SearchResult = User | Post | Comment
"#;
let result = parse_graphql_sdl_string(sdl);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Duplicate type definition"));
assert!(error_msg.contains("SearchResult"));
}
#[test]
fn test_valid_schema_with_query_and_mutations() {
let sdl = r#"
type Query {
hello: String!
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}
type User {
id: ID!
name: String!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse valid SDL");
assert!(!schema.queries.is_empty());
assert!(!schema.mutations.is_empty());
assert!(schema.types.contains_key("User"));
}
#[test]
fn test_int_default_value() {
let sdl = r#"
type Query {
items(limit: Int = 10): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments.len(), 1);
assert_eq!(query.arguments[0].default_value, Some("10".to_string()));
}
#[test]
fn test_string_default_value() {
let sdl = r#"
type Query {
search(query: String = "default"): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments[0].default_value, Some("\"default\"".to_string()));
}
#[test]
fn test_boolean_default_value() {
let sdl = r#"
type Query {
items(active: Boolean = true): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments[0].default_value, Some("true".to_string()));
}
#[test]
fn test_list_default_value() {
let sdl = r#"
type Query {
filter(tags: [String!] = ["a", "b"]): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments[0].default_value, Some("[\"a\", \"b\"]".to_string()));
}
#[test]
fn test_enum_default_value() {
let sdl = r#"
enum Status {
ACTIVE
INACTIVE
}
type Query {
users(status: Status = ACTIVE): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments[0].default_value, Some("ACTIVE".to_string()));
}
#[test]
fn test_input_field_default_value() {
let sdl = r#"
input FilterInput {
limit: Int = 100
name: String = "test"
}
type Query {
search(filter: FilterInput!): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let input = &schema.types["FilterInput"];
assert_eq!(input.input_fields[0].default_value, Some("100".to_string()));
assert_eq!(input.input_fields[1].default_value, Some("\"test\"".to_string()));
}
#[test]
fn test_directive_argument_default_value() {
let sdl = r#"
directive @cache(ttl: Int = 3600) on FIELD_DEFINITION
type Query {
cached: String!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let cache_dir = &schema.directives[0];
assert_eq!(cache_dir.arguments[0].default_value, Some("3600".to_string()));
}
#[test]
fn test_multiple_default_values() {
let sdl = r#"
type Query {
users(limit: Int = 10, offset: Int = 0): [String!]!
}
"#;
let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
let query = &schema.queries[0];
assert_eq!(query.arguments.len(), 2);
assert_eq!(query.arguments[0].default_value, Some("10".to_string()));
assert_eq!(query.arguments[1].default_value, Some("0".to_string()));
}
#[test]
fn test_parse_graphql_introspection_value() {
let introspection = json!({
"__schema": {
"description": "Test schema",
"queryType": { "name": "Query" },
"mutationType": { "name": "Mutation" },
"subscriptionType": null,
"directives": [
{
"name": "deprecated",
"description": "Marks deprecated fields",
"locations": ["FIELD_DEFINITION", "ENUM_VALUE"],
"args": [
{
"name": "reason",
"description": "Reason text",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": "\"Deprecated\""
}
]
}
],
"types": [
{
"kind": "OBJECT",
"name": "Query",
"description": "Root query",
"fields": [
{
"name": "user",
"description": "Lookup a user",
"args": [
{
"name": "id",
"description": "User ID",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"defaultValue": null
}
],
"type": { "kind": "OBJECT", "name": "User", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Mutation",
"description": "Root mutation",
"fields": [
{
"name": "createUser",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "User", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "User",
"description": "A user",
"fields": [
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "emails",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateUserInput",
"description": "User input",
"fields": null,
"inputFields": [
{
"name": "email",
"description": null,
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": "\"test@example.com\""
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "Status",
"description": null,
"fields": null,
"inputFields": null,
"enumValues": [
{
"name": "ACTIVE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ID",
"description": null,
"fields": null,
"inputFields": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": null,
"fields": null,
"inputFields": null,
"enumValues": null,
"possibleTypes": null
}
]
}
});
let schema = parse_graphql_introspection_value(&introspection).expect("Failed to parse introspection");
assert_eq!(schema.description, Some("Test schema".to_string()));
assert_eq!(schema.queries.len(), 1);
assert_eq!(schema.queries[0].name, "user");
assert_eq!(schema.mutations.len(), 1);
assert!(schema.types.contains_key("User"));
assert!(schema.types.contains_key("CreateUserInput"));
assert!(schema.types.contains_key("Status"));
assert_eq!(schema.directives.len(), 1);
let user = &schema.types["User"];
assert_eq!(user.fields[1].name, "emails");
assert!(user.fields[1].is_list);
assert!(!user.fields[1].list_item_nullable);
assert!(!user.fields[1].is_nullable);
let input = &schema.types["CreateUserInput"];
assert_eq!(
input.input_fields[0].default_value,
Some("\"test@example.com\"".to_string())
);
}
#[test]
fn test_parse_graphql_schema_from_introspection_json_file() {
let dir = tempdir().expect("temp dir");
let path = dir.path().join("schema.json");
let introspection = json!({
"data": {
"__schema": {
"description": null,
"queryType": { "name": "Query" },
"mutationType": null,
"subscriptionType": null,
"directives": [],
"types": [
{
"kind": "OBJECT",
"name": "Query",
"description": null,
"fields": [
{
"name": "hello",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": null,
"fields": null,
"inputFields": null,
"enumValues": null,
"possibleTypes": null
}
]
}
}
});
fs::write(&path, introspection.to_string()).expect("write introspection file");
let schema = parse_graphql_schema(&path).expect("parse introspection file");
assert_eq!(schema.queries.len(), 1);
assert_eq!(schema.queries[0].name, "hello");
}
}