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

opaque json mappping #460

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.5.0",
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe bsp was turned off a different way in #458

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool I'll undo that.


lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
Expand All @@ -132,6 +133,7 @@ lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.settings(
name := "gsp-graphql-circe",
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

lazy val buildInfo = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
Expand All @@ -141,6 +143,7 @@ lazy val buildInfo = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.settings(
buildInfoKeys += "baseDirectory" -> (LocalRootProject / baseDirectory).value.toString
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

lazy val sql = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
Expand All @@ -166,6 +169,7 @@ lazy val sql = crossProject(JVMPlatform, JSPlatform, NativePlatform)
"com.github.jnr" % "jnr-unixsocket" % jnrUnixsocketVersion % "test"
)
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

lazy val doobie = project
.in(file("modules/doobie-pg"))
Expand Down Expand Up @@ -206,6 +210,7 @@ lazy val skunk = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.jsSettings(
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

lazy val generic = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
Expand All @@ -222,6 +227,7 @@ lazy val generic = crossProject(JVMPlatform, JSPlatform, NativePlatform)
case Scala2 => "com.chuusai" %%% "shapeless" % shapeless2Version
})
)
.platformsSettings(JSPlatform, NativePlatform)(bspEnabled := false)

lazy val demo = project
.in(file("demo"))
Expand Down
71 changes: 42 additions & 29 deletions modules/circe/src/main/scala/circemapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,68 +23,81 @@ trait CirceMappingLike[F[_]] extends Mapping[F] {

// Syntax to allow Circe-specific root effects
implicit class CirceMappingRootEffectSyntax(self: RootEffect.type) {
def computeJson(fieldName: String)(effect: (Query, Path, Env) => F[Result[Json]])(implicit pos: SourcePos): RootEffect =
self.computeCursor(fieldName)((q, p, e) => effect(q, p, e).map(_.map(circeCursor(p, e, _))))
def computeJson(fieldName: String, opaque: Boolean = false)(effect: (Query, Path, Env) => F[Result[Json]])(implicit pos: SourcePos): RootEffect =
self.computeCursor(fieldName)((q, p, e) => effect(q, p, e).map(_.map(circeCursor(p, e, _, opaque))))

def computeEncodable[A](fieldName: String)(effect: (Query, Path, Env) => F[Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootEffect =
computeJson(fieldName)((q, p, e) => effect(q, p, e).map(_.map(enc(_))))
def computeEncodable[A](fieldName: String, opaque: Boolean = false)(effect: (Query, Path, Env) => F[Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootEffect =
computeJson(fieldName, opaque)((q, p, e) => effect(q, p, e).map(_.map(enc(_))))
}

implicit class CirceMappingRootStreamSyntax(self: RootStream.type) {
def computeJson(fieldName: String)(effect: (Query, Path, Env) => Stream[F, Result[Json]])(implicit pos: SourcePos): RootStream =
self.computeCursor(fieldName)((q, p, e) => effect(q, p, e).map(_.map(circeCursor(p, e, _))))
def computeJson(fieldName: String, opaque: Boolean = false)(effect: (Query, Path, Env) => Stream[F, Result[Json]])(implicit pos: SourcePos): RootStream =
self.computeCursor(fieldName)((q, p, e) => effect(q, p, e).map(_.map(circeCursor(p, e, _, opaque))))

def computeEncodable[A](fieldName: String)(effect: (Query, Path, Env) => Stream[F, Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootStream =
computeJson(fieldName)((q, p, e) => effect(q, p, e).map(_.map(enc(_))))
def computeEncodable[A](fieldName: String, opaque: Boolean = false)(effect: (Query, Path, Env) => Stream[F, Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootStream =
computeJson(fieldName, opaque)((q, p, e) => effect(q, p, e).map(_.map(enc(_))))
}

def circeCursor(path: Path, env: Env, value: Json): Cursor =
def circeCursor(path: Path, env: Env, value: Json, opaque: Boolean = false): Cursor =
if(path.isRoot)
CirceCursor(Context(path.rootTpe), value, None, env)
CirceCursor(Context(path.rootTpe), value, None, env, opaque)
else
DeferredCursor(path, (context, parent) => CirceCursor(context, value, Some(parent), env).success)
DeferredCursor(path, (context, parent) => CirceCursor(context, value, Some(parent), env, opaque).success)

override def mkCursorForField(parent: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = {
val context = parent.context
val fieldContext = context.forFieldOrAttribute(fieldName, resultName)
(fieldMapping(context, fieldName), parent.focus) match {
case (Some(CirceField(_, json, _)), _) =>
CirceCursor(fieldContext, json, Some(parent), parent.env).success
case (Some(CursorFieldJson(_, f, _, _)), _) =>
f(parent).map(res => CirceCursor(fieldContext, focus = res, parent = Some(parent), env = parent.env))
case (None|Some(_: EffectMapping), json: Json) =>
val f = json.asObject.flatMap(_(fieldName))
f match {
case None if fieldContext.tpe.isNullable => CirceCursor(fieldContext, Json.Null, Some(parent), parent.env).success
case Some(json) => CirceCursor(fieldContext, json, Some(parent), parent.env).success
case _ =>
Result.failure(s"No field '$fieldName' for type ${context.tpe}")
}

// Create a cursor for the requested field by selecting it from `json`.
def jsonField(json: Json, opaque: Boolean): Result[Cursor] = {
val f = json.asObject.flatMap(_(fieldName))
f match {
case None if fieldContext.tpe.isNullable => CirceCursor(fieldContext, Json.Null, Some(parent), parent.env, opaque).success
case Some(json) => CirceCursor(fieldContext, json, Some(parent), parent.env, opaque).success
case _ => Result.failure(s"No field '$fieldName' of type ${context.tpe} in json blob: ${json.noSpaces}")
}
}

parent match {
// If the parent is a CirceCursor and this is an opaque cursor (i.e., the JSON is a terminal
// result and we mever consider other possible mappings once we're here) then immediately
// return a new cursor focused on the requested JSON field. Otherwise we drop through and
// delegate to the explicit type mapping for this field if there is one.
case CirceCursor(_, json, _, _ , true) => jsonField(json, true)
case _ =>
super.mkCursorForField(parent, fieldName, resultName)
(fieldMapping(context, fieldName), parent.focus) match {
case (Some(CirceField(_, json, _, opaque)), _) =>
CirceCursor(fieldContext, json, Some(parent), parent.env, opaque).success
case (Some(CursorFieldJson(_, f, _, _, opaque)), _) =>
f(parent).map(res => CirceCursor(fieldContext, focus = res, parent = Some(parent), env = parent.env, opaque))
case (None|Some(_: EffectMapping), json: Json) => jsonField(json, false)
case _ => super.mkCursorForField(parent, fieldName, resultName)
}
}

}

sealed trait CirceFieldMapping extends FieldMapping {
def withParent(tpe: Type): FieldMapping = this
}

case class CirceField(fieldName: String, value: Json, hidden: Boolean = false)(implicit val pos: SourcePos) extends CirceFieldMapping
case class CirceField(fieldName: String, value: Json, hidden: Boolean = false, opaque: Boolean = false)(implicit val pos: SourcePos) extends CirceFieldMapping

case class CursorFieldJson(fieldName: String, f: Cursor => Result[Json], required: List[String], hidden: Boolean = false)(
case class CursorFieldJson(fieldName: String, f: Cursor => Result[Json], required: List[String], hidden: Boolean = false, opaque: Boolean = false)(
implicit val pos: SourcePos
) extends CirceFieldMapping

case class CirceCursor(
context: Context,
focus: Json,
parent: Option[Cursor],
env: Env
env: Env,
opaque: Boolean = false,
) extends Cursor {
def withEnv(env0: Env): Cursor = copy(env = env.add(env0))

def mkChild(context: Context = context, focus: Json = focus): CirceCursor =
CirceCursor(context, focus, Some(this), Env.empty)
CirceCursor(context, focus, Some(this), Env.empty, opaque)

def isLeaf: Boolean =
tpe.dealias match {
Expand Down
96 changes: 96 additions & 0 deletions modules/circe/src/test/scala/Opaque.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 circetests

import cats.effect.IO
import io.circe.Json
import io.circe.literal._
import edu.gemini.grackle.circe.CirceMapping
import edu.gemini.grackle.syntax._
import munit.CatsEffectSuite

object OpaqueMapping extends CirceMapping[IO] {

val schema = schema"""
scalar Foo
type Monkey {
name: Foo
}
type Barrel {
monkey: Monkey
}
type Query {
opaque: Barrel
notOpaque: Barrel
}
"""

val QueryType = schema.ref("Query")
val MonkeyType = schema.ref("Monkey")
val BarrelType = schema.ref("Barrel")
val FooType = schema.ref("Foo")

val typeMappings: List[TypeMapping] =
List(
ObjectMapping(
tpe = QueryType,
fieldMappings =
List(
CirceField("opaque", json"""{ "monkey": { "name": "Bob" }}""", opaque = true),
CirceField("notOpaque", json"""{ "monkey": { "name": "Bob" }}"""),
)
),
ObjectMapping(
tpe = MonkeyType,
fieldMappings =
List(
CirceField("name", Json.fromString("Steve"))
)
),
LeafMapping[String](FooType)
)

}

final class OpaqueSuite extends CatsEffectSuite {

test("Opaque field should not see explicit mapping.") {

val query = """
query {
opaque {
monkey {
name
}
}
notOpaque {
monkey {
name
}
}
}
"""

val expected = json"""
{
"data" : {
"opaque" : {
"monkey" : {
"name" : "Bob"
}
},
"notOpaque" : {
"monkey" : {
"name" : "Steve"
}
}
}
}
"""

assertIO(OpaqueMapping.compileAndRunOne(query), expected)

}
}
12 changes: 6 additions & 6 deletions modules/sql/shared/src/main/scala/SqlMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -718,10 +718,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self
val fieldContext = context.forFieldOrAttribute(fieldName, resultName)
val fieldTpe = fieldContext.tpe
(fieldMapping(context, fieldName), parent) match {
case (Some(_: SqlJson), sc: SqlCursor) =>
case (Some(sj: SqlJson), sc: SqlCursor) =>
sc.asTable.flatMap { table =>
def mkCirceCursor(f: Json): Result[Cursor] =
CirceCursor(fieldContext, focus = f, parent = Some(parent), env = parent.env).success
CirceCursor(fieldContext, focus = f, parent = Some(parent), env = parent.env, opaque = sj.opaque).success
sc.mapped.selectAtomicField(context, fieldName, table).flatMap(_ match {
case Some(j: Json) if fieldTpe.isNullable => mkCirceCursor(j)
case None => mkCirceCursor(Json.Null)
Expand Down Expand Up @@ -776,7 +776,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self
def apply(fieldName: String, joins: Join*): SqlObject = apply(fieldName, joins.toList)
}

case class SqlJson(fieldName: String, columnRef: ColumnRef)(
case class SqlJson(fieldName: String, columnRef: ColumnRef, opaque: Boolean = false)(
implicit val pos: SourcePos
) extends SqlFieldMapping {
def hidden: Boolean = false
Expand Down Expand Up @@ -868,8 +868,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self
def columnsForLeaf(context: Context, fieldName: String): Result[List[SqlColumn]] =
fieldMapping(context, fieldName) match {
case Some(SqlField(_, cr, _, _, _, _)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success
case Some(SqlJson(_, cr)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success
case Some(CursorFieldJson(_, _, required, _)) =>
case Some(SqlJson(_, cr, _)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success
case Some(CursorFieldJson(_, _, required, _, _)) =>
required.flatTraverse(r => columnsForLeaf(context, r))
case Some(CursorField(_, _, _, required, _)) =>
required.flatTraverse(r => columnsForLeaf(context, r))
Expand All @@ -896,7 +896,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self
def columnForAtomicField(context: Context, fieldName: String): Result[SqlColumn] = {
fieldMapping(context, fieldName) match {
case Some(SqlField(_, cr, _, _, _, _)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success
case Some(SqlJson(_, cr)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success
case Some(SqlJson(_, cr, _)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success
case _ => Result.internalError(s"No column for atomic field '$fieldName' in context $context")
}
}
Expand Down
Loading