Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Scala 2 & 3 doobie support #208

Merged
merged 8 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.1"
val circeAuto = "io.circe" %% "circe-generic" % "0.14.1"
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