use crate::GraphQLErrorNote;
use crate::GraphQLStringParsingError;
use crate::smallvec::SmallVec;
use std::borrow::Cow;
use std::num::ParseFloatError;
use std::num::ParseIntError;
#[derive(Clone, Debug, PartialEq)]
pub enum GraphQLTokenKind<'src> {
Ampersand,
At,
Bang,
Colon,
CurlyBraceClose,
CurlyBraceOpen,
Dollar,
Ellipsis,
Equals,
ParenClose,
ParenOpen,
Pipe,
SquareBracketClose,
SquareBracketOpen,
Name(Cow<'src, str>),
IntValue(Cow<'src, str>),
FloatValue(Cow<'src, str>),
StringValue(Cow<'src, str>),
True,
False,
Null,
Eof,
Error(Box<GraphQLTokenError>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct GraphQLTokenError {
pub message: String,
pub error_notes: SmallVec<[GraphQLErrorNote; 2]>,
}
impl<'src> GraphQLTokenKind<'src> {
#[inline]
pub fn name_borrowed(s: &'src str) -> Self {
GraphQLTokenKind::Name(Cow::Borrowed(s))
}
#[inline]
pub fn name_owned(s: String) -> Self {
GraphQLTokenKind::Name(Cow::Owned(s))
}
#[inline]
pub fn int_value_borrowed(s: &'src str) -> Self {
GraphQLTokenKind::IntValue(Cow::Borrowed(s))
}
#[inline]
pub fn int_value_owned(s: String) -> Self {
GraphQLTokenKind::IntValue(Cow::Owned(s))
}
#[inline]
pub fn float_value_borrowed(s: &'src str) -> Self {
GraphQLTokenKind::FloatValue(Cow::Borrowed(s))
}
#[inline]
pub fn float_value_owned(s: String) -> Self {
GraphQLTokenKind::FloatValue(Cow::Owned(s))
}
#[inline]
pub fn string_value_borrowed(s: &'src str) -> Self {
GraphQLTokenKind::StringValue(Cow::Borrowed(s))
}
#[inline]
pub fn string_value_owned(s: String) -> Self {
GraphQLTokenKind::StringValue(Cow::Owned(s))
}
#[inline]
pub fn error(message: impl Into<String>, error_notes: SmallVec<[GraphQLErrorNote; 2]>) -> Self {
GraphQLTokenKind::Error(Box::new(GraphQLTokenError {
message: message.into(),
error_notes,
}))
}
pub fn is_punctuator(&self) -> bool {
match self {
GraphQLTokenKind::Ampersand
| GraphQLTokenKind::At
| GraphQLTokenKind::Bang
| GraphQLTokenKind::Colon
| GraphQLTokenKind::CurlyBraceClose
| GraphQLTokenKind::CurlyBraceOpen
| GraphQLTokenKind::Dollar
| GraphQLTokenKind::Ellipsis
| GraphQLTokenKind::Equals
| GraphQLTokenKind::ParenClose
| GraphQLTokenKind::ParenOpen
| GraphQLTokenKind::Pipe
| GraphQLTokenKind::SquareBracketClose
| GraphQLTokenKind::SquareBracketOpen => true,
GraphQLTokenKind::Name(_)
| GraphQLTokenKind::IntValue(_)
| GraphQLTokenKind::FloatValue(_)
| GraphQLTokenKind::StringValue(_)
| GraphQLTokenKind::True
| GraphQLTokenKind::False
| GraphQLTokenKind::Null
| GraphQLTokenKind::Eof
| GraphQLTokenKind::Error(_) => false,
}
}
pub fn as_punctuator_str(&self) -> Option<&'static str> {
match self {
GraphQLTokenKind::Ampersand => Some("&"),
GraphQLTokenKind::At => Some("@"),
GraphQLTokenKind::Bang => Some("!"),
GraphQLTokenKind::Colon => Some(":"),
GraphQLTokenKind::CurlyBraceClose => Some("}"),
GraphQLTokenKind::CurlyBraceOpen => Some("{"),
GraphQLTokenKind::Dollar => Some("$"),
GraphQLTokenKind::Ellipsis => Some("..."),
GraphQLTokenKind::Equals => Some("="),
GraphQLTokenKind::ParenClose => Some(")"),
GraphQLTokenKind::ParenOpen => Some("("),
GraphQLTokenKind::Pipe => Some("|"),
GraphQLTokenKind::SquareBracketClose => Some("]"),
GraphQLTokenKind::SquareBracketOpen => Some("["),
GraphQLTokenKind::Name(_)
| GraphQLTokenKind::IntValue(_)
| GraphQLTokenKind::FloatValue(_)
| GraphQLTokenKind::StringValue(_)
| GraphQLTokenKind::True
| GraphQLTokenKind::False
| GraphQLTokenKind::Null
| GraphQLTokenKind::Eof
| GraphQLTokenKind::Error(_) => None,
}
}
pub fn is_value(&self) -> bool {
match self {
GraphQLTokenKind::IntValue(_)
| GraphQLTokenKind::FloatValue(_)
| GraphQLTokenKind::StringValue(_)
| GraphQLTokenKind::True
| GraphQLTokenKind::False
| GraphQLTokenKind::Null => true,
GraphQLTokenKind::Ampersand
| GraphQLTokenKind::At
| GraphQLTokenKind::Bang
| GraphQLTokenKind::Colon
| GraphQLTokenKind::CurlyBraceClose
| GraphQLTokenKind::CurlyBraceOpen
| GraphQLTokenKind::Dollar
| GraphQLTokenKind::Ellipsis
| GraphQLTokenKind::Equals
| GraphQLTokenKind::ParenClose
| GraphQLTokenKind::ParenOpen
| GraphQLTokenKind::Pipe
| GraphQLTokenKind::SquareBracketClose
| GraphQLTokenKind::SquareBracketOpen
| GraphQLTokenKind::Name(_)
| GraphQLTokenKind::Eof
| GraphQLTokenKind::Error(_) => false,
}
}
pub fn is_error(&self) -> bool {
matches!(self, GraphQLTokenKind::Error(_))
}
pub fn parse_int_value(&self) -> Option<Result<i64, ParseIntError>> {
match self {
GraphQLTokenKind::IntValue(raw) => Some(raw.parse()),
_ => None,
}
}
pub fn parse_float_value(&self) -> Option<Result<f64, ParseFloatError>> {
match self {
GraphQLTokenKind::FloatValue(raw) => Some(raw.parse()),
_ => None,
}
}
pub fn parse_string_value(&self) -> Option<Result<String, GraphQLStringParsingError>> {
match self {
GraphQLTokenKind::StringValue(raw) => Some(parse_graphql_string(raw)),
_ => None,
}
}
}
fn parse_graphql_string(raw: &str) -> Result<String, GraphQLStringParsingError> {
if raw.starts_with("\"\"\"") {
parse_block_string(raw)
} else {
parse_single_line_string(raw)
}
}
fn parse_single_line_string(raw: &str) -> Result<String, GraphQLStringParsingError> {
if !raw.starts_with('"') || !raw.ends_with('"') || raw.len() < 2 {
return Err(GraphQLStringParsingError::UnterminatedString);
}
let content = &raw[1..raw.len() - 1];
let mut result = String::with_capacity(content.len());
let mut chars = content.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some('/') => result.push('/'),
Some('b') => result.push('\u{0008}'),
Some('f') => result.push('\u{000C}'),
Some('u') => {
let unicode_char = parse_unicode_escape(&mut chars)?;
result.push(unicode_char);
},
Some(other) => {
return Err(GraphQLStringParsingError::InvalidEscapeSequence(
format!("\\{other}"),
));
},
None => {
return Err(GraphQLStringParsingError::InvalidEscapeSequence(
"\\".to_string(),
));
},
}
} else {
result.push(c);
}
}
Ok(result)
}
fn parse_unicode_escape(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> Result<char, GraphQLStringParsingError> {
if chars.peek() == Some(&'{') {
chars.next(); let mut hex = String::new();
loop {
match chars.next() {
Some('}') => break,
Some(c) if c.is_ascii_hexdigit() => hex.push(c),
Some(c) => {
return Err(GraphQLStringParsingError::InvalidUnicodeEscape(format!(
"\\u{{{hex}{c}"
)));
}
None => {
return Err(GraphQLStringParsingError::InvalidUnicodeEscape(format!(
"\\u{{{hex}"
)));
}
}
}
if hex.is_empty() {
return Err(GraphQLStringParsingError::InvalidUnicodeEscape(
"\\u{}".to_string(),
));
}
let code_point = u32::from_str_radix(&hex, 16).map_err(|_| {
GraphQLStringParsingError::InvalidUnicodeEscape(format!("\\u{{{hex}}}"))
})?;
char::from_u32(code_point).ok_or_else(|| {
GraphQLStringParsingError::InvalidUnicodeEscape(format!("\\u{{{hex}}}"))
})
} else {
let mut hex = String::with_capacity(4);
for _ in 0..4 {
match chars.next() {
Some(c) if c.is_ascii_hexdigit() => hex.push(c),
Some(c) => {
return Err(GraphQLStringParsingError::InvalidUnicodeEscape(format!(
"\\u{hex}{c}"
)));
}
None => {
return Err(GraphQLStringParsingError::InvalidUnicodeEscape(format!(
"\\u{hex}"
)));
}
}
}
let code_point = u32::from_str_radix(&hex, 16).map_err(|_| {
GraphQLStringParsingError::InvalidUnicodeEscape(format!("\\u{hex}"))
})?;
char::from_u32(code_point).ok_or_else(|| {
GraphQLStringParsingError::InvalidUnicodeEscape(format!("\\u{hex}"))
})
}
}
fn graphql_lines(s: &str) -> impl Iterator<Item = &str> {
let mut rest = s;
std::iter::from_fn(move || {
if rest.is_empty() {
return None;
}
match memchr::memchr2(b'\n', b'\r', rest.as_bytes()) {
Some(i) => {
let line = &rest[..i];
if rest.as_bytes()[i] == b'\r'
&& rest.as_bytes().get(i + 1) == Some(&b'\n')
{
rest = &rest[i + 2..];
} else {
rest = &rest[i + 1..];
}
Some(line)
},
None => {
let line = rest;
rest = "";
Some(line)
},
}
})
}
fn is_graphql_blank(line: &str) -> bool {
line.bytes().all(|b| b == b' ' || b == b'\t')
}
fn parse_block_string(
raw: &str,
) -> Result<String, GraphQLStringParsingError> {
if !raw.starts_with("\"\"\"")
|| !raw.ends_with("\"\"\"")
|| raw.len() < 6
{
return Err(
GraphQLStringParsingError::UnterminatedString,
);
}
let content = &raw[3..raw.len() - 3];
let content: Cow<str> =
if content.contains("\\\"\"\"") {
Cow::Owned(
content.replace("\\\"\"\"", "\"\"\""),
)
} else {
Cow::Borrowed(content)
};
let mut common_indent: Option<usize> = None;
let mut first_non_blank: Option<usize> = None;
let mut last_non_blank: Option<usize> = None;
for (i, line) in graphql_lines(&content).enumerate() {
let blank = is_graphql_blank(line);
if !blank {
if first_non_blank.is_none() {
first_non_blank = Some(i);
}
last_non_blank = Some(i);
}
if i > 0 && !blank {
let indent = line
.bytes()
.take_while(|&b| b == b' ' || b == b'\t')
.count();
common_indent = Some(match common_indent {
Some(cur) if cur <= indent => cur,
_ => indent,
});
}
}
let common_indent = common_indent.unwrap_or(0);
let first_non_blank = match first_non_blank {
Some(i) => i,
None => return Ok(String::new()),
};
let last_non_blank = last_non_blank.unwrap_or(0);
let mut result =
String::with_capacity(content.len());
let mut need_newline = false;
for (i, line) in graphql_lines(&content).enumerate() {
if i < first_non_blank || i > last_non_blank {
continue;
}
if need_newline {
result.push('\n');
}
need_newline = true;
if i == 0 {
result.push_str(line);
} else if line.len() >= common_indent {
result.push_str(&line[common_indent..]);
} else {
result.push_str(line);
}
}
Ok(result)
}