Skip to content

Commit

Permalink
allow opaque json mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
tpolecat committed Aug 28, 2023
1 parent 5c2aadf commit ff637a8
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 35 deletions.
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

0 comments on commit ff637a8

Please sign in to comment.