Skip to content

Commit

Permalink
feature: add basic capability to encode a GraphQL program and generat…
Browse files Browse the repository at this point in the history
…e an execution plan
  • Loading branch information
tusharmath committed Jul 18, 2022
1 parent 74731ed commit ebf3768
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rules = [
RemoveUnused
]
9 changes: 9 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -11,6 +19,7 @@ lazy val root = project
libraryDependencies := Seq(
ZIOCore,
ZIOSchema,
ZIOSchemaJson,
ZIOSchemaDerivation,
ZIOTest % Test,
),
Expand Down
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.1")
5 changes: 5 additions & 0 deletions src/main/scala/com/tusharmath/compose/Endpoint.scala
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions src/main/scala/com/tusharmath/compose/ExecutionPlan.scala
Original file line number Diff line number Diff line change
@@ -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
}
62 changes: 54 additions & 8 deletions src/main/scala/com/tusharmath/compose/GraphQL.scala
Original file line number Diff line number Diff line change
@@ -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] = ()
}
}
64 changes: 62 additions & 2 deletions src/main/scala/com/tusharmath/compose/Main.scala
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 2 additions & 1 deletion src/test/scala/com/tusharmath/compose/GraphQLSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit ebf3768

Please sign in to comment.