Skip to content

Instantly share code, notes, and snippets.

@BalmungSan
Last active December 13, 2022 22:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BalmungSan/075a7485163dce26ea5a7029ec6f9fcd to your computer and use it in GitHub Desktop.
Save BalmungSan/075a7485163dce26ea5a7029ec6f9fcd to your computer and use it in GitHub Desktop.
Initial draft of the design for Neotypes-Schema — Explicit decoders
//> using scala "2.13.10"
//> using lib "org.typelevel::cats-effect:3.3.14"
//> using lib "co.fs2::fs2-core:3.4.0"
//> using lib "org.neo4j.driver:neo4j-java-driver:5.3.0"
import cats.effect.IO
import fs2.Stream
import org.neo4j.driver.summary.ResultSummary
import org.neo4j.driver.types.{IsoDuration => NeoDuration, Point => NeoPoint}
import java.time.{LocalDate => JDate, LocalDateTime => JDateTime, LocalTime => JTime, OffsetTime => JZTime, ZonedDateTime => JZDateTime}
import java.util.{Map => JMap}
import scala.collection.Factory
import scala.collection.immutable.{ArraySeq, BitSet}
import scala.jdk.CollectionConverters._
import scala.util.control.NoStackTrace
trait Driver[F[_]]
trait StreamingDriver[F[_], S[_]] extends Driver[F]
type IOStream[A] = Stream[IO, A]
/** Data types supported by Neo4j. */
object types {
/** Parent type of all Neo4j types. */
sealed trait NeoType extends Product with Serializable
/** Parent type of all Neo4j types that have named properties. */
sealed trait NeoObject extends NeoType {
def properties: Map[String, NeoType]
final def get(key: String): Option[NeoType] =
properties.get(key)
final def getAs[T](key: String)(implicit decoder: Decoder[T]): Either[exceptions.DecoderException, T] =
properties
.get(key)
.toRight(left = exceptions.PropertyNotFoundException(s"Field ${key} not found"))
.flatMap(decoder.decode)
final def keys: Set[String] =
properties.keySet
final def values: Iterable[NeoType] =
properties.values
}
/** Represents a Neo4j heterogeneous list (composite type) */
final case class NeoList(values: Iterable[NeoType]) extends NeoType
/** Represents a Neo4j heterogeneous map (composite type) */
final case class NeoMap(data: Map[String, NeoType]) extends NeoType
/** Parent type of all Neo4j structural types. */
sealed trait Entity extends NeoObject {
def elementId: String
override def properties: Map[String, Value]
}
/** Represents a Neo4j Node. */
final case class Node(
elementId: String,
labels: Set[String],
properties: Map[String, Value]
) extends Entity {
/** Checks if this Node contains the given label; case insensitive. */
def hasLabel(label: String): Boolean =
labels.contains(label.trim.toLowerCase)
}
/** Represents a Neo4j Relationship. */
final case class Relationship(
elementId: String,
relationshipType: String,
properties: Map[String, Value],
startNodeId: String,
endNodeId: String
) extends Entity {
/** Checks if this Relationship has the given type; case insensitive. */
def hasType(tpe: String): Boolean =
relationshipType == tpe.trim.toLowerCase
}
/** Represents a Neo4j Path. */
sealed trait Path extends NeoType {
def start: Node
def end: Node
def nodes: List[Node]
def relationships: List[Relationship]
def segments: List[Path.Segment]
def contains(node: Node): Boolean
def contains(relationship: Relationship): Boolean
}
object Path {
final case class EmptyPath(node: Node) extends Path {
override final val start: Node = node
override final val end: Node = node
override final val nodes: List[Node] = node :: Nil
override final val relationships: List[Relationship] = Nil
override final val segments: List[Segment] = Nil
override def contains(node: Node): Boolean =
this.node == node
override def contains(relationship: Relationship): Boolean =
false
}
final case class NonEmptyPath(segments: ::[Segment]) extends Path {
override final val start: Node =
segments.head.start
override def end: Node =
segments.last.end
override def nodes: List[Node] =
start :: segments.map(s => s.end)
override def relationships: List[Relationship] =
segments.map(s => s.relationship)
override def contains(node: Node): Boolean =
start == node || segments.exists(s => s.end == node)
override def contains(relationship: Relationship): Boolean =
segments.exists(s => s.relationship == relationship)
}
final case class Segment(start: Node, relationship: Relationship, end: Node)
}
/** Parent type of all Neo4j property types. */
sealed trait Value extends NeoType
object Value {
sealed trait SimpleValue extends Value
sealed trait NumberValue extends SimpleValue
final case class Integer(value: Int) extends NumberValue
final case class Decimal(value: Double) extends NumberValue
final case class Str(value: String) extends SimpleValue
final case class Bool(value: Boolean) extends SimpleValue
final case class Bytes(value: ArraySeq[Byte]) extends SimpleValue
final case class Point(value: NeoPoint) extends SimpleValue
final case class Duration(value: NeoDuration) extends SimpleValue
sealed trait TemporalInstantValue extends SimpleValue
final case class LocalDate(value: JDate) extends TemporalInstantValue
final case class LocalTime(value: JTime) extends TemporalInstantValue
final case class LocalDateTime(value: JDateTime) extends TemporalInstantValue
final case class Time(value: JZTime) extends TemporalInstantValue
final case class DateTme(value: JZDateTime) extends TemporalInstantValue
final case object NullValue extends SimpleValue
final case class ListValue[V <: SimpleValue](values: Iterable[V]) extends Value
}
}
/** Exceptions provided by this library. */
object exceptions {
sealed abstract class NeotypesException(message: String, cause: Option[Throwable] = None) extends Exception(message, cause.orNull)
sealed abstract class DecoderException(message: String, cause: Option[Throwable] = None) extends NeotypesException(message, cause) with NoStackTrace
final case class PropertyNotFoundException(message: String) extends DecoderException(message)
final case class IncoercibleException(message: String, cause: Option[Throwable] = None) extends DecoderException(message, cause)
final case class ChainException(parts: Iterable[DecoderException]) extends DecoderException(message = "") {
override def getMessage(): String = {
s"Multiple decoding errors:\n${parts.view.map(ex => ex.getMessage).mkString("\n")}"
}
}
object ChainException {
def from(exceptions: DecoderException*): ChainException =
new ChainException(
parts = exceptions.view.flatMap {
case ChainException(parts) => parts
case decodingException => decodingException :: Nil
}
)
}
}
trait Decoder[+T] {
def decode(value: types.NeoType): Either[exceptions.DecoderException, T]
def flatMap[U](f: T => Decoder[U]): Decoder[U]
def map[U](f: T => U): Decoder[U]
def emap[U](f: T => Either[exceptions.DecoderException, U]): Decoder[U]
def and[U](other: Decoder[U]): Decoder[(T, U)]
def or[U >: T](other: Decoder[U]): Decoder[U]
}
object Decoder {
def apply[T](implicit decoder: Decoder[T]): Decoder[T] =
decoder
def constant[T](t: T): Decoder[T] =
???
def failed[T](ex: exceptions.DecoderException): Decoder[T] =
???
def field[T](key: String)(implicit decoder: Decoder[T]): Decoder[T] =
neoObject.emap(_.getAs[T](key))
def fromMatch[T](pf: PartialFunction[types.NeoType, Either[exceptions.DecoderException, T]])(implicit ev: DummyImplicit): Decoder[T] =
???
def fromMatch[T](pf: PartialFunction[types.NeoType, T]): Decoder[T] =
fromMatch(pf.andThen(Right.apply _))
def fromNumeric[T](f: types.Value.NumberValue => Either[exceptions.DecoderException, T]): Decoder[T] = fromMatch {
case value: types.Value.NumberValue =>
f(value)
}
def fromTemporalInstant[T](f: types.Value.TemporalInstantValue => Either[exceptions.DecoderException, T]): Decoder[T] = fromMatch {
case value: types.Value.TemporalInstantValue =>
f(value)
}
implicit final val int: Decoder[Int] = fromNumeric {
case types.Value.Integer(value) =>
Right(value)
case types.Value.Decimal(value) =>
Right(value.toInt)
}
implicit final val string: Decoder[String] = fromMatch {
case types.Value.Str(value) =>
value
}
// ...
implicit final val node: Decoder[types.Node] = fromMatch {
case value: types.Node =>
value
}
implicit final val relationship: Decoder[types.Relationship] = fromMatch {
case value: types.Relationship =>
value
}
implicit final val path: Decoder[types.Path] = fromMatch {
case value: types.Path =>
value
}
implicit final val neoPoint: Decoder[NeoPoint] = fromMatch {
case types.Value.Point(value) =>
value
}
implicit final val neoDuration: Decoder[NeoDuration] = fromMatch {
case types.Value.Duration(value) =>
value
}
implicit val values: Decoder[Iterable[types.NeoType]] = fromMatch {
case types.NeoList(values) =>
values
case types.NeoMap(values) =>
values.values
case entity: types.Entity =>
entity.values
case types.Value.ListValue(values) =>
values
}
implicit val neoObject: Decoder[types.NeoObject] = fromMatch {
case value: types.NeoObject =>
value
}
implicit def option[T](implicit decoder: Decoder[T]): Decoder[Option[T]] = fromMatch {
case types.Value.NullValue =>
Right(None)
case value =>
decoder.decode(value).map(Some.apply)
}
implicit def either[A, B](implicit a: Decoder[A], b: Decoder[B]): Decoder[Either[A, B]] =
a.map(Left.apply).or(b.map(Right.apply))
implicit def collectAs[C, T](implicit factory: Factory[T, C], decoder: Decoder[T]): Decoder[C] =
???
// values.emap(_.traverseAs(factory)(decoder.decode))
implicit def list[T](implicit decoder: Decoder[T]): Decoder[List[T]] =
collectAs(List, decoder)
// ...
def and[A, B](a: Decoder[A], b: Decoder[B]): Decoder[(A, B)] =
a.and(b)
def combine[A, B, T](a: Decoder[A], b: Decoder[B])(f: (A, B) => T): Decoder[T] =
a.and(b).map(f.tupled)
def fromFunction[A, B, T](f: (A, B) => T)(implicit a: Decoder[A], b: Decoder[B]): Decoder[T] =
values.map(_.toList).emap {
case aa :: bb :: _ =>
for {
aaa <- a.decode(aa)
bbb <- b.decode(bb)
} yield f(aaa, bbb)
case values =>
Left(exceptions.IncoercibleException(message = "Wrong number of arguments"))
}
def fromFunction[A, B, T](na: String, nb: String)(f: (A, B) => T)(implicit a: Decoder[A], b: Decoder[B]): Decoder[T] =
neoObject.emap { obj =>
for {
aaa <- obj.getAs(key = na)(a)
bbb <- obj.getAs(key = nb)(b)
} yield f(aaa, bbb)
}
object tuple {
implicit def apply[A, B](implicit a: Decoder[A], b: Decoder[B]): Decoder[(A, B)] =
fromFunction(Tuple2.apply[A, B])
def named[A, B](a: (String, Decoder[A]), b: (String, Decoder[B])): Decoder[(A, B)] =
fromFunction(a._1, b._1)(Tuple2.apply[A, B])(a._2, b._2)
}
object product {
trait DerivedProductDecoder[T <: Product] extends Decoder[T]
def derive[T <: Product](implicit decoder: DerivedProductDecoder[T]): Decoder[T] =
decoder
def named[A, B, T <: Product](a: (String, Decoder[A]), b: (String, Decoder[B]))(f: (A, B) => T): Decoder[T] =
fromFunction(a._1, b._1)(f)(a._2, b._2)
def named[A, B, C, T <: Product](a: (String, Decoder[A]), b: (String, Decoder[B]), c: (String, Decoder[C]))(f: (A, B, C) => T): Decoder[T] =
???
def apply[A, B, T <: Product](a: Decoder[A], b: Decoder[B])(f: (A, B) => T): Decoder[T] =
fromFunction(f)(a, b)
}
object coproduct {
sealed trait DiscriminatorStrategy[S]
object DiscriminatorStrategy {
final case object NodeLabel extends DiscriminatorStrategy[String]
final case object RelationshipType extends DiscriminatorStrategy[String]
final case class Field[T](name: String, decoder: Decoder[T]) extends DiscriminatorStrategy[T]
object Field {
def apply[T](name: String)(implicit decoder: Decoder[T], ev: DummyImplicit): Field[T] =
new Field(name, decoder)
}
}
trait DerivedCoproductInstances[T] {
def options: List[(String, Decoder[T])]
}
private[Decoder] final class CoproductDerivePartiallyApplied[T](private val dummy: Boolean) extends AnyVal {
def apply(strategy: DiscriminatorStrategy[String])(implicit instances: DerivedCoproductInstances[T]): Decoder[T] =
coproduct.apply(strategy)(instances.options : _*)
}
def derive[T]: CoproductDerivePartiallyApplied[T] =
new CoproductDerivePartiallyApplied(dummy = true)
def apply[S, T](strategy: DiscriminatorStrategy[S])(options: (S, Decoder[T])*): Decoder[T] = strategy match {
case DiscriminatorStrategy.NodeLabel =>
Decoder.node.flatMap { node =>
options.collectFirst {
case (label, decoder) => //if (node.hasLabel(label))=>
decoder
}.getOrElse(
Decoder.failed(exceptions.IncoercibleException(s"Unexpected node labels: ${node.labels}"))
)
}
case DiscriminatorStrategy.RelationshipType =>
Decoder.relationship.flatMap { relationship =>
options.collectFirst {
case (label, decoder) => //if (relationship.hasType(tpe = label))=>
decoder
}.getOrElse(
Decoder.failed(exceptions.IncoercibleException(s"Unexpected relationship type: ${relationship.relationshipType}"))
)
}
case DiscriminatorStrategy.Field(fieldName, fieldDecoder) =>
Decoder.field(key = fieldName)(fieldDecoder).flatMap { label =>
options.collectFirst {
case (`label`, decoder) =>
decoder
}.getOrElse(
Decoder.failed(exceptions.IncoercibleException(s"Unexpected field label: ${label}"))
)
}
}
}
}
trait DeferredQuery {
def query[T](decoder: Decoder[T]): ValueQuery[T]
def execute: ExecuteQuery
}
trait ExecuteQuery {
def void[F[_]](driver: Driver[F]): F[Unit]
def resultSummary[F[_]](driver: Driver[F]): F[ResultSummary]
}
trait ValueQuery[T] {
def single[F[_]](driver: Driver[F]): F[T]
def list[F[_]](driver: Driver[F]): F[List[T]]
def collectAs[F[_], C](factory: Factory[T, C], driver: Driver[F]): F[C]
def stream[F[_], S[_]](driver: StreamingDriver[F, S]): S[T]
def withResultSummary: ValueWithResultSummaryQuery[T]
}
trait ValueWithResultSummaryQuery[T] {
def single[F[_]](driver: Driver[F]): F[(T, ResultSummary)]
def list[F[_]](driver: Driver[F]): F[(List[T], ResultSummary)]
def collectAs[F[_], C](factory: Factory[T, C], driver: Driver[F]): F[(C, T)]
def stream[F[_], S[_]](driver: StreamingDriver[F, S]): S[Either[ResultSummary, T]]
}
// Base: Una consulta
val query: DeferredQuery = ???
val driver: StreamingDriver[IO, IOStream] = ???
// A. Todos los tipos de datos primitivos soportados por Neo4j; e.g. Int o String.
{
val decoder = Decoder.int
val result: IO[Int] = query.query(decoder).single(driver)
}
// B. Descartar el resultado de una operación y retornar el valor Unit.
{
val result: IO[Unit] = query.execute.void(driver)
}
// C. El ResultSummary de la consulta.
{
val result: IO[ResultSummary] = query.execute.resultSummary(driver)
}
// D. Tuplas de tipos soportados por neotypes.
{
val decoder = Decoder.tuple(Decoder.int, Decoder.string)
val result: IO[(Int, String)] = query.query(decoder).single(driver)
}
// E. Cualquier tipo de colección cuyos elementos sean soportados por neotypes; incluido el tipo Option.
{
val decoder = Decoder.int
val result: IO[List[Int]] = query.query(decoder).list(driver)
}
{
val decoder = Decoder.int
val result: IO[BitSet] = query.query(decoder).collectAs(BitSet, driver)
}
{
val decoder = Decoder.list(Decoder.int)
val result: IO[List[Int]] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.collectAs(BitSet, Decoder.int)
val result: IO[BitSet] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.option(Decoder.int)
val result: IO[Option[Int]] = query.query(decoder).single(driver)
}
// F. Clases definidas por los usuarios cuyos campos sean soportados por neotypes.
final case class User(name: String, age: Int)
{
val decoder = Decoder.neoObject.emap { obj =>
for {
name <- obj.getAs(key = "name")(Decoder.string)
age <- obj.getAs[Int](key = "age")
} yield User(name, age)
}
val result: IO[User] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.product.named(
"name" -> Decoder.string,
"age" -> Decoder.int
)(User.apply)
val result: IO[User] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.product(
Decoder.string,
Decoder.int
)(User.apply)
val result: IO[User] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.fromFunction(User.apply)
val result: IO[User] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.product.derive[User](???)
val result: IO[User] = query.query(decoder).single(driver)
}
// G. Tipos de datos algebraicos definidos por los usuarios.
sealed trait Problem
object Problem {
final case class Error(msg: String) extends Problem
final case class Warning(msg: String) extends Problem
final case object Unknown extends Problem
implicit final val errorDecoder = Decoder.product.derive[Error](???)
implicit final val warningDecoder = Decoder.product.derive[Warning](???)
implicit final val unknownDecoder = Decoder.constant(Unknown)
}
{
val decoder = Decoder.node.flatMap { node =>
if (node.hasLabel("error")) Problem.errorDecoder
else if (node.hasLabel("warning")) Problem.warningDecoder
else if (node.hasLabel("unknown")) Problem.unknownDecoder
else Decoder.failed(exceptions.IncoercibleException(s"Unexpected labels: ${node.labels}"))
}
val result: IO[Problem] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.coproduct(strategy = Decoder.coproduct.DiscriminatorStrategy.RelationshipType)(
"error" -> Problem.errorDecoder,
"warning" -> Problem.warningDecoder,
"unknown" -> Problem.unknownDecoder
)
val result: IO[Problem] = query.query(decoder).single(driver)
}
{
val decoder = Decoder.coproduct.derive[Problem](strategy = Decoder.coproduct.DiscriminatorStrategy.Field(name = "type"))(???)
val result: IO[Problem] = query.query(decoder).single(driver)
}
// H. Poder renombrar campos.
{
val decoder = Decoder.product.named(
"personName" -> Decoder.string,
"personAge" -> Decoder.int
)(User.apply)
val result: IO[User] = query.query(decoder).single(driver)
}
// I. Poder aplicar validaciones o transformaciones personalizadas a un campo; permitiendo incluso cambiar el tipo de dato o wrappers.
final case class Id(int: Int)
object Id {
def from(int: Int): Option[Id] =
if (int >= 0) Some(Id(int)) else None
}
final case class Record(id: Id, data: String)
{
val decoder = Decoder.product.named(
"id" -> Decoder.int.emap { i =>
Id.from(i).toRight(
left = exceptions.IncoercibleException(s"${i} is not a valid ID because is negative")
)
},
"data" -> Decoder.string
)(Record.apply)
val result: IO[Record] = query.query(decoder).single(driver)
}
// J. Poder combinar varios campos independientes en un único resultado.
final case class Combined(id: Int, data: (String, Int))
{
val decoder = Decoder.product.named(
"id" -> Decoder.int,
"dataStr" -> Decoder.string,
"dataInt" -> Decoder.int
) {
case (id, dataStr, dataInt) =>
Combined(id, data = (dataStr, dataInt))
}
val result: IO[Combined] = query.query(decoder).single(driver)
}
// K. Poder dividir un único campo en varios resultados independientes.
final case class Divided(id: Int, dataStr: String, dataInt: Int)
{
val decoder = Decoder.product.named(
"id" -> Decoder.int,
"data" -> Decoder.tuple[String, Int]
) {
case (id, (dataStr, dataInt)) =>
Divided(id, dataStr, dataInt)
}
val result: IO[Divided] = query.query(decoder).single(driver)
}
// L. Poder anidar los resultados.
final case class Nested(foo: Foo, bar: Bar)
final case class Foo(a: Int, b: String)
final case class Bar(c: Int, d: String)
{
val decoder = Decoder.combine(
Decoder.product.named(
"a" -> Decoder.int,
"b" -> Decoder.string,
)(Foo.apply),
Decoder.product.named(
"c" -> Decoder.int,
"d" -> Decoder.string,
)(Bar.apply)
)(Nested.apply)
val result: IO[Nested] = query.query(decoder).single(driver)
}
// M. Poder usar valores por defecto para campos opcionales; sin tener que usar el tipo Option.
final case class Optional(id: Int, opt1: Option[String], opt2: Int = 0)
{
val decoder = Decoder.product.named(
"id" -> Decoder.int,
"data" -> Decoder.option[String]
) {
case (id, opt) =>
Optional(id, opt1 = opt)
}
val result: IO[Optional] = query.query(decoder).single(driver)
}
// N. Poder leer los resultados como un Stream perezoso, que evite cargar toda la data en memoria.
{
val decoder = Decoder.int
val result: IOStream[Int] = query.query(decoder).stream(driver)
}
// O. Poder acceder al ResultSummary y a los registros de forma simultánea.
{
val decoder = Decoder.int
val result: IO[(Int, ResultSummary)] = query.query(decoder).withResultSummary.single(driver)
}
@BalmungSan
Copy link
Author

Real implementation being worked here: neotypes/neotypes#584

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment