Skip to content

Commit

Permalink
Rework interfaces and implementations
Browse files Browse the repository at this point in the history
+ Object type fields mapped at the interface level and interface type
  fields which are implemented or overridden at the object type level
  are now explicitly represented internally. This allows both more
  efficient lookup of inherited field mappings and correct lookup of
  overriden field mappings.
+ Field mapping lookup is now more effectively indexed TypeMappings.
  This might give a noticeable performance improvement for ValueMapping.
  Now that this indexing in centralised in TypeMappings, the
  per-ObjectMapping field indices have been removed. If this proves
  problematic for applications it could be reinstated.
+ Schema validation now enforces the uniqueness of interfaces in
  implements clauses.
+ Schema validation now enforces that object and interface types must
  directly implement all transitively implemented interfaces. The
  allInterfaces method on InterfaceType has been deprecated because with
  the preceeding validation change it would equivalent to interfaces.
+ The Mapping specific logic of mkCursorForField has been extracted to
  mkCursorForMappedField allowing simpler mapping-specific
  implementations.
+ Previously introspection did not report interfaces implemented by
  interfaces.
+ Added Schema#implementations which returns the implementing Object
  types of an interface.
+ The unsafe TypeMappings constructor has been deprecated and renamed to
  unchecked.
+ TypeMappings#unsafe has been renamed to unchecked and hidden
+ The implementations of hasField, nullableHasField, hasPath and
  hasListPath in Cursor had incorrect semantics and appear to be unused,
  so rather than fix them, they have been removed.
+ Various tests have been updated to conform to the newly implemented
  validation rules and changes to field mapping lookup.
  • Loading branch information
milessabin committed Jun 5, 2024
1 parent fe57582 commit f195926
Show file tree
Hide file tree
Showing 22 changed files with 756 additions and 321 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ThisBuild / scalaVersion := Scala2
ThisBuild / crossScalaVersions := Seq(Scala2, Scala3)
ThisBuild / tlJdkRelease := Some(11)

