use crate::graphql::{
document::{Document, Operation},
types::ParsedQuery,
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum GraphQLParseError {
#[error("Failed to parse GraphQL query: {0}")]
Syntax(String),
#[error("No query or mutation operation found")]
MissingOperation,
#[error("Unknown operation named '{0}'")]
UnknownOperation(String),
#[error(
"Document defines {0} operations but no `operationName` was provided — the request must \
name the operation to execute"
)]
AmbiguousOperation(usize),
#[error("No fields in selection set")]
EmptySelection,
}
impl GraphQLParseError {
#[must_use]
pub const fn is_operation_selection(&self) -> bool {
matches!(self, Self::UnknownOperation(_) | Self::AmbiguousOperation(_))
}
}
pub fn parse_query(source: &str) -> Result<ParsedQuery, GraphQLParseError> {
parse_query_with_operation_name(source, None)
}
pub fn parse_query_with_operation_name(
source: &str,
operation_name: Option<&str>,
) -> Result<ParsedQuery, GraphQLParseError> {
let doc = crate::graphql::complexity::parse_graphql_document(source)
.map_err(|e| GraphQLParseError::Syntax(e.to_string()))?;
let operation = select_operation(&doc, operation_name)?;
parse_selected_operation(operation, &doc, source)
}
fn select_operation<'d>(
doc: &'d Document,
operation_name: Option<&str>,
) -> Result<&'d Operation, GraphQLParseError> {
let mut operations = doc.operations.iter();
let Some(requested) = operation_name else {
let first = operations.next().ok_or(GraphQLParseError::MissingOperation)?;
let extra = operations.count();
if extra > 0 {
return Err(GraphQLParseError::AmbiguousOperation(extra + 1));
}
return Ok(first);
};
if doc.operations.is_empty() {
return Err(GraphQLParseError::MissingOperation);
}
operations
.find(|op| op.name.as_deref() == Some(requested))
.ok_or_else(|| GraphQLParseError::UnknownOperation(requested.to_string()))
}
pub fn parse_selected_operation(
operation: &Operation,
doc: &Document,
source: &str,
) -> Result<ParsedQuery, GraphQLParseError> {
let root_field = operation
.selections
.first()
.map(|s| s.name.clone())
.ok_or(GraphQLParseError::EmptySelection)?;
Ok(ParsedQuery {
operation_type: operation.kind.as_str().to_string(),
operation_name: operation.name.clone(),
root_field,
selections: operation.selections.clone(),
variables: operation.variables.clone(),
fragments: doc.fragments.clone(),
source: std::sync::Arc::from(source),
})
}
#[must_use]
pub fn operation_selection_error(e: &GraphQLParseError) -> crate::error::FraiseQLError {
if e.is_operation_selection() {
crate::error::FraiseQLError::Validation {
message: e.to_string(),
path: Some("operationName".to_string()),
}
} else {
crate::error::FraiseQLError::Parse {
message: e.to_string(),
location: "query".to_string(),
}
}
}