diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index f0190db279..ba17b1d910 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -9,6 +9,7 @@ import caliban.interop.zio.{ IsZIOJsonDecoder, IsZIOJsonEncoder } import caliban.rendering.ValueRenderer import zio.stream.Stream +import scala.annotation.tailrec import scala.util.control.NonFatal import scala.util.hashing.MurmurHash3 @@ -67,6 +68,38 @@ sealed trait ResponseValue extends Serializable { self => } } object ResponseValue { + + def at(path: List[PathValue])(value: ResponseValue): ResponseValue = { + def loop(path: List[PathValue], value: ResponseValue): ResponseValue = path match { + case Nil => value + case PathValue.Key(key) :: tail => + value match { + case ObjectValue(fields) => + fields.find(_._1 == key) match { + case Some((_, v)) => loop(tail, v) + case None => Value.NullValue + } + case ListValue(values) => + ListValue(values.map(loop(path, _))) + case _ => Value.NullValue + } + case PathValue.Index(index) :: tail => + value match { + case ListValue(values) => + val idx = index + if (idx < values.size) { + loop(tail, values(idx)) + } else { + Value.NullValue + } + case _ => Value.NullValue + } + case _ => Value.NullValue + } + + loop(path, value) + } + case class ListValue(values: List[ResponseValue]) extends ResponseValue { override def toString: String = ValueRenderer.responseListValueRenderer.renderCompact(this) } diff --git a/federation/src/main/scala/caliban/federation/FederationSupport.scala b/federation/src/main/scala/caliban/federation/FederationSupport.scala index e4d695f94c..c785998115 100644 --- a/federation/src/main/scala/caliban/federation/FederationSupport.scala +++ b/federation/src/main/scala/caliban/federation/FederationSupport.scala @@ -13,6 +13,11 @@ abstract class FederationSupport( ) { import FederationHelpers._ + // This is a bit of a hack to determine if we are using the v1 version of the federation spec + // All of the v2 directives come through schema directives while the v1 is through the supported directives field instead + private val isV1 = supportedDirectives.nonEmpty && schemaDirectives.isEmpty + private val extraTypes = if (isV1) List(fieldSetSchema.toType_()) else Nil + /** * Accepts a GraphQL and returns a GraphQL with the minimum settings to support federation. This variant does not * provide any stitching capabilities, it merely makes this schema consumable by a graphql federation gateway. @@ -20,17 +25,17 @@ abstract class FederationSupport( * @return A new schema which has been augmented with federation types */ def federate[R](original: GraphQL[R]): GraphQL[R] = { - import caliban.schema.Schema.auto._ - case class Query( - _service: _Service, - _fieldSet: FieldSet = FieldSet("") + _service: _Service ) + implicit val serviceSchema = Schema.gen[R, _Service] + implicit val querySchema = Schema.gen[R, Query] + graphQL( RootResolver(Query(_service = _Service(original.withSchemaDirectives(schemaDirectives).render))), supportedDirectives - ) |+| original + ).withAdditionalTypes(extraTypes) |+| original } def federated[R](resolver: EntityResolver[R], others: EntityResolver[R]*): GraphQLAspect[Nothing, R] = @@ -57,7 +62,6 @@ abstract class FederationSupport( val resolvers = resolver +: otherResolvers.toList val genericSchema = new GenericSchema[R] {} - import genericSchema.auto._ implicit val entitySchema: Schema[R, _Entity] = new Schema[R, _Entity] { override def nullable: Boolean = true @@ -84,14 +88,17 @@ abstract class FederationSupport( case class Query( _entities: RepresentationsArgs => List[_Entity], - _service: ZQuery[Any, Nothing, _Service], - _fieldSet: FieldSet = FieldSet("") + _service: ZQuery[Any, Nothing, _Service] ) val withSDL = original .withAdditionalTypes(resolvers.map(_.toType).flatMap(Types.collectTypes(_))) .withSchemaDirectives(schemaDirectives) + implicit val representationsArgsSchema: Schema[Any, RepresentationsArgs] = Schema.gen + implicit val serviceSchema: Schema[R, _Service] = genericSchema.gen[R, _Service] + implicit val querySchema: Schema[R, Query] = genericSchema.gen[R, Query] + graphQL[R, Query, Unit, Unit]( RootResolver( Query( @@ -100,7 +107,7 @@ abstract class FederationSupport( ) ), supportedDirectives - ) |+| original + ).withAdditionalTypes(extraTypes) |+| original } } diff --git a/federation/src/test/scala/caliban/federation/FederationV1Spec.scala b/federation/src/test/scala/caliban/federation/FederationV1Spec.scala index 7a160ea1fd..995a070aef 100644 --- a/federation/src/test/scala/caliban/federation/FederationV1Spec.scala +++ b/federation/src/test/scala/caliban/federation/FederationV1Spec.scala @@ -150,6 +150,30 @@ object FederationV1Spec extends ZIOSpecDefault { ) ) } + }, + test("introspection should include _Any and _FieldSet scalars") { + val interpreter = (graphQL(resolver) @@ federated(entityResolver)).interpreter + + val query = gqldoc("""{ __schema { types { name } } }""") + interpreter + .flatMap(_.execute(query)) + .map(d => + ResponseValue.at( + PathValue.Key("__schema") :: PathValue.Key("types") :: PathValue.Key("name") :: Nil + )(d.data) + ) + .map(responseValue => + assertTrue( + responseValue + .is(_.subtype[ListValue]) + .values + .contains(StringValue("_Any")), + responseValue + .is(_.subtype[ListValue]) + .values + .contains(StringValue("_FieldSet")) + ) + ) } ) } diff --git a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala index b51fe35b12..5b58bbf25b 100644 --- a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala +++ b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala @@ -2,6 +2,7 @@ package caliban.federation.v2x import caliban.InputValue.{ ListValue, ObjectValue } import caliban.Macros.gqldoc +import caliban.TestUtils.resolver import caliban.Value.StringValue import caliban.parsing.Parser import caliban.parsing.adt.{ Definition, Directive } @@ -11,7 +12,6 @@ import io.circe.Json import io.circe.parser.decode import zio.ZIO import zio.test.Assertion.{ hasSameElements, isSome } -import zio.test.{ assertTrue, ZIOSpecDefault } import zio.test._ object FederationV2Spec extends ZIOSpecDefault { @@ -182,6 +182,30 @@ object FederationV2Spec extends ZIOSpecDefault { ) ) } + }, + test("introspection doesn't contain _FieldSet scalar") { + import caliban.federation.v2_3._ + val interpreter = (graphQL(resolver) @@ federated).interpreter + val query = gqldoc("""{ __schema { types { name } } }""") + interpreter + .flatMap(_.execute(query)) + .map(d => + ResponseValue.at( + PathValue.Key("__schema") :: PathValue.Key("types") :: PathValue.Key("name") :: Nil + )(d.data) + ) + .map(responseValue => + assertTrue( + !responseValue + .is(_.subtype[ResponseValue.ListValue]) + .values + .contains(StringValue("_Any")), + !responseValue + .is(_.subtype[ResponseValue.ListValue]) + .values + .contains(StringValue("_FieldSet")) + ) + ) } )