use crate::parser::grammar::name;
use crate::parser::grammar::variable;
use crate::Parser;
use crate::SyntaxKind;
use crate::TokenKind;
use crate::S;
use crate::T;
use std::ops::ControlFlow;
#[derive(Clone, Copy)]
pub(crate) enum Constness {
Const,
NotConst,
}
pub(crate) fn value(p: &mut Parser, constness: Constness, pop_on_error: bool) {
match p.peek() {
Some(T![$]) => {
if let Constness::Const = constness {
let error_message = "unexpected variable value in a Const context";
if pop_on_error {
p.err_and_pop(error_message);
} else {
p.err(error_message);
}
}
variable::variable(p);
}
Some(TokenKind::Int) => {
let _g = p.start_node(SyntaxKind::INT_VALUE);
p.bump(SyntaxKind::INT);
}
Some(TokenKind::Float) => {
let _g = p.start_node(SyntaxKind::FLOAT_VALUE);
p.bump(SyntaxKind::FLOAT);
}
Some(TokenKind::StringValue) => {
let _g = p.start_node(SyntaxKind::STRING_VALUE);
p.bump(SyntaxKind::STRING);
}
Some(TokenKind::Name) => {
let node = p.peek_data().unwrap();
match node {
"true" => {
let _g = p.start_node(SyntaxKind::BOOLEAN_VALUE);
p.bump(SyntaxKind::true_KW);
}
"false" => {
let _g = p.start_node(SyntaxKind::BOOLEAN_VALUE);
p.bump(SyntaxKind::false_KW);
}
"null" => {
let _g = p.start_node(SyntaxKind::NULL_VALUE);
p.bump(SyntaxKind::null_KW)
}
_ => enum_value(p),
}
}
Some(T!['[']) => list_value(p, constness),
Some(T!['{']) => object_value(p, constness),
_ => {
let error_message = "expected a valid Value";
if pop_on_error {
p.err_and_pop(error_message);
} else {
p.err(error_message);
}
}
}
}
pub(crate) fn enum_value(p: &mut Parser) {
let _g = p.start_node(SyntaxKind::ENUM_VALUE);
match p.peek_token() {
Some(token) if token.kind == TokenKind::Name => {
let name = token.data;
if matches!(name, "true" | "false" | "null") {
p.err("invalid Enum Value");
}
name::name(p);
}
_ => p.err("expected Enum Value"),
}
}
pub(crate) fn list_value(p: &mut Parser, constness: Constness) {
let _g = p.start_node(SyntaxKind::LIST_VALUE);
p.bump(S!['[']);
p.peek_while(|p, node| {
if node == T![']'] {
p.bump(S![']']);
ControlFlow::Break(())
} else if node == TokenKind::Eof {
ControlFlow::Break(())
} else if p.recursion_limit.check_and_increment() {
p.limit_err("parser recursion limit reached");
ControlFlow::Break(())
} else {
value(p, constness, true);
p.recursion_limit.decrement();
ControlFlow::Continue(())
}
});
}
pub(crate) fn object_value(p: &mut Parser, constness: Constness) {
let _g = p.start_node(SyntaxKind::OBJECT_VALUE);
p.bump(S!['{']);
p.peek_while_kind(TokenKind::Name, |p| {
object_field(p, constness);
});
p.expect(T!['}'], S!['}']);
}
pub(crate) fn object_field(p: &mut Parser, constness: Constness) {
let _guard = p.start_node(SyntaxKind::OBJECT_FIELD);
name::name(p);
if let Some(T![:]) = p.peek() {
p.bump(S![:]);
if p.recursion_limit.check_and_increment() {
p.limit_err("parser recursion limit reached");
return;
}
value(p, constness, true);
p.recursion_limit.decrement()
}
}
pub(crate) fn default_value(p: &mut Parser) {
let _g = p.start_node(SyntaxKind::DEFAULT_VALUE);
p.bump(S![=]);
value(p, Constness::Const, false);
}
#[cfg(test)]
mod test {
use crate::cst;
use crate::cst::CstNode;
use crate::Parser;
#[test]
fn it_returns_string_for_string_value_into() {
let schema = r#"
enum Test @dir__one(string: "string value", int_value: -10, float_value: -1.123e+4, bool: false) {
INVENTORY
} "#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(cst.errors.is_empty());
let document = cst.document();
for definition in document.definitions() {
if let cst::Definition::EnumTypeDefinition(enum_) = definition {
for directive in enum_.directives().unwrap().directives() {
for argument in directive.arguments().unwrap().arguments() {
if let cst::Value::StringValue(val) =
argument.value().expect("Cannot get argument value.")
{
let source = val.source_string();
assert_eq!(source, r#""string value""#);
let contents: String = val.into();
assert_eq!(contents, "string value");
}
}
}
}
}
}
#[test]
fn it_unescapes_strings() {
let schema = r#"
"String with\tescapes\r\n"
scalar StringWithEscapes
"String with unicode \uadf8\ub77c\ud504\ud050\uc5d8"
scalar StringWithUnicode
"""
Escapes\nshould\nnot\nmatter
including \q nonexistent \W ones
\""" is the only one \""
"""
scalar BlockStringRaw
"#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(cst.errors.is_empty());
let mut expected = vec![
"String with\tescapes\r\n",
"String with unicode 그라프큐엘",
r#"
Escapes\nshould\nnot\nmatter
including \q nonexistent \W ones
""" is the only one \""
"#
.trim(),
]
.into_iter();
let document = cst.document();
for definition in document.definitions() {
let cst::Definition::ScalarTypeDefinition(scalar) = definition else {
continue;
};
let description = scalar.description().unwrap().string_value().unwrap();
let s = String::from(description);
assert_eq!(s, expected.next().unwrap());
}
}
#[test]
fn it_returns_i64_for_int_values() {
let schema = r#"
enum Test @dir__one(int_value: -10) {
INVENTORY
} "#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(cst.errors.is_empty());
let document = cst.document();
for definition in document.definitions() {
if let cst::Definition::EnumTypeDefinition(enum_) = definition {
for directive in enum_.directives().unwrap().directives() {
for argument in directive.arguments().unwrap().arguments() {
if let cst::Value::IntValue(val) =
argument.value().expect("Cannot get argument value.")
{
let i: i32 = val.try_into().unwrap();
assert_eq!(i, -10);
}
}
}
}
}
}
#[test]
#[allow(clippy::float_cmp)]
fn it_returns_f64_for_float_values() {
let schema = r#"
enum Test @dir__one(float_value: -1.123E4) {
INVENTORY
} "#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(cst.errors.is_empty());
let document = cst.document();
for definition in document.definitions() {
if let cst::Definition::EnumTypeDefinition(enum_) = definition {
for directive in enum_.directives().unwrap().directives() {
for argument in directive.arguments().unwrap().arguments() {
if let cst::Value::FloatValue(val) =
argument.value().expect("Cannot get argument value.")
{
let f: f64 = val.try_into().unwrap();
assert_eq!(f, -1.123E4);
}
}
}
}
}
}
#[test]
fn it_returns_bool_for_boolean_values() {
let schema = r#"
enum Test @dir__one(bool_value: false) {
INVENTORY
} "#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(cst.errors.is_empty());
let document = cst.document();
for definition in document.definitions() {
if let cst::Definition::EnumTypeDefinition(enum_) = definition {
for directive in enum_.directives().unwrap().directives() {
for argument in directive.arguments().unwrap().arguments() {
if let cst::Value::BooleanValue(val) =
argument.value().expect("Cannot get argument value.")
{
let b: bool = val.try_into().unwrap();
assert!(!b);
}
}
}
}
}
}
#[test]
fn it_parses_variable_names() {
let input = "
query GraphQuery($graph_id: ID!, $variant: String) {
service(id: $graph_id) {
schema(tag: $variant) {
document
}
}
}
";
let parser = Parser::new(input);
let cst = parser.parse();
assert_eq!(0, cst.errors().len());
let doc = cst.document();
for def in doc.definitions() {
if let cst::Definition::OperationDefinition(op_def) = def {
assert_eq!(op_def.name().unwrap().text(), "GraphQuery");
let variable_defs = op_def.variable_definitions();
let variables: Vec<String> = variable_defs
.iter()
.flat_map(|v| v.variable_definitions())
.filter_map(|v| Some(v.variable()?.text().to_string()))
.collect();
assert_eq!(
variables.as_slice(),
["graph_id".to_string(), "variant".to_string()]
);
}
}
}
#[test]
fn it_parse_mutation_with_escaped_char() {
let input = r#"mutation {
createStore(draft: {
name: [{ locale: "en", value: "\"my store\"" }]
}) {
name(locale: "en")
}
}"#;
let parser = Parser::new(input);
let cst = parser.parse();
assert!(cst.errors.is_empty());
}
#[test]
fn it_parse_mutation_without_escaped_char() {
let input = r#"mutation {
createStore(draft: {
name: [{ locale: "en", value: "my store" }]
}) {
name(locale: "en")
}
}"#;
let parser = Parser::new(input);
let cst = parser.parse();
assert!(cst.errors.is_empty());
}
#[test]
fn it_parse_mutation_without_escaped_char_with_error() {
let input = r#"mutation {
createStore(draft: {
name: [{ locale: "en", value: "\"my store" }]
}) {
name(locale: "en")
}
}"#;
let parser = Parser::new(input);
let cst = parser.parse();
assert!(cst.errors.is_empty());
}
#[test]
fn it_parse_mutation_with_escaped_chars_and_without() {
let input = r#"mutation {
createStore(draft: {
name: [{ locale: "en", value: "my \a store" }]
}) {
name(locale: "en")
}
}"#;
let parser = Parser::new(input);
let cst = parser.parse();
assert!(!cst.errors.is_empty());
}
#[test]
fn it_returns_error_for_unfinished_string_value_in_list() {
let schema = r#"extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external])
type Vehicle @key(fields: "id") {
id: ID!,
type: String,
modelCode: String,
brandName: String,
launchDate: String
}
"#;
let parser = Parser::new(schema);
let cst = parser.parse();
assert!(!cst.errors.is_empty());
}
}