Skip to content

Commit

Permalink
Merge pull request #208 from theiterators/doobie-support
Browse files Browse the repository at this point in the history
Basic Scala 2 & 3 doobie support
  • Loading branch information
pk044 committed Jun 20, 2022
2 parents 8f31515 + 09d880e commit 0efa601
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 10 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A library maintained by [Iterators](https://www.iteratorshq.com).
* [SBT](#sbt)
* [Examples](#examples)
* [slick](#--kebs-generates-slick-mappers-for-your-case-class-wrappers-kebs-slick)
* [doobie](#--kebs-generates-doobie-mappers-for-your-case-class-wrappers-kebs-doobie)
* [spray-json](#--kebs-eliminates-spray-json-induced-boilerplate-kebs-spray-json)
* [play-json](#--kebs-eliminates-play-json-induced-boilerplate-kebs-play-json)
* [akka-http](#--kebs-generates-akka-http-unmarshaller-kebs-akka-http)
Expand All @@ -25,14 +26,18 @@ A library maintained by [Iterators](https://www.iteratorshq.com).
### Why?

`kebs` is for eliminating some common sources of Scala boilerplate code that arise when you use
Slick (`kebs-slick`), Spray (`kebs-spray-json`), Play (`kebs-play-json`), Circe (`kebs-circe`), Akka HTTP (`kebs-akka-http`).
Slick (`kebs-slick`), Doobie (`kebs-doobie`), Spray (`kebs-spray-json`), Play (`kebs-play-json`), Circe (`kebs-circe`), Akka HTTP (`kebs-akka-http`).

### SBT

Support for `slick`

`libraryDependencies += "pl.iterators" %% "kebs-slick" % "1.9.3"`

Support for `doobie`

`libraryDependencies += "pl.iterators" %% "kebs-doobie" % "1.9.3"`

Support for `spray-json`

`libraryDependencies += "pl.iterators" %% "kebs-spray-json" % "1.9.3"`
Expand Down Expand Up @@ -371,6 +376,17 @@ import MyPostgresProfile.api._
}
```

#### - kebs generates doobie mappers for your case-class wrappers (kebs-doobie)

kebs-doobie works similarly to [kebs-slick](#--kebs-generates-slick-mappers-for-your-case-class-wrappers-kebs-slick). It provides doobie's `Meta` instances for:

* Instances of `CaseClass1Rep` (value classes, tagged types, opaque types)
* Instances of `InstanceConverter`
* Enumeratum for Scala 2
* Native enums for Scala 3

To make the magic happen, do `import pl.iterators.kebs._` and `import pl.iterators.kebs.enums._` (or `import pl.iterators.kebs.enums.uppercase._` or `import pl.iterators.kebs.enums.lowercase._`).

#### - kebs eliminates spray-json induced boilerplate (kebs-spray-json)

Writing JSON formats in spray can be really unwieldy. For every case-class you want serialized, you have to count the number of fields it has.
Expand Down
39 changes: 30 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ val scala_2_12 = "2.12.15"
val scala_2_13 = "2.13.8"
val scala_30 = "3.0.2"
val scala_31 = "3.1.2"
val mainScalaVersion = scala_2_13
val mainScalaVersion = scala_31
val supportedScalaVersions = Seq(scala_2_12, scala_2_13, scala_30, scala_31)

ThisBuild / crossScalaVersions := supportedScalaVersions
ThisBuild / scalaVersion := mainScalaVersion

ThisBuild / conflictWarning := ConflictWarning.disable


Expand Down Expand Up @@ -135,6 +134,8 @@ val slick = "com.typesafe.slick" %% "slick" % "3.3.3"
val optionalSlick = optional(slick)
val playJson = "com.typesafe.play" %% "play-json" % "2.9.2"
val slickPg = "com.github.tminglei" %% "slick-pg" % "0.20.3"
val doobie = "org.tpolecat" %% "doobie-core" % "1.0.0-RC1"
val doobiePg = "org.tpolecat" %% "doobie-postgres" % "1.0.0-RC1"
val sprayJson = "io.spray" %% "spray-json" % "1.3.6"
val circe = "io.circe" %% "circe-core" % "0.14.2"
val circeAuto = "io.circe" %% "circe-generic" % "0.14.2"
Expand Down Expand Up @@ -188,6 +189,12 @@ lazy val slickSettings = commonSettings ++ Seq(
libraryDependencies += optionalEnumeratum.cross(CrossVersion.for3Use2_13)
)

lazy val doobieSettings = commonSettings ++ Seq(
libraryDependencies += doobie,
libraryDependencies += (doobiePg % "test"),
libraryDependencies += optionalEnumeratum.cross(CrossVersion.for3Use2_13),
)

lazy val macroUtilsSettings = commonMacroSettings ++ Seq(
libraryDependencies += (scalaCheck % "test").cross(CrossVersion.for3Use2_13),
libraryDependencies += optionalEnumeratum
Expand Down Expand Up @@ -268,8 +275,7 @@ lazy val macroUtils = project
.settings(
name := "macro-utils",
description := "Macros supporting Kebs library",
moduleName := "kebs-macro-utils",
crossScalaVersions := supportedScalaVersions
moduleName := "kebs-macro-utils"
)

lazy val slickSupport = project
Expand All @@ -285,6 +291,18 @@ lazy val slickSupport = project
crossScalaVersions := supportedScalaVersions
)

lazy val doobieSupport = project
.in(file("doobie"))
.dependsOn(instances, opaque)
.settings(doobieSettings: _*)
.settings(publishSettings: _*)
.settings(
name := "doobie",
description := "Library to eliminate the boilerplate code that comes with the use of Doobie",
moduleName := "kebs-doobie",
crossScalaVersions := supportedScalaVersions
)

lazy val sprayJsonMacros = project
.in(file("spray-json-macros"))
.dependsOn(macroUtils)
Expand Down Expand Up @@ -393,14 +411,17 @@ lazy val opaque = project
.in(file("opaque"))
.dependsOn(macroUtils)
.settings(opaqueSettings: _*)
.settings(disableScala("2.13"))
.settings(disableScala("2.12"))
.settings(publishSettings: _*)
.settings(disableScala("2"))
.settings(
name := "opaque",
description := "Representation of opaque types",
moduleName := "kebs-opaque",
crossScalaVersions := Seq(scala_30)
)
crossScalaVersions := supportedScalaVersions,
releaseCrossBuild := false,
publish / skip := (scalaBinaryVersion.value == "2.13")
)

lazy val taggedMeta = project
.in(file("tagged-meta"))
Expand Down Expand Up @@ -451,8 +472,7 @@ lazy val instances = project
.settings(
name := "instances",
description := "Standard type mappings",
moduleName := "kebs-instances",
crossScalaVersions := supportedScalaVersions
moduleName := "kebs-instances"
)

import sbtrelease.ReleasePlugin.autoImport._
Expand All @@ -465,6 +485,7 @@ lazy val kebs = project
opaque,
macroUtils,
slickSupport,
doobieSupport,
sprayJsonMacros,
sprayJsonSupport,
playJsonSupport,
Expand Down
21 changes: 21 additions & 0 deletions doobie/src/main/scala-2/pl/iterators/kebs/Kebs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package pl.iterators.kebs

import doobie.Meta
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.macros.CaseClass1Rep

import scala.reflect.ClassTag

trait Kebs {
implicit def caseClass1RepMeta[A, M](implicit cc1Rep: CaseClass1Rep[A, M], m: Meta[M]): Meta[A] = m.imap(cc1Rep.apply)(cc1Rep.unapply)

implicit def caseClass1RepArrayMeta[A, M](implicit cc1Rep: CaseClass1Rep[A, M], m: Meta[Array[M]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[A]] = m.imap(_.map(cc1Rep.apply))(_.map(cc1Rep.unapply))

implicit def caseClass1RepOptionArrayMeta[A, M](implicit cc1Rep: CaseClass1Rep[A, M], m: Meta[Array[Option[M]]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[Option[A]]] = m.imap(_.map(_.map(cc1Rep.apply)))(_.map(_.map(cc1Rep.unapply)))

implicit def instanceConverterMeta[A, M](implicit instanceConverter: InstanceConverter[A, M], m: Meta[M]): Meta[A] = m.imap(instanceConverter.decode)(instanceConverter.encode)

implicit def instanceConverterArrayMeta[A, M](implicit instanceConverter: InstanceConverter[A, M], m: Meta[Array[M]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[A]] = m.imap(_.map(instanceConverter.decode))(_.map(instanceConverter.encode))

implicit def instanceConverterOptionArrayMeta[A, M](implicit instanceConverter: InstanceConverter[A, M], m: Meta[Array[Option[M]]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[Option[A]]] = m.imap(_.map(_.map(instanceConverter.decode)))(_.map(_.map(instanceConverter.encode)))
}
27 changes: 27 additions & 0 deletions doobie/src/main/scala-2/pl/iterators/kebs/enums/KebsEnums.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pl.iterators.kebs.enums

import doobie.Meta
import enumeratum.EnumEntry
import pl.iterators.kebs.macros.enums.EnumOf

import scala.reflect.ClassTag

trait KebsEnums {
implicit def enumMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[String]): Meta[E] = m.imap(e.`enum`.withName)(_.toString)
implicit def enumArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[String]], cte: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(e.`enum`.withName))(_.map(_.toString))
implicit def enumOptionArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[Option[String]]], cte: ClassTag[Option[E]]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(e.`enum`.withName)))(_.map(_.map(_.toString)))

trait Uppercase {
implicit def enumUppercaseMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[String]): Meta[E] = m.imap(e.`enum`.withNameUppercaseOnly)(_.toString.toUpperCase)
implicit def enumUppercaseArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[String]], cte: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(e.`enum`.withNameUppercaseOnly))(_.map(_.toString.toUpperCase))
implicit def enumUppercaseOptionArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[Option[String]]], cte: ClassTag[E]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(e.`enum`.withNameUppercaseOnly)))(_.map(_.map(_.toString.toUpperCase)))
}

trait Lowercase {
implicit def enumLowercaseMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[String]): Meta[E] = m.imap(e.`enum`.withNameLowercaseOnly)(_.toString.toLowerCase)
implicit def enumLowercaseArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[String]], cte: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(e.`enum`.withNameLowercaseOnly))(_.map(_.toString.toLowerCase))
implicit def enumLowercaseOptionArrayMeta[E <: EnumEntry](implicit e: EnumOf[E], m: Meta[Array[Option[String]]], cte: ClassTag[E]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(e.`enum`.withNameLowercaseOnly)))(_.map(_.map(_.toString.toLowerCase)))
}
}

object KebsEnums extends KebsEnums
8 changes: 8 additions & 0 deletions doobie/src/main/scala-2/pl/iterators/kebs/enums/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pl.iterators.kebs

package object enums extends KebsEnums {
object uppercase extends Uppercase

object lowercase extends Lowercase

}
3 changes: 3 additions & 0 deletions doobie/src/main/scala-2/pl/iterators/kebs/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package pl.iterators

package object kebs extends Kebs
24 changes: 24 additions & 0 deletions doobie/src/main/scala-3/pl/iterators/kebs/Kebs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pl.iterators.kebs

import doobie.{Get, Put, Meta}
import pl.iterators.kebs.enums.KebsEnums
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.macros.CaseClass1Rep

import scala.reflect.ClassTag

trait Kebs {
inline given[A, M](using cc1Rep: CaseClass1Rep[A, M], m: Meta[M]): Meta[A] = m.imap(cc1Rep.apply)(cc1Rep.unapply)

inline given[A, M](using cc1Rep: CaseClass1Rep[A, M], m: Meta[Option[M]]): Meta[Option[A]] = m.imap(_.map(cc1Rep.apply))(_.map(cc1Rep.unapply))

inline given[A, M](using cc1Rep: CaseClass1Rep[A, M], m: Meta[Array[M]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[A]] = m.imap(_.map(cc1Rep.apply))(_.map(cc1Rep.unapply))

inline given[A, M](using cc1Rep: CaseClass1Rep[A, M], m: Meta[Array[Option[M]]], cta: ClassTag[Option[A]]): Meta[Array[Option[A]]] = m.imap(_.map(_.map(cc1Rep.apply)))(_.map(_.map(cc1Rep.unapply)))

inline given[A, M](using instanceConverter: InstanceConverter[A, M], m: Meta[M]): Meta[A] = m.imap(instanceConverter.decode)(instanceConverter.encode)

inline given[A, M](using instanceConverter: InstanceConverter[A, M], m: Meta[Array[M]], cta: ClassTag[A], ctm: ClassTag[M]): Meta[Array[A]] = m.imap(_.map(instanceConverter.decode))(_.map(instanceConverter.encode))

inline given[A, M](using instanceConverter: InstanceConverter[A, M], m: Meta[Array[Option[M]]], cta: ClassTag[Option[A]]): Meta[Array[Option[A]]] = m.imap(_.map(_.map(instanceConverter.decode)))(_.map(_.map(instanceConverter.encode)))
}
24 changes: 24 additions & 0 deletions doobie/src/main/scala-3/pl/iterators/kebs/enums/KebsEnums.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pl.iterators.kebs.enums

import doobie.Meta
import pl.iterators.kebs.macros.enums.{EnumOf, EnumLike}
import scala.reflect.ClassTag
import scala.reflect.Enum

trait KebsEnums {
inline given enumMeta[E <: Enum](using e: EnumOf[E]): Meta[E] = Meta.StringMeta.imap(e.`enum`.valueOf)(_.toString)
inline given enumArrayMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[String]], ct: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(e.`enum`.valueOf))(_.map(_.toString))
inline given enumOptionArrayMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[Option[String]]], ct: ClassTag[Option[E]]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(e.`enum`.valueOf)))(_.map(_.map(_.toString)))

trait Uppercase {
inline given enumUppercaseMeta[E <: Enum](using e: EnumOf[E]): Meta[E] = Meta.StringMeta.imap(s => e.`enum`.values.find(_.toString.toUpperCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s")))(_.toString.toUpperCase)
inline given enumUppercaseArrayMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[String]], ct: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(s => e.`enum`.values.find(_.toString.toUpperCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s"))))(_.map(_.toString.toUpperCase))
inline given enumUppercaseOptionArrayMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[Option[String]]], ct: ClassTag[Option[E]]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(s => e.`enum`.values.find(_.toString.toUpperCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s")))))(_.map(_.map(_.toString.toUpperCase)))
}

trait Lowercase {
inline given enumLowercaseMeta[E <: Enum](using e: EnumOf[E]): Meta[E] = Meta.StringMeta.imap(s => e.`enum`.values.find(_.toString.toLowerCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s")))(_.toString.toLowerCase)
inline given enumLowercaseMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[String]], ct: ClassTag[E]): Meta[Array[E]] = m.imap(_.map(s => e.`enum`.values.find(_.toString.toLowerCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s"))))(_.map(_.toString.toLowerCase))
inline given enumLowercaseOptionArrayMeta[E <: Enum](using e: EnumOf[E], m: Meta[Array[Option[String]]], ct: ClassTag[Option[E]]): Meta[Array[Option[E]]] = m.imap(_.map(_.map(s => e.`enum`.values.find(_.toString.toLowerCase == s).getOrElse(throw new IllegalArgumentException(s"enum case not found: $s")))))(_.map(_.map(_.toString.toLowerCase)))
}
}
8 changes: 8 additions & 0 deletions doobie/src/main/scala-3/pl/iterators/kebs/enums/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pl.iterators.kebs

package object enums extends KebsEnums {
object uppercase extends Uppercase

object lowercase extends Lowercase

}
3 changes: 3 additions & 0 deletions doobie/src/main/scala-3/pl/iterators/kebs/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package pl.iterators

package object kebs extends Kebs
76 changes: 76 additions & 0 deletions doobie/src/test/scala-2/ComplexTypesTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import enumeratum.{Enum, EnumEntry}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

import java.util.Currency
import doobie._
import doobie.implicits._
import doobie.postgres._
import doobie.postgres.implicits._
import pl.iterators.kebs.enums._
import pl.iterators.kebs._
import pl.iterators.kebs.instances.KebsInstances._

class ComplexTypesTests extends AnyFunSuite with Matchers {
case class Name(name: String)
sealed trait EyeColor extends EnumEntry
object EyeColor extends Enum[EyeColor] {
case object Blue extends EyeColor
case object Green extends EyeColor
case object Brown extends EyeColor
case object Other extends EyeColor
def values = findValues
}
case class Person(name: Name, eyeColor: EyeColor, preferredCurrency: Currency, relatives: List[Name], eyeballsInTheJar: Array[EyeColor])

test("Put & Get exist") {
"implicitly[Get[Name]]" should compile
"implicitly[Put[Name]]" should compile
"implicitly[Get[List[Name]]]" should compile
"implicitly[Put[List[Name]]]" should compile
"implicitly[Get[Array[Name]]]" should compile
"implicitly[Put[Array[Name]]]" should compile
"implicitly[Get[Vector[Name]]]" should compile
"implicitly[Put[Vector[Name]]]" should compile
"implicitly[Get[List[Option[Name]]]]" should compile
"implicitly[Put[List[Option[Name]]]]" should compile
"implicitly[Get[Array[Option[Name]]]]" should compile
"implicitly[Put[Array[Option[Name]]]]" should compile
"implicitly[Get[Vector[Option[Name]]]]" should compile
"implicitly[Put[Vector[Option[Name]]]]" should compile

"implicitly[Get[Currency]]" should compile
"implicitly[Put[Currency]]" should compile
"implicitly[Get[List[Currency]]]" should compile
"implicitly[Put[List[Currency]]]" should compile
"implicitly[Get[Array[Currency]]]" should compile
"implicitly[Put[Array[Currency]]]" should compile
"implicitly[Get[Vector[Currency]]]" should compile
"implicitly[Put[Vector[Currency]]]" should compile
"implicitly[Get[List[Option[Currency]]]]" should compile
"implicitly[Put[List[Option[Currency]]]]" should compile
"implicitly[Get[Array[Option[Currency]]]]" should compile
"implicitly[Put[Array[Option[Currency]]]]" should compile
"implicitly[Get[Vector[Option[Currency]]]]" should compile
"implicitly[Put[Vector[Option[Currency]]]]" should compile

"implicitly[Get[EyeColor]]" should compile
"implicitly[Put[EyeColor]]" should compile
"implicitly[Get[List[EyeColor]]]" should compile
"implicitly[Put[List[EyeColor]]]" should compile
"implicitly[Get[Array[EyeColor]]]" should compile
"implicitly[Put[Array[EyeColor]]]" should compile
"implicitly[Get[Vector[EyeColor]]]" should compile
"implicitly[Put[Vector[EyeColor]]]" should compile
"implicitly[Get[List[Option[EyeColor]]]]" should compile
"implicitly[Put[List[Option[EyeColor]]]]" should compile
"implicitly[Get[Array[Option[EyeColor]]]]" should compile
"implicitly[Put[Array[Option[EyeColor]]]]" should compile
"implicitly[Get[Vector[Option[EyeColor]]]]" should compile
"implicitly[Put[Vector[Option[EyeColor]]]]" should compile
}

test("Query should compile") {
"""sql"SELECT * FROM people".query[Person].unique""" should compile
}
}
Loading

0 comments on commit 0efa601

Please sign in to comment.