Skip to content

Commit

Permalink
codegen: Generate the GQLDeprecated annotation (#2107)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnspade authored Feb 2, 2024
1 parent d8fada1 commit cd13787
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 35 deletions.
7 changes: 7 additions & 0 deletions codegen-sbt/src/sbt-test/codegen/test-compile/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ lazy val root = project
.enablePlugins(CalibanPlugin)
.settings(
libraryDependencies ++= Seq(
"com.github.ghostdogpr" %% "caliban" % Version.pluginVersion,
"com.github.ghostdogpr" %% "caliban-client" % Version.pluginVersion
),
Compile / caliban / calibanSettings ++= Seq(
Expand All @@ -18,6 +19,12 @@ lazy val root = project
.effect("scala.util.Try")
.addDerives(false)
),
calibanSetting(file("src/main/graphql/schema.graphql"))(
_.genType(Codegen.GenType.Schema)
.scalarMapping("Json" -> "String")
.effect("F")
.abstractEffectType(true)
),
calibanSetting(file("src/main/graphql/genview/schema.graphql"))(
_.clientName("Client").packageName("genview").genView(true)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
directive @lazy on FIELD_DEFINITION

# Schema
schema {
query: Q
Expand All @@ -22,6 +24,9 @@ type Q {
# test for scala.Option / Option conflicts
defaultOptionForProductId(id: String!): Option
availableOptionsForProductId(id: String!): [Option!]

# nested @lazy fields
cant: Canterbury!
}

# Input object
Expand Down Expand Up @@ -55,6 +60,15 @@ type Character {
oldNicknames: [String!]! @deprecated
# Deprecated field + comment newline
oldName2: String! @deprecated(reason: "foo\nbar")
# Deprecated field + comment with double quotes and newlines
"""a deprecated field"""
oldName3: String!
@deprecated(reason: """
This field is deprecated for the following reasons:
1. "Outdated data model": The field relies on an outdated data model.
2. "Performance issues": Queries using this field have performance issues.
Please use `name` instead.
""")
}

# Enum
Expand Down Expand Up @@ -93,4 +107,17 @@ type Product {
configuredOption: Option
availableOptions: [Option!]!
specialOrderOption: [Option]
}
}

type Canterbury {
officer: Officer!
}

type Officer {
dossier: Dossier! @lazy
}

type Dossier {
homeWorld: String!
faction: String! @lazy
}
94 changes: 61 additions & 33 deletions tools/src/main/scala/caliban/tools/SchemaWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,24 @@ object SchemaWriter {
def reservedType(typeDefinition: ObjectTypeDefinition): Boolean =
typeDefinition.name == "Query" || typeDefinition.name == "Mutation" || typeDefinition.name == "Subscription"

def containsNestedDirective(
field: FieldDefinition,
directive: String
): Boolean =
typeNameToDefinitionMap.get(Type.innerType(field.ofType)).fold(false) { t =>
hasFieldWithDirective(t, directive)
} || typeNameToNestedFields
.getOrElse(Type.innerType(field.ofType), List.empty)
.exists(t => hasFieldWithDirective(t, directive))

def writeRootField(field: FieldDefinition, od: ObjectTypeDefinition): String = {
val argsTypeName = if (field.args.nonEmpty) s" ${argsName(field, od)} =>" else ""
s"${safeName(field.name)} :$argsTypeName ${writeEffectType(field.ofType)}"
val fieldType =
if (isEffectTypeAbstract && containsNestedDirective(field, LazyDirective))
writeParameterizedType(field.ofType)
else
writeType(field.ofType)
s"${safeName(field.name)} :$argsTypeName $effect[$fieldType]"
}

def isAbstractEffectful(typedef: ObjectTypeDefinition): Boolean =
Expand All @@ -80,7 +95,7 @@ object SchemaWriter {

def writeRootQueryOrMutationDef(op: ObjectTypeDefinition): String =
s"""
|${writeDescription(op.description)}final case class ${op.name}${generic(op, isRootDefinition = true)}(
|${writeTypeAnnotations(op)}final case class ${op.name}${generic(op, isRootDefinition = true)}(
|${op.fields.map(c => writeRootField(c, op)).mkString(",\n")}
|)$derivesSchema""".stripMargin

Expand All @@ -93,7 +108,7 @@ object SchemaWriter {

def writeRootSubscriptionDef(op: ObjectTypeDefinition): String =
s"""
|${writeDescription(op.description)}final case class ${op.name}(
|${writeTypeAnnotations(op)}final case class ${op.name}(
|${op.fields.map(c => writeSubscriptionField(c, op)).mkString(",\n")}
|)$derivesSchema""".stripMargin

Expand All @@ -102,26 +117,26 @@ object SchemaWriter {
case Nil => ""
case nonEmpty => s" extends ${nonEmpty.mkString(" with ")}"
}
s"""${writeDescription(typedef.description)}final case class ${typedef.name}${generic(typedef)}(${typedef.fields
s"""${writeTypeAnnotations(typedef)}final case class ${typedef.name}${generic(typedef)}(${typedef.fields
.map(field => writeField(field, inheritedFromInterface(typedef, field).getOrElse(typedef), isMethod = false))
.mkString(", ")})$extendRendered$derivesSchema"""
}

def writeInputObject(typedef: InputObjectTypeDefinition): String = {
val name = typedef.name
val maybeAnnotation = if (preserveInputNames) s"""@GQLInputName("$name")\n""" else ""
s"""$maybeAnnotation${writeDescription(typedef.description)}final case class $name(${typedef.fields
s"""$maybeAnnotation${writeTypeAnnotations(typedef)}final case class $name(${typedef.fields
.map(writeInputValue)
.mkString(", ")})$derivesSchemaAndArgBuilder"""
}

def writeEnum(typedef: EnumTypeDefinition): String =
s"""${writeDescription(typedef.description)}sealed trait ${typedef.name} extends scala.Product with scala.Serializable$derivesSchemaAndArgBuilder
s"""${writeTypeAnnotations(typedef)}sealed trait ${typedef.name} extends scala.Product with scala.Serializable$derivesSchemaAndArgBuilder

object ${typedef.name} {
${typedef.enumValuesDefinition
.map(v =>
s"${writeDescription(v.description)}case object ${safeName(v.enumValue)} extends ${typedef.name}$derivesSchemaAndArgBuilder"
s"${writeEnumAnnotations(v)}case object ${safeName(v.enumValue)} extends ${typedef.name}$derivesSchemaAndArgBuilder"
)
.mkString("\n")}
}
Expand All @@ -131,28 +146,18 @@ object SchemaWriter {
unions.map(x => writeUnionSealedTrait(x)).mkString("\n")

def writeUnionSealedTrait(union: UnionTypeDefinition): String =
s"""${writeDescription(
union.description
s"""${writeTypeAnnotations(
union
)}sealed trait ${union.name} extends scala.Product with scala.Serializable$derivesSchema"""

def writeInterface(interface: InterfaceTypeDefinition): String =
s"""@GQLInterface
${writeDescription(interface.description)}sealed trait ${interface.name} extends scala.Product with scala.Serializable $derivesSchema {
${writeTypeAnnotations(interface)}sealed trait ${interface.name} extends scala.Product with scala.Serializable $derivesSchema {
${interface.fields.map(field => writeField(field, interface, isMethod = true)).mkString("\n")}
}
"""

def writeField(inputField: FieldDefinition, of: TypeDefinition, isMethod: Boolean): String = {
def containsNestedDirective(
field: FieldDefinition,
directive: String
): Boolean =
typeNameToDefinitionMap.get(Type.innerType(field.ofType)).fold(false) { t =>
hasFieldWithDirective(t, directive)
} || typeNameToNestedFields
.getOrElse(Type.innerType(field.ofType), List.empty)
.exists(t => hasFieldWithDirective(t, directive))

val field = resolveNewTypeFieldDef(inputField).getOrElse(inputField)

val fieldIsEffectWrapped = field.directives.exists(_.name == LazyDirective)
Expand All @@ -167,11 +172,11 @@ object SchemaWriter {
val GQLNewTypeDirective = writeGQLNewTypeDirective(field.directives)

if (field.args.nonEmpty) {
s"""$GQLNewTypeDirective${writeDescription(field.description)}${if (isMethod) "def " else ""}${safeName(
s"""$GQLNewTypeDirective${writeFieldAnnotations(field)}${if (isMethod) "def " else ""}${safeName(
field.name
)} : ${argsName(field, of)} => $fieldType"""
} else {
s"""$GQLNewTypeDirective${writeDescription(field.description)}${if (isMethod) "def " else ""}${safeName(
s"""$GQLNewTypeDirective${writeFieldAnnotations(field)}${if (isMethod) "def " else ""}${safeName(
field.name
)} : $fieldType"""
}
Expand All @@ -190,7 +195,7 @@ object SchemaWriter {

val inputDef = resolveNewTypeInputDef(value).getOrElse(value)

s"""$GQLNewTypeInputDirective${writeDescription(inputDef.description)}${safeName(inputDef.name)} : ${writeType(
s"""$GQLNewTypeInputDirective${writeInputAnnotations(inputDef)}${safeName(inputDef.name)} : ${writeType(
inputDef.ofType
)}"""
}
Expand Down Expand Up @@ -251,18 +256,41 @@ object SchemaWriter {
def escapeDoubleQuotes(input: String): String =
input.replace("\"", "\\\"")

def writeDescription(description: Option[String]): String =
description.fold("") {
case d if d.contains("\n") =>
s"""@GQLDescription(\"\"\"${escapeDoubleQuotes(d)}\"\"\")
|""".stripMargin
case d =>
s"""@GQLDescription("${escapeDoubleQuotes(d)}")
|""".stripMargin
def writeTypeAnnotations(definition: TypeDefinition): String =
writeDescriptionAndDeprecation(definition.description, definition.directives)

def writeFieldAnnotations(definition: FieldDefinition): String =
writeDescriptionAndDeprecation(definition.description, definition.directives)

def writeInputAnnotations(definition: InputValueDefinition): String =
writeDescriptionAndDeprecation(definition.description, definition.directives)

def writeEnumAnnotations(definition: EnumValueDefinition): String =
writeDescriptionAndDeprecation(definition.description, definition.directives)

def writeDescriptionAndDeprecation(description: Option[String], directives: List[Directive]): String =
s"${writeDescription(description)} ${writeDeprecation(directives)}"

def escapeAndWrap(value: String, annotation: String): String = {
val escapedValue = escapeDoubleQuotes(value)
if (escapedValue.contains("\n")) {
s"""@$annotation(\"\"\"$escapedValue\"\"\")
|""".stripMargin
} else {
s"""@$annotation("$escapedValue")
|""".stripMargin
}
}

def writeEffectType(t: Type) =
s"$effect[${writeType(t)}]"
def writeDeprecation(directives: List[Directive]): String =
Directives.deprecationReason(directives).fold("") { d =>
escapeAndWrap(d, "GQLDeprecated")
}

def writeDescription(description: Option[String]): String =
description.fold("") { d =>
escapeAndWrap(d, "GQLDescription")
}

def writeType(t: Type): String = {
def write(name: String): String = scalarMappings
Expand Down
61 changes: 60 additions & 1 deletion tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,51 @@ object SchemaWriterSpec extends ZIOSpecDefault {
|
|}"""
),
(
"GQLDeprecated with reason",
gen(s"""
type Captain {
"foo" shipName: String! @deprecated(reason: "bar")
}
"""),
"""import caliban.schema.Annotations._
|
|object Types {
|
| final case class Captain(
| @GQLDescription("foo")
| @GQLDeprecated("bar")
| shipName: String
| )
|
|}"""
),
(
"GQLDeprecated with multiline reason and escaped quotes",
gen(s"""
type ExampleType {
oldField: String @deprecated(reason: \"\"\"
This field is deprecated for the following reasons:
1. "Outdated data model": The field relies on an outdated data model.
2. "Performance issues": Queries using this field have performance issues.
Please use `newField` instead.
\"\"\")
newField: String
}

"""),
"""import caliban.schema.Annotations._
|
|object Types {
|
| final case class ExampleType(
| @GQLDeprecated(""" + "\"\"\"This field is deprecated for the following reasons:\n1. \\\"Outdated data model\\\": The field relies on an outdated data model.\n2. \\\"Performance issues\\\": Queries using this field have performance issues.\nPlease use `newField` instead." + "\"\"\")" + """
| oldField: scala.Option[String],
| newField: scala.Option[String]
| )
|
|}"""
),
(
"schema",
gen("""
Expand Down Expand Up @@ -712,16 +757,30 @@ object SchemaWriterSpec extends ZIOSpecDefault {
|type Baz {
| x: String!
| y: String! @lazy
|}
|
|type Query {
| foo: Foo!
|}""",
effect = "F",
isEffectTypeAbstract = true
),
"""object Types {
"""import Types._
|
|object Types {
|
| final case class Foo[F[_]](bar: Bar[F])
| final case class Bar[F[_]](baz: F[Baz[F]])
| final case class Baz[F[_]](x: String, y: F[String])
|
|}
|
|object Operations {
|
| final case class Query[F[_]](
| foo: F[Foo[F]]
| )
|
|}"""
),
(
Expand Down

0 comments on commit cd13787

Please sign in to comment.