use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use super::sql_generator::FilterOperator;
use super::{
ErrorCode, ExecutionContext, Filter, GraphQLConfig, GraphQLMetrics, GraphQLSchema,
OperationType, QueryPlan, QueryValidator, Selection, SqlGenerator,
};
#[derive(Debug, Clone)]
pub struct GraphQLRequest {
pub query: String,
pub operation_name: Option<String>,
pub variables: Option<HashMap<String, serde_json::Value>>,
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
impl GraphQLRequest {
pub fn new(query: impl Into<String>) -> Self {
Self {
query: query.into(),
operation_name: None,
variables: None,
extensions: None,
}
}
pub fn with_operation(mut self, name: impl Into<String>) -> Self {
self.operation_name = Some(name.into());
self
}
pub fn with_variables(mut self, vars: HashMap<String, serde_json::Value>) -> Self {
self.variables = Some(vars);
self
}
pub fn var(mut self, name: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
let vars = self.variables.get_or_insert_with(HashMap::new);
vars.insert(name.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct GraphQLResponse {
pub data: Option<serde_json::Value>,
pub errors: Option<Vec<GraphQLError>>,
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
impl GraphQLResponse {
pub fn success(data: serde_json::Value) -> Self {
Self {
data: Some(data),
errors: None,
extensions: None,
}
}
pub fn error(error: GraphQLError) -> Self {
Self {
data: None,
errors: Some(vec![error]),
extensions: None,
}
}
pub fn errors(errors: Vec<GraphQLError>) -> Self {
Self {
data: None,
errors: Some(errors),
extensions: None,
}
}
pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
let extensions = self.extensions.get_or_insert_with(HashMap::new);
extensions.insert(key.into(), value);
self
}
pub fn has_errors(&self) -> bool {
self.errors.as_ref().map(|e| !e.is_empty()).unwrap_or(false)
}
pub fn to_json(&self) -> serde_json::Value {
let mut result = serde_json::Map::new();
if let Some(ref data) = self.data {
result.insert("data".to_string(), data.clone());
}
if let Some(ref errors) = self.errors {
let error_array: Vec<_> = errors.iter().map(|e| e.to_json()).collect();
result.insert("errors".to_string(), serde_json::Value::Array(error_array));
}
if let Some(ref extensions) = self.extensions {
result.insert(
"extensions".to_string(),
serde_json::Value::Object(
extensions
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
),
);
}
serde_json::Value::Object(result)
}
}
#[derive(Debug, Clone)]
pub struct GraphQLError {
pub message: String,
pub locations: Option<Vec<ErrorLocation>>,
pub path: Option<Vec<PathSegment>>,
pub extensions: Option<HashMap<String, serde_json::Value>>,
pub code: ErrorCode,
}
impl GraphQLError {
pub fn new(message: impl Into<String>, code: ErrorCode) -> Self {
Self {
message: message.into(),
locations: None,
path: None,
extensions: None,
code,
}
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::new(message, ErrorCode::ParseError)
}
pub fn validation_error(message: impl Into<String>) -> Self {
Self::new(message, ErrorCode::ValidationError)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(message, ErrorCode::Unauthorized)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(message, ErrorCode::NotFound)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(message, ErrorCode::InternalError)
}
pub fn with_location(mut self, line: u32, column: u32) -> Self {
self.locations = Some(vec![ErrorLocation { line, column }]);
self
}
pub fn with_path(mut self, path: Vec<PathSegment>) -> Self {
self.path = Some(path);
self
}
pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
let extensions = self.extensions.get_or_insert_with(HashMap::new);
extensions.insert(key.into(), value);
self
}
pub fn to_json(&self) -> serde_json::Value {
let mut result = serde_json::Map::new();
result.insert(
"message".to_string(),
serde_json::Value::String(self.message.clone()),
);
if let Some(ref locations) = self.locations {
let loc_array: Vec<_> = locations
.iter()
.map(|l| {
let mut loc = serde_json::Map::new();
loc.insert("line".to_string(), serde_json::Value::Number(l.line.into()));
loc.insert(
"column".to_string(),
serde_json::Value::Number(l.column.into()),
);
serde_json::Value::Object(loc)
})
.collect();
result.insert("locations".to_string(), serde_json::Value::Array(loc_array));
}
if let Some(ref path) = self.path {
let path_array: Vec<_> = path.iter().map(|s| s.to_json()).collect();
result.insert("path".to_string(), serde_json::Value::Array(path_array));
}
let mut extensions = self.extensions.clone().unwrap_or_default();
extensions.insert(
"code".to_string(),
serde_json::Value::String(format!("{:?}", self.code)),
);
result.insert(
"extensions".to_string(),
serde_json::Value::Object(extensions.into_iter().collect()),
);
serde_json::Value::Object(result)
}
}
impl std::fmt::Display for GraphQLError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for GraphQLError {}
#[derive(Debug, Clone, Copy)]
pub struct ErrorLocation {
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone)]
pub enum PathSegment {
Field(String),
Index(usize),
}
impl PathSegment {
pub fn to_json(&self) -> serde_json::Value {
match self {
PathSegment::Field(name) => serde_json::Value::String(name.clone()),
PathSegment::Index(idx) => serde_json::Value::Number((*idx).into()),
}
}
}
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub operation_type: OperationType,
pub operation_name: Option<String>,
pub selections: Vec<ParsedSelection>,
pub variable_definitions: Vec<VariableDefinition>,
pub fragments: HashMap<String, FragmentDefinition>,
}
#[derive(Debug, Clone)]
pub struct ParsedSelection {
pub name: String,
pub alias: Option<String>,
pub arguments: HashMap<String, serde_json::Value>,
pub selections: Vec<ParsedSelection>,
pub directives: Vec<Directive>,
}
impl ParsedSelection {
pub fn response_key(&self) -> &str {
self.alias.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Clone)]
pub struct VariableDefinition {
pub name: String,
pub var_type: String,
pub default_value: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct FragmentDefinition {
pub name: String,
pub type_condition: String,
pub selections: Vec<ParsedSelection>,
}
#[derive(Debug, Clone)]
pub struct Directive {
pub name: String,
pub arguments: HashMap<String, serde_json::Value>,
}
#[derive(Debug)]
pub struct GraphQLEngine {
config: Arc<GraphQLConfig>,
schema: Arc<GraphQLSchema>,
sql_generator: Arc<SqlGenerator>,
validator: QueryValidator,
metrics: Arc<GraphQLMetrics>,
backend: Option<crate::backend::BackendConfig>,
}
impl GraphQLEngine {
pub fn new(config: GraphQLConfig, schema: GraphQLSchema) -> Self {
let config = Arc::new(config);
let schema = Arc::new(schema);
Self {
sql_generator: Arc::new(SqlGenerator::new(schema.clone())),
validator: QueryValidator::new(config.clone()),
metrics: Arc::new(GraphQLMetrics::new()),
config,
schema,
backend: None,
}
}
pub fn with_backend(mut self, backend: crate::backend::BackendConfig) -> Self {
self.backend = Some(backend);
self
}
pub async fn execute(&self, request: GraphQLRequest) -> GraphQLResponse {
self.execute_with_context(request, ExecutionContext::default())
.await
}
pub async fn execute_with_context(
&self,
request: GraphQLRequest,
context: ExecutionContext,
) -> GraphQLResponse {
let start = Instant::now();
let document = match self.parse(&request.query) {
Ok(doc) => doc,
Err(e) => {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
};
if let Err(e) = self.validate(&document) {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
if let Err(e) = self.authorize(&document, &context) {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
let plan = match self.plan(&document, &request.variables) {
Ok(p) => p,
Err(e) => {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
};
let sql_queries = match self.sql_generator.generate(&plan) {
Ok(queries) => queries,
Err(e) => {
let error = GraphQLError::internal(format!("SQL generation failed: {}", e));
self.metrics.record_error(&error);
return GraphQLResponse::error(error);
}
};
let results = match self.execute_queries(&sql_queries, &context).await {
Ok(r) => r,
Err(e) => {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
};
let data = match self.shape_response(&document, &results) {
Ok(d) => d,
Err(e) => {
self.metrics.record_error(&e);
return GraphQLResponse::error(e);
}
};
let elapsed = start.elapsed();
self.metrics.record_query(elapsed, document.operation_type);
GraphQLResponse::success(data).with_extension(
"timing",
serde_json::json!({
"durationMs": elapsed.as_millis()
}),
)
}
#[allow(clippy::result_large_err)]
fn parse(&self, query: &str) -> Result<ParsedDocument, GraphQLError> {
let query = query.trim();
let (operation_type, remaining) = if query.starts_with("mutation") {
(
OperationType::Mutation,
query.strip_prefix("mutation").unwrap_or(query),
)
} else if query.starts_with("subscription") {
(
OperationType::Subscription,
query.strip_prefix("subscription").unwrap_or(query),
)
} else if query.starts_with("query") {
(
OperationType::Query,
query.strip_prefix("query").unwrap_or(query),
)
} else if query.starts_with("{") {
(OperationType::Query, query)
} else {
return Err(GraphQLError::parse_error("Invalid query format"));
};
let remaining = remaining.trim();
let (operation_name, remaining) = if remaining.starts_with('{') {
(None, remaining)
} else if let Some(brace_pos) = remaining.find('{') {
let name_part = remaining[..brace_pos].trim();
let name = name_part.split('(').next().unwrap_or(name_part).trim();
if name.is_empty() {
(None, &remaining[brace_pos..])
} else {
(Some(name.to_string()), &remaining[brace_pos..])
}
} else {
return Err(GraphQLError::parse_error("Missing selection set"));
};
let selections = self.parse_selection_set(remaining)?;
Ok(ParsedDocument {
operation_type,
operation_name,
selections,
variable_definitions: Vec::new(),
fragments: HashMap::new(),
})
}
#[allow(clippy::result_large_err)]
fn parse_selection_set(&self, input: &str) -> Result<Vec<ParsedSelection>, GraphQLError> {
let input = input.trim();
if !input.starts_with('{') {
return Err(GraphQLError::parse_error("Expected '{'"));
}
let mut depth = 0;
let mut end_pos = 0;
for (i, c) in input.chars().enumerate() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end_pos = i;
break;
}
}
_ => {}
}
}
if depth != 0 {
return Err(GraphQLError::parse_error("Unmatched braces"));
}
let inner = &input[1..end_pos].trim();
self.parse_fields(inner)
}
#[allow(clippy::result_large_err)]
fn parse_fields(&self, input: &str) -> Result<Vec<ParsedSelection>, GraphQLError> {
let mut selections = Vec::new();
let mut current_pos = 0;
let chars: Vec<char> = input.chars().collect();
while current_pos < chars.len() {
while current_pos < chars.len() && chars[current_pos].is_whitespace() {
current_pos += 1;
}
if current_pos >= chars.len() {
break;
}
let field_start = current_pos;
while current_pos < chars.len()
&& (chars[current_pos].is_alphanumeric() || chars[current_pos] == '_')
{
current_pos += 1;
}
if current_pos == field_start {
current_pos += 1;
continue;
}
let mut field_name: String = chars[field_start..current_pos].iter().collect();
let mut alias = None;
while current_pos < chars.len() && chars[current_pos].is_whitespace() {
current_pos += 1;
}
if current_pos < chars.len() && chars[current_pos] == ':' {
alias = Some(field_name);
current_pos += 1;
while current_pos < chars.len() && chars[current_pos].is_whitespace() {
current_pos += 1;
}
let name_start = current_pos;
while current_pos < chars.len()
&& (chars[current_pos].is_alphanumeric() || chars[current_pos] == '_')
{
current_pos += 1;
}
field_name = chars[name_start..current_pos].iter().collect();
}
while current_pos < chars.len() && chars[current_pos].is_whitespace() {
current_pos += 1;
}
let mut arguments = HashMap::new();
if current_pos < chars.len() && chars[current_pos] == '(' {
let args_start = current_pos;
let mut depth = 1;
current_pos += 1;
while current_pos < chars.len() && depth > 0 {
match chars[current_pos] {
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
current_pos += 1;
}
let args_str: String = chars[args_start + 1..current_pos - 1].iter().collect();
arguments = self.parse_arguments(&args_str)?;
}
while current_pos < chars.len() && chars[current_pos].is_whitespace() {
current_pos += 1;
}
let nested_selections = if current_pos < chars.len() && chars[current_pos] == '{' {
let nested_start = current_pos;
let mut depth = 1;
current_pos += 1;
while current_pos < chars.len() && depth > 0 {
match chars[current_pos] {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
current_pos += 1;
}
let nested_str: String = chars[nested_start..current_pos].iter().collect();
self.parse_selection_set(&nested_str)?
} else {
Vec::new()
};
selections.push(ParsedSelection {
name: field_name,
alias,
arguments,
selections: nested_selections,
directives: Vec::new(),
});
}
Ok(selections)
}
#[allow(clippy::result_large_err)]
fn parse_arguments(
&self,
input: &str,
) -> Result<HashMap<String, serde_json::Value>, GraphQLError> {
let mut arguments = HashMap::new();
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(colon_pos) = part.find(':') {
let key = part[..colon_pos].trim().to_string();
let value_str = part[colon_pos + 1..].trim();
let value = self.parse_value(value_str)?;
arguments.insert(key, value);
}
}
Ok(arguments)
}
#[allow(clippy::result_large_err)]
fn parse_value(&self, input: &str) -> Result<serde_json::Value, GraphQLError> {
let input = input.trim();
if input == "null" {
Ok(serde_json::Value::Null)
} else if input == "true" {
Ok(serde_json::Value::Bool(true))
} else if input == "false" {
Ok(serde_json::Value::Bool(false))
} else if input.starts_with('"') && input.ends_with('"') {
Ok(serde_json::Value::String(
input[1..input.len() - 1].to_string(),
))
} else if let Ok(n) = input.parse::<i64>() {
Ok(serde_json::Value::Number(n.into()))
} else if let Ok(n) = input.parse::<f64>() {
Ok(serde_json::json!(n))
} else {
Ok(serde_json::Value::String(input.to_string()))
}
}
#[allow(clippy::result_large_err)]
fn validate(&self, document: &ParsedDocument) -> Result<(), GraphQLError> {
self.validator.validate(document, &self.schema)
}
#[allow(clippy::result_large_err)]
fn authorize(
&self,
_document: &ParsedDocument,
_context: &ExecutionContext,
) -> Result<(), GraphQLError> {
Ok(())
}
#[allow(clippy::result_large_err)]
fn plan(
&self,
document: &ParsedDocument,
_variables: &Option<HashMap<String, serde_json::Value>>,
) -> Result<QueryPlan, GraphQLError> {
let selections: Vec<_> = document
.selections
.iter()
.map(|s| self.selection_to_plan(s))
.collect();
Ok(QueryPlan::Multiple { plans: selections })
}
fn selection_to_plan(&self, selection: &ParsedSelection) -> QueryPlan {
let filters = self.extract_filters(&selection.arguments);
let limit = selection
.arguments
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let offset = selection
.arguments
.get("offset")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let sel = Selection {
table_name: super::to_snake_case(&selection.name),
fields: selection
.selections
.iter()
.filter(|s| s.selections.is_empty())
.map(|s| s.name.clone())
.collect(),
relationships: selection
.selections
.iter()
.filter(|s| !s.selections.is_empty())
.map(|s| (s.name.clone(), self.selection_to_plan(s)))
.collect(),
};
QueryPlan::Single {
selection: sel,
filters,
limit,
offset,
}
}
fn extract_filters(&self, arguments: &HashMap<String, serde_json::Value>) -> Vec<Filter> {
let mut filters = Vec::new();
if let Some(id) = arguments.get("id") {
filters.push(Filter {
field: "id".to_string(),
operator: FilterOperator::Eq,
value: id.clone(),
});
}
if let Some(where_obj) = arguments.get("where") {
if let Some(obj) = where_obj.as_object() {
for (field, condition) in obj {
if let Some(cond_obj) = condition.as_object() {
for (op, value) in cond_obj {
let operator = match op.as_str() {
"eq" => FilterOperator::Eq,
"ne" => FilterOperator::Ne,
"gt" => FilterOperator::Gt,
"gte" => FilterOperator::Gte,
"lt" => FilterOperator::Lt,
"lte" => FilterOperator::Lte,
"contains" => FilterOperator::Contains,
"startsWith" => FilterOperator::StartsWith,
"endsWith" => FilterOperator::EndsWith,
"in" => FilterOperator::In,
_ => continue,
};
filters.push(Filter {
field: field.clone(),
operator,
value: value.clone(),
});
}
}
}
}
}
filters
}
async fn execute_queries(
&self,
queries: &[super::SqlQuery],
_context: &ExecutionContext,
) -> Result<Vec<Vec<serde_json::Value>>, GraphQLError> {
let Some(bcfg) = self.backend.clone() else {
return Ok(queries.iter().map(|_| Vec::new()).collect());
};
use crate::backend::BackendClient;
let mut out = Vec::with_capacity(queries.len());
for q in queries {
let mut client = BackendClient::connect(&bcfg)
.await
.map_err(|e| GraphQLError::internal(format!("backend connect: {}", e)))?;
let qr = client
.simple_query(&q.sql)
.await
.map_err(|e| GraphQLError::internal(format!("backend query: {}", e)))?;
let rows: Vec<serde_json::Value> = qr
.rows
.iter()
.map(|row| {
let mut obj = serde_json::Map::new();
for (i, c) in qr.columns.iter().enumerate() {
let v = row
.get(i)
.map(graphql_cell_to_json)
.unwrap_or(serde_json::Value::Null);
obj.insert(c.name.clone(), v);
}
serde_json::Value::Object(obj)
})
.collect();
out.push(rows);
}
Ok(out)
}
#[allow(clippy::result_large_err)]
fn shape_response(
&self,
document: &ParsedDocument,
results: &[Vec<serde_json::Value>],
) -> Result<serde_json::Value, GraphQLError> {
let mut data = serde_json::Map::new();
for (i, selection) in document.selections.iter().enumerate() {
let key = selection.response_key().to_string();
let rows = results.get(i).cloned().unwrap_or_default();
data.insert(key, serde_json::Value::Array(rows));
}
Ok(serde_json::Value::Object(data))
}
pub fn schema(&self) -> &GraphQLSchema {
&self.schema
}
pub fn config(&self) -> &GraphQLConfig {
&self.config
}
pub fn metrics(&self) -> &GraphQLMetrics {
&self.metrics
}
pub fn generate_sdl(&self) -> String {
self.schema.to_sdl()
}
}
fn graphql_cell_to_json(v: &crate::backend::TextValue) -> serde_json::Value {
match v {
crate::backend::TextValue::Null => serde_json::Value::Null,
crate::backend::TextValue::Text(s) => serde_json::Value::String(s.clone()),
}
}
impl Clone for GraphQLEngine {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
schema: self.schema.clone(),
sql_generator: self.sql_generator.clone(),
validator: QueryValidator::new(self.config.clone()),
metrics: self.metrics.clone(),
backend: self.backend.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graphql::introspector::GraphQLSchema;
fn create_test_engine() -> GraphQLEngine {
let config = GraphQLConfig::default();
let schema = GraphQLSchema::new();
GraphQLEngine::new(config, schema)
}
#[test]
fn test_parse_simple_query() {
let engine = create_test_engine();
let query = "query { users { id name } }";
let doc = engine.parse(query).unwrap();
assert_eq!(doc.operation_type, OperationType::Query);
assert_eq!(doc.selections.len(), 1);
assert_eq!(doc.selections[0].name, "users");
assert_eq!(doc.selections[0].selections.len(), 2);
}
#[tokio::test]
async fn offline_execute_shapes_field_key() {
use crate::graphql::introspector::{ColumnDefinition, SchemaIntrospector, TableDefinition};
let tabledef = TableDefinition {
name: "gqlitem".to_string(),
schema: "public".to_string(),
columns: vec![ColumnDefinition {
name: "id".to_string(),
data_type: "text".to_string(),
nullable: true,
is_primary_key: true,
has_default: false,
}],
foreign_keys: Vec::new(),
};
let schema = SchemaIntrospector::new().build_schema(&[tabledef]);
let engine = GraphQLEngine::new(GraphQLConfig::default(), schema);
let resp = engine
.execute(GraphQLRequest::new("{ gqlitems { id } }"))
.await;
assert!(
resp.errors.is_none(),
"unexpected errors: {:?}",
resp.errors
);
let data = resp.data.expect("data present");
assert!(
data.get("gqlitems").is_some(),
"missing field key: {}",
data
);
assert!(data["gqlitems"].is_array(), "field should be an array");
}
#[test]
fn test_parse_named_query() {
let engine = create_test_engine();
let query = "query GetUsers { users { id } }";
let doc = engine.parse(query).unwrap();
assert_eq!(doc.operation_name, Some("GetUsers".to_string()));
}
#[test]
fn test_parse_mutation() {
let engine = create_test_engine();
let query = "mutation { createUser(name: \"test\") { id } }";
let doc = engine.parse(query).unwrap();
assert_eq!(doc.operation_type, OperationType::Mutation);
}
#[test]
fn test_parse_with_arguments() {
let engine = create_test_engine();
let query = "{ user(id: \"123\") { name } }";
let doc = engine.parse(query).unwrap();
let user_selection = &doc.selections[0];
assert_eq!(user_selection.name, "user");
assert!(user_selection.arguments.contains_key("id"));
}
#[test]
fn test_parse_with_alias() {
let engine = create_test_engine();
let query = "{ myUser: user(id: \"123\") { name } }";
let doc = engine.parse(query).unwrap();
let selection = &doc.selections[0];
assert_eq!(selection.alias, Some("myUser".to_string()));
assert_eq!(selection.name, "user");
assert_eq!(selection.response_key(), "myUser");
}
#[test]
fn test_graphql_request_builder() {
let request = GraphQLRequest::new("{ users { id } }")
.with_operation("GetUsers")
.var("limit", 10);
assert_eq!(request.query, "{ users { id } }");
assert_eq!(request.operation_name, Some("GetUsers".to_string()));
assert!(request.variables.unwrap().contains_key("limit"));
}
#[test]
fn test_graphql_response_success() {
let response = GraphQLResponse::success(serde_json::json!({"users": []}));
assert!(response.data.is_some());
assert!(!response.has_errors());
}
#[test]
fn test_graphql_response_error() {
let error = GraphQLError::parse_error("Syntax error");
let response = GraphQLResponse::error(error);
assert!(response.data.is_none());
assert!(response.has_errors());
}
#[test]
fn test_graphql_error_to_json() {
let error = GraphQLError::validation_error("Field not found")
.with_location(1, 10)
.with_path(vec![PathSegment::Field("users".to_string())]);
let json = error.to_json();
assert_eq!(json["message"], "Field not found");
assert!(json["locations"].is_array());
assert!(json["path"].is_array());
}
#[tokio::test]
async fn test_execute_simple_query() {
let engine = create_test_engine();
let request = GraphQLRequest::new("{ users { id name } }");
let response = engine.execute(request).await;
assert!(!response.has_errors());
assert!(response.data.is_some());
}
#[test]
fn test_parse_nested_selections() {
let engine = create_test_engine();
let query = "{ users { id posts { title comments { content } } } }";
let doc = engine.parse(query).unwrap();
let users = &doc.selections[0];
assert_eq!(users.selections.len(), 2);
let posts = &users.selections[1];
assert_eq!(posts.name, "posts");
assert_eq!(posts.selections.len(), 2); }
#[test]
fn test_parse_value() {
let engine = create_test_engine();
assert_eq!(engine.parse_value("null").unwrap(), serde_json::Value::Null);
assert_eq!(
engine.parse_value("true").unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
engine.parse_value("false").unwrap(),
serde_json::Value::Bool(false)
);
assert_eq!(
engine.parse_value("\"hello\"").unwrap(),
serde_json::Value::String("hello".to_string())
);
assert_eq!(engine.parse_value("42").unwrap(), serde_json::json!(42));
}
}