From 230598b3a831d18c4a8288d3ae80c31a3f8405f1 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 18 Jun 2024 21:16:45 +1000 Subject: [PATCH 1/8] Add transformer that excludes fields / inputs based on tags --- .../caliban/schema/SchemaDerivation.scala | 10 +- .../caliban/schema/DerivationUtils.scala | 9 +- .../caliban/introspection/adt/__Field.scala | 3 +- .../introspection/adt/__InputValue.scala | 3 +- .../scala/caliban/schema/Annotations.scala | 5 + .../src/main/scala/caliban/schema/Types.scala | 6 +- .../caliban/transformers/Transformer.scala | 59 ++++++- .../transformers/TransformerSpec.scala | 152 +++++++++++++++++- 8 files changed, 235 insertions(+), 12 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 40bbd51d4..773b2b6c3 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -83,7 +83,8 @@ trait CommonSchemaDerivation[R] { p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined, p.annotations.collectFirst { case GQLDeprecated(reason) => reason }, Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty), - () => Some(tpe) + () => Some(tpe), + getTags(p.annotations) ) ) .toList, @@ -122,7 +123,8 @@ trait CommonSchemaDerivation[R] { Some(SchemaUtils.SemanticNonNull) else None } - ).filter(_.nonEmpty) + ).filter(_.nonEmpty), + getTags(p.annotations) ) } .toList, @@ -286,6 +288,10 @@ trait CommonSchemaDerivation[R] { private def getDescription[Typeclass[_], Type](ctx: ReadOnlyParam[Typeclass, Type]): Option[String] = getDescription(ctx.annotations) + + private def getTags(annotations: Seq[Any]): Set[String] = + annotations.collect { case GQLTag(tags @ _*) => tags }.flatten.toSet + } trait SchemaDerivation[R] extends CommonSchemaDerivation[R] { diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index cc1469057..37b2fa00d 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -44,6 +44,9 @@ private object DerivationUtils { def getDeprecatedReason(annotations: Seq[Any]): Option[String] = annotations.collectFirst { case GQLDeprecated(reason) => reason } + def getTags(annotations: Seq[Any]): Set[String] = + annotations.collect { case GQLTag(dir) => dir }.toSet + def mkEnum(annotations: List[Any], info: TypeInfo, subTypes: List[(String, __Type, List[Any])]): __Type = makeEnum( Some(getName(annotations, info)), @@ -113,7 +116,8 @@ private object DerivationUtils { isDeprecated = deprecationReason.isDefined, deprecationReason = deprecationReason, directives = Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), - parentType = () => Some(tpe) + parentType = () => Some(tpe), + getTags(p.annotations) ) }, Some(info.full), @@ -168,7 +172,8 @@ private object DerivationUtils { if (enableSemanticNonNull && isSemanticNonNull) Some(SchemaUtils.SemanticNonNull) else None } - ).filter(_.nonEmpty) + ).filter(_.nonEmpty), + getTags(fieldAnnotations) ) }, getDirectives(annotations), diff --git a/core/src/main/scala/caliban/introspection/adt/__Field.scala b/core/src/main/scala/caliban/introspection/adt/__Field.scala index f4c597ded..887d05cee 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Field.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Field.scala @@ -12,7 +12,8 @@ case class __Field( `type`: () => __Type, isDeprecated: Boolean = false, deprecationReason: Option[String] = None, - @GQLExcluded directives: Option[List[Directive]] = None + @GQLExcluded directives: Option[List[Directive]] = None, + @GQLExcluded tags: Set[String] = Set.empty ) { final override lazy val hashCode: Int = super.hashCode() diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index 3ab5e0612..a0f132e62 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -16,7 +16,8 @@ case class __InputValue( isDeprecated: Boolean = false, deprecationReason: Option[String] = None, @GQLExcluded directives: Option[List[Directive]] = None, - @GQLExcluded parentType: () => Option[__Type] = () => None + @GQLExcluded parentType: () => Option[__Type] = () => None, + @GQLExcluded tags: Set[String] = Set.empty ) { def toInputValueDefinition: InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index 39b662b9f..cd5b751a5 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -81,4 +81,9 @@ object Annotations extends AnnotationsVersionSpecific { */ case class GQLOneOfInput() extends StaticAnnotation + /** + * Compile-time annotation that can be used in conjunction with [[caliban.transformers.Transformer]] to + * customize schema generation. + */ + case class GQLTag(tags: String*) extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index 3ddf7eaf9..94a12b3cc 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -68,7 +68,8 @@ object Types { `type`: () => __Type, isDeprecated: Boolean = false, deprecationReason: Option[String] = None, - directives: Option[List[Directive]] = None + directives: Option[List[Directive]] = None, + tags: Set[String] = Set.empty ): __Field = __Field( name, @@ -79,7 +80,8 @@ object Types { `type`, isDeprecated, deprecationReason, - directives + directives, + tags ) def makeInputObject( diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index 0303b166c..3c4cce2d7 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -19,7 +19,7 @@ abstract class Transformer[-R] { self => * Set of type names that this transformer applies to. * Needed for applying optimizations when combining transformers. */ - protected val typeNames: collection.Set[String] + protected def typeNames: collection.Set[String] protected def transformStep[R1 <: R](step: ObjectStep[R1], field: Field): ObjectStep[R1] @@ -326,20 +326,73 @@ object Transformer { } } + object ExcludeTags { + + /** + * A transformer that allows excluding tagged fields and input arguments. + * + * {{{ + * ExcludeTaggedFields("TagA", "TagB") + * }}} + * + * @param f tuples in the format of `(TypeName -> fieldToBeExcluded)` + */ + def apply(f: String*): Transformer[Any] = + if (f.isEmpty) Empty else new ExcludeTags(f.toSet) + } + + final private class ExcludeTags(tags: Set[String]) extends Transformer[Any] { + private val map: mutable.HashMap[String, Set[String]] = mutable.HashMap.empty + + private def shouldKeep(tpe: __Type, field: __Field): Boolean = { + val keep = field.tags.intersect(tags).isEmpty + if (!keep) map.updateWith(tpe.name.getOrElse("")) { + case Some(set) => Some(set + field.name) + case None => Some(Set(field.name)) + } + keep + } + + val typeVisitor: TypeVisitor = + TypeVisitor.fields.filterWith((t, field) => shouldKeep(t, field)) |+| + TypeVisitor.fields.modify { field => + def loop(arg: __InputValue): Option[__InputValue] = + if (arg._type.isNullable && arg.tags.intersect(tags).nonEmpty) None + else { + lazy val newType = arg._type.mapInnerType { t => + t.copy(inputFields = t.inputFields(_).map(_.flatMap(loop))) + } + Some(arg.copy(`type` = () => newType)) + } + + field.copy(args = field.args(_).flatMap(loop)) + } + + protected def typeNames: collection.Set[String] = map.keySet + + protected def transformStep[R](step: ObjectStep[R], field: Field): ObjectStep[R] = + map.getOrElse(step.name, null) match { + case null => step + case excl => step.copy(fields = name => if (!excl(name)) step.fields(name) else NullStep) + } + } + final private class Combined[-R](left: Transformer[R], right: Transformer[R]) extends Transformer[R] { val typeVisitor: TypeVisitor = left.typeVisitor |+| right.typeVisitor - protected val typeNames: mutable.HashSet[String] = { + protected def typeNames: mutable.HashSet[String] = { val set = mutable.HashSet.from(left.typeNames) set ++= right.typeNames set } + private lazy val materializedTypeNames = typeNames + protected def transformStep[R1 <: R](step: ObjectStep[R1], field: Field): ObjectStep[R1] = right.transformStep(left.transformStep(step, field), field) override def apply[R1 <: R](step: ObjectStep[R1], field: Field): ObjectStep[R1] = - if (typeNames(step.name)) transformStep(step, field) else step + if (materializedTypeNames(step.name)) transformStep(step, field) else step } private def mapFunctionStep[R](step: Step[R])(f: Map[String, InputValue] => Map[String, InputValue]): Step[R] = diff --git a/core/src/test/scala/caliban/transformers/TransformerSpec.scala b/core/src/test/scala/caliban/transformers/TransformerSpec.scala index 3dd2d64f2..19b9701b3 100644 --- a/core/src/test/scala/caliban/transformers/TransformerSpec.scala +++ b/core/src/test/scala/caliban/transformers/TransformerSpec.scala @@ -2,6 +2,7 @@ package caliban.transformers import caliban.Macros.gqldoc import caliban._ +import caliban.schema.Annotations.GQLTag import caliban.schema.ArgBuilder.auto._ import caliban.schema.Schema.auto._ import zio.test._ @@ -218,6 +219,155 @@ object TransformerSpec extends ZIOSpecDefault { | c(arg: String!): String! |}""".stripMargin ) - } + }, + suite("ExcludeTag")( + test("fields") { + case class Query( + a: String, + @GQLTag("schemaA") + b: Int, + @GQLTag("schemaB") + c: Double, + @GQLTag("schemaA", "schemaB") + d: Boolean + ) + val api: GraphQL[Any] = graphQL(RootResolver(Query("a", 2, 3d, true))) + val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA")) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaB")) + val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA", "schemaB")) + + for { + _ <- Configurator.setSkipValidation(true) + res0 <- api.interpreterUnsafe.execute("""{ a b c d }""").map(_.data.toString) + resA <- apiA.interpreterUnsafe.execute("""{ a b c d }""").map(_.data.toString) + resB <- apiB.interpreterUnsafe.execute("""{ a b c d }""").map(_.data.toString) + resC <- apiC.interpreterUnsafe.execute("""{ a b c d }""").map(_.data.toString) + rendered = api.render + renderedA = apiA.render + renderedB = apiB.render + renderedC = apiC.render + } yield assertTrue( + res0 == """{"a":"a","b":2,"c":3.0,"d":true}""", + resA == """{"a":"a","b":null,"c":3.0,"d":null}""", + resB == """{"a":"a","b":2,"c":null,"d":null}""", + resC == """{"a":"a","b":null,"c":null,"d":null}""", + rendered == + """schema { + | query: Query + |} + | + |type Query { + | a: String! + | b: Int! + | c: Float! + | d: Boolean! + |}""".stripMargin, + renderedA == + """schema { + | query: Query + |} + | + |type Query { + | a: String! + | c: Float! + |}""".stripMargin, + renderedB == + """schema { + | query: Query + |} + | + |type Query { + | a: String! + | b: Int! + |}""".stripMargin, + renderedC == + """schema { + | query: Query + |} + | + |type Query { + | a: String! + |}""".stripMargin + ) + }, + test("input fields") { + case class Nested( + a: String, + @GQLTag("schemaA") + b: Option[Int], + @GQLTag("schemaB") + c: Option[Double], + @GQLTag("schemaA", "schemaB") + d: Option[Boolean] + ) + case class Args(a: String, b: String, l: List[String], nested: Nested) + case class Query(foo: Args => String) + val api: GraphQL[Any] = graphQL(RootResolver(Query(_ => "value"))) + val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA")) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaB")) + val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA", "schemaB")) + + val rendered = api.render + val renderedA = apiA.render + val renderedB = apiB.render + val renderedC = apiC.render + + assertTrue( + rendered == + """schema { + | query: Query + |} + | + |input NestedInput { + | a: String! + | b: Int + | c: Float + | d: Boolean + |} + | + |type Query { + | foo(a: String!, b: String!, l: [String!]!, nested: NestedInput!): String! + |}""".stripMargin, + renderedA == + """schema { + | query: Query + |} + | + |input NestedInput { + | a: String! + | c: Float + |} + | + |type Query { + | foo(a: String!, b: String!, l: [String!]!, nested: NestedInput!): String! + |}""".stripMargin, + renderedB == + """schema { + | query: Query + |} + | + |input NestedInput { + | a: String! + | b: Int + |} + | + |type Query { + | foo(a: String!, b: String!, l: [String!]!, nested: NestedInput!): String! + |}""".stripMargin, + renderedC == + """schema { + | query: Query + |} + | + |input NestedInput { + | a: String! + |} + | + |type Query { + | foo(a: String!, b: String!, l: [String!]!, nested: NestedInput!): String! + |}""".stripMargin + ) + } + ) ) } From 2bde0f854de4630005c381994e20052b7f60dfa3 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 18 Jun 2024 21:20:22 +1000 Subject: [PATCH 2/8] Fix scaladoc --- core/src/main/scala/caliban/transformers/Transformer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index 3c4cce2d7..f3cfa1e85 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -332,7 +332,7 @@ object Transformer { * A transformer that allows excluding tagged fields and input arguments. * * {{{ - * ExcludeTaggedFields("TagA", "TagB") + * ExcludeTags("TagA", "TagB") * }}} * * @param f tuples in the format of `(TypeName -> fieldToBeExcluded)` From 74415289c63e2d23718e6868922c6b6e670b7481 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 18 Jun 2024 21:41:20 +1000 Subject: [PATCH 3/8] Fix Scala 3 derivation --- core/src/main/scala-3/caliban/schema/DerivationUtils.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 37b2fa00d..999192105 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -45,7 +45,7 @@ private object DerivationUtils { annotations.collectFirst { case GQLDeprecated(reason) => reason } def getTags(annotations: Seq[Any]): Set[String] = - annotations.collect { case GQLTag(dir) => dir }.toSet + annotations.collect { case GQLTag(tags*) => tags }.flatten.toSet def mkEnum(annotations: List[Any], info: TypeInfo, subTypes: List[(String, __Type, List[Any])]): __Type = makeEnum( @@ -117,7 +117,7 @@ private object DerivationUtils { deprecationReason = deprecationReason, directives = Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), parentType = () => Some(tpe), - getTags(p.annotations) + getTags(fieldAnnotations) ) }, Some(info.full), From c9878a3dc6cf87929c3789a46e25c3949fef183e Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Wed, 19 Jun 2024 12:09:38 +1000 Subject: [PATCH 4/8] Define tags as non-introspectable directives --- .../caliban/schema/SchemaDerivation.scala | 9 ++------ .../caliban/schema/DerivationUtils.scala | 9 ++------ .../scala/caliban/execution/Fragment.scala | 6 +++--- .../caliban/introspection/adt/__Field.scala | 7 ++++--- .../introspection/adt/__InputValue.scala | 8 ++++--- .../caliban/introspection/adt/__Type.scala | 13 ++++++------ .../scala/caliban/parsing/adt/Directive.scala | 21 +++++++++++++++++-- .../caliban/rendering/DocumentRenderer.scala | 13 +++++++----- .../scala/caliban/schema/Annotations.scala | 12 +++++++++-- .../src/main/scala/caliban/schema/Types.scala | 6 ++---- .../caliban/transformers/Transformer.scala | 4 ++-- .../scala/caliban/wrappers/DeferSupport.scala | 3 ++- .../test/scala/caliban/RenderingSpec.scala | 15 +++++++++++++ .../transformers/TransformerSpec.scala | 3 ++- 14 files changed, 83 insertions(+), 46 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 773b2b6c3..69f950b1b 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -83,8 +83,7 @@ trait CommonSchemaDerivation[R] { p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined, p.annotations.collectFirst { case GQLDeprecated(reason) => reason }, Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty), - () => Some(tpe), - getTags(p.annotations) + () => Some(tpe) ) ) .toList, @@ -123,8 +122,7 @@ trait CommonSchemaDerivation[R] { Some(SchemaUtils.SemanticNonNull) else None } - ).filter(_.nonEmpty), - getTags(p.annotations) + ).filter(_.nonEmpty) ) } .toList, @@ -289,9 +287,6 @@ trait CommonSchemaDerivation[R] { private def getDescription[Typeclass[_], Type](ctx: ReadOnlyParam[Typeclass, Type]): Option[String] = getDescription(ctx.annotations) - private def getTags(annotations: Seq[Any]): Set[String] = - annotations.collect { case GQLTag(tags @ _*) => tags }.flatten.toSet - } trait SchemaDerivation[R] extends CommonSchemaDerivation[R] { diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 999192105..cc1469057 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -44,9 +44,6 @@ private object DerivationUtils { def getDeprecatedReason(annotations: Seq[Any]): Option[String] = annotations.collectFirst { case GQLDeprecated(reason) => reason } - def getTags(annotations: Seq[Any]): Set[String] = - annotations.collect { case GQLTag(tags*) => tags }.flatten.toSet - def mkEnum(annotations: List[Any], info: TypeInfo, subTypes: List[(String, __Type, List[Any])]): __Type = makeEnum( Some(getName(annotations, info)), @@ -116,8 +113,7 @@ private object DerivationUtils { isDeprecated = deprecationReason.isDefined, deprecationReason = deprecationReason, directives = Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), - parentType = () => Some(tpe), - getTags(fieldAnnotations) + parentType = () => Some(tpe) ) }, Some(info.full), @@ -172,8 +168,7 @@ private object DerivationUtils { if (enableSemanticNonNull && isSemanticNonNull) Some(SchemaUtils.SemanticNonNull) else None } - ).filter(_.nonEmpty), - getTags(fieldAnnotations) + ).filter(_.nonEmpty) ) }, getDirectives(annotations), diff --git a/core/src/main/scala/caliban/execution/Fragment.scala b/core/src/main/scala/caliban/execution/Fragment.scala index 7a403c747..0dc1eb87b 100644 --- a/core/src/main/scala/caliban/execution/Fragment.scala +++ b/core/src/main/scala/caliban/execution/Fragment.scala @@ -1,7 +1,7 @@ package caliban.execution import caliban.Value.{ BooleanValue, IntValue, StringValue } -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } case class Fragment(name: Option[String], directives: List[Directive]) {} @@ -9,7 +9,7 @@ object Fragment { object IsDeferred { def unapply(fragment: Fragment): Option[Option[String]] = fragment.directives.collectFirst { - case Directive("defer", args, _) if args.get("if").forall { + case Directive(Directives.Defer, args, _, _) if args.get("if").forall { case BooleanValue(v) => v case _ => true } => @@ -20,7 +20,7 @@ object Fragment { object IsStream { def unapply(field: Field): Option[(Option[String], Option[Int])] = - field.directives.collectFirst { case Directive("stream", args, _) => + field.directives.collectFirst { case Directive(Directives.Stream, args, _, _) => ( args.get("label").collect { case StringValue(v) => v }, args.get("initialCount").collect { case v: IntValue => v.toInt } diff --git a/core/src/main/scala/caliban/introspection/adt/__Field.scala b/core/src/main/scala/caliban/introspection/adt/__Field.scala index 887d05cee..e46fe9c22 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Field.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Field.scala @@ -2,7 +2,7 @@ package caliban.introspection.adt import caliban.Value.StringValue import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.{ FieldDefinition, InputValueDefinition } -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } import caliban.schema.Annotations.GQLExcluded case class __Field( @@ -12,8 +12,7 @@ case class __Field( `type`: () => __Type, isDeprecated: Boolean = false, deprecationReason: Option[String] = None, - @GQLExcluded directives: Option[List[Directive]] = None, - @GQLExcluded tags: Set[String] = Set.empty + @GQLExcluded directives: Option[List[Directive]] = None ) { final override lazy val hashCode: Int = super.hashCode() @@ -35,6 +34,8 @@ case class __Field( lazy val allArgs: List[__InputValue] = args(__DeprecatedArgs.include) + private[caliban] lazy val _tags: Set[String] = directives.fold(Set.empty[String])(Directives.internal.tags) + private[caliban] lazy val _type: __Type = `type`() private[caliban] lazy val allArgNames: Set[String] = diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index a0f132e62..f80ab7d3b 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -2,7 +2,7 @@ package caliban.introspection.adt import caliban.Value.StringValue import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.InputValueDefinition -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } import caliban.parsing.Parser import caliban.schema.Annotations.GQLExcluded @@ -16,9 +16,9 @@ case class __InputValue( isDeprecated: Boolean = false, deprecationReason: Option[String] = None, @GQLExcluded directives: Option[List[Directive]] = None, - @GQLExcluded parentType: () => Option[__Type] = () => None, - @GQLExcluded tags: Set[String] = Set.empty + @GQLExcluded parentType: () => Option[__Type] = () => None ) { + def toInputValueDefinition: InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) val allDirectives = (if (isDeprecated) @@ -32,6 +32,8 @@ case class __InputValue( InputValueDefinition(description, name, _type.toType(), default, allDirectives) } + private[caliban] lazy val _tags: Set[String] = directives.fold(Set.empty[String])(Directives.internal.tags) + private[caliban] lazy val _type: __Type = `type`() private[caliban] lazy val _parentType = parentType() diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index fb77f0014..d74be7f6a 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -60,12 +60,13 @@ case class __Type( Some( ScalarTypeDefinition( description, - name.getOrElse(""), - directives - .getOrElse(Nil) ++ - specifiedBy - .map(url => Directive("specifiedBy", Map("url" -> StringValue(url)), directives.size)) - .toList + name.getOrElse(""), { + val dirs = directives.getOrElse(Nil) + dirs ++ + specifiedBy + .map(url => Directive("specifiedBy", Map("url" -> StringValue(url)), dirs.size)) + .toList + } ) ) case __TypeKind.OBJECT => diff --git a/core/src/main/scala/caliban/parsing/adt/Directive.scala b/core/src/main/scala/caliban/parsing/adt/Directive.scala index e3cc12df1..655c101c8 100644 --- a/core/src/main/scala/caliban/parsing/adt/Directive.scala +++ b/core/src/main/scala/caliban/parsing/adt/Directive.scala @@ -2,14 +2,31 @@ package caliban.parsing.adt import caliban.{ InputValue, Value } -case class Directive(name: String, arguments: Map[String, InputValue] = Map.empty, index: Int = 0) +case class Directive( + name: String, + arguments: Map[String, InputValue] = Map.empty, + index: Int = 0, + isIntrospectable: Boolean = true +) object Directives { + final val Defer = "defer" + final val DeprecatedDirective = "deprecated" final val LazyDirective = "lazy" final val NewtypeDirective = "newtype" - final val DeprecatedDirective = "deprecated" final val OneOf = "oneOf" + final val Stream = "stream" + + // We prefix these with `_caliban_tag_` to avoid conflicts with user-defined directives + private[caliban] object internal { + final val Tag = "_caliban_tag" + + def tags(directives: List[Directive]): Set[String] = + directives.collect { case Directive(Tag, args, _, _) if args.nonEmpty => args.keySet } + .foldLeft(Set.empty[String])(_ ++ _) + + } def isDeprecated(directives: List[Directive]): Boolean = directives.exists(_.name == DeprecatedDirective) diff --git a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala index 0be015ad1..8eccb16f3 100644 --- a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala +++ b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala @@ -572,11 +572,14 @@ 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 append '@' - writer append d.name - inputArgumentsRenderer.unsafeRender(d.arguments, indent, writer) - } + override def unsafeRender(d: Directive, indent: Option[Int], writer: StringBuilder): Unit = + if (d.isIntrospectable) { + writer append '@' + writer append d.name + inputArgumentsRenderer.unsafeRender(d.arguments, indent, writer) + } else { + writer.deleteCharAt(writer.size - 1) + } } private lazy val fieldDefinitionsRenderer: Renderer[List[FieldDefinition]] = diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index cd5b751a5..75174b494 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -1,6 +1,7 @@ package caliban.schema -import caliban.parsing.adt.Directive +import caliban.{ InputValue, Value } +import caliban.parsing.adt.{ Directive, Directives } import scala.annotation.StaticAnnotation @@ -85,5 +86,12 @@ object Annotations extends AnnotationsVersionSpecific { * Compile-time annotation that can be used in conjunction with [[caliban.transformers.Transformer]] to * customize schema generation. */ - case class GQLTag(tags: String*) extends StaticAnnotation + case class GQLTag(tags: String*) + extends GQLDirective( + Directive( + Directives.internal.Tag, + tags.map(_ -> Value.NullValue).toMap, + isIntrospectable = false + ) + ) } diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index 94a12b3cc..3ddf7eaf9 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -68,8 +68,7 @@ object Types { `type`: () => __Type, isDeprecated: Boolean = false, deprecationReason: Option[String] = None, - directives: Option[List[Directive]] = None, - tags: Set[String] = Set.empty + directives: Option[List[Directive]] = None ): __Field = __Field( name, @@ -80,8 +79,7 @@ object Types { `type`, isDeprecated, deprecationReason, - directives, - tags + directives ) def makeInputObject( diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index f3cfa1e85..3ccaabdc5 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -345,7 +345,7 @@ object Transformer { private val map: mutable.HashMap[String, Set[String]] = mutable.HashMap.empty private def shouldKeep(tpe: __Type, field: __Field): Boolean = { - val keep = field.tags.intersect(tags).isEmpty + val keep = field._tags.intersect(tags).isEmpty if (!keep) map.updateWith(tpe.name.getOrElse("")) { case Some(set) => Some(set + field.name) case None => Some(Set(field.name)) @@ -357,7 +357,7 @@ object Transformer { TypeVisitor.fields.filterWith((t, field) => shouldKeep(t, field)) |+| TypeVisitor.fields.modify { field => def loop(arg: __InputValue): Option[__InputValue] = - if (arg._type.isNullable && arg.tags.intersect(tags).nonEmpty) None + if (arg._type.isNullable && arg._tags.intersect(tags).nonEmpty) None else { lazy val newType = arg._type.mapInnerType { t => t.copy(inputFields = t.inputFields(_).map(_.flatMap(loop))) diff --git a/core/src/main/scala/caliban/wrappers/DeferSupport.scala b/core/src/main/scala/caliban/wrappers/DeferSupport.scala index f0982f754..9730b7128 100644 --- a/core/src/main/scala/caliban/wrappers/DeferSupport.scala +++ b/core/src/main/scala/caliban/wrappers/DeferSupport.scala @@ -2,12 +2,13 @@ package caliban.wrappers import caliban.execution.Feature import caliban.introspection.adt.{ __Directive, __DirectiveLocation, __InputValue } +import caliban.parsing.adt.Directives import caliban.schema.Types import caliban.{ GraphQL, GraphQLAspect } object DeferSupport { private[caliban] val deferDirective = __Directive( - "defer", + Directives.Defer, Some(""), Set(__DirectiveLocation.FRAGMENT_SPREAD, __DirectiveLocation.INLINE_FRAGMENT), _ => diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index 441ea8ae4..0cfdcb8f6 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -132,6 +132,21 @@ object RenderingSpec extends ZIOSpecDefault { val renderedType = DocumentRenderer.typesRenderer.render(List(testType)).trim assertTrue(renderedType == "type TestType @testdirective(object: {key1: \"value1\", key2: \"value2\"})") }, + test("only introspectable directives are rendered") { + val all = List( + Directive("d0", isIntrospectable = false), + Directive("d1"), + Directive("d2", isIntrospectable = false), + Directive("d3"), + Directive("d4"), + Directive("d5", isIntrospectable = false), + Directive("d6", isIntrospectable = false) + ) + val filtered = all.filter(_.isIntrospectable) + val renderedAll = DocumentRenderer.directivesRenderer.render(all) + val renderedFiltered = DocumentRenderer.directivesRenderer.render(filtered) + assertTrue(renderedAll == renderedFiltered, renderedAll == " @d1 @d3 @d4") + }, test( "it should escape \", \\, backspace, linefeed, carriage-return and tab inside a normally quoted description string" ) { diff --git a/core/src/test/scala/caliban/transformers/TransformerSpec.scala b/core/src/test/scala/caliban/transformers/TransformerSpec.scala index 19b9701b3..a492ec7af 100644 --- a/core/src/test/scala/caliban/transformers/TransformerSpec.scala +++ b/core/src/test/scala/caliban/transformers/TransformerSpec.scala @@ -2,7 +2,8 @@ package caliban.transformers import caliban.Macros.gqldoc import caliban._ -import caliban.schema.Annotations.GQLTag +import caliban.parsing.adt.Directive +import caliban.schema.Annotations.{ GQLDirective, GQLTag } import caliban.schema.ArgBuilder.auto._ import caliban.schema.Schema.auto._ import zio.test._ From a9320c3200a4d9deb4ad8a9e03e865f77058a710 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 25 Jun 2024 14:43:19 +1000 Subject: [PATCH 5/8] Change transformer to work on directives instead --- .../schema/AnnotationsVersionSpecific.scala | 18 +++++++- .../schema/AnnotationsVersionSpecific.scala | 10 +++++ .../caliban/introspection/adt/__Field.scala | 4 +- .../introspection/adt/__InputValue.scala | 7 +--- .../scala/caliban/parsing/adt/Directive.scala | 10 ----- .../scala/caliban/schema/Annotations.scala | 23 ---------- .../caliban/transformers/Transformer.scala | 37 +++++++++------- .../transformers/TransformerSpec.scala | 42 +++++++++---------- 8 files changed, 72 insertions(+), 79 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/AnnotationsVersionSpecific.scala b/core/src/main/scala-2/caliban/schema/AnnotationsVersionSpecific.scala index 7f068f25f..9f129cd1a 100644 --- a/core/src/main/scala-2/caliban/schema/AnnotationsVersionSpecific.scala +++ b/core/src/main/scala-2/caliban/schema/AnnotationsVersionSpecific.scala @@ -1,3 +1,19 @@ package caliban.schema -trait AnnotationsVersionSpecific +import caliban.parsing.adt.Directive + +import scala.annotation.StaticAnnotation + +trait AnnotationsVersionSpecific { + + /** + * Annotation used to provide directives to a schema type + */ + class GQLDirective(val directive: Directive) extends StaticAnnotation + + object GQLDirective { + def unapply(annotation: GQLDirective): Option[Directive] = + Some(annotation.directive) + } + +} diff --git a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala index 7a38c2021..633b4e624 100644 --- a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala +++ b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala @@ -12,4 +12,14 @@ trait AnnotationsVersionSpecific { */ case class GQLField() extends StaticAnnotation + /** + * Annotation used to provide directives to a schema type + */ + open class GQLDirective(val directive: Directive) extends StaticAnnotation + + object GQLDirective { + def unapply(annotation: GQLDirective): Option[Directive] = + Some(annotation.directive) + } + } diff --git a/core/src/main/scala/caliban/introspection/adt/__Field.scala b/core/src/main/scala/caliban/introspection/adt/__Field.scala index e46fe9c22..f4c597ded 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Field.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Field.scala @@ -2,7 +2,7 @@ package caliban.introspection.adt import caliban.Value.StringValue import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.{ FieldDefinition, InputValueDefinition } -import caliban.parsing.adt.{ Directive, Directives } +import caliban.parsing.adt.Directive import caliban.schema.Annotations.GQLExcluded case class __Field( @@ -34,8 +34,6 @@ case class __Field( lazy val allArgs: List[__InputValue] = args(__DeprecatedArgs.include) - private[caliban] lazy val _tags: Set[String] = directives.fold(Set.empty[String])(Directives.internal.tags) - private[caliban] lazy val _type: __Type = `type`() private[caliban] lazy val allArgNames: Set[String] = diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index f80ab7d3b..0217a8524 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -1,9 +1,9 @@ package caliban.introspection.adt import caliban.Value.StringValue -import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.InputValueDefinition -import caliban.parsing.adt.{ Directive, Directives } import caliban.parsing.Parser +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.InputValueDefinition +import caliban.parsing.adt.Directive import caliban.schema.Annotations.GQLExcluded import scala.annotation.tailrec @@ -18,7 +18,6 @@ case class __InputValue( @GQLExcluded directives: Option[List[Directive]] = None, @GQLExcluded parentType: () => Option[__Type] = () => None ) { - def toInputValueDefinition: InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) val allDirectives = (if (isDeprecated) @@ -32,8 +31,6 @@ case class __InputValue( InputValueDefinition(description, name, _type.toType(), default, allDirectives) } - private[caliban] lazy val _tags: Set[String] = directives.fold(Set.empty[String])(Directives.internal.tags) - private[caliban] lazy val _type: __Type = `type`() private[caliban] lazy val _parentType = parentType() diff --git a/core/src/main/scala/caliban/parsing/adt/Directive.scala b/core/src/main/scala/caliban/parsing/adt/Directive.scala index 655c101c8..6f604b2e1 100644 --- a/core/src/main/scala/caliban/parsing/adt/Directive.scala +++ b/core/src/main/scala/caliban/parsing/adt/Directive.scala @@ -18,16 +18,6 @@ object Directives { final val OneOf = "oneOf" final val Stream = "stream" - // We prefix these with `_caliban_tag_` to avoid conflicts with user-defined directives - private[caliban] object internal { - final val Tag = "_caliban_tag" - - def tags(directives: List[Directive]): Set[String] = - directives.collect { case Directive(Tag, args, _, _) if args.nonEmpty => args.keySet } - .foldLeft(Set.empty[String])(_ ++ _) - - } - def isDeprecated(directives: List[Directive]): Boolean = directives.exists(_.name == DeprecatedDirective) diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index 75174b494..f6e852f50 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -33,16 +33,6 @@ object Annotations extends AnnotationsVersionSpecific { */ case class GQLName(value: String) extends StaticAnnotation - /** - * Annotation used to provide directives to a schema type - */ - class GQLDirective(val directive: Directive) extends StaticAnnotation - - object GQLDirective { - def unapply(annotation: GQLDirective): Option[Directive] = - Some(annotation.directive) - } - /** * Annotation to make a sealed trait an interface instead of a union type or an enum * @@ -81,17 +71,4 @@ object Annotations extends AnnotationsVersionSpecific { * Annotation to make a sealed trait as a GraphQL @oneOf input */ case class GQLOneOfInput() extends StaticAnnotation - - /** - * Compile-time annotation that can be used in conjunction with [[caliban.transformers.Transformer]] to - * customize schema generation. - */ - case class GQLTag(tags: String*) - extends GQLDirective( - Directive( - Directives.internal.Tag, - tags.map(_ -> Value.NullValue).toMap, - isIntrospectable = false - ) - ) } diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index 3ccaabdc5..3598aef0c 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -3,6 +3,8 @@ package caliban.transformers import caliban.InputValue import caliban.execution.Field import caliban.introspection.adt._ +import caliban.parsing.adt.Directive +import caliban.schema.Annotations.GQLDirective import caliban.schema.Step import caliban.schema.Step.{ FunctionStep, MetadataFunctionStep, NullStep, ObjectStep } @@ -326,38 +328,45 @@ object Transformer { } } - object ExcludeTags { + object ExcludeDirectives { /** - * A transformer that allows excluding tagged fields and input arguments. + * A transformer that allows excluding fields and inputs with specific directives. * * {{{ - * ExcludeTags("TagA", "TagB") - * }}} + * case object Experimental extends GQLDirective(Directive("experimental")) + * case object Internal extends GQLDirective(Directive("internal")) * - * @param f tuples in the format of `(TypeName -> fieldToBeExcluded)` + * ExcludeDirectives(Experimental, Internal) + * }}} */ - def apply(f: String*): Transformer[Any] = - if (f.isEmpty) Empty else new ExcludeTags(f.toSet) + def apply(directives: GQLDirective*): Transformer[Any] = + if (directives.isEmpty) Empty else new ExcludeDirectives(directives.map(_.directive).toSet) } - final private class ExcludeTags(tags: Set[String]) extends Transformer[Any] { + final private class ExcludeDirectives(set: Set[Directive]) extends Transformer[Any] { private val map: mutable.HashMap[String, Set[String]] = mutable.HashMap.empty - private def shouldKeep(tpe: __Type, field: __Field): Boolean = { - val keep = field._tags.intersect(tags).isEmpty - if (!keep) map.updateWith(tpe.name.getOrElse("")) { + private def hasMatchingDirectives(directives: Option[List[Directive]]): Boolean = + directives match { + case None | Some(Nil) => false + case Some(dirs) => dirs.exists(d => set.contains(d)) + } + + private def shouldKeepType(tpe: __Type, field: __Field): Boolean = { + val matched = hasMatchingDirectives(field.directives) + if (matched) map.updateWith(tpe.name.getOrElse("")) { case Some(set) => Some(set + field.name) case None => Some(Set(field.name)) } - keep + !matched } val typeVisitor: TypeVisitor = - TypeVisitor.fields.filterWith((t, field) => shouldKeep(t, field)) |+| + TypeVisitor.fields.filterWith((t, field) => shouldKeepType(t, field)) |+| TypeVisitor.fields.modify { field => def loop(arg: __InputValue): Option[__InputValue] = - if (arg._type.isNullable && arg._tags.intersect(tags).nonEmpty) None + if (arg._type.isNullable && hasMatchingDirectives(arg.directives)) None else { lazy val newType = arg._type.mapInnerType { t => t.copy(inputFields = t.inputFields(_).map(_.flatMap(loop))) diff --git a/core/src/test/scala/caliban/transformers/TransformerSpec.scala b/core/src/test/scala/caliban/transformers/TransformerSpec.scala index a492ec7af..3061202f3 100644 --- a/core/src/test/scala/caliban/transformers/TransformerSpec.scala +++ b/core/src/test/scala/caliban/transformers/TransformerSpec.scala @@ -3,7 +3,7 @@ package caliban.transformers import caliban.Macros.gqldoc import caliban._ import caliban.parsing.adt.Directive -import caliban.schema.Annotations.{ GQLDirective, GQLTag } +import caliban.schema.Annotations.GQLDirective import caliban.schema.ArgBuilder.auto._ import caliban.schema.Schema.auto._ import zio.test._ @@ -221,21 +221,21 @@ object TransformerSpec extends ZIOSpecDefault { |}""".stripMargin ) }, - suite("ExcludeTag")( + suite("ExcludeTag") { + case class SchemaA() extends GQLDirective(Directive("schemaA", isIntrospectable = false)) + case class SchemaB() extends GQLDirective(Directive("schemaB", isIntrospectable = false)) + test("fields") { case class Query( a: String, - @GQLTag("schemaA") - b: Int, - @GQLTag("schemaB") - c: Double, - @GQLTag("schemaA", "schemaB") - d: Boolean + @SchemaA b: Int, + @SchemaB c: Double, + @SchemaA @SchemaB d: Boolean ) val api: GraphQL[Any] = graphQL(RootResolver(Query("a", 2, 3d, true))) - val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA")) - val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaB")) - val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA", "schemaB")) + val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA())) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaB())) + val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA(), SchemaB())) for { _ <- Configurator.setSkipValidation(true) @@ -290,23 +290,19 @@ object TransformerSpec extends ZIOSpecDefault { | a: String! |}""".stripMargin ) - }, - test("input fields") { + } + test("input fields") { case class Nested( a: String, - @GQLTag("schemaA") - b: Option[Int], - @GQLTag("schemaB") - c: Option[Double], - @GQLTag("schemaA", "schemaB") - d: Option[Boolean] + @SchemaA b: Option[Int], + @SchemaB c: Option[Double], + @SchemaA @SchemaB d: Option[Boolean] ) case class Args(a: String, b: String, l: List[String], nested: Nested) case class Query(foo: Args => String) val api: GraphQL[Any] = graphQL(RootResolver(Query(_ => "value"))) - val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA")) - val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaB")) - val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeTags("schemaA", "schemaB")) + val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA())) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaB())) + val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA(), SchemaB())) val rendered = api.render val renderedA = apiA.render @@ -369,6 +365,6 @@ object TransformerSpec extends ZIOSpecDefault { |}""".stripMargin ) } - ) + } ) } From 0cd3db91d8998053eeca9d210fddd3189645a57f Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 25 Jun 2024 14:47:28 +1000 Subject: [PATCH 6/8] Fix for Scala 3 --- .../scala-3/caliban/schema/AnnotationsVersionSpecific.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala index 633b4e624..2ba19891e 100644 --- a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala +++ b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala @@ -1,5 +1,7 @@ package caliban.schema +import caliban.parsing.adt.Directive + import scala.annotation.StaticAnnotation trait AnnotationsVersionSpecific { From 622de0817f1fc4b81ad3076726a9d197fd0e4787 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 27 Jun 2024 13:28:18 +1000 Subject: [PATCH 7/8] PR comment --- .../schema/AnnotationsVersionSpecific.scala | 3 ++- .../caliban/rendering/DocumentRenderer.scala | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala index efeba79cf..87436a846 100644 --- a/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala +++ b/core/src/main/scala-3/caliban/schema/AnnotationsVersionSpecific.scala @@ -13,7 +13,8 @@ trait AnnotationsVersionSpecific { * '''NOTE''' This annotation is not safe for use with ahead-of-time compilation (e.g., generating a GraalVM native-image executable) */ case class GQLField() extends StaticAnnotation - + + /** * Annotation that can be used on a case class / case object to have all the public methods on it derived as fields. * * If you wish to exclude a public method from being derived as a field, you can annotate it with [[GQLExclude]]. diff --git a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala index 8eccb16f3..78c76a70b 100644 --- a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala +++ b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala @@ -76,7 +76,9 @@ object DocumentRenderer extends Renderer[Document] { typeDefinitionsRenderer.contramap(_.flatMap(_.toTypeDefinition)) private[caliban] lazy val directivesRenderer: Renderer[List[Directive]] = - directiveRenderer.list(Renderer.spaceOrEmpty, omitFirst = false).contramap(_.sortBy(_.name)) + directiveRenderer + .list(Renderer.spaceOrEmpty, omitFirst = false) + .contramap(_.filter(_.isIntrospectable).sortBy(_.name)) private[caliban] lazy val descriptionRenderer: Renderer[Option[String]] = new Renderer[Option[String]] { @@ -572,14 +574,11 @@ 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 = - if (d.isIntrospectable) { - writer append '@' - writer append d.name - inputArgumentsRenderer.unsafeRender(d.arguments, indent, writer) - } else { - writer.deleteCharAt(writer.size - 1) - } + override def unsafeRender(d: Directive, indent: Option[Int], writer: StringBuilder): Unit = { + writer append '@' + writer append d.name + inputArgumentsRenderer.unsafeRender(d.arguments, indent, writer) + } } private lazy val fieldDefinitionsRenderer: Renderer[List[FieldDefinition]] = From 5dd159e08a26451748fba26cd2cf9886f6ae2877 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 27 Jun 2024 14:23:05 +1000 Subject: [PATCH 8/8] Add overload of `ExcludeDirectives.apply` that takes a predicate --- .../scala/caliban/transformers/Transformer.scala | 13 ++++++++++--- .../caliban/transformers/TransformerSpec.scala | 6 +++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index 3598aef0c..079f98d27 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -341,16 +341,23 @@ object Transformer { * }}} */ def apply(directives: GQLDirective*): Transformer[Any] = - if (directives.isEmpty) Empty else new ExcludeDirectives(directives.map(_.directive).toSet) + if (directives.isEmpty) Empty else new ExcludeDirectives(directives.map(_.directive).toSet.contains) + + /** + * A transformer that allows excluding fields and inputs with specific directives based on a predicate. + */ + def apply(predicate: Directive => Boolean): Transformer[Any] = + new ExcludeDirectives(predicate) + } - final private class ExcludeDirectives(set: Set[Directive]) extends Transformer[Any] { + final private class ExcludeDirectives(predicate: Directive => Boolean) extends Transformer[Any] { private val map: mutable.HashMap[String, Set[String]] = mutable.HashMap.empty private def hasMatchingDirectives(directives: Option[List[Directive]]): Boolean = directives match { case None | Some(Nil) => false - case Some(dirs) => dirs.exists(d => set.contains(d)) + case Some(dirs) => dirs.exists(predicate) } private def shouldKeepType(tpe: __Type, field: __Field): Boolean = { diff --git a/core/src/test/scala/caliban/transformers/TransformerSpec.scala b/core/src/test/scala/caliban/transformers/TransformerSpec.scala index 3061202f3..5c595fd5a 100644 --- a/core/src/test/scala/caliban/transformers/TransformerSpec.scala +++ b/core/src/test/scala/caliban/transformers/TransformerSpec.scala @@ -221,7 +221,7 @@ object TransformerSpec extends ZIOSpecDefault { |}""".stripMargin ) }, - suite("ExcludeTag") { + suite("ExcludeDirectives") { case class SchemaA() extends GQLDirective(Directive("schemaA", isIntrospectable = false)) case class SchemaB() extends GQLDirective(Directive("schemaB", isIntrospectable = false)) @@ -234,7 +234,7 @@ object TransformerSpec extends ZIOSpecDefault { ) val api: GraphQL[Any] = graphQL(RootResolver(Query("a", 2, 3d, true))) val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA())) - val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaB())) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(_.name == "schemaB")) val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA(), SchemaB())) for { @@ -301,7 +301,7 @@ object TransformerSpec extends ZIOSpecDefault { case class Query(foo: Args => String) val api: GraphQL[Any] = graphQL(RootResolver(Query(_ => "value"))) val apiA: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA())) - val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaB())) + val apiB: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(_.name == "schemaB")) val apiC: GraphQL[Any] = api.transform(Transformer.ExcludeDirectives(SchemaA(), SchemaB())) val rendered = api.render