Skip to content
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
4 changes: 2 additions & 2 deletions modules/core/src/main/scala/minimizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object QueryMinimizer {
def minimizeDocument(doc: Document): String = {
import OperationDefinition._
import OperationType._
import SchemaRenderer.renderDescription
import SchemaRenderer.{renderDescription, renderString}
import Selection._
import Value._

Expand Down Expand Up @@ -141,7 +141,7 @@ object QueryMinimizer {
case Variable(name) => s"$$${name.value}"
case IntValue(value) => value.toString
case FloatValue(value) => value.toString
case StringValue(value) => s""""$value""""
case StringValue(value) => renderString(value)
case BooleanValue(value) => value.toString
case NullValue => "null"
case EnumValue(name) => name.value
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ object Query {
}

case class Binding(name: String, value: Value) {
def render: String = s"$name: $value"
def render: String = s"$name: ${SchemaRenderer.renderValue(value)}"
}

type UntypedVarDefs = List[UntypedVarDef]
Expand Down
35 changes: 28 additions & 7 deletions modules/core/src/main/scala/schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2450,9 +2450,7 @@ object SchemaRenderer {
val args =
if (args0.isEmpty) ""
else
args0
.map { case Binding(nme, v) => s"$nme: ${renderValue(v)}" }
.mkString("(", ", ", ")")
args0.map(_.render).mkString("(", ", ", ")")
s"@$name$args"
}

Expand Down Expand Up @@ -2580,15 +2578,38 @@ object SchemaRenderer {
def renderValue(value: Value): String = value match {
case IntValue(i) => i.toString
case FloatValue(f) => f.toString
case StringValue(s) => s""""$s""""
case StringValue(s) => renderString(s)
case BooleanValue(b) => b.toString
case IDValue(i) => s""""$i""""
case IDValue(i) => renderString(i)
case EnumValue(e) => e
case ListValue(elems) => elems.map(renderValue).mkString("[", ", ", "]")
case ObjectValue(fields) =>
fields
.map { case (name, value) => s"$name : ${renderValue(value)}" }
.map { case (name, value) => s"$name: ${renderValue(value)}" }
.mkString("{", ", ", "}")
case _ => "null"
case VariableRef(name) => s"$$$name"
case NullValue => "null"
case AbsentValue => "null"
}

/**
* Renders a string as a GraphQL quoted string
*/
def renderString(str: String): String = {
val sb = new StringBuilder(str.length + 2)
sb.append('"')
str.foreach {
case '"' => sb.append("\\\"")
case '\\' => sb.append("\\\\")
case '\b' => sb.append("\\b")
case '\f' => sb.append("\\f")
case '\n' => sb.append("\\n")
case '\r' => sb.append("\\r")
case '\t' => sb.append("\\t")
case c if c.isControl => sb.append(f"\\u${c.toInt}%04x")
case c => sb.append(c)
}
sb.append('"')
sb.toString
}
}
10 changes: 10 additions & 0 deletions modules/core/src/test/scala/minimizer/MinimizerSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,16 @@ final class MinimizerSuite extends CatsEffectSuite {
run(query, expected)
}

test("minimize string arguments which need escapes") {
val query =
"""query { character(name: "he said \"hi\"", tag: "a \\ and a \n and a \t") { id } }"""

val expected =
"""query{character(name:"he said \"hi\"",tag:"a \\ and a \n and a \t"){id}}"""

run(query, expected)
}

test("minimize block string description") {
val query = "\"\"\"\nA \"character\".\n\nWith a \\ backslash.\n\"\"\" query Foo { x }"

Expand Down
60 changes: 60 additions & 0 deletions modules/core/src/test/scala/query/QueryRenderSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA)
// Copyright (c) 2016-2025 Grackle Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package query

import munit.CatsEffectSuite

import grackle.Query._
import grackle.Value._

final class QueryRenderSuite extends CatsEffectSuite {
test("binding renders the value") {
assertEquals(Binding("id", StringValue("some-id")).render, "id: \"some-id\"")
}

test("select without a child renders the name only") {
assertEquals(Select("name").render, "name")
}

test("select renders the alias and the child") {
assertEquals(Select("hero", Some("r2"), Select("name")).render, "r2:hero { name }")
}

test("untyped select without arguments renders no parentheses") {
assertEquals(UntypedSelect("character", None, Nil, Nil, Empty).render, "character")
}

test("untyped select renders argument values") {
val query = UntypedSelect(
"character",
None,
List(Binding("id", StringValue("some-id")), Binding("n", IntValue(3))),
Nil,
Empty)
assertEquals(query.render, "character(id: \"some-id\", n: 3)")
}

test("nested untyped select renders arguments at each level") {
val child = UntypedSelect("friends", None, List(Binding("first", IntValue(1))), Nil, Empty)
val query =
UntypedSelect("hero", None, List(Binding("ep", EnumValue("JEDI"))), Nil, child)
assertEquals(query.render, "hero(ep: JEDI) { friends(first: 1) }")
}

test("group renders its members in braces") {
assertEquals(Group(List(Select("name"), Select("age"))).render, "{name, age}")
}
}
84 changes: 84 additions & 0 deletions modules/core/src/test/scala/schema/ValueRenderSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA)
// Copyright (c) 2016-2025 Grackle Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package schema

import munit.CatsEffectSuite

import grackle.SchemaRenderer.renderValue
import grackle.Value._

final class ValueRenderSuite extends CatsEffectSuite {
test("int value renders a negative number") {
assertEquals(renderValue(IntValue(-23)), "-23")
}

test("float value keeps the decimal point") {
assertEquals(renderValue(FloatValue(1.0)), 1.0.toString())
}

test("string value is quoted") {
assertEquals(renderValue(StringValue("some-id")), "\"some-id\"")
}

test("empty string value renders as a pair of quotes") {
assertEquals(renderValue(StringValue("")), "\"\"")
}

test("boolean value renders without quotes") {
assertEquals(renderValue(BooleanValue(false)), "false")
assertEquals(renderValue(BooleanValue(true)), "true")
}

test("ID value is quoted like a string") {
assertEquals(renderValue(IDValue("42")), "\"42\"")
}

test("enum value renders unquoted") {
assertEquals(renderValue(EnumValue("NORTH")), "NORTH")
}

test("empty list value renders as empty brackets") {
assertEquals(renderValue(ListValue(Nil)), "[]")
}

test("list value renders nested elements") {
assertEquals(
renderValue(ListValue(List(IntValue(1), ListValue(List(NullValue))))),
"[1, [null]]")
}

test("empty object value renders as empty braces") {
assertEquals(renderValue(ObjectValue(Nil)), "{}")
}

test("object value renders its fields") {
assertEquals(
renderValue(ObjectValue(List(("id", StringValue("a")), ("n", IntValue(2))))),
"{id: \"a\", n: 2}")
}

test("variable reference gets a dollar prefix") {
assertEquals(renderValue(VariableRef("id")), "$id")
}

test("null value renders as null") {
assertEquals(renderValue(NullValue), "null")
}

test("absent value renders as null") {
assertEquals(renderValue(AbsentValue), "null")
}
}
39 changes: 39 additions & 0 deletions modules/core/src/test/scala/sdl/SDLSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,45 @@ final class SDLSuite extends CatsEffectSuite {
}
}

/**
* String values with might escape string quotes
*/
val trickyStrings: List[(String, String)] =
List(
"quotes" -> "he said \"hi\"",
"backslash" -> "a backslash \\",
"triple quotes" -> s"a ${TQ} b",
"newline" -> "first\nsecond",
"carriage return" -> "first\rsecond",
"tab" -> "first\tsecond",
"control character" -> ("first" + 1.toChar + "second")
)

trickyStrings.foreach {
case (label, str) =>
test(s"string default value round trips: $label") {
val rendered = SchemaRenderer.renderValue(grackle.Value.StringValue(str))

val schema =
s"""|type Query {
| foo(bar: String! = $rendered): Int
|}""".stripMargin

assertEquals(
schemaParser
.parseText(schema)
.map(
_.definition("Query")
.collect { case o: grackle.ObjectType => o }
.flatMap(_.fields.find(_.name == "foo"))
.flatMap(_.args.find(_.name == "bar"))
.flatMap(_.defaultValue)),
Some(grackle.Value.StringValue(str)).success,
clue = s"rendered as: ${escape(rendered)}"
)
}
}

/**
* Renders control characters visibly, so that failure clues are legible.
*/
Expand Down
Loading