ThisBuild / tlBaseVersion := "0.19"
ThisBuild / tlBaseVersion := "0.20"
ThisBuild / startYear := Some(2019)
ThisBuild / licenses := Seq(License.Apache2)
ThisBuild / developers := List(
Expand Down
17 changes: 5 additions & 12 deletions modules/circe/src/main/scala/circemapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,15 @@ trait CirceMappingLike[F[_]] extends Mapping[F] {
else
DeferredCursor(path, (context, parent) => CirceCursor(context, value, Some(parent), env).success)

override def mkCursorForField(parent: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = {
val context = parent.context
val fieldContext = context.forFieldOrAttribute(fieldName, resultName)
(typeMappings.fieldMapping(context, fieldName), parent.focus) match {
case (Some(CirceField(_, json, _)), _) =>
override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] =
(fm, parent.focus) match {
case (CirceField(_, json, _), _) =>
CirceCursor(fieldContext, json, Some(parent), parent.env).success
case (Some(CursorFieldJson(_, f, _, _)), _) =>
case (CursorFieldJson(_, f, _, _), _) =>
f(parent).map(res => CirceCursor(fieldContext, focus = res, parent = Some(parent), env = parent.env))
case _ =>
super.mkCursorForField(parent, fieldName, resultName)
super.mkCursorForMappedField(parent, fieldContext, fm)
}
}

sealed trait CirceFieldMapping extends FieldMapping {
def subtree: Boolean = true
Expand Down Expand Up @@ -172,10 +169,6 @@ trait CirceMappingLike[F[_]] extends Mapping[F] {
else
Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe")

def hasField(fieldName: String): Boolean =
tpe.hasField(fieldName) && focus.asObject.exists(_.contains(fieldName)) ||
typeMappings.fieldMapping(context, fieldName).isDefined

def field(fieldName: String, resultName: Option[String]): Result[Cursor] = {
val localField =
for {
Expand Down
15 changes: 2 additions & 13 deletions modules/core/src/main/scala/composedmapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,15 @@ import Cursor.AbstractCursor
import syntax._

abstract class ComposedMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] {
override def mkCursorForField(parent: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = {
val context = parent.context
val fieldContext = context.forFieldOrAttribute(fieldName, resultName)
typeMappings.fieldMapping(context, fieldName) match {
case Some(_) =>
ComposedCursor(fieldContext, parent.env).success
case _ =>
super.mkCursorForField(parent, fieldName, resultName)
}
}
override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] =
ComposedCursor(fieldContext, parent.env).success

case class ComposedCursor(context: Context, env: Env) extends AbstractCursor {
val focus = null
val parent = None

def withEnv(env0: Env): Cursor = copy(env = env.add(env0))

override def hasField(fieldName: String): Boolean =
typeMappings.fieldMapping(context, fieldName).isDefined

override def field(fieldName: String, resultName: Option[String]): Result[Cursor] =
mkCursorForField(this, fieldName, resultName)
}
Expand Down
56 changes: 0 additions & 56 deletions modules/core/src/main/scala/cursor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,6 @@ trait Cursor {
*/
def narrow(subtpe: TypeRef): Result[Cursor]

/** Does the value at this `Cursor` have a field named `fieldName`? */
def hasField(fieldName: String): Boolean

/**
* Yield a `Cursor` corresponding to the value of the field `fieldName` of the
* value at this `Cursor`, or an error on the left hand side if there is no
Expand All @@ -161,18 +158,6 @@ trait Cursor {
case _ => false
})

/**
* Does the possibly nullable value at this `Cursor` have a field named
* `fieldName`?
*/
def nullableHasField(fieldName: String): Boolean =
if (isNullable)
asNullable match {
case Result.Success(Some(c)) => c.nullableHasField(fieldName)
case _ => false
}
else hasField(fieldName)

/**
* Yield a `Cursor` corresponding to the value of the possibly nullable field
* `fieldName` of the value at this `Cursor`, or an error on the left hand
Expand All @@ -186,19 +171,6 @@ trait Cursor {
}
else field(fieldName, None)

/** Does the value at this `Cursor` have a field identified by the path `fns`? */
def hasPath(fns: List[String]): Boolean = fns match {
case Nil => true
case fieldName :: rest =>
nullableHasField(fieldName) && {
nullableField(fieldName) match {
case Result.Success(c) =>
!c.isList && c.hasPath(rest)
case _ => false
}
}
}

/**
* Yield a `Cursor` corresponding to the value of the field identified by path
* `fns` starting from the value at this `Cursor`, or an error on the left
Expand All @@ -213,28 +185,6 @@ trait Cursor {
}
}

/**
* Does the value at this `Cursor` generate a list along the path `fns`?
*
* `true` if `fns` is a valid path from the value at this `Cursor` and passes
* through at least one field with a list type.
*/
def hasListPath(fns: List[String]): Boolean = {
def loop(c: Cursor, fns: List[String], seenList: Boolean): Boolean = fns match {
case Nil => seenList
case fieldName :: rest =>
c.nullableHasField(fieldName) && {
c.nullableField(fieldName) match {
case Result.Success(c) =>
loop(c, rest, c.isList)
case _ => false
}
}
}

loop(this, fns, false)
}

/**
* Yield a list of `Cursor`s corresponding to the values generated by
* following the path `fns` from the value at this `Cursor`, or an error on
Expand Down Expand Up @@ -306,8 +256,6 @@ object Cursor {
def narrow(subtpe: TypeRef): Result[Cursor] =
Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe")

def hasField(fieldName: String): Boolean = false

def field(fieldName: String, resultName: Option[String]): Result[Cursor] =
Result.internalError(s"No field '$fieldName' for type ${tpe.underlying}")
}
Expand Down Expand Up @@ -346,8 +294,6 @@ object Cursor {

def narrow(subtpe: TypeRef): Result[Cursor] = underlying.narrow(subtpe)

def hasField(fieldName: String): Boolean = underlying.hasField(fieldName)

def field(fieldName: String, resultName: Option[String]): Result[Cursor] = underlying.field(fieldName, resultName)
}

Expand Down Expand Up @@ -387,8 +333,6 @@ object Cursor {
def focus: Any = Result.internalError(s"Empty cursor has no focus")
def withEnv(env0: Env): DeferredCursor = copy(env = env.add(env0))

override def hasField(fieldName: String): Boolean = fieldName == deferredPath.head

override def field(fieldName: String, resultName: Option[String]): Result[Cursor] =
if(fieldName != deferredPath.head) Result.internalError(s"No field '$fieldName' for type ${tpe.underlying}")
else
Expand Down
7 changes: 2 additions & 5 deletions modules/core/src/main/scala/introspection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,12 @@ object Introspection {
case _ => None
}),
ValueField("interfaces", flipNullityDealias andThen {
case ot: ObjectType => Some(ot.interfaces.map(_.nullable))
case tf: TypeWithFields => Some(tf.interfaces.map(_.nullable))
case _ => None
}),
ValueField("possibleTypes", flipNullityDealias andThen {
case u: UnionType => Some(u.members.map(_.nullable))
case i: InterfaceType =>
Some(allTypes.collect {
case o: ObjectType if o.interfaces.exists(_ =:= i) => NullableType(o)
})
case i: InterfaceType => Some(targetSchema.implementations(i).map(_.nullable))
case _ => None
}),
ValueField("enumValues", flipNullityDealias andThen {
Expand Down
Loading

0 comments on commit f195926

Please sign in to comment.