diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index 006c95f70c..5a6bc72585 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -5,22 +5,22 @@ import caliban.interop.circe._ import caliban.interop.tapir.IsTapirSchema import caliban.interop.jsoniter.IsJsoniterCodec import caliban.interop.zio.{ IsZIOJsonDecoder, IsZIOJsonEncoder } +import caliban.rendering.ValueRenderer import zio.stream.Stream -sealed trait InputValue { - def toInputString: String = toString +sealed trait InputValue { self => + def toInputString: String = ValueRenderer.inputValueRenderer.renderCompact(self) } object InputValue extends ValueJsonCompat { case class ListValue(values: List[InputValue]) extends InputValue { override def toString: String = values.mkString("[", ",", "]") - override def toInputString: String = values.map(_.toInputString).mkString("[", ", ", "]") + override def toInputString: String = ValueRenderer.inputListValueRenderer.render(this) } case class ObjectValue(fields: Map[String, InputValue]) extends InputValue { override def toString: String = fields.map { case (name, value) => s""""$name:${value.toString}"""" }.mkString("{", ",", "}") - override def toInputString: String = - fields.map { case (name, value) => s"""$name: ${value.toInputString}""" }.mkString("{", ", ", "}") + override def toInputString: String = ValueRenderer.inputObjectValueRenderer.render(this) } case class VariableValue(name: String) extends InputValue { override def toString: String = s"$$$name" @@ -60,11 +60,11 @@ sealed trait ResponseValue { self => } object ResponseValue extends ValueJsonCompat { case class ListValue(values: List[ResponseValue]) extends ResponseValue { - override def toString: String = values.mkString("[", ",", "]") + override def toString: String = ValueRenderer.responseListValueRenderer.renderCompact(this) } case class ObjectValue(fields: List[(String, ResponseValue)]) extends ResponseValue { override def toString: String = - fields.map { case (name, value) => s""""$name":${value.toString}""" }.mkString("{", ",", "}") + ValueRenderer.responseObjectValueRenderer.renderCompact(this) override def hashCode: Int = fields.toSet.hashCode() override def equals(other: Any): Boolean = @@ -114,7 +114,7 @@ object Value { } case class EnumValue(value: String) extends Value { override def toString: String = s""""${value.replace("\"", "\\\"")}"""" - override def toInputString: String = s"""${value.replace("\"", "\\\"")}""" + override def toInputString: String = ValueRenderer.enumInputValueRenderer.render(this) } object IntValue { diff --git a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala index 14e2cb8fde..984bb0cbb1 100644 --- a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala +++ b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala @@ -40,14 +40,14 @@ object DocumentRenderer extends Renderer[Document] { // Estimate the size of the underlying definitions to prevent re-allocations val sizeEstimate = value.sourceMapper.size.getOrElse { val numDefs = value.definitions.length - numDefs * 64 // A naive estimate but a fast one, we just want to get into the ballpark of the actual size + numDefs * 16 // A naive estimate but a fast one, we just want to get into the ballpark of the actual size } write.ensureCapacity(sizeEstimate) documentRenderer.unsafeRender(value, indent, write) } private[caliban] lazy val documentRenderer: Renderer[Document] = Renderer.combine( - directiveDefinitionRenderer.list("\n").contramap(_.directiveDefinitions), + directiveDefinitionRenderer.list(Renderer.char('\n')).contramap(_.directiveDefinitions), schemaRenderer.optional.contramap(_.schemaDefinition), operationDefinitionRenderer.list.contramap(_.operationDefinitions), typeDefinitionsRenderer.contramap(_.typeDefinitions), @@ -79,6 +79,10 @@ object DocumentRenderer extends Renderer[Document] { private lazy val directiveDefinitionRenderer: Renderer[DirectiveDefinition] = new Renderer[DirectiveDefinition] { + private val inputRenderer = inputValueDefinitionRenderer.list(Renderer.char(',') ++ Renderer.space) + private val locationsRenderer: Renderer[Set[DirectiveLocation]] = + locationRenderer.set(Renderer.space ++ Renderer.char('|') ++ Renderer.space) + override def unsafeRender(value: DirectiveDefinition, indent: Option[Int], write: StringBuilder): Unit = value match { case DirectiveDefinition(description, name, args, isRepeatable, locations) => @@ -87,42 +91,40 @@ object DocumentRenderer extends Renderer[Document] { write ++= name if (args.nonEmpty) { write += '(' - args.foreach(inputValueDefinitionRenderer.unsafeRender(_, indent, write)) + inputRenderer.unsafeRender(args, indent, write) write += ')' } if (isRepeatable) write ++= " repeatable" write ++= " on" - var first = true - locations.foreach { location => - write += ' ' - if (first) first = false - else write ++= "| " - unsafeRenderLocation(location, write) - } + locationsRenderer.unsafeRender(locations, indent, write) } - private def unsafeRenderLocation(location: DirectiveLocation, builder: StringBuilder): Unit = - location match { - case ExecutableDirectiveLocation.QUERY => builder ++= "QUERY" - case ExecutableDirectiveLocation.MUTATION => builder ++= "MUTATION" - case ExecutableDirectiveLocation.SUBSCRIPTION => builder ++= "SUBSCRIPTION" - case ExecutableDirectiveLocation.FIELD => builder ++= "FIELD" - case ExecutableDirectiveLocation.FRAGMENT_DEFINITION => builder ++= "FRAGMENT_DEFINITION" - case ExecutableDirectiveLocation.FRAGMENT_SPREAD => builder ++= "FRAGMENT_SPREAD" - case ExecutableDirectiveLocation.INLINE_FRAGMENT => builder ++= "INLINE_FRAGMENT" - case TypeSystemDirectiveLocation.SCHEMA => builder ++= "SCHEMA" - case TypeSystemDirectiveLocation.SCALAR => builder ++= "SCALAR" - case TypeSystemDirectiveLocation.OBJECT => builder ++= "OBJECT" - case TypeSystemDirectiveLocation.FIELD_DEFINITION => builder ++= "FIELD_DEFINITION" - case TypeSystemDirectiveLocation.ARGUMENT_DEFINITION => builder ++= "ARGUMENT_DEFINITION" - case TypeSystemDirectiveLocation.INTERFACE => builder ++= "INTERFACE" - case TypeSystemDirectiveLocation.UNION => builder ++= "UNION" - case TypeSystemDirectiveLocation.ENUM => builder ++= "ENUM" - case TypeSystemDirectiveLocation.ENUM_VALUE => builder ++= "ENUM_VALUE" - case TypeSystemDirectiveLocation.INPUT_OBJECT => builder ++= "INPUT_OBJECT" - case TypeSystemDirectiveLocation.INPUT_FIELD_DEFINITION => builder ++= "INPUT_FIELD_DEFINITION" - case TypeSystemDirectiveLocation.VARIABLE_DEFINITION => builder ++= "VARIABLE_DEFINITION" + private[caliban] lazy val locationRenderer: Renderer[DirectiveLocation] = + new Renderer[DirectiveLocation] { + override def unsafeRender(location: DirectiveLocation, indent: Option[Int], writer: StringBuilder): Unit = + location match { + case ExecutableDirectiveLocation.QUERY => writer ++= "QUERY" + case ExecutableDirectiveLocation.MUTATION => writer ++= "MUTATION" + case ExecutableDirectiveLocation.SUBSCRIPTION => writer ++= "SUBSCRIPTION" + case ExecutableDirectiveLocation.FIELD => writer ++= "FIELD" + case ExecutableDirectiveLocation.FRAGMENT_DEFINITION => writer ++= "FRAGMENT_DEFINITION" + case ExecutableDirectiveLocation.FRAGMENT_SPREAD => writer ++= "FRAGMENT_SPREAD" + case ExecutableDirectiveLocation.INLINE_FRAGMENT => writer ++= "INLINE_FRAGMENT" + case TypeSystemDirectiveLocation.SCHEMA => writer ++= "SCHEMA" + case TypeSystemDirectiveLocation.SCALAR => writer ++= "SCALAR" + case TypeSystemDirectiveLocation.OBJECT => writer ++= "OBJECT" + case TypeSystemDirectiveLocation.FIELD_DEFINITION => writer ++= "FIELD_DEFINITION" + case TypeSystemDirectiveLocation.ARGUMENT_DEFINITION => writer ++= "ARGUMENT_DEFINITION" + case TypeSystemDirectiveLocation.INTERFACE => writer ++= "INTERFACE" + case TypeSystemDirectiveLocation.UNION => writer ++= "UNION" + case TypeSystemDirectiveLocation.ENUM => writer ++= "ENUM" + case TypeSystemDirectiveLocation.ENUM_VALUE => writer ++= "ENUM_VALUE" + case TypeSystemDirectiveLocation.INPUT_OBJECT => writer ++= "INPUT_OBJECT" + case TypeSystemDirectiveLocation.INPUT_FIELD_DEFINITION => writer ++= "INPUT_FIELD_DEFINITION" + case TypeSystemDirectiveLocation.VARIABLE_DEFINITION => writer ++= "VARIABLE_DEFINITION" + } } + } private[caliban] lazy val typesRenderer: Renderer[List[__Type]] = @@ -167,7 +169,7 @@ object DocumentRenderer extends Renderer[Document] { private[caliban] lazy val variableDefinitionsRenderer: Renderer[List[VariableDefinition]] = new Renderer[List[VariableDefinition]] { - private val inner = variableDefinition.list(", ") + private val inner = variableDefinition.list(Renderer.char(',') ++ Renderer.space) override def unsafeRender(value: List[VariableDefinition], indent: Option[Int], write: StringBuilder): Unit = if (value.nonEmpty) { @@ -188,15 +190,15 @@ object DocumentRenderer extends Renderer[Document] { } - private def loop(selection: Selection, indent: Option[Int], builder: StringBuilder): Unit = + private def loop(selection: Selection, indent: Option[Int], builder: StringBuilder): Unit = { + newlineOrSpace(indent, builder) + pad(indent, builder) selection match { case Selection.Field(alias, name, arguments, directives, selectionSet, _) => - builder += '\n' - pad(indent, builder) alias.foreach { a => builder ++= a builder += ':' - builder += ' ' + space(indent, builder) } builder ++= name inputArgumentsRenderer.unsafeRender(arguments, indent, builder) @@ -210,14 +212,10 @@ object DocumentRenderer extends Renderer[Document] { builder += '}' } case Selection.FragmentSpread(name, directives) => - newlineOrSpace(indent, builder) - pad(indent, builder) builder ++= "..." builder ++= name directivesRenderer.unsafeRender(directives, indent, builder) case Selection.InlineFragment(typeCondition, dirs, selectionSet) => - newlineOrSpace(indent, builder) - pad(indent, builder) builder ++= "..." typeCondition.foreach { t => builder ++= " on " @@ -233,27 +231,25 @@ object DocumentRenderer extends Renderer[Document] { builder += '}' } } + } } - private lazy val inputArgumentsRenderer: Renderer[Map[String, InputValue]] = new Renderer[Map[String, InputValue]] { - override def unsafeRender(arguments: Map[String, InputValue], indent: Option[Int], builder: StringBuilder): Unit = - if (arguments.nonEmpty) { - builder += '(' - var first = true - arguments.foreach { case (name, value) => - if (first) first = false - else { - builder += ',' - space(indent, builder) - } - builder ++= name - builder += ':' - space(indent, builder) - ValueRenderer.inputValueRenderer.unsafeRender(value, indent, builder) + private[caliban] lazy val inputArgumentsRenderer: Renderer[Map[String, InputValue]] = + new Renderer[Map[String, InputValue]] { + private val inner = + map( + Renderer.string, + ValueRenderer.inputValueRenderer, + Renderer.char(',') ++ Renderer.space + ) + + override def unsafeRender(arguments: Map[String, InputValue], indent: Option[Int], builder: StringBuilder): Unit = + if (arguments.nonEmpty) { + builder += '(' + inner.unsafeRender(arguments, indent, builder) + builder += ')' } - builder += ')' - } - } + } lazy val schemaRenderer: Renderer[SchemaDefinition] = new Renderer[SchemaDefinition] { override def unsafeRender(definition: SchemaDefinition, indent: Option[Int], write: StringBuilder): Unit = @@ -266,8 +262,8 @@ object DocumentRenderer extends Renderer[Document] { def renderOp(name: String, op: Option[String]): Unit = op.foreach { o => if (first) { - newline(indent, write) first = false + newline(indent, write) } else newlineOrComma(indent, write) pad(increment(indent), write) write ++= name @@ -330,6 +326,8 @@ object DocumentRenderer extends Renderer[Document] { } private[caliban] lazy val unionRenderer: Renderer[UnionTypeDefinition] = new Renderer[UnionTypeDefinition] { + private val memberRenderer = Renderer.string.list(Renderer.space ++ Renderer.char('|') ++ Renderer.space) + override def unsafeRender(value: UnionTypeDefinition, indent: Option[Int], write: StringBuilder): Unit = value match { case UnionTypeDefinition(description, name, directives, members) => @@ -341,16 +339,7 @@ object DocumentRenderer extends Renderer[Document] { space(indent, write) write += '=' space(indent, write) - var first = true - members.foreach { member => - if (first) first = false - else { - space(indent, write) - write += '|' - space(indent, write) - } - write ++= member - } + memberRenderer.unsafeRender(members, indent, write) } } @@ -369,6 +358,8 @@ object DocumentRenderer extends Renderer[Document] { } private[caliban] lazy val enumRenderer: Renderer[EnumTypeDefinition] = new Renderer[EnumTypeDefinition] { + private val memberRenderer = enumValueDefinitionRenderer.list(Renderer.newlineOrComma) + override def unsafeRender(value: EnumTypeDefinition, indent: Option[Int], write: StringBuilder): Unit = value match { case EnumTypeDefinition(description, name, directives, values) => @@ -379,7 +370,8 @@ object DocumentRenderer extends Renderer[Document] { directivesRenderer.unsafeRender(directives, indent, write) space(indent, write) write += '{' - values.foreach(enumValueDefinitionRenderer.unsafeRender(_, increment(indent), write)) + newline(indent, write) + memberRenderer.unsafeRender(values, increment(indent), write) write += '}' } } @@ -389,7 +381,6 @@ object DocumentRenderer extends Renderer[Document] { override def unsafeRender(value: EnumValueDefinition, indent: Option[Int], write: StringBuilder): Unit = value match { case EnumValueDefinition(description, name, directives) => - newlineOrComma(indent, write) descriptionRenderer.unsafeRender(description, indent, write) write ++= name directivesRenderer.unsafeRender(directives, indent, write) @@ -398,6 +389,8 @@ object DocumentRenderer extends Renderer[Document] { private[caliban] lazy val inputObjectTypeDefinition: Renderer[InputObjectTypeDefinition] = new Renderer[InputObjectTypeDefinition] { + private val fieldsRenderer = inputValueDefinitionRenderer.list(Renderer.newlineOrSpace) + override def unsafeRender(value: InputObjectTypeDefinition, indent: Option[Int], write: StringBuilder): Unit = value match { case InputObjectTypeDefinition(description, name, directives, fields) => @@ -408,7 +401,7 @@ object DocumentRenderer extends Renderer[Document] { directivesRenderer.unsafeRender(directives, indent, write) space(indent, write) write += '{' - fields.foreach(inputValueDefinitionRenderer.unsafeRender(_, increment(indent), write)) + fieldsRenderer.unsafeRender(fields, increment(indent), write) newline(indent, write) write += '}' newline(indent, write) @@ -424,7 +417,6 @@ object DocumentRenderer extends Renderer[Document] { ): Unit = definition match { case InputValueDefinition(description, name, valueType, defaultValue, directives) => - newlineOrSpace(indent, builder) descriptionRenderer.unsafeRender(description, indent, builder) pad(indent, builder) builder ++= name @@ -439,19 +431,31 @@ object DocumentRenderer extends Renderer[Document] { private[caliban] lazy val objectTypeDefinitionRenderer: Renderer[ObjectTypeDefinition] = new Renderer[ObjectTypeDefinition] { override def unsafeRender(value: ObjectTypeDefinition, indent: Option[Int], write: StringBuilder): Unit = - value match { - case ObjectTypeDefinition(description, name, interfaces, directives, fields) => - unsafeRenderObjectLike("type", description, name, interfaces, directives, fields, indent, write) - } + unsafeRenderObjectLike( + "type", + value.description, + value.name, + value.implements, + value.directives, + value.fields, + indent, + write + ) } private[caliban] lazy val interfaceTypeDefinitionRenderer: Renderer[InterfaceTypeDefinition] = new Renderer[InterfaceTypeDefinition] { override def unsafeRender(value: InterfaceTypeDefinition, indent: Option[Int], write: StringBuilder): Unit = - value match { - case InterfaceTypeDefinition(description, name, implements, directives, fields) => - unsafeRenderObjectLike("interface", description, name, implements, directives, fields, indent, write) - } + unsafeRenderObjectLike( + "interface", + value.description, + value.name, + value.implements, + value.directives, + value.fields, + indent, + write + ) } private def unsafeRenderObjectLike( @@ -484,7 +488,7 @@ object DocumentRenderer extends Renderer[Document] { if (fields.nonEmpty) { space(indent, writer) writer += '{' - fields.foreach(fieldDefinitionRenderer.unsafeRender(_, increment(indent), writer)) + fieldDefinitionsRenderer.unsafeRender(fields, increment(indent), writer) newline(indent, writer) writer += '}' } @@ -492,32 +496,32 @@ object DocumentRenderer extends Renderer[Document] { private lazy val directiveRenderer: Renderer[Directive] = new Renderer[Directive] { override def unsafeRender(d: Directive, indent: Option[Int], writer: StringBuilder): Unit = { - writer += ' ' writer += '@' writer ++= d.name - var first = true - if (d.arguments.nonEmpty) { - writer += '(' - d.arguments.foreach { case (name, value) => - if (first) { - first = false - } else { - writer += ',' - space(indent, writer) - } - writer ++= name - writer += ':' - space(indent, writer) - ValueRenderer.inputValueRenderer.unsafeRender(value, indent, writer) - } - writer += ')' - } - + inputArgumentsRenderer.unsafeRender(d.arguments, indent, writer) } } + private[caliban] lazy val fieldDefinitionsRenderer: Renderer[List[FieldDefinition]] = + fieldDefinitionRenderer.list(Renderer.newlineOrSpace, omitFirst = false) + + private[caliban] lazy val fieldDefinitionRenderer: Renderer[FieldDefinition] = new Renderer[FieldDefinition] { + override def unsafeRender(definition: FieldDefinition, indent: Option[Int], builder: StringBuilder): Unit = + definition match { + case FieldDefinition(description, name, arguments, tpe, directives) => + descriptionRenderer.unsafeRender(description, indent, builder) + pad(indent, builder) + builder ++= name + inlineInputValueDefinitionsRenderer.unsafeRender(arguments, indent, builder) + builder += ':' + space(indent, builder) + typeRenderer.unsafeRender(tpe, indent, builder) + directivesRenderer.unsafeRender(directives, None, builder) + } + } + private[caliban] lazy val directivesRenderer: Renderer[List[Directive]] = - directiveRenderer.list + directiveRenderer.list(Renderer.char(' '), omitFirst = false) private[caliban] lazy val descriptionRenderer: Renderer[Option[String]] = new Renderer[Option[String]] { @@ -526,7 +530,7 @@ object DocumentRenderer extends Renderer[Document] { override def unsafeRender(description: Option[String], indent: Option[Int], writer: StringBuilder): Unit = description.foreach { case value if value.contains('\n') => - def valueEscaped() = unsafeFastEscapeQuote(value, writer) + def valueEscaped(): Unit = unsafeFastEscapeQuote(value, writer) writer ++= tripleQuote // check if it ends in quote but it is already escaped @@ -554,22 +558,6 @@ object DocumentRenderer extends Renderer[Document] { } } - private[caliban] lazy val fieldDefinitionRenderer: Renderer[FieldDefinition] = new Renderer[FieldDefinition] { - override def unsafeRender(definition: FieldDefinition, indent: Option[Int], builder: StringBuilder): Unit = - definition match { - case FieldDefinition(description, name, arguments, tpe, directives) => - newlineOrSpace(indent, builder) - descriptionRenderer.unsafeRender(description, indent, builder) - pad(indent, builder) - builder ++= name - unsafeRenderArguments(arguments, indent, builder) - builder += ':' - space(indent, builder) - typeRenderer.unsafeRender(tpe, indent, builder) - directivesRenderer.unsafeRender(directives, None, builder) - } - } - private[caliban] lazy val typeRenderer: Renderer[Type] = new Renderer[Type] { override def unsafeRender(value: Type, indent: Option[Int], write: StringBuilder): Unit = { def loop(t: Type): Unit = t match { @@ -586,27 +574,10 @@ object DocumentRenderer extends Renderer[Document] { } } - private[caliban] def isBuiltinScalar(name: String): Boolean = - name == "Int" || name == "Float" || name == "String" || name == "Boolean" || name == "ID" - - private def unsafeRenderArguments( - definitions: List[InputValueDefinition], - indent: Option[Int], - builder: StringBuilder - ): Unit = - if (definitions.nonEmpty) { - builder += '(' - var first = true - definitions.foreach { definition => - if (first) first = false - else { - builder += ',' - space(indent, builder) - } - inlineInputValueDefinitionRenderer.unsafeRender(definition, indent, builder) - } - builder += ')' - } + private[caliban] lazy val inlineInputValueDefinitionsRenderer: Renderer[List[InputValueDefinition]] = + (Renderer.char('(') ++ + inlineInputValueDefinitionRenderer.list(Renderer.char(',') ++ Renderer.space) ++ + Renderer.char(')')).when(_.nonEmpty) private[caliban] lazy val inlineInputValueDefinitionRenderer: Renderer[InputValueDefinition] = new Renderer[InputValueDefinition] { @@ -644,13 +615,13 @@ object DocumentRenderer extends Renderer[Document] { def map[K, V]( keyRender: Renderer[K], valueRender: Renderer[V], - separatorRenderer: Renderer[Any] + separator: Renderer[Any] ): Renderer[Map[K, V]] = new Renderer[Map[K, V]] { override def unsafeRender(value: Map[K, V], indent: Option[Int], write: StringBuilder): Unit = { var first = true value.foreach { case (k, v) => if (first) first = false - else separatorRenderer.render(()) + else separator.unsafeRender((), indent, write) keyRender.unsafeRender(k, indent, write) write.append(':') space(indent, write) @@ -659,6 +630,9 @@ object DocumentRenderer extends Renderer[Document] { } } + private[caliban] def isBuiltinScalar(name: String): Boolean = + name == "Int" || name == "Float" || name == "String" || name == "Boolean" || name == "ID" + private def space(indentation: Option[Int], writer: StringBuilder): Unit = if (indentation.isDefined) writer += ' ' diff --git a/core/src/main/scala/caliban/rendering/Renderer.scala b/core/src/main/scala/caliban/rendering/Renderer.scala index a1754be5f4..13ba72fead 100644 --- a/core/src/main/scala/caliban/rendering/Renderer.scala +++ b/core/src/main/scala/caliban/rendering/Renderer.scala @@ -1,5 +1,11 @@ package caliban.rendering +/** + * The inverse of a `Parser` over some type A. + * A renderer can be used to render a value of type A to a string in either a regular or compact format. + * + * For specializations actually relevant to graphql see [[caliban.rendering.ValueRenderer]] and [[caliban.rendering.DocumentRenderer]] + */ trait Renderer[-A] { self => def render(a: A): String = { val sb = new StringBuilder @@ -13,62 +19,147 @@ trait Renderer[-A] { self => sb.toString() } - def ++[A1 <: A](that: Renderer[A1]): Renderer[A1] = new Renderer[A1] { - override protected[caliban] def unsafeRender(value: A1, indent: Option[Int], write: StringBuilder): Unit = { - self.unsafeRender(value, indent, write) - that.unsafeRender(value, indent, write) - } - + /** + * Combines this renderer with another renderer sequentially. Semantically equivalent to `this andThen that` + */ + def ++[A1 <: A](that: Renderer[A1]): Renderer[A1] = self match { + case Renderer.Combined(renderers) => Renderer.Combined(renderers :+ that) + case _ => Renderer.Combined(List(self, that)) } + /** + * Contramaps the input of this renderer with the given function producing a renderer that now operates on type B + */ def contramap[B](f: B => A): Renderer[B] = new Renderer[B] { override def unsafeRender(value: B, indent: Option[Int], write: StringBuilder): Unit = self.unsafeRender(f(value), indent, write) } + /** + * Returns an optional renderer that will only render the value if it is defined + */ def optional: Renderer[Option[A]] = new Renderer[Option[A]] { override def unsafeRender(value: Option[A], indent: Option[Int], write: StringBuilder): Unit = value.foreach(self.unsafeRender(_, indent, write)) } - def list: Renderer[List[A]] = new Renderer[List[A]] { - override def unsafeRender(value: List[A], indent: Option[Int], write: StringBuilder): Unit = - value.foreach(self.unsafeRender(_, indent, write)) + /** + * Returns a renderer that renders a list of A where the underlying renderer is responsible for rendering the + * separator between each element. + */ + def list: Renderer[List[A]] = + list(Renderer.empty) + + /** + * Returns a renderer that renders a list of A but where the separator is rendered by provided argument renderer. + * The second parameter determines whether to print the separator before the first element or not. + */ + def list[A1 <: A](separator: Renderer[A1], omitFirst: Boolean = true): Renderer[List[A1]] = new Renderer[List[A1]] { + override protected[caliban] def unsafeRender(value: List[A1], indent: Option[Int], write: StringBuilder): Unit = { + var first = omitFirst + value.foreach { v => + if (first) first = false + else separator.unsafeRender(v, indent, write) + self.unsafeRender(v, indent, write) + } + } } - def list(separator: String): Renderer[List[A]] = new Renderer[List[A]] { - override def unsafeRender(value: List[A], indent: Option[Int], write: StringBuilder): Unit = { + /** + * Returns a renderer that renders a set of A but where the separator is rendered by provided argument renderer. + */ + def set[A1 <: A](separator: Renderer[A1]): Renderer[Set[A1]] = new Renderer[Set[A1]] { + override protected[caliban] def unsafeRender(value: Set[A1], indent: Option[Int], write: StringBuilder): Unit = { var first = true value.foreach { v => if (first) first = false - else write.append(separator) + else separator.unsafeRender(v, indent, write) self.unsafeRender(v, indent, write) } } } + /** + * Returns a renderer that will only render when the provided predicate is true. + */ + def when[A1 <: A](pred: A1 => Boolean): Renderer[A1] = new Renderer[A1] { + override protected[caliban] def unsafeRender(value: A1, indent: Option[Int], write: StringBuilder): Unit = + if (pred(value)) self.unsafeRender(value, indent, write) + } + + /** + * Protected method for implementers to override. This method provides the actual unsafe rendering logic. + * @param value the value to render + * @param indent the current indentation level. This will be None if the renderer is rendering in compact mode or Some(n) + * if the renderer is rendering in regular mode where n is the current indentation level. + * @param write the string builder to write to + */ protected[caliban] def unsafeRender(value: A, indent: Option[Int], write: StringBuilder): Unit } object Renderer { - def combine[A](renderers: Renderer[A]*): Renderer[A] = new Renderer[A] { - override def unsafeRender(value: A, indent: Option[Int], write: StringBuilder): Unit = - renderers.foreach(_.unsafeRender(value, indent, write)) - } + def combine[A](renderers: Renderer[A]*): Renderer[A] = + Combined(renderers.toList) + /** + * A Renderer which always renders a single character. + */ def char(char: Char): Renderer[Any] = new Renderer[Any] { override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = write.append(char) } + /** + * A Renderer which always renders a string. + */ def string(str: String): Renderer[Any] = new Renderer[Any] { override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = write.append(str) } + /** + * A Renderer which simply renders the input string + */ lazy val string: Renderer[String] = new Renderer[String] { override def unsafeRender(value: String, indent: Option[Int], write: StringBuilder): Unit = write.append(value) } + + /** + * A Renderer which doesn't render anything. + */ + lazy val empty: Renderer[Any] = new Renderer[Any] { + override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = () + } + + /** + * A Renderer which renders a space character when in non-compact mode. + */ + lazy val space: Renderer[Any] = new Renderer[Any] { + + override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = + if (indent.isDefined) write.append(' ') + } + + /** + * A Renderer which renders a newline character when in non-compact mode otherwise it renders a comma + */ + lazy val newlineOrComma: Renderer[Any] = new Renderer[Any] { + override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = + if (indent.isDefined) write.append('\n') else write.append(',') + } + + /** + * A Renderer which renders a newline character when in non-compact mode otherwise it renders a space + */ + lazy val newlineOrSpace: Renderer[Any] = new Renderer[Any] { + override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = + if (indent.isDefined) write.append('\n') else write.append(' ') + } + + private final case class Combined[-A](renderers: List[Renderer[A]]) extends Renderer[A] { + override def unsafeRender(value: A, indent: Option[Int], write: StringBuilder): Unit = + renderers.foreach(_.unsafeRender(value, indent, write)) + } } diff --git a/core/src/main/scala/caliban/rendering/ValueRenderer.scala b/core/src/main/scala/caliban/rendering/ValueRenderer.scala index 2b6962caac..56bfe823b5 100644 --- a/core/src/main/scala/caliban/rendering/ValueRenderer.scala +++ b/core/src/main/scala/caliban/rendering/ValueRenderer.scala @@ -1,6 +1,8 @@ package caliban.rendering -import caliban.Value.{ FloatValue, IntValue, StringValue } +import caliban.Value.FloatValue.{ BigDecimalNumber, DoubleNumber, FloatNumber } +import caliban.Value.IntValue.{ BigIntNumber, IntNumber, LongNumber } +import caliban.Value.StringValue import caliban.{ InputValue, ResponseValue, Value } import scala.annotation.switch @@ -10,27 +12,8 @@ object ValueRenderer { lazy val inputValueRenderer: Renderer[InputValue] = new Renderer[InputValue] { override protected[caliban] def unsafeRender(value: InputValue, indent: Option[Int], write: StringBuilder): Unit = value match { - case InputValue.ListValue(values) => - write += '[' - var first = true - values.foreach { value => - if (first) first = false - else write ++= ", " - unsafeRender(value, indent, write) - } - write += ']' - case InputValue.ObjectValue(fields) => - write += '{' - var first = true - fields.foreach { field => - if (first) first = false - else write ++= ", " - write ++= field._1 - write += ':' - write += ' ' - unsafeRender(field._2, indent, write) - } - write += '}' + case in: InputValue.ListValue => inputListValueRenderer.unsafeRender(in, indent, write) + case in: InputValue.ObjectValue => inputObjectValueRenderer.unsafeRender(in, indent, write) case InputValue.VariableValue(name) => write += '$' write ++= name @@ -39,11 +22,38 @@ object ValueRenderer { unsafeFastEscape(str, write) write += '"' case Value.EnumValue(value) => unsafeFastEscape(value, write) - case Value.BooleanValue(value) => if (value) write ++= "true" else write ++= "false" + case Value.BooleanValue(value) => write append value case Value.NullValue => write ++= "null" - case v => write ++= v.toInputString + case IntNumber(value) => write append value + case LongNumber(value) => write append value + case BigIntNumber(value) => write append value + case FloatNumber(value) => write append value + case DoubleNumber(value) => write append value + case BigDecimalNumber(value) => write append value } + } + + lazy val inputObjectValueRenderer: Renderer[InputValue.ObjectValue] = + Renderer.char('{') ++ DocumentRenderer + .map( + Renderer.string, + inputValueRenderer, + Renderer.char(',') ++ Renderer.space + ) + .contramap[InputValue.ObjectValue](_.fields) ++ Renderer.char('}') + lazy val inputListValueRenderer: Renderer[InputValue.ListValue] = + Renderer.char('[') ++ inputValueRenderer + .list(Renderer.char(',') ++ Renderer.space) + .contramap[InputValue.ListValue](_.values) ++ Renderer.char(']') + + lazy val enumInputValueRenderer: Renderer[Value.EnumValue] = new Renderer[Value.EnumValue] { + override protected[caliban] def unsafeRender( + value: Value.EnumValue, + indent: Option[Int], + write: StringBuilder + ): Unit = + unsafeFastEscape(value.value, write) } lazy val responseValueRenderer: Renderer[ResponseValue] = new Renderer[ResponseValue] { @@ -53,42 +63,60 @@ object ValueRenderer { write: StringBuilder ): Unit = value match { - case ResponseValue.ListValue(values) => - write += '[' - var first = true - values.foreach { value => - if (first) first = false - else write += ',' - unsafeRender(value, indent, write) - } - write += ']' - case ResponseValue.ObjectValue(fields) => - write += '{' - var first = true - fields.foreach { field => - if (first) first = false - else write ++= ", " - write ++= field._1 - write += ':' - write += ' ' - unsafeRender(field._2, indent, write) - } - write += '}' - case StringValue(str) => + case ResponseValue.ListValue(values) => + responseListValueRenderer.unsafeRender(ResponseValue.ListValue(values), indent, write) + case in: ResponseValue.ObjectValue => + responseObjectValueRenderer.unsafeRender(in, indent, write) + case StringValue(str) => write += '"' unsafeFastEscape(str, write) write += '"' - case Value.EnumValue(value) => + case Value.EnumValue(value) => write += '"' unsafeFastEscape(value, write) write += '"' - case Value.BooleanValue(value) => if (value) write ++= "true" else write ++= "false" - case _: ResponseValue.StreamValue => write ++= "" - case Value.NullValue => write ++= "null" - case v => write ++= v.toString + case Value.BooleanValue(value) => write append value + case Value.NullValue => write append "null" + case IntNumber(value) => write append value + case LongNumber(value) => write append value + case FloatNumber(value) => write append value + case DoubleNumber(value) => write append value + case BigDecimalNumber(value) => write append value + case BigIntNumber(value) => write append value + case ResponseValue.StreamValue(_) => write append "" } } + lazy val responseListValueRenderer: Renderer[ResponseValue.ListValue] = + Renderer.char('[') ++ responseValueRenderer + .list(Renderer.char(',') ++ Renderer.space) + .contramap[ResponseValue.ListValue](_.values) ++ Renderer.char(']') + + lazy val responseObjectValueRenderer: Renderer[ResponseValue.ObjectValue] = new Renderer[ResponseValue.ObjectValue] { + override protected[caliban] def unsafeRender( + value: ResponseValue.ObjectValue, + indent: Option[Int], + write: StringBuilder + ): Unit = { + write += '{' + var first = true + value.fields.foreach { field => + if (first) first = false + else { + write += ',' + if (indent.nonEmpty) write += ' ' + } + write += '"' + write ++= field._1 + write += '"' + write += ':' + if (indent.nonEmpty) write += ' ' + responseValueRenderer.unsafeRender(field._2, indent, write) + } + write += '}' + } + } + private def unsafeFastEscape(str: String, write: StringBuilder): Unit = { var i = 0 while (i < str.length) { diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index d67dd109a0..64ed1c05a6 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -169,7 +169,7 @@ object RenderingSpec extends ZIOSpecDefault { test("it should render compact") { val rendered = DocumentRenderer.renderCompact(graphQL(resolver).toDocument) assertTrue( - rendered == """schema{query:Query} "Description of custom scalar emphasizing proper captain ship names" scalar CaptainShipName @tag @specifiedBy(url:"http://someUrl") union Role @uniondirective=Captain|Engineer|Mechanic|Pilot enum Origin @enumdirective{,BELT,EARTH,MARS,MOON @deprecated(reason:"Use: EARTH | MARS | BELT")} input CharacterInput @inputobjdirective{ name:String! @external nicknames:[String!]! @required origin:Origin!}interface Human{ name:String! @external}type Captain{ shipName:CaptainShipName!}type Character implements Human @key(name:"name"){ name:String! @external nicknames:[String!]! @required origin:Origin! role:Role}type Engineer{ shipName:String!}type Mechanic{ shipName:String!}type Narrator implements Human{ name:String!}type Pilot{ shipName:String!}"Queries" type Query{ "Return all characters from a given origin" characters(origin:Origin):[Character!]! character(name:String!):Character @deprecated(reason:"Use `characters`") charactersIn(names:[String!]! @lowercase):[Character!]! exists(character:CharacterInput!):Boolean! human:Human!}""" + rendered == """schema{query:Query} "Description of custom scalar emphasizing proper captain ship names" scalar CaptainShipName @tag @specifiedBy(url:"http://someUrl") union Role @uniondirective=Captain|Engineer|Mechanic|Pilot enum Origin @enumdirective{BELT,EARTH,MARS,MOON @deprecated(reason:"Use: EARTH | MARS | BELT")} input CharacterInput @inputobjdirective{name:String! @external nicknames:[String!]! @required origin:Origin!}interface Human{ name:String! @external}type Captain{ shipName:CaptainShipName!}type Character implements Human @key(name:"name"){ name:String! @external nicknames:[String!]! @required origin:Origin! role:Role}type Engineer{ shipName:String!}type Mechanic{ shipName:String!}type Narrator implements Human{ name:String!}type Pilot{ shipName:String!}"Queries" type Query{ "Return all characters from a given origin" characters(origin:Origin):[Character!]! character(name:String!):Character @deprecated(reason:"Use `characters`") charactersIn(names:[String!]! @lowercase):[Character!]! exists(character:CharacterInput!):Boolean! human:Human!}""" ) } )