-
-
Save vendethiel/29088fda384bc290d7e55d3f7ae3566a to your computer and use it in GitHub Desktop.
Shapeless: derive Slick's GetResult for arbitrary case classes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import slick.jdbc.{GetResult, PositionedResult} | |
import scala.annotation.implicitNotFound | |
import scala.reflect.runtime.universe.TypeTag | |
/** | |
* A type class that allows the user of GenericGetResult to support their own type. | |
* This is mostly used to support `newtype`s (like wrappers around UUIDs). | |
* | |
* A user just has to use the helper to make the type known to GenericGetResult. | |
* | |
* -------- | |
* | |
* Example: | |
* | |
* > case class UserId(value: UUID) | |
* > case class User(id: UserId) | |
* | |
* Obviously, GenericGetResult doesn't know about UserId, | |
* which makes GenericGetResult.generate[User] currently an error. | |
* So we can manually give a JDBCParser for UserId, and make it work: | |
* | |
* > implicit val parseUserId: JDBCParser.make(r ⇒ UserId(r.nextUUID())) | |
* | |
* @tparam T The type for which we want to parse from DB | |
*/ | |
trait JDBCParser[T] { | |
def parse(r: PositionedResult): T | |
} | |
object JDBCParser { | |
/** | |
* A simple helper function. | |
* @param fn The parser function to call | |
* @tparam T The type the parser will return. | |
* @return The parsed value. | |
*/ | |
def make[T](fn: PositionedResult ⇒ T) = new JDBCParser[T] { | |
override def parse(r: PositionedResult): T = fn(r) | |
} | |
} | |
/** | |
* The backbones of the GenericGetResult mechanism. | |
* This trait will serve for the Aux pattern (from shapeless). | |
* | |
* @tparam I The type to generate a GetResult for. | |
*/ | |
@implicitNotFound("Not all fields of ${I} have a JDBC reader") | |
trait GenericGetResult[I] { | |
type Out | |
def value: Out | |
} | |
object GenericGetResult { | |
import shapeless._ | |
import cats.data.Reader | |
import cats.sequence._ | |
/** | |
* Unused when directly going through `generate`, this method is otherwise the starting point when using this: | |
* This is used to trigger the unification process! | |
* | |
* > GenericGetResult.of[MyCaseClass] | |
* | |
* The line above returns a Reader[PositionedResult, MyCaseClass]. | |
*/ | |
def of[T](implicit ev: GenericGetResult[T]): ev.Out = ev.value | |
/** | |
* This is used to fake prolog-style declarations. | |
* This case class, the Aux pattern, is a trick from the shapeless library, | |
* where we the type I is the "input", and the type O is the "output". | |
* | |
* The reason we need this is because in Scala, a parameter can't depend on the value of some other parameter. | |
* This is invalid: | |
* > def fn[T](a: Typeclass[T], b: a.Out) = ??? | |
* This is valid: | |
* > def fn[T, O](a: Typeclass[T], b: Typeclass[O])(implicit ev: Aux[T, O]) = ??? | |
* | |
* In more details: | |
* A good synergy for how implicit resolution works for Aux is a type-level where: | |
* type parameters (class A[T]) are inputs. | |
* type members (class A { type T }) are outputs. | |
* This is how the Scala compiler does implicit resolution. It will *not* try to start a unification from a type member. | |
* It will, however, try to start the unification resolution from a type parameter. | |
* | |
* So, when the compiler see Aux[A, B], if A is known but B isn't, then it will go through the known implicits | |
* and try to unify both type variables on every implicit in scope, until one works. | |
* | |
* @tparam I The "input". | |
* @tparam O The "output". | |
*/ | |
type Aux[I, O] = GenericGetResult[I] { type Out = O } | |
/** | |
* Convenient wrapper function to create a Aux. | |
* @param v The value | |
* @tparam I The "input" (see Aux). | |
* @tparam O The "output" (see Aux). | |
* @return A Aux with its Out & value members set. | |
*/ | |
def instance[I, O](v: O): GenericGetResult.Aux[I, O] = new GenericGetResult[I] { | |
type Out = O | |
def value = v | |
} | |
/** | |
* Just a shorthand syntax for Reader. | |
* @tparam A The type that the reader reads. | |
*/ | |
type JDBCReader[A] = Reader[PositionedResult, A] | |
/* Read an Int. */ | |
implicit def ints: GenericGetResult.Aux[Int, JDBCReader[Int]] = | |
instance(Reader(_.nextInt)) | |
/* Read a Double. */ | |
implicit def doubles: GenericGetResult.Aux[Double, JDBCReader[Double]] = | |
instance(Reader(_.nextDouble)) | |
/* Read a Float. */ | |
implicit def floats: GenericGetResult.Aux[Float, JDBCReader[Float]] = | |
instance(Reader(_.nextFloat)) | |
/* Read a String. */ | |
implicit def strings: GenericGetResult.Aux[String, JDBCReader[String]] = | |
instance(Reader(_.nextString)) | |
/* Read a Seq[T]. We need a TypeTag because _.nextArray needs one. */ | |
implicit def seq[T : TypeTag]: GenericGetResult.Aux[Seq[T], JDBCReader[Seq[T]]] = { | |
import slickPgSupport._ | |
instance(Reader(_.nextArray[T])) | |
} | |
/* This uses the JDBCParser typeclass provided earlier. | |
As explained in JDBCParser's comment, this is useful for custom types that aren't known here. | |
*/ | |
implicit def userSuppliedReader[T](implicit parser: JDBCParser[T]): GenericGetResult.Aux[T, JDBCReader[T]] = | |
instance(Reader(r ⇒ parser.parse(r))) | |
/* This is the end-of-recursion case. */ | |
implicit def hnil: GenericGetResult.Aux[HNil, HNil] = | |
instance(HNil) | |
/** | |
* This is the base recursion case. | |
* | |
* @tparam H The "H"ead of the HList, the first type we'll convert, which is in input position of the Aux. | |
* @tparam T The "T"ail of the HList, with every other types we need to convert (and a HNil at the end), | |
* which is also in input position of the Aux. | |
* @tparam R The "R"est to convert, which is in output position of the Aux. | |
* @param g The converter for H. | |
* @param n A converter from the "T"ail (input) to the "R"est (output). | |
* | |
* | |
* Note: Following the "Aux's first type parameter is input, the second type parameter is output", | |
* it means that the return type, Aux[H :: T, g.Out :: R], has: | |
* | |
* The input H :: T. | |
* - We get a converter for H (parameter g). | |
* - We get a Aux from T to R, the output type. | |
* The output g.Out :: R. | |
* - g.Out is the what came out of our parameter g, so, what H got converted to. | |
* - R is the "R"est that we have to convert. This is where the recursion happens! | |
* | |
* The recursion happens in the parameter n, because the compiler knows the type of T, | |
* and it needs to find a R using unification. | |
* If the "T"ail is HNil, the recursion is done: The compiler finds GenericGetResult.Aux[HNil, HNil], and stops looking. | |
* However, if there are more than one element in the list, it will invoke hcons again. | |
* | |
* ------------- | |
* | |
* Step-by-step: | |
* | |
* > GenericGetResult.of[Int :: Double :: HNil] | |
* (:: is shapeless.::, a HList constructor) | |
* | |
* Then H = Int, T = Double :: HNil. | |
* The compiler finds g: GenericGetResult[H = Int] (that's `ints`). | |
* Now it needs to unify GenericGetResult.Aux[T = Double :: HNil, R]. | |
* | |
* It finds hcons again, this time with: | |
* H = Double, T = HNil. | |
* The compiler finds g: GenericGetResult[H = Double] (that's `doubles`). | |
* Now it needs to unify GenericGetResult.Aux[H = HNil, R]. | |
* It finds `hnil` and stops recursing. | |
* | |
* so, the inner hcons (with H = Double, T = HNil) has a result type of: | |
* > GenericRetResult.Aux[H = Double :: T = HNil, g.Out = JDBCReader[Double] :: R = HNil] | |
* | |
* the outer hcons (with H = Int, T = Double :: HNil) has a result type of: | |
* > GenericGetResult.Aux[H = Int :: (T = Double :: HNil), g.Out = JDBCReader[Int] :: (R = JDBCReader[Double] :: HNil)] | |
*/ | |
implicit def hcons[H, T <: HList, R <: HList]( | |
implicit g: GenericGetResult[H], | |
n: GenericGetResult.Aux[T, R] | |
): GenericGetResult.Aux[H :: T, g.Out :: R] = | |
instance(g.value :: n.value) | |
/** | |
* This function uses Kittens to transform any case class into a HList, sequence the Reader, and then back to the case class. | |
* | |
* This case class: | |
* > case class Ex(a: Int, b: Double, c: Int) | |
* gives this HList: | |
* > Int :: Double :: Int :: HNil | |
* | |
* @param gen The generator. Takes a T as input and returns a L. | |
* @param v The converter from L to O. | |
* @param ev The "splitting" mechanism of a case class-and-back. | |
* @tparam T The only known parameter at the beginning: the case class type. | |
* @tparam L The HList type that will be used for implicit resolution (starts in hcons, see its documentation). | |
* @tparam O The post-conversion HList of L. | |
* @return A Reader[T] that can be ran. | |
*/ | |
implicit def genReader[T, L <: HList, O <: HList]( | |
implicit gen: Generic.Aux[T, L], | |
v: GenericGetResult.Aux[L, O], | |
ev: Sequencer.Aux[O, JDBCReader, L] | |
): GenericGetResult.Aux[T, JDBCReader[T]] = | |
instance(v.value.sequence.map(gen.from)) | |
/** | |
* Convenient helper to create a GetResult. | |
* @param ev The evidence that we can go from a T to a JDBCReader[T], which is the type of ev.value (the Out type) | |
* @tparam T The case class type. | |
* @return A GetResult[T]. | |
*/ | |
def generate[T](implicit ev: GenericGetResult.Aux[T, JDBCReader[T]]): GetResult[T] = { | |
GetResult[T](r ⇒ ev.value.run(r)) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package sample.shapeless | |
import scala.annotation.implicitNotFound | |
// same as Lib.scala, but doesn't use IO | |
trait JDBC { | |
def nextInt: Int | |
def nextDouble: Double | |
def nextFloat: Float | |
def nextVector[T: JDBCGenerator]: Vector[T] | |
} | |
trait JDBCGenerator[T] { def generate: Vector[T] } | |
object yourowngenerators { | |
implicit object IntGen extends JDBCGenerator[Int] { | |
def generate: Vector[Int] = Vector(100, 110, 120) | |
} | |
} | |
class JDBCimpl(a: Int, b: Double, c: Float) extends JDBC { | |
override def nextInt: Int = a | |
override def nextDouble: Double = b | |
override def nextFloat: Float = c | |
override def nextVector[T: JDBCGenerator]: Vector[T] = implicitly[JDBCGenerator[T]].generate | |
} | |
@implicitNotFound("Not all fields of ${I} have a JDBC reader") | |
trait Results[I] { | |
type Out | |
def value: Out | |
} | |
object Results { | |
import shapeless._ | |
import cats.data.Reader | |
import cats.sequence._ //requires kittens | |
def of[T](implicit ev: Results[T]): ev.Out = ev.value | |
type Aux[I, O] = Results[I] { type Out = O } | |
def instance[I, O](v: O): Results.Aux[I, O] = new Results[I] { | |
type Out = O | |
def value = v | |
} | |
type JDBCReader[A] = Reader[JDBC, A] | |
implicit def ints: Results.Aux[Int, JDBCReader[Int]] = instance(Reader(_.nextInt)) | |
implicit def doubles: Results.Aux[Double, JDBCReader[Double]] = instance(Reader(_.nextDouble)) | |
implicit def float: Results.Aux[Float, JDBCReader[Float]] = instance(Reader(_.nextFloat)) | |
implicit def arrOf[T: JDBCGenerator]: Results.Aux[Vector[T], JDBCReader[Vector[T]]] = instance(Reader(_.nextVector[T])) | |
implicit def hnil: Results.Aux[HNil, HNil] = instance(HNil) | |
implicit def hcons[H, T <: HList, R <: HList]( | |
implicit g: Results[H], | |
n: Results.Aux[T, R] | |
): Results.Aux[H :: T, g.Out :: R] = | |
instance(g.value :: n.value) | |
implicit def gen[T, L <: HList, O <: HList]( | |
implicit gen: Generic.Aux[T, L], | |
v: Results.Aux[L, O], | |
ev: Sequencer.Aux[O, JDBCReader, L] | |
): Results.Aux[T, Reader[JDBC, T]] = | |
instance(v.value.sequence.map(gen.from)) | |
} | |
object Test { | |
def main(args: Array[String]): Unit = { | |
case class Foo(a: Int, b: Double, c: Int, d: Vector[Int]) | |
def r: JDBC = new JDBCimpl(1, 2, 3) | |
import yourowngenerators._ | |
val d = Results.of[Foo].run(r) | |
print(d) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import shapeless._ // requires.shapeless | |
import cats._, implicits._, data.Kleisli // requires.cats | |
import cats.sequence._ //requires kittens | |
import cats.effect.IO //requires cats-effect | |
case class GetResult() // replace with slick's | |
case class DB(val g: GetResult) { | |
def nextInt: IO[Int] = ??? //IO(g.nextInt) | |
def nextDouble: IO[Double] = ??? | |
def nextFloat: IO[Float] = ??? | |
} | |
trait Results[I] { | |
type Out | |
def value: Out | |
} | |
object Results { | |
def of[T](implicit ev: Results[T]): ev.Out = ev.value | |
type Aux[I, O] = Results[I] { type Out = O } | |
def instance[I, O](v: O): Results.Aux[I, O] = new Results[I] { | |
type Out = O | |
def value = v | |
} | |
implicit def ints: Results.Aux[Int, Kleisli[IO, DB, Int]] = | |
instance(Kleisli(r => r.nextInt)) | |
implicit def doubles: Results.Aux[Double, Kleisli[IO, DB, Double]] = | |
instance(Kleisli(r => r.nextDouble)) | |
implicit def floats: Results.Aux[Float, Kleisli[IO, DB, Float]] = | |
instance(Kleisli(r => r.nextFloat)) | |
implicit def hnil: Results.Aux[HNil, HNil] = instance(HNil) | |
implicit def hcons[H, T <: HList, R <: HList]( | |
implicit g: Results[H], | |
n: Results.Aux[T, R]): Results.Aux[H :: T, g.Out :: R] = | |
instance(g.value :: n.value) | |
implicit def gen[T, L <: HList, O <: HList]( | |
implicit gen: Generic.Aux[T, L], | |
v: Results.Aux[L, O], | |
ev: Sequencer.Aux[O, Kleisli[IO, DB, ?], L]): Results.Aux[T, Kleisli[IO, DB, T]] = | |
instance(v.value.sequence.map(gen.from)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object Test { | |
case class Foo(a: Int, b: Double) | |
def db: DB = ??? | |
val d: IO[Foo] = Results.of[Foo].run(db) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment