From 556313e10180231f141a421f58d3e9c53105c39a Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 22 Oct 2024 00:48:01 +0800 Subject: [PATCH] add support for federation 2.9 and 2.10 --- .../scala/caliban/federation/package.scala | 22 ++-- .../v2x/FederationDirectivesV2_10.scala | 117 ++++++++++++++++++ .../v2x/FederationDirectivesV2_9.scala | 52 ++++++++ .../caliban/federation/v2x/FederationV2.scala | 16 +++ .../caliban/federation/v2x/Versions.scala | 18 +-- .../federation/v2x/FederationV2Spec.scala | 25 ++++ 6 files changed, 233 insertions(+), 17 deletions(-) create mode 100644 federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_10.scala create mode 100644 federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_9.scala diff --git a/federation/src/main/scala/caliban/federation/package.scala b/federation/src/main/scala/caliban/federation/package.scala index 59a3ab4de..558ab9386 100644 --- a/federation/src/main/scala/caliban/federation/package.scala +++ b/federation/src/main/scala/caliban/federation/package.scala @@ -1,24 +1,28 @@ package caliban import caliban.federation.v2x.{ + FederationDirectivesV2_10, FederationDirectivesV2_3, FederationDirectivesV2_5, FederationDirectivesV2_6, FederationDirectivesV2_8, + FederationDirectivesV2_9, FederationV2, Versions } package object federation { - lazy val v1 = new FederationV1 with FederationDirectives - lazy val v2_0 = new FederationV2(List(Versions.v2_0)) - lazy val v2_1 = new FederationV2(List(Versions.v2_1)) - lazy val v2_3 = new FederationV2(List(Versions.v2_3)) with FederationDirectivesV2_3 - lazy val v2_4 = new FederationV2(List(Versions.v2_4)) with FederationDirectivesV2_3 - lazy val v2_5 = new FederationV2(List(Versions.v2_5)) with FederationDirectivesV2_5 - lazy val v2_6 = new FederationV2(List(Versions.v2_6)) with FederationDirectivesV2_6 - lazy val v2_7 = new FederationV2(List(Versions.v2_7)) with FederationDirectivesV2_6 - lazy val v2_8 = new FederationV2(List(Versions.v2_8)) with FederationDirectivesV2_8 + lazy val v1 = new FederationV1 with FederationDirectives + lazy val v2_0 = new FederationV2(List(Versions.v2_0)) + lazy val v2_1 = new FederationV2(List(Versions.v2_1)) + lazy val v2_3 = new FederationV2(List(Versions.v2_3)) with FederationDirectivesV2_3 + lazy val v2_4 = new FederationV2(List(Versions.v2_4)) with FederationDirectivesV2_3 + lazy val v2_5 = new FederationV2(List(Versions.v2_5)) with FederationDirectivesV2_5 + lazy val v2_6 = new FederationV2(List(Versions.v2_6)) with FederationDirectivesV2_6 + lazy val v2_7 = new FederationV2(List(Versions.v2_7)) with FederationDirectivesV2_6 + lazy val v2_8 = new FederationV2(List(Versions.v2_8)) with FederationDirectivesV2_8 + lazy val v2_9 = new FederationV2(List(Versions.v2_9)) with FederationDirectivesV2_9 + lazy val v2_10 = new FederationV2(List(Versions.v2_10, FederationV2.connect)) with FederationDirectivesV2_10 } diff --git a/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_10.scala b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_10.scala new file mode 100644 index 000000000..7ecee5436 --- /dev/null +++ b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_10.scala @@ -0,0 +1,117 @@ +package caliban.federation.v2x + +import caliban.{ InputValue, Value } +import caliban.parsing.adt.Directive +import caliban.schema.Annotations.GQLDirective + +trait FederationDirectivesV2_10 extends FederationDirectivesV2_9 { + + case class JSONSelection(select: String) + case class HTTPHeaderMapping( + name: String, + from: Option[String], + value: Option[String] + ) + + sealed trait Method { + def url: String + def body: Option[JSONSelection] = None + } + + object Method { + case class GET(url: String) extends Method + case class DELETE(url: String) extends Method + case class POST(url: String, override val body: Option[JSONSelection]) extends Method + case class PUT(url: String, override val body: Option[JSONSelection]) extends Method + case class PATCH(url: String, override val body: Option[JSONSelection]) extends Method + } + + case class ConnectHTTP( + method: Method, + headers: List[HTTPHeaderMapping] = Nil + ) + + def Connect( + http: ConnectHTTP, + selection: JSONSelection, + source: Option[String], + entity: Option[Boolean] + ): Directive = { + val connectBuilder = Map.newBuilder[String, InputValue] + val httpBuilder = Map.newBuilder[String, InputValue] + + http.method match { + case Method.GET(url) => "GET" -> Value.StringValue(url) + case Method.DELETE(url) => "DELETE" -> Value.StringValue(url) + case Method.POST(url, _) => "POST" -> Value.StringValue(url) + case Method.PUT(url, _) => "PUT" -> Value.StringValue(url) + case Method.PATCH(url, _) => "PATCH" -> Value.StringValue(url) + } + http.method.body.foreach(body => httpBuilder += "body" -> Value.StringValue(body.select)) + if (http.headers.nonEmpty) + httpBuilder += "headers" -> InputValue.ListValue( + http.headers.map(h => + InputValue.ObjectValue( + Map( + "name" -> Value.StringValue(h.name), + "from" -> h.from.fold[InputValue](Value.NullValue)(from => + InputValue.ObjectValue(Map("from" -> Value.StringValue(from))) + ), + "value" -> h.value.fold[InputValue](Value.NullValue)(value => + InputValue.ObjectValue(Map("value" -> Value.StringValue(value))) + ) + ) + ) + ) + ) + + connectBuilder += "http" -> InputValue.ObjectValue(httpBuilder.result()) + connectBuilder += "selection" -> Value.StringValue(selection.select) + source.foreach(s => connectBuilder += "source" -> Value.StringValue(s)) + entity.foreach(e => connectBuilder += "entity" -> Value.BooleanValue(e)) + + Directive("source", httpBuilder.result()) + } + + case class GQLConnect( + http: ConnectHTTP, + selection: JSONSelection, + source: Option[String] = None, + entity: Option[Boolean] = None + ) extends GQLDirective(Connect(http, selection, source, entity)) + + def Source( + name: String, + baseURL: String, + headers: List[HTTPHeaderMapping] = Nil + ): Directive = { + val sourceBuilder = Map.newBuilder[String, InputValue] + sourceBuilder += "name" -> Value.StringValue(name) + sourceBuilder += "baseURL" -> Value.StringValue(baseURL) + if (headers.nonEmpty) + sourceBuilder += "headers" -> InputValue.ListValue( + headers.map(h => + InputValue.ObjectValue( + Map( + "name" -> Value.StringValue(h.name), + "from" -> h.from.fold[InputValue](InputValue.ObjectValue(Map()))(from => + InputValue.ObjectValue(Map("from" -> Value.StringValue(from))) + ), + "value" -> h.value.fold[InputValue](InputValue.ObjectValue(Map()))(value => + InputValue.ObjectValue(Map("value" -> Value.StringValue(value))) + ) + ) + ) + ) + ) + + Directive("source", sourceBuilder.result()) + } + + case class GQLSource( + name: String, + baseURL: String, + headers: List[HTTPHeaderMapping] = Nil + ) extends GQLDirective(Source(name, baseURL, headers)) + +} diff --git a/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_9.scala b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_9.scala new file mode 100644 index 000000000..7d0a6a336 --- /dev/null +++ b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_9.scala @@ -0,0 +1,52 @@ +package caliban.federation.v2x + +import caliban.InputValue +import caliban.InputValue.ListValue +import caliban.Value.{ BooleanValue, IntValue, StringValue } +import caliban.parsing.adt.Directive +import caliban.schema.Annotations.GQLDirective + +trait FederationDirectivesV2_9 extends FederationDirectivesV2_8 { + + def Cost(weight: Int) = Directive("cost", Map("weight" -> IntValue(weight))) + + case class GQLCost(weight: Int) extends GQLDirective(Cost(weight)) + + def ListSize( + assumedSize: Option[Int] = None, + slicingArguments: Option[List[String]] = None, + sizedFields: Option[List[String]] = None, + requireOneSlicingArgument: Boolean = true + ) = { + val builder = Map.newBuilder[String, InputValue] + + assumedSize.foreach(size => builder += "assumedSize" -> IntValue(size)) + slicingArguments.foreach { args => + builder += "slicingArguments" -> ListValue(args.map(StringValue.apply)) + } + sizedFields.foreach { fields => + builder += "sizedFields" -> ListValue(fields.map(StringValue.apply)) + } + builder += "requireOneSlicingArgument" -> BooleanValue(requireOneSlicingArgument) + + Directive( + "listSize", + builder.result() + ) + } + + case class GQLListSize( + assumedSize: Option[Int] = None, + slicingArguments: Option[List[String]] = None, + sizedFields: Option[List[String]] = None, + requireOneSlicingArgument: Boolean = true + ) extends GQLDirective( + ListSize( + assumedSize, + slicingArguments, + sizedFields, + requireOneSlicingArgument + ) + ) + +} diff --git a/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala b/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala index e6f6a782f..6b93066dd 100644 --- a/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala +++ b/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala @@ -10,6 +10,7 @@ class FederationV2(extensions: List[Extension]) object FederationV2 { val federationV2Url = "https://specs.apollo.dev/federation" + val connectUrl = "https://specs.apollo.dev/connect" def DefaultDirectives: List[Import] = List( Import("@key"), @@ -62,4 +63,19 @@ object FederationV2 { `import` = v2_7.`import` :+ Import("@context") :+ Import("@fromContext") ) + private[v2x] val v2_9 = Link( + url = s"$federationV2Url/v2.9", + `import` = v2_8.`import` :+ Import("@cost") :+ Import("@listSize") + ) + + private[v2x] val v2_10 = Link( + url = s"$federationV2Url/v2.10", + `import` = v2_9.`import` + ) + + val connect: Link = Link( + url = s"$connectUrl/v0.1", + `import` = List(Import("@connect"), Import("@source")) + ) + } diff --git a/federation/src/main/scala/caliban/federation/v2x/Versions.scala b/federation/src/main/scala/caliban/federation/v2x/Versions.scala index 9e464f8a5..b395532ea 100644 --- a/federation/src/main/scala/caliban/federation/v2x/Versions.scala +++ b/federation/src/main/scala/caliban/federation/v2x/Versions.scala @@ -2,13 +2,15 @@ package caliban.federation.v2x object Versions { - val v2_0 = FederationV2.v2_0 - val v2_1 = FederationV2.v2_1 - val v2_3 = FederationV2.v2_3 - val v2_4 = FederationV2.v2_4 - val v2_5 = FederationV2.v2_5 - val v2_6 = FederationV2.v2_6 - val v2_7 = FederationV2.v2_7 - val v2_8 = FederationV2.v2_8 + val v2_0 = FederationV2.v2_0 + val v2_1 = FederationV2.v2_1 + val v2_3 = FederationV2.v2_3 + val v2_4 = FederationV2.v2_4 + val v2_5 = FederationV2.v2_5 + val v2_6 = FederationV2.v2_6 + val v2_7 = FederationV2.v2_7 + val v2_8 = FederationV2.v2_8 + val v2_9 = FederationV2.v2_9 + val v2_10 = FederationV2.v2_10 } diff --git a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala index 2627781d5..d94502ea1 100644 --- a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala +++ b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala @@ -207,6 +207,31 @@ object FederationV2Spec extends ZIOSpecDefault { .contains(StringValue("_FieldSet")) ) ) + }, + test("connect spec includes the correct directives") { + import caliban.federation.v2_10._ + makeSchemaDirectives(federated(_)).map { schemaDirectives => + val linkDirectives = schemaDirectives.filter(_.name == "link") + val connectDirective = linkDirectives + .find(_.arguments.get("url").exists { + case StringValue(value) => value.startsWith("https://specs.apollo.dev/connect") + case _ => false + }) + + assertTrue( + connectDirective.get == Directive( + "link", + Map( + "url" -> StringValue("https://specs.apollo.dev/connect/v0.1"), + "import" -> ListValue( + StringValue("@connect") :: + StringValue("@source") :: Nil + ) + ) + ) + ) + } + } )