From ebf37683b3c3780b7cf12eac8cba444ffebd9c00 Mon Sep 17 00:00:00 2001 From: Tushar Mathur Date: Mon, 18 Jul 2022 18:01:23 +0530 Subject: [PATCH] feature: add basic capability to encode a GraphQL program and generate an execution plan --- .scalafix.conf | 3 + build.sbt | 9 +++ project/Dependencies.scala | 1 + project/plugins.sbt | 1 + .../com/tusharmath/compose/Endpoint.scala | 5 ++ .../tusharmath/compose/ExecutionPlan.scala | 26 ++++++++ .../com/tusharmath/compose/GraphQL.scala | 62 +++++++++++++++--- .../scala/com/tusharmath/compose/Main.scala | 64 ++++++++++++++++++- .../com/tusharmath/compose/GraphQLSpec.scala | 3 +- 9 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 .scalafix.conf create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/com/tusharmath/compose/ExecutionPlan.scala diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..8025b84 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,3 @@ +rules = [ + RemoveUnused +] \ No newline at end of file diff --git a/build.sbt b/build.sbt index 28b95e6..4449add 100644 --- a/build.sbt +++ b/build.sbt @@ -2,6 +2,14 @@ import Dependencies._ val scala3Version = "2.13.8" +// Flags +Global / semanticdbEnabled := true +Global / onChangedBuildSource := ReloadOnSourceChanges +Global / scalacOptions := Seq( + "-Ywarn-unused:imports", +) + +// Projects lazy val root = project .in(file(".")) .settings( @@ -11,6 +19,7 @@ lazy val root = project libraryDependencies := Seq( ZIOCore, ZIOSchema, + ZIOSchemaJson, ZIOSchemaDerivation, ZIOTest % Test, ), diff --git a/project/Dependencies.scala b/project/Dependencies.scala index aff39b6..0766800 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,5 +6,6 @@ object Dependencies { val ZIOCore = "dev.zio" %% "zio" % zioVersion val ZIOTest = "dev.zio" %% "zio-test" % zioVersion val ZIOSchema = "dev.zio" %% "zio-schema" % zioSchemaVersion + val ZIOSchemaJson = "dev.zio" %% "zio-schema-json" % zioSchemaVersion val ZIOSchemaDerivation = "dev.zio" %% "zio-schema-derivation" % zioSchemaVersion } diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..9d1cd95 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.1") diff --git a/src/main/scala/com/tusharmath/compose/Endpoint.scala b/src/main/scala/com/tusharmath/compose/Endpoint.scala index 71863fb..6213d12 100644 --- a/src/main/scala/com/tusharmath/compose/Endpoint.scala +++ b/src/main/scala/com/tusharmath/compose/Endpoint.scala @@ -1,6 +1,11 @@ package com.tusharmath.compose final case class Endpoint(host: String, port: Int, path: String, body: Array[Byte], protocol: Protocol) +object Endpoint { + trait EndpointExecutor { + def apply(endpoint: Endpoint): Any + } +} sealed trait Protocol object Protocol { diff --git a/src/main/scala/com/tusharmath/compose/ExecutionPlan.scala b/src/main/scala/com/tusharmath/compose/ExecutionPlan.scala new file mode 100644 index 0000000..0ec4dfb --- /dev/null +++ b/src/main/scala/com/tusharmath/compose/ExecutionPlan.scala @@ -0,0 +1,26 @@ +package com.tusharmath.compose +import zio.schema.{DeriveSchema, DynamicValue} + +sealed trait ExecutionPlan {} + +object ExecutionPlan { + + implicit val schema = DeriveSchema.gen[ExecutionPlan] + + def fromGraphQL[A, B](graphQL: GraphQL[A, B]): ExecutionPlan = + graphQL match { + case GraphQL.Pipe(f, g) => Sequence(fromGraphQL(f), fromGraphQL(g)) + case GraphQL.Zip2(g, f) => Combine(fromGraphQL(g), fromGraphQL(f)) + case GraphQL.FromMap(i, source, o) => Dictionary(source.map { case (k, v) => (i.toDynamic(k), o.toDynamic(v)) }) + case GraphQL.Select(input, path, output) => Select(path) + case GraphQL.Constant(b, schema) => Constant(schema.toDynamic(b)) + case GraphQL.Identity() => Identity + } + + case class Constant(value: DynamicValue) extends ExecutionPlan + case class Combine(left: ExecutionPlan, right: ExecutionPlan) extends ExecutionPlan + case class Sequence(first: ExecutionPlan, second: ExecutionPlan) extends ExecutionPlan + case class Dictionary(value: Map[DynamicValue, DynamicValue]) extends ExecutionPlan + case class Select(path: List[String]) extends ExecutionPlan + case object Identity extends ExecutionPlan +} diff --git a/src/main/scala/com/tusharmath/compose/GraphQL.scala b/src/main/scala/com/tusharmath/compose/GraphQL.scala index cf66475..81367ad 100644 --- a/src/main/scala/com/tusharmath/compose/GraphQL.scala +++ b/src/main/scala/com/tusharmath/compose/GraphQL.scala @@ -1,15 +1,61 @@ package com.tusharmath.compose -import zio.schema.Schema +import com.tusharmath.compose.GraphQL.{Pipe, Zip2} +import zio.schema.{AccessorBuilder, DynamicValue, Schema} +import zio.prelude.NonEmptyList -sealed trait GraphQL[-A, +B] {} +sealed trait GraphQL[A, B] { self => + def <<<[X](other: GraphQL[X, A]): GraphQL[X, B] = self compose other + def >>>[C](other: GraphQL[B, C]): GraphQL[A, C] = self pipe other + + def compose[X](other: GraphQL[X, A]): GraphQL[X, B] = Pipe(other, self) + def pipe[C](other: GraphQL[B, C]): GraphQL[A, C] = Pipe(self, other) + def zip[C](other: GraphQL[A, C]): GraphQL[A, (B, C)] = Zip2(self, other) + def &&[C](other: GraphQL[A, C]): GraphQL[A, (B, C)] = self zip other +} object GraphQL { - case class Constant[B](b: B, schema: Schema[B]) extends GraphQL[Any, B] - case class Identity[A](schema: Schema[A]) extends GraphQL[A, A] - case class Compose[A, B, C](g: GraphQL[B, C], f: GraphQL[A, B]) extends GraphQL[A, C] - case class Zip2[A, B, C](g: GraphQL[A, B], f: GraphQL[A, C]) extends GraphQL[A, (B, C)] - case class Load[A](endpoint: Endpoint, schema: Schema[A]) extends GraphQL[Any, A] + def constant[B](a: B)(implicit schema: Schema[B]): GraphQL[Unit, B] = Constant(a, schema) + + def fromMap[A, B](source: Map[A, B])(implicit input: Schema[A], output: Schema[B]): GraphQL[A, B] = + FromMap(input, source, output) + + final case class FromMap[A, B](input: Schema[A], source: Map[A, B], output: Schema[B]) extends GraphQL[A, B] + final case class Constant[B](b: B, schema: Schema[B]) extends GraphQL[Unit, B] + final case class Identity[A]() extends GraphQL[A, A] + final case class Pipe[A, B, C](f: GraphQL[A, B], g: GraphQL[B, C]) extends GraphQL[A, C] + final case class Zip2[A, B, C](g: GraphQL[A, B], f: GraphQL[A, C]) extends GraphQL[A, (B, C)] + final case class Select[A, B](input: Schema[A], path: NonEmptyList[String], output: Schema[B]) extends GraphQL[A, B] + + def execute[A, B](graphQL: GraphQL[A, B])(a: A): Either[String, B] = + graphQL match { + case Constant(b, _) => Right(b) + case Identity() => Right(a.asInstanceOf[B]) + case Pipe(f, g) => execute(f)(a).flatMap(execute(g)) + case FromMap(_, map, _) => map.get(a).toRight("No value found for " + a) + case Zip2(g, f) => + for { + a <- execute(g)(a) + b <- execute(f)(a) + } yield (a, b) + + case Select(input, path, output) => + input.toDynamic(a) match { + case record @ DynamicValue.Record(_) => Left("TODO") + case _ => Left(s"Cannot select field}") + } + } + + object Accessors extends AccessorBuilder { + override type Lens[S, A] = GraphQL[S, A] + override type Prism[S, A] = Unit + override type Traversal[S, A] = Unit + + override def makeLens[S, A](product: Schema.Record[S], term: Schema.Field[A]): Lens[S, A] = + GraphQL.Select(product, NonEmptyList(term.label), term.schema) + + override def makePrism[S, A](sum: Schema.Enum[S], term: Schema.Case[A, S]): Prism[S, A] = () - def constant[A](a: A)(implicit schema: Schema[A]): GraphQL[Any, A] = Constant(a, schema) + override def makeTraversal[S, A](collection: Schema.Collection[S, A], element: Schema[A]): Traversal[S, A] = () + } } diff --git a/src/main/scala/com/tusharmath/compose/Main.scala b/src/main/scala/com/tusharmath/compose/Main.scala index 53fd3b3..5e89e84 100644 --- a/src/main/scala/com/tusharmath/compose/Main.scala +++ b/src/main/scala/com/tusharmath/compose/Main.scala @@ -1,7 +1,67 @@ package com.tusharmath.compose +import zio.schema.DeriveSchema +import zio.schema.codec.JsonCodec + object Main extends App { - def hello(): Unit = { - println("Hello!") + + case class Round(name: String, id: Round.Id) + object Round { + case class Id(id: Long) + implicit val schema = DeriveSchema.gen[Round] + implicit val matchId = DeriveSchema.gen[Id] + val accessors = schema.makeAccessors(GraphQL.Accessors) } + + case class Contest( + name: String, + id: Contest.Id, + entryFee: Double, + size: Long, +// users: List[User.Id], + roundId: Round.Id, + ) + + object Contest { + + case class Id(id: Long) + + implicit val schema = DeriveSchema.gen[Contest] + implicit val contestId = DeriveSchema.gen[Id] + + val (name, id, entryFee, size, roundId) = schema.makeAccessors(GraphQL.Accessors) + } + + case class User(name: String, id: User.Id, age: Int) + object User { + case class Id(id: Long) + + implicit val schema = DeriveSchema.gen[User] + implicit val userId = DeriveSchema.gen[Id] + + val (name, id, age) = schema.makeAccessors(GraphQL.Accessors) + } + + val getUser1 = GraphQL.constant(User("Tushar Mathur", User.Id(1), 30)) + + val getUser2 = GraphQL.constant(User("Aiswarya Prakasan", User.Id(2), 90)) + + val getRound = GraphQL.fromMap { + Map( + Round.Id(1) -> Round("Round 1", Round.Id(1)), + Round.Id(2) -> Round("Round 2", Round.Id(2)), + Round.Id(3) -> Round("Round 3", Round.Id(3)), + ) + } + + val getContest = GraphQL.constant(Contest("Contest 1", Contest.Id(1), 100.0, 10, Round.Id(1))) + + // From contest prepare round details + val program = getContest >>> Contest.roundId >>> getRound && getContest + + val plan = ExecutionPlan.fromGraphQL(program) + + val encoded = new String(JsonCodec.encode(ExecutionPlan.schema)(plan).toArray) + + println("Encoded: " + encoded) } diff --git a/src/test/scala/com/tusharmath/compose/GraphQLSpec.scala b/src/test/scala/com/tusharmath/compose/GraphQLSpec.scala index e874ec6..1af203f 100644 --- a/src/test/scala/com/tusharmath/compose/GraphQLSpec.scala +++ b/src/test/scala/com/tusharmath/compose/GraphQLSpec.scala @@ -3,7 +3,8 @@ package com.tusharmath.compose import com.tusharmath.compose.GraphQLSpec.test import zio.test.{assertTrue, ZIOSpecDefault} -object GraphQLSpec extends ZIOSpecDefault: +object GraphQLSpec extends ZIOSpecDefault { override def spec = test("Some test") { assertTrue(true) } +}