Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EffectfulQueryBuilder for higher-level API on top of RootEffect / GenericMapping #1

Open
wants to merge 4 commits into
base: add-contramap-cursor-builder
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/generic/src/main/scala-2/genericmapping2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge
): MkObjectCursorBuilder[T] =
new MkObjectCursorBuilder[T] {
def apply(tpe: Type): ObjectCursorBuilder[T] = {
def fieldMap: Map[String, (Context, T, Option[Cursor], Env) => Result[Cursor]] = {
def fieldMap: FieldMap[T] = {
val keys: List[String] = unsafeToList[Symbol](keys0()).map(_.name)
val elems = unsafeToList[CursorBuilder[Any]](elems0.instances)
keys.zip(elems.zipWithIndex).map {
Expand Down
34 changes: 34 additions & 0 deletions modules/generic/src/main/scala/Argument.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package edu.gemini.grackle
package generic

import edu.gemini.grackle._
import edu.gemini.grackle.Query._
import cats.syntax.all._

final case class Argument[A](name: String, extract: Value => Option[A])
final case class FieldArguments(query: String, extractors: List[Argument[_]])

object FieldArguments {
def moveArgsToEnv(fieldArguments: List[FieldArguments]): PartialFunction[Select, Result[Query]] =
PartialFunction
.fromFunction[Select, Option[Result[Query]]] {
case Select(query, bindings, child) =>
fieldArguments.collectFirst {
case se if se.query === query => Result(Environment(buildEnv(bindings, se), Select(query, Nil, child)))
}
}
.andThen { case Some(value) => value }

def buildEnv(bindings: List[Binding], se: FieldArguments): Cursor.Env =
bindings.foldLeft(Cursor.Env.empty) {
case (accEnv, Binding(name, value)) =>
val newEnv = se.extractors
.collectFirst { case e if e.name === name => e.extract(value) }
.map(e => Cursor.Env(name -> e))
.getOrElse(Cursor.Env.empty)
accEnv.add(newEnv)
}
}
33 changes: 33 additions & 0 deletions modules/generic/src/main/scala/EffMappingBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package edu.gemini.grackle
package generic

import edu.gemini.grackle._
import cats._

final case class EffMappingBuilder[F[_]](queries: List[EffQuery[F]]) {

def withQuery(eq: EffQuery[F]): EffMappingBuilder[F] = EffMappingBuilder(eq :: queries)

// NOTE - there's an assumption that schema must have a Query top-level type. This would go in docs.
def build(schema0: Schema)(implicit m: Monad[F]): GenericMapping[F] = {
val rootQueryType = schema0.ref("Query")
new GenericMapping[F] {
def effects: List[RootEffect] = queries.map(_.buildRootEffect(this))
final override val schema: Schema = schema0
final override val typeMappings: List[TypeMapping] =
List(ObjectMapping(rootQueryType, effects))
final override val selectElaborator: QueryCompiler.SelectElaborator =
new QueryCompiler.SelectElaborator(
Map(rootQueryType -> FieldArguments.moveArgsToEnv(queries.flatMap(_.argExtractors)))
)
}
}
}

object EffMappingBuilder {
def empty[F[_]]: EffMappingBuilder[F] = EffMappingBuilder(Nil)
def single[F[_]](eq: EffQuery[F]) = EffMappingBuilder(List(eq))
}
106 changes: 106 additions & 0 deletions modules/generic/src/main/scala/EffQuery.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package edu.gemini.grackle
package generic

import edu.gemini.grackle._
import edu.gemini.grackle.Cursor.Env
import cats._
import cats.syntax.all._

import scala.reflect.ClassTag

final case class EffQuery[F[_]](
topLevelField: String,
argExtractors: List[FieldArguments],
effectfulCursor: (Path, Env) => F[Result[Cursor]]
) {
def buildRootEffect(mapping: GenericMapping[F]): mapping.RootEffect =
mapping.RootEffect.computeCursor(topLevelField)((_, path, env) => effectfulCursor(path, env))
}

object EffQuery {
def noArgs[F[_]: MonadThrow, A: CursorBuilder](topLevelField: String, process: F[A]): EffQuery[F] =
EffQuery(topLevelField, Nil, effectfulCursor[F, A](_ => process.map(Result(_))))

def arg1[F[_]: MonadThrow, Arg1: ClassTag, A: CursorBuilder](
topLevelField: String,
process: Arg1 => F[A],
arg: Argument[Arg1]
): EffQuery[F] =
EffQuery(
topLevelField,
List(FieldArguments(topLevelField, List(arg))),
effectfulCursor(processWithArg[F, Arg1, A](arg.name, _, process))
)

def arg2[F[_]: MonadThrow, Arg1: ClassTag, Arg2: ClassTag, A: CursorBuilder](
topLevelField: String,
process: (Arg1, Arg2) => F[A],
arg1: Argument[Arg1],
arg2: Argument[Arg2]
): EffQuery[F] =
EffQuery(
topLevelField,
List(FieldArguments(topLevelField, List(arg1, arg2))),
effectfulCursor(processWithArg2[F, Arg1, Arg2, A](arg1.name, arg2.name, _, process.curried))
)

def arg3[F[_]: MonadThrow, Arg1: ClassTag, Arg2: ClassTag, Arg3: ClassTag, A: CursorBuilder](
topLevelField: String,
process: (Arg1, Arg2, Arg3) => F[A],
arg1: Argument[Arg1],
arg2: Argument[Arg2],
arg3: Argument[Arg3]
): EffQuery[F] =
EffQuery(
topLevelField,
List(FieldArguments(topLevelField, List(arg1, arg2, arg3))),
effectfulCursor(processWithArg3[F, Arg1, Arg2,Arg3, A](arg1.name, arg2.name, arg3.name, _, process.curried))
)

def effectfulCursor[F[_]: MonadThrow, A: CursorBuilder](
process: Env => F[Result[A]]
)(path: Path, env: Env): F[Result[Cursor]] =
process(env)
.map(_.flatMap(GenericMapping.genericCursor(path, env, _)))
.handleError(e => Result.failure[Cursor](errorMessage(e)))

def processWithArg[F[_]: Applicative, Arg: ClassTag, A](
argName: String,
env: Env,
process: Arg => F[A]
): F[Result[A]] =
argOrFail[Arg](argName, env).traverse(process)

def processWithArg2[F[_]: Applicative, Arg1: ClassTag, Arg2: ClassTag, A](
arg1Name: String,
arg2Name: String,
env: Env,
process: Arg1 => Arg2 => F[A]
): F[Result[A]] =
argOrFail[Arg1](arg1Name, env).map(process).flatTraverse(processWithArg[F, Arg2, A](arg2Name, env, _))

def processWithArg3[F[_]: Applicative, Arg1: ClassTag, Arg2: ClassTag, Arg3: ClassTag, A](
arg1Name: String,
arg2Name: String,
arg3Name: String,
env: Env,
process: Arg1 => Arg2 => Arg3 => F[A]
): F[Result[A]] =
argOrFail[Arg1](arg1Name, env)
.map(process)
.flatTraverse(processWithArg2[F, Arg2, Arg3, A](arg2Name, arg3Name, env, _))

private def argOrFail[Arg1: ClassTag](name: String, env: Env) =
env.get[Arg1](name).fold(missingArgFailure[Arg1](name, env))(Result(_))

private def missingArgFailure[A](argName: String, env: Env): Result[A] =
Result.failure[A](s"Missing argument `$argName` in $env")

private def errorMessage(error: Throwable): String =
Option(error.getMessage)
.orElse(Option(error.getCause).map(_.getMessage))
.getOrElse("Effectful query failed without a useful error message.")
}
61 changes: 61 additions & 0 deletions modules/generic/src/main/scala/EffQueryBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package edu.gemini.grackle
package generic

import edu.gemini.grackle._
import cats._

import scala.reflect.ClassTag

object EffQueryBuilder {
def noArgs[F[_]] = EffQueryBuilder[F](Nil)
}

// TODO split this out into an external facing module, with internal separate. Make the intention obvious.
final case class EffQueryBuilder[F[_]](argExtractors: List[FieldArguments]) {
def withInt(name: String) = withArg[Int](name)
def withId(name: String) = withArgPf[String](name, { case Value.IDValue(s) => s })
def withString(name: String) = withArg[String](name)
def withFloat(name: String) = withArg[Double](name)
def withBool(name: String) = withArg[Boolean](name)

def withArg[Arg: FromQueryValue: ClassTag](name: String) =
withArgPf[Arg](name, FromQueryValue.toPartialFunction[Arg])

def withArgPf[Arg: ClassTag](name: String, extractor: PartialFunction[Value, Arg]): EffQueryBuilderArg1[F, Arg] =
EffQueryBuilderArg1(Argument(name, extractor.lift))

def build[A: CursorBuilder](query: String, process: F[A])(implicit f: MonadThrow[F]): EffQuery[F] = EffQuery.noArgs(query, process)

}

final case class EffQueryBuilderArg1[F[_], Arg1: ClassTag](arg: Argument[Arg1]) {
def withInt(name: String) = withArg[Int](name)
def withId(name: String) = withArgPf[String](name, { case Value.IDValue(s) => s })
def withString(name: String) = withArg[String](name)
def withFloat(name: String) = withArg[Double](name)
def withBool(name: String) = withArg[Boolean](name)

def withArg[Arg: FromQueryValue: ClassTag](name: String) =
withArgPf[Arg](name, FromQueryValue.toPartialFunction[Arg])

def withArgPf[Arg: ClassTag](
name: String,
extractor: PartialFunction[Value, Arg]
): EffQueryBuilderArg2[F, Arg1, Arg] =
EffQueryBuilderArg2(arg, Argument(name, extractor.lift))

def build[A: CursorBuilder](query: String, process: Arg1 => F[A])(implicit f: MonadThrow[F]): EffQuery[F] =
EffQuery.arg1(query, process, arg)
}

final case class EffQueryBuilderArg2[F[_], Arg1: ClassTag, Arg2: ClassTag](
arg1: Argument[Arg1],
arg2: Argument[Arg2]
) {

def build[A: CursorBuilder](query: String, process: (Arg1, Arg2) => F[A])(implicit f: MonadThrow[F]): EffQuery[F] =
EffQuery.arg2(query, process, arg1, arg2)
}
52 changes: 52 additions & 0 deletions modules/generic/src/main/scala/FromQueryValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2016-2020 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package edu.gemini.grackle
package generic

import edu.gemini.grackle._
import cats.syntax.all._

import scala.util.Try
import java.time.ZonedDateTime

trait FromQueryValue[A] {
def attempt(value: Value): Option[A]

final def flatMap[B](f: A => Option[B]): FromQueryValue[B] = attempt(_).flatMap(f)
}

object FromQueryValue {
def apply[A: FromQueryValue] = implicitly[FromQueryValue[A]]
def toPartialFunction[A: FromQueryValue]: PartialFunction[Value, A] =
PartialFunction.fromFunction(FromQueryValue[A].attempt).andThen { case Some(a) => a }

implicit val string: FromQueryValue[String] = {
case Value.StringValue(s) => Some(s)
case _ => None
}

implicit val float: FromQueryValue[Double] = {
case Value.FloatValue(s) => Some(s)
case _ => None
}

implicit val int: FromQueryValue[Int] = {
case Value.IntValue(s) => Some(s)
case _ => None
}

implicit val bool: FromQueryValue[Boolean] = {
case Value.BooleanValue(s) => Some(s)
case _ => None
}

implicit val zdt: FromQueryValue[ZonedDateTime] = string.flatMap(s => Try(ZonedDateTime.parse(s)).toOption)

implicit def opt[A: FromQueryValue]: FromQueryValue[Option[A]] = FromQueryValue[A].attempt(_).map(Option(_))

implicit def list[A: FromQueryValue]: FromQueryValue[List[A]] = {
case Value.ListValue(elems) => elems.traverse(x => FromQueryValue[A].attempt(x))
case _ => None
}
}
15 changes: 10 additions & 5 deletions modules/generic/src/main/scala/genericmapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ trait GenericMappingLike[F[_]] extends ScalaVersionSpecificGenericMappingLike[F]

type CursorBuilder[T] = edu.gemini.grackle.generic.CursorBuilder[T]

def genericCursor[T](path: Path, env: Env, t: T)(implicit cb: => CursorBuilder[T]): Result[Cursor] =
if(path.isRoot)
cb.build(Context(path.rootTpe), t, None, env)
else
DeferredCursor(path, (context, parent) => cb.build(context, t, Some(parent), env)).rightIor
def genericCursor[T](path: Path, env: Env, t: T)(implicit cb: => CursorBuilder[T]): Result[Cursor] =
GenericMapping.genericCursor(path, env, t)(cb)

override def mkCursorForField(parent: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = {
val context = parent.context
Expand Down Expand Up @@ -56,3 +53,11 @@ trait GenericMappingLike[F[_]] extends ScalaVersionSpecificGenericMappingLike[F]
def transformField[U](fieldName: String)(f: T => Result[U])(implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T]
}
}

object GenericMapping {
def genericCursor[T](path: Path, env: Env, t: T)(implicit cb: => CursorBuilder[T]): Result[Cursor] =
if(path.isRoot)
cb.build(Context(path.rootTpe), t, None, env)
else
DeferredCursor(path, (context, parent) => cb.build(context, t, Some(parent), env)).rightIor
}
Loading