Skip to content

Make block strings parsing conform to GraphQL spec. #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 86 additions & 30 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,41 +173,57 @@ where

fn unquote_block_string(src: &str) -> Result<String, Error<Token<'_>, Token<'_>>> {
debug_assert!(src.starts_with("\"\"\"") && src.ends_with("\"\"\""));
let indent = src[3..src.len() - 3]
.lines()
.skip(1)
.filter_map(|line| {
let trimmed = line.trim_start().len();
if trimmed > 0 {
Some(line.len() - trimmed)
} else {
None // skip whitespace-only lines
}
})
.min()
.unwrap_or(0);
let mut result = String::with_capacity(src.len() - 6);
let mut lines = src[3..src.len() - 3].lines();
if let Some(first) = lines.next() {
let stripped = first.trim();
if !stripped.is_empty() {
result.push_str(stripped);
result.push('\n');
let lines = src[3..src.len() - 3].lines();

let mut common_indent = usize::MAX;
let mut first_non_empty_line: Option<usize> = None;
let mut last_non_empty_line = 0;
for (idx, line) in lines.clone().enumerate() {
let indent = line.len() - line.trim_start().len();
if indent == line.len() {
continue;
}
}
let mut last_line = 0;
for line in lines {
last_line = result.len();
if line.len() > indent {
result.push_str(&line[indent..].replace(r#"\""""#, r#"""""#));

first_non_empty_line.get_or_insert(idx);
last_non_empty_line = idx;

if idx != 0 {
common_indent = std::cmp::min(common_indent, indent);
}
result.push('\n');
}
if result[last_line..].trim().is_empty() {
result.truncate(last_line);

if first_non_empty_line.is_none() {
// The block string contains only whitespace.
return Ok("".to_string());
}
let first_non_empty_line = first_non_empty_line.unwrap();

let mut result = String::with_capacity(src.len() - 6);
let mut lines = lines
.enumerate()
// Skip leading and trailing empty lines.
.skip(first_non_empty_line)
.take(last_non_empty_line - first_non_empty_line + 1)
// Remove indent, except the first line.
.map(|(idx, line)| {
if idx != 0 && line.len() >= common_indent {
&line[common_indent..]
} else {
line
}
})
// Handle escaped triple-quote (\""").
.map(|x| x.replace(r#"\""""#, r#"""""#));

if let Some(line) = lines.next() {
result.push_str(&line);

Ok(result)
for line in lines {
result.push_str("\n");
result.push_str(&line);
}
}
return Ok(result);
}

fn unquote_string(s: &str) -> Result<String, Error<Token, Token>> {
Expand Down Expand Up @@ -390,6 +406,7 @@ where

#[cfg(test)]
mod tests {
use super::unquote_block_string;
use super::unquote_string;
use super::Number;

Expand Down Expand Up @@ -422,4 +439,43 @@ mod tests {
"\u{0009} hello \u{000A} there"
);
}

#[test]
fn block_string_leading_and_trailing_empty_lines() {
let block = &triple_quote(" \n\n Hello,\n World!\n\n Yours,\n GraphQL.\n\n\n");
assert_eq!(
unquote_block_string(&block),
Result::Ok("Hello,\n World!\n\nYours,\n GraphQL.".to_string())
);
}

#[test]
fn block_string_indent() {
let block = &triple_quote("Hello \n\n Hello,\n World!\n");
assert_eq!(
unquote_block_string(&block),
Result::Ok("Hello \n\nHello,\n World!".to_string())
);
}

#[test]
fn block_string_escaping() {
let block = triple_quote(r#"\""""#);
assert_eq!(
unquote_block_string(&block),
Result::Ok("\"\"\"".to_string())
);
}

#[test]
fn block_string_empty() {
let block = triple_quote("");
assert_eq!(unquote_block_string(&block), Result::Ok("".to_string()));
let block = triple_quote(" \n\t\n");
assert_eq!(unquote_block_string(&block), Result::Ok("".to_string()));
}

fn triple_quote(input: &str) -> String {
return format!("\"\"\"{}\"\"\"", input);
}
}
6 changes: 1 addition & 5 deletions tests/queries/kitchen-sink_canonical.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
}

fragment frag on Friend {
foo(size: $size, bar: $b, obj: {block: """

block string uses \"""

""", key: "value"})
foo(size: $size, bar: $b, obj: {block: "block string uses \"\"\"", key: "value"})
}

{
Expand Down
6 changes: 4 additions & 2 deletions tests/schemas/directive_descriptions.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
Directs the executor to include this field or fragment only when the `if` argument is true.
Directs the executor to include this field or
fragment only when the `if` argument is true.
"""
directive @include(
"""
Expand All @@ -9,7 +10,8 @@ directive @include(
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

"""
Directs the executor to skip this field or fragment when the `if` argument is true.
Directs the executor to skip this field or
fragment when the `if` argument is true.
"""
directive @skip(
"""
Expand Down
14 changes: 6 additions & 8 deletions tests/schemas/directive_descriptions_canonical.graphql
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"""
Directs the executor to include this field or fragment only when the `if` argument is true.
Directs the executor to include this field or
fragment only when the `if` argument is true.
"""
directive @include("""
Included when true.
""" if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

"""
Directs the executor to skip this field or fragment when the `if` argument is true.
Directs the executor to skip this field or
fragment when the `if` argument is true.
"""
directive @skip("""
Skipped when true.
""" if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT