diff --git a/build.sbt b/build.sbt index 20526874e..75c7d72cd 100644 --- a/build.sbt +++ b/build.sbt @@ -218,7 +218,7 @@ lazy val tools = project "dev.zio" %% "zio-json" % zioJsonVersion % Test ) ) - .dependsOn(core, clientJVM) + .dependsOn(core, clientJVM, quickAdapter % Test) lazy val tracing = project .in(file("tracing")) diff --git a/client/src/main/scala/caliban/client/IntrospectionClient.scala b/client/src/main/scala/caliban/client/IntrospectionClient.scala index abf7a76d4..f3d8a6997 100644 --- a/client/src/main/scala/caliban/client/IntrospectionClient.scala +++ b/client/src/main/scala/caliban/client/IntrospectionClient.scala @@ -150,8 +150,17 @@ object IntrospectionClient { arguments = List(Argument("includeDeprecated", includeDeprecated, "Boolean")) ) def inputFields[A](innerSelection: SelectionBuilder[__InputValue, A]): SelectionBuilder[__Type, Option[List[A]]] = - Field("inputFields", OptionOf(ListOf(Obj(innerSelection)))) - def ofType[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__Type, Option[A]] = + inputFields(None)(innerSelection) + def inputFields[A](includeDeprecated: Option[Boolean])( + innerSelection: SelectionBuilder[__InputValue, A] + ): SelectionBuilder[__Type, Option[List[A]]] = + Field( + "inputFields", + OptionOf(ListOf(Obj(innerSelection))), + arguments = List(Argument("includeDeprecated", includeDeprecated, "Boolean")) + ) + + def ofType[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__Type, Option[A]] = Field("ofType", OptionOf(Obj(innerSelection))) } @@ -160,7 +169,15 @@ object IntrospectionClient { def name: SelectionBuilder[__Field, String] = Field("name", Scalar()) def description: SelectionBuilder[__Field, Option[String]] = Field("description", OptionOf(Scalar())) def args[A](innerSelection: SelectionBuilder[__InputValue, A]): SelectionBuilder[__Field, List[A]] = - Field("args", ListOf(Obj(innerSelection))) + args(None)(innerSelection) + def args[A]( + includeDeprecated: Option[Boolean] + )(innerSelection: SelectionBuilder[__InputValue, A]): SelectionBuilder[__Field, List[A]] = + Field( + "args", + ListOf(Obj(innerSelection)), + arguments = List(Argument("includeDeprecated", includeDeprecated, "Boolean")) + ) def `type`[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__Field, A] = Field("type", Obj(innerSelection)) def isDeprecated: SelectionBuilder[__Field, Boolean] = Field("isDeprecated", Scalar()) @@ -173,6 +190,9 @@ object IntrospectionClient { def description: SelectionBuilder[__InputValue, Option[String]] = Field("description", OptionOf(Scalar())) def `type`[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__InputValue, A] = Field("type", Obj(innerSelection)) + def isDeprecated: SelectionBuilder[__InputValue, Boolean] = Field("isDeprecated", Scalar()) + def deprecationReason: SelectionBuilder[__InputValue, Option[String]] = + Field("deprecationReason", OptionOf(Scalar())) def defaultValue: SelectionBuilder[__InputValue, Option[String]] = Field("defaultValue", OptionOf(Scalar())) } diff --git a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala index 18288ea43..06a49dc5b 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala @@ -24,6 +24,7 @@ sealed trait CalibanSettings { final def effect(effect: String): Self = withSettings(_.effect(effect)) final def abstractEffectType(abstractEffectType: Boolean): Self = withSettings(_.abstractEffectType(abstractEffectType)) + final def supportDeprecatedArgs(value: Boolean): Self = withSettings(_.supportDeprecatedArgs(value)) final def supportIsRepeatable(value: Boolean): Self = withSettings(_.supportIsRepeatable(value)) final def preserveInputNames(value: Boolean): Self = withSettings(_.preserveInputNames(value)) final def addDerives(value: Boolean): Self = withSettings(_.addDerives(value)) diff --git a/codegen-sbt/src/main/scala/caliban/codegen/OptionsParser.scala b/codegen-sbt/src/main/scala/caliban/codegen/OptionsParser.scala index db80b5aa7..d1db0acd2 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/OptionsParser.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/OptionsParser.scala @@ -23,7 +23,8 @@ object OptionsParser { supportIsRepeatable: Option[Boolean], addDerives: Option[Boolean], envForDerives: Option[String], - excludeDeprecated: Option[Boolean] + excludeDeprecated: Option[Boolean], + supportDeprecatedArgs: Option[Boolean] ) private object DescriptorUtils { @@ -76,7 +77,8 @@ object OptionsParser { rawOpts.supportIsRepeatable, rawOpts.addDerives, rawOpts.envForDerives, - rawOpts.excludeDeprecated + rawOpts.excludeDeprecated, + rawOpts.supportDeprecatedArgs ) }.option case _ => ZIO.none diff --git a/codegen-sbt/src/test/scala/caliban/codegen/OptionsParserSpec.scala b/codegen-sbt/src/test/scala/caliban/codegen/OptionsParserSpec.scala index eb09dce6c..0881b36b4 100644 --- a/codegen-sbt/src/test/scala/caliban/codegen/OptionsParserSpec.scala +++ b/codegen-sbt/src/test/scala/caliban/codegen/OptionsParserSpec.scala @@ -31,6 +31,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -61,6 +62,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -92,6 +94,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -141,6 +144,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -172,6 +176,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -203,6 +208,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -234,6 +240,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -265,6 +272,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -296,6 +304,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -327,6 +336,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -358,6 +368,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -389,6 +400,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -420,6 +432,7 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, + None, None ) ) @@ -451,7 +464,40 @@ object OptionsParserSpec extends ZIOSpecDefault { None, None, None, - Some(true) + Some(true), + None + ) + ) + ) + } + }, + test("provide supportDeprecatedArgs & supportIsRepeatable") { + val input = List("schema", "output", "--supportDeprecatedArgs", "false", "--supportIsRepeatable", "false") + OptionsParser.fromArgs(input).map { result => + assertTrue( + result == + Some( + Options( + "schema", + "output", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(false), + None, + None, + None, + Some(false) ) ) ) diff --git a/core/src/main/scala/caliban/parsing/adt/Definition.scala b/core/src/main/scala/caliban/parsing/adt/Definition.scala index 60d921620..e9526aeac 100644 --- a/core/src/main/scala/caliban/parsing/adt/Definition.scala +++ b/core/src/main/scala/caliban/parsing/adt/Definition.scala @@ -81,7 +81,11 @@ object Definition { def name: String def description: Option[String] def directives: List[Directive] + + final def isDeprecated: Boolean = Directives.isDeprecated(directives) + final def deprecationReason: Option[String] = Directives.deprecationReason(directives) } + object TypeDefinition { final case class ObjectTypeDefinition( @@ -142,7 +146,10 @@ object Definition { ofType: Type, defaultValue: Option[InputValue], directives: List[Directive] - ) + ) { + def isDeprecated: Boolean = Directives.isDeprecated(directives) + def deprecationReason: Option[String] = Directives.deprecationReason(directives) + } final case class FieldDefinition( description: Option[String], @@ -150,9 +157,19 @@ object Definition { args: List[InputValueDefinition], ofType: Type, directives: List[Directive] - ) + ) { + def isDeprecated: Boolean = Directives.isDeprecated(directives) + def deprecationReason: Option[String] = Directives.deprecationReason(directives) + } - final case class EnumValueDefinition(description: Option[String], enumValue: String, directives: List[Directive]) + final case class EnumValueDefinition( + description: Option[String], + enumValue: String, + directives: List[Directive] + ) { + def isDeprecated: Boolean = Directives.isDeprecated(directives) + def deprecationReason: Option[String] = Directives.deprecationReason(directives) + } } diff --git a/tools/src/main/scala/caliban/tools/CalibanCommonSettings.scala b/tools/src/main/scala/caliban/tools/CalibanCommonSettings.scala index 3c36490b1..9b54f78c8 100644 --- a/tools/src/main/scala/caliban/tools/CalibanCommonSettings.scala +++ b/tools/src/main/scala/caliban/tools/CalibanCommonSettings.scala @@ -20,7 +20,8 @@ final case class CalibanCommonSettings( supportIsRepeatable: Option[Boolean], addDerives: Option[Boolean], envForDerives: Option[String], - excludeDeprecated: Option[Boolean] + excludeDeprecated: Option[Boolean], + supportDeprecatedArgs: Option[Boolean] ) { private[caliban] def toOptions(schemaPath: String, toPath: String): Options = @@ -43,7 +44,8 @@ final case class CalibanCommonSettings( supportIsRepeatable = supportIsRepeatable, addDerives = addDerives, envForDerives = envForDerives, - excludeDeprecated = excludeDeprecated + excludeDeprecated = excludeDeprecated, + supportDeprecatedArgs = supportDeprecatedArgs ) private[caliban] def combine(r: => CalibanCommonSettings): CalibanCommonSettings = @@ -65,7 +67,8 @@ final case class CalibanCommonSettings( supportIsRepeatable = r.supportIsRepeatable.orElse(this.supportIsRepeatable), addDerives = r.addDerives.orElse(this.addDerives), envForDerives = r.envForDerives.orElse(this.envForDerives), - excludeDeprecated = r.excludeDeprecated.orElse(this.excludeDeprecated) + excludeDeprecated = r.excludeDeprecated.orElse(this.excludeDeprecated), + supportDeprecatedArgs = r.supportDeprecatedArgs.orElse(this.supportDeprecatedArgs) ) def clientName(value: String): CalibanCommonSettings = this.copy(clientName = Some(value)) @@ -90,6 +93,8 @@ final case class CalibanCommonSettings( def addDerives(addDerives: Boolean): CalibanCommonSettings = this.copy(addDerives = Some(addDerives)) def envForDerives(envForDerives: String): CalibanCommonSettings = this.copy(envForDerives = Some(envForDerives)) def excludeDeprecated(value: Boolean): CalibanCommonSettings = this.copy(excludeDeprecated = Some(value)) + def supportDeprecatedArgs(value: Boolean): CalibanCommonSettings = + this.copy(supportDeprecatedArgs = Some(value)) } object CalibanCommonSettings { @@ -112,6 +117,7 @@ object CalibanCommonSettings { supportIsRepeatable = None, addDerives = None, envForDerives = None, - excludeDeprecated = None + excludeDeprecated = None, + supportDeprecatedArgs = None ) } diff --git a/tools/src/main/scala/caliban/tools/ClientWriter.scala b/tools/src/main/scala/caliban/tools/ClientWriter.scala index 3905f6e8f..48c99f7ac 100644 --- a/tools/src/main/scala/caliban/tools/ClientWriter.scala +++ b/tools/src/main/scala/caliban/tools/ClientWriter.scala @@ -125,17 +125,7 @@ object ClientWriter { } val deprecated = field.directives.find(_.name == "deprecated") match { case None => "" - case Some(directive) => - val body = - directive.arguments.collectFirst { case ("reason", StringValue(reason)) => - reason - }.getOrElse("") - - val quotes = - if (body.contains("\n")) tripleQuotes - else doubleQuotes - - "@deprecated(" + quotes + body + quotes + """, "")""" + "\n" + case Some(directive) => writeDeprecated(Directives.deprecationReason(directive :: Nil)) } val fieldType = safeTypeName(getTypeName(field.ofType)) val isScalar = typesMap @@ -237,18 +227,19 @@ object ClientWriter { writeTypeBuilder(field.ofType, "Obj(innerSelection)") ) } - val args = field.args match { + val filteredArgs = if (excludeDeprecated) field.args.filterNot(_.isDeprecated) else field.args + val args = filteredArgs match { case Nil => "" case list => s"(${writeArgumentFields(list)})" } - val argBuilder = field.args match { + val argBuilder = filteredArgs match { case Nil => "" case list => s", arguments = List(${list.zipWithIndex.map { case (arg, idx) => s"""Argument("${arg.name}", ${safeName(arg.name)}, "${arg.ofType.toString}")(encoder$idx)""" }.mkString(", ")})" } - val implicits = field.args match { + val implicits = filteredArgs match { case Nil => "" case list => s"(implicit ${list.zipWithIndex.map { case (arg, idx) => @@ -269,7 +260,7 @@ object ClientWriter { outputType, interfaceTypes.map(_.name), unionTypes.map(_.name), - field.args, + filteredArgs, owner ) FieldInfo( @@ -434,10 +425,10 @@ object ClientWriter { s"type $objectName" } - def writeObject(typedef: ObjectTypeDefinition, genView: Boolean, excludeDeprecated: Boolean): String = { + def writeObject(typedef: ObjectTypeDefinition, genView: Boolean): String = { val allFields = if (excludeDeprecated) - typedef.fields.filterNot(field => field.directives.find(_.name == "deprecated").isDefined) + typedef.fields.filterNot(_.isDeprecated) else typedef.fields @@ -637,7 +628,8 @@ object ClientWriter { typedef: InputObjectTypeDefinition ): String = { val inputObjectName = safeTypeName(typedef.name) - val formattedFields = typedef.fields + val fields = if (excludeDeprecated) typedef.fields.filterNot(_.isDeprecated) else typedef.fields + val formattedFields = fields .map(f => s""""${f.name}" -> ${writeInputValue( f.ofType, @@ -645,7 +637,7 @@ object ClientWriter { inputObjectName )}""" ) - s"""final case class $inputObjectName(${writeArgumentFields(typedef.fields)}) + s"""final case class $inputObjectName(${writeArgumentFields(fields)}) |object $inputObjectName { | implicit val encoder: ArgEncoder[$inputObjectName] = new ArgEncoder[$inputObjectName] { | override def encode(value: $inputObjectName): __Value = @@ -762,8 +754,12 @@ object ClientWriter { def writeArgumentFields( args: List[InputValueDefinition] - ): String = - s"${args.map(arg => s"${safeName(arg.name)} : ${writeType(arg.ofType)}${writeDefaultArgument(arg)}").mkString(", ")}" + ): String = { + def maybeDeprecated(arg: InputValueDefinition) = + if (arg.isDeprecated) writeDeprecated(arg.deprecationReason) else "" + + s"${args.map(arg => s"${maybeDeprecated(arg)}${safeName(arg.name)} : ${writeType(arg.ofType)}${writeDefaultArgument(arg)}").mkString(", ")}" + } def writeDefaultArgument(arg: InputValueDefinition): String = arg.ofType match { @@ -772,6 +768,16 @@ object ClientWriter { case _ => "" } + def writeDeprecated(reason: Option[String]): String = + reason match { + case None => + "@deprecated\n" + case Some(body) if body.contains("\n") => + s"@deprecated($tripleQuotes$body$tripleQuotes)\n" + case Some(body) => + s"@deprecated($doubleQuotes$body$doubleQuotes)\n" + } + def writeType(t: Type): String = t match { case NamedType(name, true) => safeTypeName(name) case NamedType(name, false) => s"scala.Option[${safeTypeName(name)}]" @@ -826,7 +832,7 @@ object ClientWriter { directives = typedef.directives, fields = typedef.fields ) - val content = writeObject(objDef, genView, excludeDeprecated) + val content = writeObject(objDef, genView) val fullContent = if (splitFiles) s"""import caliban.client.FieldBuilder._ @@ -861,7 +867,7 @@ object ClientWriter { schemaDef.exists(_.subscription.getOrElse("Subscription") == obj.name) ) .map { typedef => - val content = writeObject(typedef, genView, excludeDeprecated) + val content = writeObject(typedef, genView) val fullContent = if (splitFiles) s"""import caliban.client.FieldBuilder._ @@ -897,7 +903,7 @@ object ClientWriter { .map { case typedef if excludeDeprecated => val valuesWithoutDeprecated = - typedef.enumValuesDefinition.filterNot(value => value.directives.find(_.name == "deprecated").isDefined) + typedef.enumValuesDefinition.filterNot(_.isDeprecated) typedef.copy(enumValuesDefinition = valuesWithoutDeprecated) case typedef => typedef diff --git a/tools/src/main/scala/caliban/tools/Codegen.scala b/tools/src/main/scala/caliban/tools/Codegen.scala index 0df693b51..a8455ebc5 100644 --- a/tools/src/main/scala/caliban/tools/Codegen.scala +++ b/tools/src/main/scala/caliban/tools/Codegen.scala @@ -80,7 +80,16 @@ object Codegen { def generate(arguments: Options, genType: GenType): Task[List[File]] = generate( - getSchemaLoader(arguments.schemaPath, arguments.headers, arguments.supportIsRepeatable.getOrElse(true)), + getSchemaLoader( + arguments.schemaPath, + arguments.headers, { + val default = IntrospectionClient.Config.default + IntrospectionClient.Config( + supportDeprecatedArgs = arguments.supportDeprecatedArgs.getOrElse(default.supportDeprecatedArgs), + supportIsRepeatable = arguments.supportIsRepeatable.getOrElse(default.supportIsRepeatable) + ) + } + ), arguments, genType ) @@ -88,9 +97,9 @@ object Codegen { private def getSchemaLoader( path: String, schemaPathHeaders: Option[List[Options.Header]], - supportIsRepeatable: Boolean + config: IntrospectionClient.Config ): SchemaLoader = - if (path.startsWith("http")) SchemaLoader.fromIntrospection(path, schemaPathHeaders, supportIsRepeatable) + if (path.startsWith("http")) SchemaLoader.fromIntrospection(path, schemaPathHeaders, config) else SchemaLoader.fromFile(path) def getPackageAndObjectName(arguments: Options): (Option[String], String) = { diff --git a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala index 9f5975289..f93502f9f 100644 --- a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala +++ b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala @@ -18,14 +18,28 @@ import zio.{ RIO, ZIO } object IntrospectionClient { + def introspect( + uri: String, + headers: Option[List[Options.Header]] + ): RIO[SttpClient, Document] = + introspect(uri, headers, Config.default) + + @deprecated("Use overloaded method that accepts a config instead", "2.8.2") def introspect( uri: String, headers: Option[List[Options.Header]], supportIsRepeatable: Boolean = true + ): RIO[SttpClient, Document] = + introspect(uri, headers, Config.default.supportIsRepeatable(supportIsRepeatable)) + + def introspect( + uri: String, + headers: Option[List[Options.Header]], + config: IntrospectionClient.Config ): RIO[SttpClient, Document] = for { parsedUri <- ZIO.fromEither(Uri.parse(uri)).mapError(cause => new Exception(s"Invalid URL: $cause")) - baseReq = introspection(supportIsRepeatable).toRequest(parsedUri) + baseReq = introspection(config).toRequest(parsedUri, dropNullInputValues = true) req = headers.map(_.map(h => h.name -> h.value).toMap).fold(baseReq)(baseReq.headers) result <- sendRequest(req) } yield result @@ -55,10 +69,12 @@ object IntrospectionClient { name: String, description: Option[String], `type`: Type, - defaultValue: Option[String] + defaultValue: Option[String], + isDeprecated: Boolean, + deprecationReason: Option[String] ): InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) - InputValueDefinition(description, name, `type`, default, Nil) + InputValueDefinition(description, name, `type`, default, directives(isDeprecated, deprecationReason)) } private def mapTypeRef(kind: __TypeKind, name: Option[String], of: Option[Type]): Type = @@ -199,27 +215,39 @@ object IntrospectionClient { }).mapN(mapTypeRef _) }).mapN(mapTypeRef _) - private val inputValue: SelectionBuilder[__InputValue, InputValueDefinition] = - ( - __InputValue.name ~ - __InputValue.description ~ - __InputValue.`type`(typeRef) ~ - __InputValue.defaultValue - ).mapN(mapInputValue _) + private def inputValue(implicit + config: Config + ): SelectionBuilder[__InputValue, InputValueDefinition] = + if (config.supportDeprecatedArgs) + ( + __InputValue.name ~ + __InputValue.description ~ + __InputValue.`type`(typeRef) ~ + __InputValue.defaultValue ~ + __InputValue.isDeprecated ~ + __InputValue.deprecationReason + ).mapN(mapInputValue _) + else + ( + __InputValue.name ~ + __InputValue.description ~ + __InputValue.`type`(typeRef) ~ + __InputValue.defaultValue + ).mapN(mapInputValue(_, _, _, _, isDeprecated = false, None)) - private val fullType: SelectionBuilder[__Type, Option[TypeDefinition]] = + private def fullType(implicit config: Config): SelectionBuilder[__Type, Option[TypeDefinition]] = (__Type.kind ~ __Type.name ~ __Type.description ~ __Type.fields(Some(true)) { (__Field.name ~ __Field.description ~ - __Field.args(inputValue) ~ + __Field.args(if (config.supportDeprecatedArgs) Some(true) else None)(inputValue) ~ __Field.`type`(typeRef) ~ __Field.isDeprecated ~ __Field.deprecationReason).mapN(mapField _) } ~ - __Type.inputFields(inputValue) ~ + __Type.inputFields(if (config.supportDeprecatedArgs) Some(true) else None)(inputValue) ~ __Type.interfaces(typeRef) ~ __Type.enumValues(Some(true)) { (__EnumValue.name ~ @@ -229,14 +257,20 @@ object IntrospectionClient { } ~ __Type.possibleTypes(typeRef)).mapN(mapType _) - def introspection(supportIsRepeatable: Boolean): SelectionBuilder[RootQuery, Document] = + @deprecated("Use overloaded method that accepts a list of experimental features", "2.8.2") + def introspection(supportIsRepeatable: Boolean): SelectionBuilder[RootQuery, Document] = { + val cfg = Config.default.supportIsRepeatable(supportIsRepeatable) + introspection(cfg) + } + + def introspection(implicit config: Config): SelectionBuilder[RootQuery, Document] = Query.__schema { (__Schema.queryType(__Type.name) ~ __Schema.mutationType(__Type.name) ~ __Schema.subscriptionType(__Type.name)).mapN(mapSchema _) ~ __Schema.types(fullType).map(_.flatten.filterNot(_.name.startsWith("__"))) ~ __Schema.directives { - if (supportIsRepeatable) + if (config.supportIsRepeatable) (__Directive.name ~ __Directive.description ~ __Directive.locations ~ @@ -249,4 +283,26 @@ object IntrospectionClient { __Directive.args(inputValue)).mapN(mapDirective(_, _, _, _, isRepeatable = false)) } }.map { case (schema, types, directives) => Document(schema :: types ++ directives, SourceMapper.empty) } + + final class Config( + val supportDeprecatedArgs: Boolean = true, + val supportIsRepeatable: Boolean = true + ) { + + def supportDeprecatedArgs(value: Boolean): Config = + new Config(supportDeprecatedArgs = value, supportIsRepeatable = supportIsRepeatable) + + def supportIsRepeatable(value: Boolean): Config = + new Config(supportDeprecatedArgs = supportDeprecatedArgs, supportIsRepeatable = value) + + override def toString: String = + s"Config(supportDeprecatedArgs = $supportDeprecatedArgs, supportIsRepeatable = $supportIsRepeatable)" + } + + object Config { + val default: Config = new Config() + + def apply(supportDeprecatedArgs: Boolean, supportIsRepeatable: Boolean): Config = + new Config(supportDeprecatedArgs, supportIsRepeatable) + } } diff --git a/tools/src/main/scala/caliban/tools/Options.scala b/tools/src/main/scala/caliban/tools/Options.scala index 6dfa9b2ed..7d412a9bb 100644 --- a/tools/src/main/scala/caliban/tools/Options.scala +++ b/tools/src/main/scala/caliban/tools/Options.scala @@ -19,7 +19,8 @@ final case class Options( supportIsRepeatable: Option[Boolean], addDerives: Option[Boolean], envForDerives: Option[String], - excludeDeprecated: Option[Boolean] + excludeDeprecated: Option[Boolean], + supportDeprecatedArgs: Option[Boolean] ) object Options { diff --git a/tools/src/main/scala/caliban/tools/RemoteSchema.scala b/tools/src/main/scala/caliban/tools/RemoteSchema.scala index bfe3465b7..1ec70e779 100644 --- a/tools/src/main/scala/caliban/tools/RemoteSchema.scala +++ b/tools/src/main/scala/caliban/tools/RemoteSchema.scala @@ -181,6 +181,8 @@ object RemoteSchema { name = definition.name, description = definition.description, `type` = toType(definition.ofType, definitions), + isDeprecated = isDeprecated(definition.directives), + deprecationReason = deprecationReason(definition.directives), defaultValue = definition.defaultValue.map(_.toInputString), directives = toDirectives(definition.directives) ) @@ -266,8 +268,7 @@ object RemoteSchema { case e: EnumTypeDefinition => toEnumType(e) case u: UnionTypeDefinition => toUnionType(u, definitions) case i: InterfaceTypeDefinition => toInterfaceType(i, definitions) - case i: InputObjectTypeDefinition => - toInputObjectType(i, definitions) + case i: InputObjectTypeDefinition => toInputObjectType(i, definitions) } private def toDirective( diff --git a/tools/src/main/scala/caliban/tools/SchemaLoader.scala b/tools/src/main/scala/caliban/tools/SchemaLoader.scala index d0e9daf2b..0d3d47bb7 100644 --- a/tools/src/main/scala/caliban/tools/SchemaLoader.scala +++ b/tools/src/main/scala/caliban/tools/SchemaLoader.scala @@ -27,23 +27,55 @@ object SchemaLoader { case class FromString private[SchemaLoader] (schema: String) extends SchemaLoader { override def load: Task[Document] = ZIO.fromEither(Parser.parseQuery(schema)) } + + @deprecated("Use FromIntrospectionV2", "2.8.2") case class FromIntrospection private[SchemaLoader] ( url: String, headers: Option[List[Options.Header]], supportIsRepeatable: Boolean + ) extends SchemaLoader { + private val proxy = fromIntrospectionWith(url, headers)(_.supportIsRepeatable(supportIsRepeatable)) + override def load: Task[Document] = proxy.load + } + + case class FromIntrospectionV2 private[SchemaLoader] ( + url: String, + headers: Option[List[Options.Header]], + config: IntrospectionClient.Config ) extends SchemaLoader { override def load: Task[Document] = - IntrospectionClient.introspect(url, headers, supportIsRepeatable).provideLayer(HttpClientZioBackend.layer()) + IntrospectionClient.introspect(url, headers, config).provideLayer(HttpClientZioBackend.layer()) } def fromCaliban[R](api: GraphQL[R]): SchemaLoader = FromCaliban(api) def fromDocument(doc: Document): SchemaLoader = FromDocument(doc) def fromFile(path: String): SchemaLoader = FromFile(path) def fromString(schema: String): SchemaLoader = FromString(schema) + + def fromIntrospection( + url: String, + headers: Option[List[Options.Header]] + ): SchemaLoader = + fromIntrospectionWith(url, headers)(identity) + + @deprecated("Use overloaded method providing a config instead") def fromIntrospection( url: String, headers: Option[List[Options.Header]], supportIsRepeatable: Boolean = true ): SchemaLoader = - FromIntrospection(url, headers, supportIsRepeatable) + fromIntrospectionWith(url, headers)(_.supportIsRepeatable(supportIsRepeatable)) + + def fromIntrospection( + url: String, + headers: Option[List[Options.Header]], + config: IntrospectionClient.Config + ): SchemaLoader = + FromIntrospectionV2(url, headers, config) + + def fromIntrospectionWith( + url: String, + headers: Option[List[Options.Header]] + )(config: IntrospectionClient.Config => IntrospectionClient.Config): SchemaLoader = + fromIntrospection(url, headers, config(IntrospectionClient.Config.default)) } diff --git a/tools/src/main/scala/caliban/tools/compiletime/Config.scala b/tools/src/main/scala/caliban/tools/compiletime/Config.scala index 80ac34eea..ed7213152 100644 --- a/tools/src/main/scala/caliban/tools/compiletime/Config.scala +++ b/tools/src/main/scala/caliban/tools/compiletime/Config.scala @@ -15,7 +15,8 @@ trait Config { enableFmt: Boolean = true, extensibleEnums: Boolean = false, supportIsRepeatable: Boolean = true, - excludeDeprecated: Boolean = false + excludeDeprecated: Boolean = false, + supportDeprecatedArgs: Boolean = true ) { private[caliban] def toCalibanCommonSettings: CalibanCommonSettings = CalibanCommonSettings( @@ -36,7 +37,8 @@ trait Config { supportIsRepeatable = Some(supportIsRepeatable), addDerives = None, envForDerives = None, - excludeDeprecated = Some(excludeDeprecated) + excludeDeprecated = Some(excludeDeprecated), + supportDeprecatedArgs = Some(supportDeprecatedArgs) ) private[caliban] def asScalaCode: String = { @@ -53,7 +55,8 @@ trait Config { | enableFmt = $enableFmt, | extensibleEnums = $extensibleEnums, | supportIsRepeatable = $supportIsRepeatable, - | excludeDeprecated = $excludeDeprecated + | excludeDeprecated = $excludeDeprecated, + | supportDeprecatedArgs = $supportDeprecatedArgs |) """.stripMargin.trim } diff --git a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment newline.scala b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment newline.scala index 8d8552202..e0d6e3886 100644 --- a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment newline.scala +++ b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment newline.scala @@ -9,11 +9,8 @@ object Client { /** * name */ - @deprecated( - """foo -bar""", - "" - ) + @deprecated("""foo +bar""") def name: SelectionBuilder[Character, String] = _root_.caliban.client.SelectionBuilder.Field("name", Scalar()) } diff --git a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment.scala b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment.scala index 085d2f5e6..99f0a0dc0 100644 --- a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment.scala +++ b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field + comment.scala @@ -9,9 +9,9 @@ object Client { /** * name */ - @deprecated("blah", "") + @deprecated("blah") def name: SelectionBuilder[Character, String] = _root_.caliban.client.SelectionBuilder.Field("name", Scalar()) - @deprecated("", "") + @deprecated def nicknames: SelectionBuilder[Character, List[String]] = _root_.caliban.client.SelectionBuilder.Field("nicknames", ListOf(Scalar())) } diff --git a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment newline.scala b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment newline.scala new file mode 100644 index 000000000..190c6ea28 --- /dev/null +++ b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment newline.scala @@ -0,0 +1,31 @@ +import caliban.client.FieldBuilder._ +import caliban.client._ + +object Client { + + type Query = _root_.caliban.client.Operations.RootQuery + object Query { + def characters( + first: Int, + @deprecated("""foo +bar""") + last: scala.Option[Int] = None, + @deprecated + origins: scala.Option[List[scala.Option[String]]] = None + )(implicit + encoder0: ArgEncoder[Int], + encoder1: ArgEncoder[scala.Option[Int]], + encoder2: ArgEncoder[scala.Option[List[scala.Option[String]]]] + ): SelectionBuilder[_root_.caliban.client.Operations.RootQuery, scala.Option[String]] = + _root_.caliban.client.SelectionBuilder.Field( + "characters", + OptionOf(Scalar()), + arguments = List( + Argument("first", first, "Int!")(encoder0), + Argument("last", last, "Int")(encoder1), + Argument("origins", origins, "[String]")(encoder2) + ) + ) + } + +} diff --git a/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment.scala b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment.scala new file mode 100644 index 000000000..c53340f69 --- /dev/null +++ b/tools/src/test/resources/snapshots/ClientWriterSpec/deprecated field argument + comment.scala @@ -0,0 +1,30 @@ +import caliban.client.FieldBuilder._ +import caliban.client._ + +object Client { + + type Query = _root_.caliban.client.Operations.RootQuery + object Query { + def characters( + first: Int, + @deprecated("foo bar") + last: scala.Option[Int] = None, + @deprecated + origins: scala.Option[List[scala.Option[String]]] = None + )(implicit + encoder0: ArgEncoder[Int], + encoder1: ArgEncoder[scala.Option[Int]], + encoder2: ArgEncoder[scala.Option[List[scala.Option[String]]]] + ): SelectionBuilder[_root_.caliban.client.Operations.RootQuery, scala.Option[String]] = + _root_.caliban.client.SelectionBuilder.Field( + "characters", + OptionOf(Scalar()), + arguments = List( + Argument("first", first, "Int!")(encoder0), + Argument("last", last, "Int")(encoder1), + Argument("origins", origins, "[String]")(encoder2) + ) + ) + } + +} diff --git a/tools/src/test/resources/snapshots/ClientWriterSpec/input object with deprecated fields.scala b/tools/src/test/resources/snapshots/ClientWriterSpec/input object with deprecated fields.scala new file mode 100644 index 000000000..80c6d17c8 --- /dev/null +++ b/tools/src/test/resources/snapshots/ClientWriterSpec/input object with deprecated fields.scala @@ -0,0 +1,28 @@ +import caliban.client._ +import caliban.client.__Value._ + +object Client { + + final case class CharacterInput( + name: String, + @deprecated + nickname: scala.Option[String] = None, + @deprecated("no longer used") + address: scala.Option[String] = None + ) + object CharacterInput { + implicit val encoder: ArgEncoder[CharacterInput] = new ArgEncoder[CharacterInput] { + override def encode(value: CharacterInput): __Value = + __ObjectValue( + List( + "name" -> implicitly[ArgEncoder[String]].encode(value.name), + "nickname" -> value.nickname.fold(__NullValue: __Value)(value => + implicitly[ArgEncoder[String]].encode(value) + ), + "address" -> value.address.fold(__NullValue: __Value)(value => implicitly[ArgEncoder[String]].encode(value)) + ) + ) + } + } + +} diff --git a/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala b/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala index f60a3ef3c..ca4a4bae7 100644 --- a/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala +++ b/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala @@ -218,6 +218,15 @@ object ClientWriterSpec extends SnapshotTest { } """) }, + snapshotTest("input object with deprecated fields") { + gen(""" + input CharacterInput { + name: String! + nickname: String @deprecated + address: String @deprecated(reason: "no longer used") + } + """) + }, snapshotTest("union") { gen(""" union Role = Captain_ | Pilot @@ -252,6 +261,26 @@ object ClientWriterSpec extends SnapshotTest { } """) }, + snapshotTest("deprecated field argument + comment") { + gen(""" + type Query { + characters( + first: Int!, + last: Int @deprecated(reason: "foo bar"), + origins: [String] @deprecated + ): String + }""") + }, + snapshotTest("deprecated field argument + comment newline") { + gen(""" + type Query { + characters( + first: Int!, + last: Int @deprecated(reason: "foo\nbar"), + origins: [String] @deprecated + ): String + }""") + }, snapshotTest("default arguments for optional and list arguments") { gen(""" type Query { diff --git a/tools/src/test/scala/caliban/tools/CodegenSpec.scala b/tools/src/test/scala/caliban/tools/CodegenSpec.scala index 3fe5b459e..e3ce6afde 100644 --- a/tools/src/test/scala/caliban/tools/CodegenSpec.scala +++ b/tools/src/test/scala/caliban/tools/CodegenSpec.scala @@ -68,7 +68,8 @@ object CodegenSpec extends ZIOSpecDefault { supportIsRepeatable = None, addDerives = None, envForDerives = None, - excludeDeprecated = None + excludeDeprecated = None, + supportDeprecatedArgs = None ) getPackageAndObjectName(arguments) diff --git a/tools/src/test/scala/caliban/tools/IntrospectionClientSpec.scala b/tools/src/test/scala/caliban/tools/IntrospectionClientSpec.scala new file mode 100644 index 000000000..0eeb0a5da --- /dev/null +++ b/tools/src/test/scala/caliban/tools/IntrospectionClientSpec.scala @@ -0,0 +1,38 @@ +package caliban.tools + +import caliban._ +import caliban.introspection.Introspector +import caliban.quick._ +import caliban.schema.Annotations._ +import caliban.schema.Schema.auto._ +import caliban.schema.ArgBuilder.auto._ +import zio.test._ +import zio.{ durationInt, Clock } + +object IntrospectionClientSpec extends ZIOSpecDefault { + + case class Args(@GQLDeprecated("Use nameV2") name: Option[String] = Some("defaultValue"), nameV2: String) + case class Queries(getObject: Args => String) + + object Resolvers { + def getObject(@GQLDeprecated("foobar") args: Args): String = args.name.getOrElse("") + } + + val queries = Queries(getObject = Resolvers.getObject) + val api = graphQL(RootResolver(queries)).withAdditionalDirectives(Introspector.directives) + + def spec = suite("IntrospectionClientSpec")( + test("is isomorphic") { + val port = 8091 + for { + _ <- api.runServer(port = port, apiPath = "/api/graphql").fork + _ <- Clock.ClockLive.sleep(2.seconds) + introspectedSchema = + SchemaLoader.fromIntrospectionWith(s"http://localhost:$port/api/graphql", None)(_.supportDeprecatedArgs(true)) + codeSchema = SchemaLoader.fromCaliban(api) + res <- SchemaComparison.compare(introspectedSchema, codeSchema) + } yield assertTrue(res.isEmpty) + } + ) + +} diff --git a/tools/src/test/scala/caliban/tools/RemoteSchemaSpec.scala b/tools/src/test/scala/caliban/tools/RemoteSchemaSpec.scala index 47f21f5f5..682569b3b 100644 --- a/tools/src/test/scala/caliban/tools/RemoteSchemaSpec.scala +++ b/tools/src/test/scala/caliban/tools/RemoteSchemaSpec.scala @@ -4,10 +4,11 @@ import caliban._ import caliban.introspection.adt._ import caliban.schema._ import caliban.schema.Schema.auto._ +import caliban.schema.ArgBuilder.auto._ import zio._ import zio.test.Assertion._ import zio.test._ -import schema.Annotations._ +import caliban.schema.Annotations._ import caliban.Macros.gqldoc import caliban.execution.Feature import caliban.transformers.Transformer @@ -20,6 +21,8 @@ object RemoteSchemaSpec extends ZIOSpecDefault { sealed trait UnionType extends Product with Serializable case class UnionValue1(field: String) extends UnionType + case class Args(@GQLDeprecated("Use nameV2") name: String = "defaultValue", nameV2: String) + case class Object( field: Int, optionalField: Option[Float], @@ -29,7 +32,7 @@ object RemoteSchemaSpec extends ZIOSpecDefault { ) object Resolvers { - def getObject(arg: String = "defaultValue"): Object = + def getObject(args: Args): Object = Object( field = 1, optionalField = None, @@ -39,7 +42,7 @@ object RemoteSchemaSpec extends ZIOSpecDefault { } case class Queries( - getObject: String => Object + getObject: Args => Object ) val queries = Queries( @@ -47,12 +50,10 @@ object RemoteSchemaSpec extends ZIOSpecDefault { ) val api = graphQL( - RootResolver( - queries - ) + RootResolver(queries) ) - def spec = suite("ParserSpec")( + def spec = suite("RemoteSchemaSpec")( test("is isomorphic") { for { introspected <- SchemaLoader.fromCaliban(api).load @@ -60,7 +61,11 @@ object RemoteSchemaSpec extends ZIOSpecDefault { remoteAPI <- ZIO.succeed(fromRemoteSchema(remoteSchema)) sdl = api.render remoteSDL = remoteAPI.render - } yield assertTrue(remoteSDL == sdl) + res <- SchemaComparison.compare( + SchemaLoader.fromCaliban(api), + SchemaLoader.fromCaliban(remoteAPI) + ) + } yield assertTrue(res.isEmpty, sdl == remoteSDL) }, test("properly resolves interface types") { @GQLInterface diff --git a/tools/src/test/scala/caliban/tools/compiletime/ConfigSpec.scala b/tools/src/test/scala/caliban/tools/compiletime/ConfigSpec.scala index 06532b633..9c72b1e0b 100644 --- a/tools/src/test/scala/caliban/tools/compiletime/ConfigSpec.scala +++ b/tools/src/test/scala/caliban/tools/compiletime/ConfigSpec.scala @@ -19,7 +19,8 @@ object ConfigSpec extends ZIOSpecDefault { enableFmt = false, extensibleEnums = true, supportIsRepeatable = true, - excludeDeprecated = true + excludeDeprecated = true, + supportDeprecatedArgs = true ) private val toCalibanCommonSettingsSpec = @@ -45,7 +46,8 @@ object ConfigSpec extends ZIOSpecDefault { supportIsRepeatable = Some(true), addDerives = None, envForDerives = None, - excludeDeprecated = Some(true) + excludeDeprecated = Some(true), + supportDeprecatedArgs = Some(true) ) ) ) @@ -68,7 +70,8 @@ object ConfigSpec extends ZIOSpecDefault { | enableFmt = true, | extensibleEnums = false, | supportIsRepeatable = true, - | excludeDeprecated = false + | excludeDeprecated = false, + | supportDeprecatedArgs = true |) """.stripMargin.trim ) @@ -88,7 +91,8 @@ object ConfigSpec extends ZIOSpecDefault { | enableFmt = false, | extensibleEnums = true, | supportIsRepeatable = true, - | excludeDeprecated = true + | excludeDeprecated = true, + | supportDeprecatedArgs = true |) """.stripMargin.trim ) @@ -110,7 +114,8 @@ object ConfigSpec extends ZIOSpecDefault { splitFiles = false, enableFmt = true, extensibleEnums = false, - excludeDeprecated = false + excludeDeprecated = false, + supportDeprecatedArgs = true ) ) )