Skip to content
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

Support block string syntax #1219

Merged
merged 3 commits into from
Jan 16, 2018
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
7 changes: 7 additions & 0 deletions lib/graphql/compatibility/query_parser_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def test_it_parses_inputs
nullValue: null
nullValueInObject: {a: null, b: "b"}
nullValueInArray: ["a", null, "b"]
blockString: """
Hello,
World
"""
)
}
|
Expand Down Expand Up @@ -100,6 +104,9 @@ def test_it_parses_inputs
assert_equal 'a', values[0]
assert_instance_of GraphQL::Language::Nodes::NullValue, values[1]
assert_equal 'b', values[2]

block_str_value = inputs[12].value
assert_equal "Hello,\n World", block_str_value
end

def test_it_doesnt_parse_nonsense_variables
Expand Down
1 change: 1 addition & 0 deletions lib/graphql/language.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true
require "graphql/language/block_string"
require "graphql/language/printer"
require "graphql/language/definition_slice"
require "graphql/language/document_from_schema_definition"
Expand Down
47 changes: 47 additions & 0 deletions lib/graphql/language/block_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true
module GraphQL
module Language
module BlockString
# Remove leading and trailing whitespace from a block string.
# See "Block Strings" in https://github.com/facebook/graphql/blob/master/spec/Section%202%20--%20Language.md
def self.trim_whitespace(str)
lines = str.split("\n")
common_indent = nil

# find the common whitespace
lines.each_with_index do |line, idx|
if idx == 0
next
end
line_length = line.size
line_indent = line[/\A */].size
if line_indent < line_length && (common_indent.nil? || line_indent < common_indent)
common_indent = line_indent
end
end

# Remove the common whitespace
if common_indent
lines.each_with_index do |line, idx|
if idx == 0
next
else
line[0, common_indent] = ""
end
end
end

# Remove leading & trailing blank lines
while lines.first.empty?
lines.shift
end
while lines.last.empty?
lines.pop
end

# Rebuild the string
lines.join("\n")
end
end
end
end
197 changes: 129 additions & 68 deletions lib/graphql/language/lexer.rb

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions lib/graphql/language/lexer.rl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
RBRACKET = ']';
COLON = ':';
QUOTE = '"';
BLOCK_QUOTE = '"""';
ESCAPED_BLOCK_QUOTE = '\\"""';
BLOCK_STRING_CHAR = (ESCAPED_BLOCK_QUOTE | ^'"""');
ESCAPED_QUOTE = '\\"';
STRING_CHAR = (ESCAPED_QUOTE | ^'"');
VAR_SIGN = '$';
Expand All @@ -44,7 +47,7 @@
PIPE = '|';

QUOTED_STRING = QUOTE STRING_CHAR* QUOTE;

BLOCK_STRING = BLOCK_QUOTE BLOCK_STRING_CHAR* BLOCK_QUOTE;
# catch-all for anything else. must be at the bottom for precedence.
UNKNOWN_CHAR = /./;

Expand Down Expand Up @@ -75,7 +78,8 @@
RBRACKET => { emit(:RBRACKET, ts, te, meta) };
LBRACKET => { emit(:LBRACKET, ts, te, meta) };
COLON => { emit(:COLON, ts, te, meta) };
QUOTED_STRING => { emit_string(ts + 1, te, meta) };
QUOTED_STRING => { emit_string(ts, te, meta, block: false) };
BLOCK_STRING => { emit_string(ts, te, meta, block: true) };
VAR_SIGN => { emit(:VAR_SIGN, ts, te, meta) };
DIR_SIGN => { emit(:DIR_SIGN, ts, te, meta) };
ELLIPSIS => { emit(:ELLIPSIS, ts, te, meta) };
Expand Down Expand Up @@ -188,8 +192,13 @@ module GraphQL
PACK_DIRECTIVE = "c*"
UTF_8_ENCODING = "UTF-8"

def self.emit_string(ts, te, meta)
value = meta[:data][ts...te - 1].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
def self.emit_string(ts, te, meta, block:)
quotes_length = block ? 3 : 1
ts += quotes_length
value = meta[:data][ts...te - quotes_length].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
if block
value = GraphQL::Language::BlockString.trim_whitespace(value)
end
if value !~ VALID_STRING
meta[:tokens] << token = GraphQL::Language::Token.new(
name: :BAD_UNICODE_ESCAPE,
Expand Down
4 changes: 3 additions & 1 deletion spec/graphql/directive_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
describe "child fields" do
let(:query_string) { <<-GRAPHQL
{
__type(name: "Cheese") {
__type(name: """
Cheese
""") {
fields { name }
fields @skip(if: true) { isDeprecated }
}
Expand Down
70 changes: 70 additions & 0 deletions spec/graphql/language/block_string_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require "spec_helper"

describe GraphQL::Language::BlockString do
describe "trimming whitespace" do
def trim_whitespace(str)
GraphQL::Language::BlockString.trim_whitespace(str)
end

it "matches the examples in graphql-js" do
# these are taken from:
# https://github.com/graphql/graphql-js/blob/36ec0e9d34666362ff0e2b2b18edeb98e3c9abee/src/language/__tests__/blockStringValue-test.js#L12
# A set of [before, after] pairs:
examples = [
[
# Removes common whitespace:
"
Hello,
World!

Yours,
GraphQL.
",
"Hello,\n World!\n\nYours,\n GraphQL."
],
[
# Removes leading and trailing newlines:
"

Hello,
World!

Yours,
GraphQL.

",
"Hello,\n World!\n\nYours,\n GraphQL."
],
[
# Removes blank lines (with whitespace _and_ newlines:)
"\n \n
Hello,
World!

Yours,
GraphQL.

\n \n",
"Hello,\n World!\n\nYours,\n GraphQL."
],
[
# Retains indentation from the first line
" Hello,\n World!\n\n Yours,\n GraphQL.",
" Hello,\n World!\n\nYours,\n GraphQL.",
],
[
# Doesn't alter trailing spaces
"\n \n Hello, \n World! \n\n Yours, \n GraphQL. ",
"Hello, \n World! \n\nYours, \n GraphQL. ",

],
]

examples.each_with_index do |(before, after), idx|
transformed_str = trim_whitespace(before)
assert_equal(after, transformed_str, "Example ##{idx + 1}")
end
end
end
end
9 changes: 9 additions & 0 deletions spec/graphql/language/lexer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
assert_equal tokens[0], tokens[1].prev_token
end

describe "block strings" do
let(:query_string) { %|{ a(b: """\nc\n d\n""")}|}

it "tokenizes them" do
str_token = tokens[5]
assert_equal "c\n d", str_token.value
end
end

it "unescapes escaped characters" do
assert_equal "\" \\ / \b \f \n \r \t", subject.tokenize('"\\" \\\\ \\/ \\b \\f \\n \\r \\t"').first.to_s
end
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
operation_name: operation_name,
max_depth: max_depth,
)
query.query_string = '{ __type(name: "Cheese") { name } }'
query.query_string = '{ __type(name: """Cheese""") { name } }'
assert_equal "Cheese", query.result["data"] ["__type"]["name"]
end
end
Expand Down