use apollo_parser::cst::{self, CstNode as _};
use crate::graphql::{
types::{Directive, FieldSelection, FragmentDefinition, GraphQLArgument, GraphQLType},
value_json::{self, LiteralError},
};
const RECURSION_LIMIT: usize = 500;
#[derive(Debug, Clone)]
pub struct Document {
pub operations: Vec<Operation>,
pub fragments: Vec<FragmentDefinition>,
pub(super) scored: std::sync::OnceLock<super::complexity::Scored>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationKind {
Query,
Mutation,
Subscription,
}
impl OperationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Query => "query",
Self::Mutation => "mutation",
Self::Subscription => "subscription",
}
}
}
#[derive(Debug, Clone)]
pub struct Operation {
pub kind: OperationKind,
pub name: Option<String>,
pub variables: Vec<crate::graphql::types::VariableDefinition>,
pub directives: Vec<Directive>,
pub selections: Vec<FieldSelection>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentError(String);
impl std::fmt::Display for DocumentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub fn parse(source: &str) -> Result<Document, DocumentError> {
let tree = apollo_parser::Parser::new(source).recursion_limit(RECURSION_LIMIT).parse();
let at = Locator::new(source);
if let Some(error) = tree.errors().next() {
return Err(at.error(error.index(), &readable(error.message())));
}
let mut document = Document {
operations: Vec::new(),
fragments: Vec::new(),
scored: std::sync::OnceLock::new(),
};
let reader = Reader { at: &at };
for definition in tree.document().definitions() {
match definition {
cst::Definition::OperationDefinition(op) => {
document.operations.push(reader.operation(&op)?);
},
cst::Definition::FragmentDefinition(fragment) => {
document.fragments.push(reader.fragment(&fragment)?);
},
other => {
let text = other.syntax().text().to_string();
let head = text.split(['{', '(', '\n']).next().unwrap_or("").trim().to_string();
return Err(at.error(
offset(other.syntax()),
&format!(
"`{head}` is a type-system definition; a request document holds \
operations and fragments only"
),
));
},
}
}
Ok(document)
}
fn readable(message: &str) -> String {
const TOKENS: [(&str, &str); 14] = [
("BANG", "!"),
("L_PAREN", "("),
("R_PAREN", ")"),
("L_CURLY", "{"),
("R_CURLY", "}"),
("L_BRACK", "["),
("R_BRACK", "]"),
("AT", "@"),
("DOLLAR", "$"),
("AMP", "&"),
("PIPE", "|"),
("SPREAD", "..."),
("EQ", "="),
("COLON", ":"),
];
let Some((expected, got)) =
message.strip_prefix("expected ").and_then(|rest| rest.split_once(", got "))
else {
return message.to_string();
};
let expected = TOKENS.iter().find(|(kind, _)| *kind == expected).map_or_else(
|| expected.to_lowercase().replace('_', " "),
|(_, token)| format!("`{token}`"),
);
let got = if got == "EOF" {
"the end of the document".to_string()
} else {
format!("`{got}`")
};
format!("expected {expected}, found {got}")
}
struct Locator<'s> {
source: &'s str,
}
impl<'s> Locator<'s> {
const fn new(source: &'s str) -> Self {
Self { source }
}
fn position(&self, offset: usize) -> (usize, usize) {
let before = self.source.get(..offset).unwrap_or(self.source);
let line = before.matches('\n').count() + 1;
let column = before.rsplit('\n').next().map_or(0, |l| l.chars().count()) + 1;
(line, column)
}
fn error(&self, offset: usize, message: &str) -> DocumentError {
let (line, column) = self.position(offset);
DocumentError(format!("{message} at {line}:{column}"))
}
}
fn offset(node: &apollo_parser::SyntaxNode) -> usize {
usize::from(node.text_range().start())
}
struct Reader<'a, 's> {
at: &'a Locator<'s>,
}
impl Reader<'_, '_> {
fn incomplete(&self, node: &apollo_parser::SyntaxNode, what: &str) -> DocumentError {
self.at.error(offset(node), &format!("incomplete {what}"))
}
fn name(
&self,
name: Option<cst::Name>,
parent: &apollo_parser::SyntaxNode,
what: &str,
) -> Result<String, DocumentError> {
name.map(|n| n.text().to_string()).ok_or_else(|| self.incomplete(parent, what))
}
fn operation(&self, op: &cst::OperationDefinition) -> Result<Operation, DocumentError> {
let kind = match op.operation_type() {
Some(t) if t.mutation_token().is_some() => OperationKind::Mutation,
Some(t) if t.subscription_token().is_some() => OperationKind::Subscription,
_ => OperationKind::Query,
};
let variables = op
.variable_definitions()
.into_iter()
.flat_map(|defs| defs.variable_definitions())
.map(|def| self.variable(&def))
.collect::<Result<_, _>>()?;
Ok(Operation {
kind,
name: op.name().map(|n| n.text().to_string()),
variables,
directives: self.directives(op.directives())?,
selections: self.selection_set(op.selection_set())?,
})
}
fn fragment(&self, f: &cst::FragmentDefinition) -> Result<FragmentDefinition, DocumentError> {
let name = self.name(f.fragment_name().and_then(|n| n.name()), f.syntax(), "fragment")?;
let type_condition = self.name(
f.type_condition().and_then(|t| t.named_type()).and_then(|t| t.name()),
f.syntax(),
"fragment type condition",
)?;
let selections = self.selection_set(f.selection_set())?;
let fragment_spreads = spreads(&selections);
Ok(FragmentDefinition {
name,
type_condition,
selections,
fragment_spreads,
})
}
fn variable(
&self,
def: &cst::VariableDefinition,
) -> Result<crate::graphql::types::VariableDefinition, DocumentError> {
let name =
self.name(def.variable().and_then(|v| v.name()), def.syntax(), "variable definition")?;
let ty = def.ty().ok_or_else(|| self.incomplete(def.syntax(), "variable type"))?;
let default_value = match def.default_value() {
None => None,
Some(default) => {
let value = default
.value()
.ok_or_else(|| self.incomplete(default.syntax(), "default value"))?;
Some(self.literal(&value)?)
},
};
Ok(crate::graphql::types::VariableDefinition {
name,
var_type: self.graphql_type(&ty)?,
default_value,
})
}
fn graphql_type(&self, ty: &cst::Type) -> Result<GraphQLType, DocumentError> {
match ty {
cst::Type::NamedType(named) => Ok(GraphQLType {
name: self.name(named.name(), named.syntax(), "type")?,
nullable: true,
list: false,
list_nullable: false,
}),
cst::Type::ListType(list) => {
let inner = list.ty().ok_or_else(|| self.incomplete(list.syntax(), "list type"))?;
Ok(GraphQLType {
name: format!("[{}]", self.graphql_type(&inner)?.name),
nullable: true,
list: true,
list_nullable: true,
})
},
cst::Type::NonNullType(non_null) => {
let mut parsed = match (non_null.named_type(), non_null.list_type()) {
(Some(named), _) => self.graphql_type(&cst::Type::NamedType(named))?,
(None, Some(list)) => self.graphql_type(&cst::Type::ListType(list))?,
(None, None) => return Err(self.incomplete(non_null.syntax(), "type")),
};
parsed.nullable = false;
if parsed.list {
parsed.list_nullable = false;
}
Ok(parsed)
},
}
}
fn selection_set(
&self,
set: Option<cst::SelectionSet>,
) -> Result<Vec<FieldSelection>, DocumentError> {
set.into_iter()
.flat_map(|set| set.selections())
.map(|selection| self.selection(&selection))
.collect()
}
fn selection(&self, selection: &cst::Selection) -> Result<FieldSelection, DocumentError> {
Ok(match selection {
cst::Selection::Field(field) => FieldSelection {
name: self.name(field.name(), field.syntax(), "field")?,
alias: field.alias().and_then(|a| a.name()).map(|n| n.text().to_string()),
arguments: self.arguments(field.arguments())?,
nested_fields: self.selection_set(field.selection_set())?,
directives: self.directives(field.directives())?,
},
cst::Selection::FragmentSpread(spread) => FieldSelection {
name: format!(
"...{}",
self.name(
spread.fragment_name().and_then(|n| n.name()),
spread.syntax(),
"fragment spread"
)?
),
alias: None,
arguments: Vec::new(),
nested_fields: Vec::new(),
directives: self.directives(spread.directives())?,
},
cst::Selection::InlineFragment(inline) => FieldSelection {
name: format!(
"...on {}",
inline
.type_condition()
.and_then(|t| t.named_type())
.and_then(|t| t.name())
.map(|n| n.text().to_string())
.unwrap_or_default()
),
alias: None,
arguments: Vec::new(),
nested_fields: self.selection_set(inline.selection_set())?,
directives: self.directives(inline.directives())?,
},
})
}
fn arguments(
&self,
arguments: Option<cst::Arguments>,
) -> Result<Vec<GraphQLArgument>, DocumentError> {
arguments
.into_iter()
.flat_map(|args| args.arguments())
.map(|argument| {
let name = self.name(argument.name(), argument.syntax(), "argument")?;
let value = argument
.value()
.ok_or_else(|| self.incomplete(argument.syntax(), "argument value"))?;
Ok(GraphQLArgument {
name,
value_type: value_type(&value).to_string(),
value_json: self.literal(&value)?,
})
})
.collect()
}
fn directives(
&self,
directives: Option<cst::Directives>,
) -> Result<Vec<Directive>, DocumentError> {
directives
.into_iter()
.flat_map(|ds| ds.directives())
.map(|directive| {
Ok(Directive {
name: self.name(directive.name(), directive.syntax(), "directive")?,
arguments: self.arguments(directive.arguments())?,
})
})
.collect()
}
fn literal(&self, value: &cst::Value) -> Result<String, DocumentError> {
value_json::encode_literal(value)
.map_err(|LiteralError { at, message }| self.at.error(at, &message))
}
}
const fn value_type(value: &cst::Value) -> &'static str {
match value {
cst::Value::StringValue(_) => "string",
cst::Value::IntValue(_) => "int",
cst::Value::FloatValue(_) => "float",
cst::Value::BooleanValue(_) => "boolean",
cst::Value::NullValue(_) => "null",
cst::Value::EnumValue(_) => "enum",
cst::Value::ListValue(_) => "list",
cst::Value::ObjectValue(_) => "object",
cst::Value::Variable(_) => "variable",
}
}
fn spreads(selections: &[FieldSelection]) -> Vec<String> {
let mut names = Vec::new();
for selection in selections {
if let Some(name) = selection.name.strip_prefix("...") {
if !name.starts_with("on ") {
names.push(name.to_string());
}
}
names.extend(spreads(&selection.nested_fields));
}
names
}