diff --git a/runtime/src/main/scala/tailcall/runtime/model/Blueprint.scala b/runtime/src/main/scala/tailcall/runtime/model/Blueprint.scala index c4fcde049b..62ff88371e 100644 --- a/runtime/src/main/scala/tailcall/runtime/model/Blueprint.scala +++ b/runtime/src/main/scala/tailcall/runtime/model/Blueprint.scala @@ -152,6 +152,17 @@ object Blueprint { case NamedType(_, nonNull) => NamedType(name, nonNull) case ListType(ofType, nonNull) => ListType(ofType.withName(name), nonNull) } + + final def render: String = { + def renderNonNull(tpe: Type): String = + tpe match { + case NamedType(name, true) => s"$name!" + case ListType(ofType, true) => s"[${renderNonNull(ofType)}]!" + case NamedType(name, false) => name + case ListType(ofType, false) => s"[${renderNonNull(ofType)}]" + } + renderNonNull(self) + } } final case class NamedType(name: String, nonNull: Boolean) extends Type diff --git a/runtime/src/main/scala/tailcall/runtime/service/StepGenerator.scala b/runtime/src/main/scala/tailcall/runtime/service/StepGenerator.scala index b6dd138412..9b82a4daf4 100644 --- a/runtime/src/main/scala/tailcall/runtime/service/StepGenerator.scala +++ b/runtime/src/main/scala/tailcall/runtime/service/StepGenerator.scala @@ -36,7 +36,8 @@ object StepGenerator { final case class BlueprintGenerator(rtm: EvaluationRuntime, document: Blueprint) { val rootContext: Context = Context(DynamicValue(())) - val stepRef: Map[String, Context => Step[HttpDataLoader]] = document.definitions + // A map of all the object types and a way to construct an instance of them. + val objectStepRef: Map[String, Context => Step[HttpDataLoader]] = document.definitions .collect { case obj @ Blueprint.ObjectTypeDefinition(_, _, _) => (obj.name, ctx => fromObjectDef(obj, ctx)) } .toMap @@ -44,12 +45,12 @@ object StepGenerator { val queryStep = for { query <- document.schema.flatMap(_.query) - qStep <- stepRef.get(query) + qStep <- objectStepRef.get(query) } yield qStep(rootContext) val mutationStep = for { mutation <- document.schema.flatMap(_.mutation) - mStep <- stepRef.get(mutation) + mStep <- objectStepRef.get(mutation) } yield mStep(rootContext) StepResult(queryStep, mutationStep) @@ -78,23 +79,29 @@ object StepGenerator { Step.ObjectStep(obj.name, obj.fields.map(field => field.name -> fromFieldDefinition(field, ctx)).toMap) } + /** + * This method converts create a step from a type. There + * is an implicit assumption that the type and the + * actual value, which is available in the ctx.value are + * compatible. We bailout if the types are not + * compatible with the value. + */ def fromType(tpe: model.Blueprint.Type, ctx: Context): Step[HttpDataLoader] = { tpe match { - case model.Blueprint.NamedType(name, _) => stepRef.get(name) match { - case Some(stepFunction) => ctx.value match { - case DynamicValue.Sequence(chunks) => Step - .ListStep(chunks.toList.map(value => stepFunction(ctx.copy(value = value)))) - // TODO: add unit test for some value - // case DynamicValue.SomeValue(value) => stepFunction(ctx.copy(value = value)) - case _ => stepFunction(ctx) - } + case model.Blueprint.NamedType(name, _) => objectStepRef.get(name) match { + case Some(stepFunction) => stepFunction(ctx) + // This is a case for scalar values case None => Step.PureStep(Transcoder.toResponseValue(ctx.value).getOrElse(Value.NullValue)) } - case model.Blueprint.ListType(ofType, _) => ctx.value match { - case DynamicValue.Sequence(values) => Step - .ListStep(values.map(value => fromType(ofType, ctx.copy(value = value))).toList) - case DynamicValue.SomeValue(value) => fromType(ofType, ctx.copy(value = value)) - case _ => Step.ListStep(List(fromType(ofType, ctx))) + case model.Blueprint.ListType(ofType, nonNull) => + val isNullable = !nonNull + ctx.value match { + // This should be a guarantee we should be able to typecast it safely + case DynamicValue.Sequence(values) => Step + .ListStep(values.toList.map(value => fromType(ofType, ctx.copy(value = value)))) + case DynamicValue.SomeValue(DynamicValue.Sequence(values)) if isNullable => + Step.ListStep(values.toList.map(value => fromType(ofType, ctx.copy(value = value)))) + case _ => throw new RuntimeException(s"Unexpected value received for type ${tpe.render}") } } } diff --git a/runtime/src/test/scala/tailcall/runtime/Config2GraphQLSpec.scala b/runtime/src/test/scala/tailcall/runtime/Config2GraphQLSpec.scala index 92338c9576..8915207586 100644 --- a/runtime/src/test/scala/tailcall/runtime/Config2GraphQLSpec.scala +++ b/runtime/src/test/scala/tailcall/runtime/Config2GraphQLSpec.scala @@ -7,7 +7,7 @@ import tailcall.runtime.model.Config.{Arg, Field, Type} import tailcall.runtime.model.{Config, Step} import tailcall.runtime.service.DataLoader.HttpDataLoader import tailcall.runtime.service._ -import zio.json._ +import zio.json.ast.Json import zio.test.Assertion.equalTo import zio.test.TestAspect.timeout import zio.test.{ZIOSpecDefault, assertZIO} @@ -122,16 +122,23 @@ object Config2GraphQLSpec extends ZIOSpecDefault { assertZIO(program)(equalTo(expected)) }, test("nested type") { - val value = Map("a" -> "abc".toJsonAST.toOption.get, "b" -> List(Map("bar" -> "bar")).toJsonAST.toOption.get) - .toJsonAST.toOption.get + val value = Json.Obj( + "b" -> Json.Arr( + // + Json.Obj("c" -> Json.Num(1)), + Json.Obj("c" -> Json.Num(2)), + Json.Obj("c" -> Json.Num(3)), + ) + ) + val config = Config.empty.withQuery("Query").withType( - "Query" -> Type("foo" -> Field.ofType("Foo").withSteps(Step.Constant(value))), - "Foo" -> Type("a" -> Field.ofType("String"), "b" -> Field.ofType("Bar").asList), - "Bar" -> Type("bar" -> Field.ofType("String")), + "Query" -> Type("a" -> Field.ofType("A").withSteps(Step.Constant(value))), + "A" -> Type("b" -> Field.ofType("B").asList), + "B" -> Type("c" -> Field.int), ) - val program = execute(config)("""{foo {a b {bar}}}""") - assertZIO(program)(equalTo("""{"foo":{"a":"abc","b":[{"bar":"bar"}]}}""")) + val program = execute(config)("""{a {b {c}}}""") + assertZIO(program)(equalTo("""{"a":{"b":[{"c":1},{"c":2},{"c":3}]}}""")) }, ).provide(GraphQLGenerator.default, HttpClient.default, DataLoader.http) @@ timeout(10 seconds) }