diff --git a/README.md b/README.md index e2fa6878..ef04cb1b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Integrations are available for: - [ScalaCheck](https://www.scalacheck.org): JVM and ScalaJS - [Slick](http://slick.lightbend.com/): JVM only - [Quill](http://getquill.io): JVM and ScalaJS +- [Anorm](https://playframework.github.io/anorm/): JVM only - [sttp tapir](https://github.com/softwaremill/tapir): JVM and ScalaJS ### Table of Contents @@ -58,9 +59,9 @@ Integrations are available for: 13. [Quill integration](#quill) 14. [Cats integration](#cats) 15. [Doobie integration](#doobie) -16. [Benchmarking](#benchmarking) -17. [Publishing](#publishing) - +16. [Anorm integration](#anorm) +17. [Benchmarking](#benchmarking) +18. [Publishing](#publishing) ## Quick start @@ -1134,6 +1135,7 @@ items.maximumOption // Some(SuperHigh) If you need instances, but hesitate to mix in the traits demonstrated above, you can get them using the provided methods in `enumeratum.Cats` and `enumeratum.values.Cats` - the second also provides more flexibility than the (opinionated) mix-in trait as it allows to pass a custom type class instance for the value type (methods names are prefixed with `value`). ## Doobie + [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-doobie_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-doobie_2.12) ### SBT @@ -1259,6 +1261,108 @@ sql"select shirt from clothes" ``` - Note that a type ascription to the `ValueEnumEntry` abstract class (eg. `ShirtSize.Small: ShirtSize`) is required when binding hardcoded `ValueEnumEntry`s +## Anorm + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-anorm_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-anorm_2.12) + +### SBT + +To use enumeratum with [Anorm](https://playframework.github.io/anorm/): + +```scala +libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum-anorm" % enumeratumAnormVersion +) +``` + +To use with ScalaJS: + +```scala +libraryDependencies ++= Seq( + "com.beachape" %%% "enumeratum-anorm" % enumeratumAnormVersion +) +``` + +### Usage + +#### Enum + +If you need to store enum values in text column of following table + +```sql +CREATE TABLE clothes ( + shirt varchar(100) +) +``` + +you should use following code + +```scala +import enumeratum._ + +sealed trait ShirtSize extends EnumEntry + +case object ShirtSize extends Enum[ShirtSize] with AnormEnum[ShirtSize] { + case object Small extends ShirtSize + case object Medium extends ShirtSize + case object Large extends ShirtSize + + val values = findValues +} + +case class Shirt(size: ShirtSize) + +// --- + +import java.sql.Connection +import anorm._ + +// Support Enum as readable Column +def findShirtSize(shirtId: String)(implicit con: Connection): ShirtSize = + SQL"SELECT size FROM shirt_tbl WHERE id = $shirtId". + as(SqlParser.scalar[ShirtSize].single) + +// Support Enum as parameter +def updateShirtSize(shirtId: String, size: ShirtSize)( + implicit con: Connection) = + SQL"UPDATE shirt_tbl SET size = ${size} WHERE id = ${shirtId}".execute() +``` + +#### ValueEnum + +```scala +import enumeratum.values.{ IntAnormValueEnum, IntEnum, IntEnumEntry } + +sealed abstract class ShirtSize(val value: Int) extends IntEnumEntry + +case object ShirtSize extends IntEnum[ShirtSize] + with IntAnormValueEnum[ShirtSize] { + + case object Small extends ShirtSize(1) + case object Medium extends ShirtSize(2) + case object Large extends ShirtSize(3) + + val values = findValues +} + +case class Shirt(size: ShirtSize) + +// --- + +import java.sql.Connection +import anorm._ + +// Support ValueEnum as readable Column +def findShirtSize(shirtId: String)(implicit con: Connection): ShirtSize = + SQL"SELECT size FROM shirt_tbl WHERE id = $shirtId". + as(SqlParser.scalar[ShirtSize].single) + +// Support ValueEnum as parameter +def updateShirtSize(shirtId: String, size: ShirtSize)( + implicit con: Connection) = + SQL"UPDATE shirt_tbl SET size = ${size} WHERE id = ${shirtId}".execute() +``` + ## Benchmarking Benchmarking is in the unpublished `benchmarking` project. It uses JMH and you can run them in the sbt console by issuing the following command from your command line: diff --git a/build.sbt b/build.sbt index 43ca5b2b..e5a65690 100644 --- a/build.sbt +++ b/build.sbt @@ -88,6 +88,7 @@ lazy val scala213ProjectRefs = Seq( enumeratumScalacheckJs, enumeratumPlayJsonJvm, enumeratumPlayJsonJs, + enumeratumAnorm, enumeratumArgonautJs, enumeratumArgonautJvm, enumeratumSlick, @@ -125,6 +126,7 @@ lazy val scala211ProjectRefs = Seq( enumeratumPlayJsonJvm, // TODO drop 2.11 as play-json 2.7.x supporting Scala.js 1.x is unlikely? // enumeratumPlayJsonJs, TODO re-enable once play-json supports Scala.js 1.0 + enumeratumAnorm, enumeratumArgonautJs, enumeratumArgonautJvm, enumeratumSlick, @@ -159,6 +161,7 @@ lazy val integrationProjectRefs = Seq( enumeratumCirceJs, enumeratumCirceJvm, enumeratumReactiveMongoBson, + enumeratumAnorm, enumeratumArgonautJs, enumeratumArgonautJvm, enumeratumJson4s, @@ -325,6 +328,22 @@ lazy val enumeratumPlay = Project(id = "enumeratum-play", base = file("enumeratu .settings(withCompatUnmanagedSources(jsJvmCrossProject = false, includeTestSrcs = true): _*) .dependsOn(enumeratumPlayJsonJvm % "compile->compile;test->test") +lazy val enumeratumAnorm = Project(id = "enumeratum-anorm", base = file("enumeratum-anorm")) + .settings(commonWithPublishSettings: _*) + .settings(testSettings: _*) + .settings( + version := "1.6.3-SNAPSHOT", + crossScalaVersions := Seq( + scala_2_11Version, scala_2_12Version, scala_2_13Version), + libraryDependencies ++= + Seq( + "org.playframework.anorm" %% "anorm" % "2.6.9", + "com.beachape" %% "enumeratum" % Versions.Core.stable, + //"com.beachape" %% "enumeratum-test" % Versions.Core.stable % Test + "org.eu.acolyte" %% "jdbc-scala" % "1.0.57" % Test + ) + ) + lazy val circeAggregate = aggregateProject("circe", enumeratumCirceJs, enumeratumCirceJvm) lazy val enumeratumCirce = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) diff --git a/enumeratum-anorm/src/main/scala/AnormColumn.scala b/enumeratum-anorm/src/main/scala/AnormColumn.scala new file mode 100644 index 00000000..8ce66883 --- /dev/null +++ b/enumeratum-anorm/src/main/scala/AnormColumn.scala @@ -0,0 +1,35 @@ +package enumeratum + +import anorm.{Column, TypeDoesNotMatch} + +private[enumeratum] object AnormColumn { + def column[A <: EnumEntry](enum: Enum[A], insensitive: Boolean): Column[A] = + if (insensitive) { + parse[A](enum)(enum.withNameInsensitiveOption) + } else { + parse[A](enum)(enum.withNameOption) + } + + def lowercaseOnlyColumn[A <: EnumEntry](enum: Enum[A]): Column[A] = + parse[A](enum)(enum.withNameLowercaseOnlyOption) + + def uppercaseOnlyColumn[A <: EnumEntry](enum: Enum[A]): Column[A] = + parse[A](enum)(enum.withNameUppercaseOnlyOption) + + // --- + + private def parse[A <: EnumEntry](enum: Enum[A])(extract: String => Option[A]): Column[A] = + Column.nonNull[A] { + case (s: String, _) => + extract(s) match { + case Some(result) => Right(result) + case None => Left(TypeDoesNotMatch(s"Invalid value: $s")) + } + + case (_, meta) => + Left( + TypeDoesNotMatch( + s"Column '${meta.column.qualified}' expected to be String; Found ${meta.clazz}")) + } + +} diff --git a/enumeratum-anorm/src/main/scala/AnormEnum.scala b/enumeratum-anorm/src/main/scala/AnormEnum.scala new file mode 100644 index 00000000..f728f790 --- /dev/null +++ b/enumeratum-anorm/src/main/scala/AnormEnum.scala @@ -0,0 +1,20 @@ +package enumeratum + +import java.sql.PreparedStatement + +import anorm.{Column, ToStatement} + +/** + * Provides instances for Anorm typeclasses: + * + * - [[anorm.Column]] + * - [[anorm.ToStatement]] + */ +trait AnormEnum[A <: EnumEntry] { self: Enum[A] => + implicit val column: Column[A] = + AnormColumn.column[A](self, insensitive = false) + + implicit val toStatement: ToStatement[A] = new ToStatement[A] { + def set(s: PreparedStatement, i: Int, v: A) = s.setString(i, v.entryName) + } +} diff --git a/enumeratum-anorm/src/main/scala/AnormInsensitiveEnum.scala b/enumeratum-anorm/src/main/scala/AnormInsensitiveEnum.scala new file mode 100644 index 00000000..032a86f3 --- /dev/null +++ b/enumeratum-anorm/src/main/scala/AnormInsensitiveEnum.scala @@ -0,0 +1,21 @@ +package enumeratum + +import java.sql.PreparedStatement + +import anorm.{Column, ToStatement} + +/** + * Provides insensitive instances for Anorm typeclasses: + * + * - [[anorm.Column]] + * - [[anorm.ToStatement]] + */ +trait AnormInsensitiveEnum[A <: EnumEntry] { self: Enum[A] => + implicit val column: Column[A] = + AnormColumn.column[A](self, insensitive = true) + + implicit val toStatement = new ToStatement[A] { + def set(s: PreparedStatement, i: Int, v: A) = + s.setString(i, v.entryName) + } +} diff --git a/enumeratum-anorm/src/main/scala/AnormLowercaseEnum.scala b/enumeratum-anorm/src/main/scala/AnormLowercaseEnum.scala new file mode 100644 index 00000000..5de8d10b --- /dev/null +++ b/enumeratum-anorm/src/main/scala/AnormLowercaseEnum.scala @@ -0,0 +1,21 @@ +package enumeratum + +import java.sql.PreparedStatement + +import anorm.{Column, ToStatement} + +/** + * Provides lowercase instances for Anorm typeclasses: + * + * - [[anorm.Column]] + * - [[anorm.ToStatement]] + */ +trait AnormLowercaseEnum[A <: EnumEntry] { self: Enum[A] => + implicit val column: Column[A] = + AnormColumn.lowercaseOnlyColumn[A](self) + + implicit val toStatement = new ToStatement[A] { + def set(s: PreparedStatement, i: Int, v: A) = + s.setString(i, v.entryName.toLowerCase) + } +} diff --git a/enumeratum-anorm/src/main/scala/AnormUppercaseEnum.scala b/enumeratum-anorm/src/main/scala/AnormUppercaseEnum.scala new file mode 100644 index 00000000..9d08eefb --- /dev/null +++ b/enumeratum-anorm/src/main/scala/AnormUppercaseEnum.scala @@ -0,0 +1,21 @@ +package enumeratum + +import java.sql.PreparedStatement + +import anorm.{Column, ToStatement} + +/** + * Provides uppercase instances for Anorm typeclasses: + * + * - [[anorm.Column]] + * - [[anorm.ToStatement]] + */ +trait AnormUppercaseEnum[A <: EnumEntry] { self: Enum[A] => + implicit val column: Column[A] = + AnormColumn.uppercaseOnlyColumn[A](self) + + implicit val toStatement = new ToStatement[A] { + def set(s: PreparedStatement, i: Int, v: A) = + s.setString(i, v.entryName.toUpperCase) + } +} diff --git a/enumeratum-anorm/src/main/scala/values/AnormColumn.scala b/enumeratum-anorm/src/main/scala/values/AnormColumn.scala new file mode 100644 index 00000000..59eb2f35 --- /dev/null +++ b/enumeratum-anorm/src/main/scala/values/AnormColumn.scala @@ -0,0 +1,23 @@ +package enumeratum.values + +import anorm.{Column, TypeDoesNotMatch} + +private[values] object AnormColumn { + def apply[ValueType, EntryType <: ValueEnumEntry[ValueType]]( + enum: ValueEnum[ValueType, EntryType] + )( + implicit baseColumn: Column[ValueType] + ): Column[EntryType] = Column.nonNull[EntryType] { + case (value, meta) => + baseColumn(value, meta) match { + case Left(err) => + Left(err) + + case Right(s) => + enum.withValueOpt(s) match { + case Some(obj) => Right(obj) + case None => Left(TypeDoesNotMatch(s"Invalid value: $s")) + } + } + } +} diff --git a/enumeratum-anorm/src/main/scala/values/AnormToStatement.scala b/enumeratum-anorm/src/main/scala/values/AnormToStatement.scala new file mode 100644 index 00000000..4bfb6c4f --- /dev/null +++ b/enumeratum-anorm/src/main/scala/values/AnormToStatement.scala @@ -0,0 +1,16 @@ +package enumeratum.values + +import java.sql.PreparedStatement + +import anorm.ToStatement + +private[values] object AnormToStatement { + def apply[ValueType, EntryType <: ValueEnumEntry[ValueType]]( + enum: ValueEnum[ValueType, EntryType] + )( + implicit baseToStmt: ToStatement[ValueType] + ): ToStatement[EntryType] = new ToStatement[EntryType] { + def set(s: PreparedStatement, i: Int, e: EntryType) = + baseToStmt.set(s, i, e.value) + } +} diff --git a/enumeratum-anorm/src/main/scala/values/AnormValueEnum.scala b/enumeratum-anorm/src/main/scala/values/AnormValueEnum.scala new file mode 100644 index 00000000..34e76c40 --- /dev/null +++ b/enumeratum-anorm/src/main/scala/values/AnormValueEnum.scala @@ -0,0 +1,71 @@ +package enumeratum.values + +import anorm.{Column, ToStatement} + +trait AnormValueEnum[ValueType, EntryType <: ValueEnumEntry[ValueType]] { + enum: ValueEnum[ValueType, EntryType] => + + /** + * Column instance for the entries of this enum + */ + implicit def column: Column[EntryType] + + /** + * ToStatement instance for the entries of this enum + */ + implicit def toStatement: ToStatement[EntryType] +} + +/** + * Enum implementation for Int enum members that provides instances for Anorm typeclasses + */ +trait IntAnormValueEnum[EntryType <: IntEnumEntry] extends AnormValueEnum[Int, EntryType] { + this: IntEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} + +/** + * Enum implementation for Long enum members that provides instances for Anorm typeclasses + */ +trait LongAnormValueEnum[EntryType <: LongEnumEntry] extends AnormValueEnum[Long, EntryType] { + this: LongEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} + +/** + * Enum implementation for Short enum members that provides instances for Anorm typeclasses + */ +trait ShortAnormValueEnum[EntryType <: ShortEnumEntry] extends AnormValueEnum[Short, EntryType] { + this: ShortEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} + +/** + * Enum implementation for String enum members that provides instances for Anorm typeclasses + */ +trait StringAnormValueEnum[EntryType <: StringEnumEntry] extends AnormValueEnum[String, EntryType] { + this: StringEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} + +/** + * Enum implementation for Char enum members that provides instances for Anorm typeclasses + */ +trait CharAnormValueEnum[EntryType <: CharEnumEntry] extends AnormValueEnum[Char, EntryType] { + this: CharEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} + +/** + * Enum implementation for Byte enum members that provides instances for Anorm typeclasses + */ +trait ByteAnormValueEnum[EntryType <: ByteEnumEntry] extends AnormValueEnum[Byte, EntryType] { + this: ByteEnum[EntryType] => + implicit val column: Column[EntryType] = AnormColumn(this) + implicit val toStatement: ToStatement[EntryType] = AnormToStatement(this) +} diff --git a/enumeratum-anorm/src/test/scala/AnormColumnSpec.scala b/enumeratum-anorm/src/test/scala/AnormColumnSpec.scala new file mode 100644 index 00000000..c642abfb --- /dev/null +++ b/enumeratum-anorm/src/test/scala/AnormColumnSpec.scala @@ -0,0 +1,190 @@ +package enumeratum + +import scala.util.control.NonFatal + +import anorm.{AnormException, SQL, SqlParser, TypeDoesNotMatch}, SqlParser.scalar + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import acolyte.jdbc.AcolyteDSL.withQueryResult +import acolyte.jdbc.Implicits._ +import acolyte.jdbc.{RowList, RowLists} + +final class AnormColumnSpec extends AnyWordSpec with Matchers { + "Sensitive enum" should { + "successfully parsed as Column" when { + def spec[T <: Dummy](repr: String, expected: T) = + repr in withQueryResult(RowLists.stringList :+ repr) { implicit con => + SQL("SELECT v").as(scalar[Dummy].single) mustEqual expected + } + + spec("A", Dummy.A) + spec("B", Dummy.B) + spec("c", Dummy.c) + } + + "not be parsed as Column from invalid String representation" when { + def spec(title: String, repr: String) = + title in withQueryResult(RowLists.stringList :+ repr) { implicit con => + try { + SQL("SELECT v").as(scalar[Dummy].single) + fail(s"Must not be successful: $repr") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: $repr").message) + } + } + + spec("a (!= A as sensitive)", "a") + spec("b (!= B as sensitive)", "b") + spec("C (!= c as sensitive)", "C") + } + + "not be parsed as Column from non-String values" when { + def spec(tpe: String, rowList: RowList[_]) = + tpe in withQueryResult(rowList) { implicit con => + try { + SQL("SELECT v").as(scalar[Dummy].single) + fail(s"Must not be successful: $tpe") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException( + TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message) + } + } + + spec("float", RowLists.floatList :+ 0.1F) + spec("int", RowLists.intList :+ 1) + } + } + + "Insensitive enum" should { + "successfully parsed as Column" when { + def spec[T <: InsensitiveDummy](repr: String, expected: T) = + repr in withQueryResult(RowLists.stringList :+ repr) { implicit con => + SQL("SELECT v").as(scalar[InsensitiveDummy].single) mustEqual expected + } + + spec("A", InsensitiveDummy.A) + spec("a", InsensitiveDummy.A) + + spec("B", InsensitiveDummy.B) + spec("b", InsensitiveDummy.B) + + spec("C", InsensitiveDummy.c) + spec("c", InsensitiveDummy.c) + } + + "not be parsed as Column from non-String values" when { + def spec(tpe: String, rowList: RowList[_]) = + tpe in withQueryResult(rowList) { implicit con => + try { + SQL("SELECT v").as(scalar[InsensitiveDummy].single) + fail(s"Must not be successful: $tpe") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException( + TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message) + } + } + + spec("float", RowLists.floatList :+ 0.1F) + spec("int", RowLists.intList :+ 1) + } + } + + "Lowercase enum" should { + "successfully parsed as Column" when { + def spec[T <: LowercaseDummy](repr: String, expected: T) = + repr in withQueryResult(RowLists.stringList :+ repr) { implicit con => + SQL("SELECT v").as(scalar[LowercaseDummy].single) mustEqual expected + } + + spec("apple", LowercaseDummy.Apple) + spec("banana", LowercaseDummy.Banana) + spec("cherry", LowercaseDummy.Cherry) + } + + "not be parsed as Column from invalid String representation" when { + def spec(title: String, repr: String) = + title in withQueryResult(RowLists.stringList :+ repr) { implicit con => + try { + SQL("SELECT v").as(scalar[LowercaseDummy].single) + fail(s"Must not be successful: $repr") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: $repr").message) + } + } + + spec("Apple (!= apple as lowercase)", "Apple") + spec("BANANA (!= banana as lowercase)", "BANANA") + spec("Cherry (!= cherry as lowercase)", "Cherry") + } + + "not be parsed as Column from non-String values" when { + def spec(tpe: String, rowList: RowList[_]) = + tpe in withQueryResult(rowList) { implicit con => + try { + SQL("SELECT v").as(scalar[LowercaseDummy].single) + fail(s"Must not be successful: $tpe") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException( + TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message) + } + } + + spec("float", RowLists.floatList :+ 0.1F) + spec("int", RowLists.intList :+ 1) + } + } + + "Uppercase enum" should { + "successfully parsed as Column" when { + def spec[T <: UppercaseDummy](repr: String, expected: T) = + repr in withQueryResult(RowLists.stringList :+ repr) { implicit con => + SQL("SELECT v").as(scalar[UppercaseDummy].single) mustEqual expected + } + + spec("APPLE", UppercaseDummy.Apple) + spec("BANANA", UppercaseDummy.Banana) + spec("CHERRY", UppercaseDummy.Cherry) + } + + "not be parsed as Column from invalid String representation" when { + def spec(title: String, repr: String) = + title in withQueryResult(RowLists.stringList :+ repr) { implicit con => + try { + SQL("SELECT v").as(scalar[UppercaseDummy].single) + fail(s"Must not be successful: $repr") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: $repr").message) + } + } + + spec("Apple (!= APPLE as uppercase)", "Apple") + spec("banana (!= BANANA as uppercase)", "banana") + spec("cherry (!= CHERRY as uppercase)", "Cherry") + } + + "not be parsed as Column from non-String values" when { + def spec(tpe: String, rowList: RowList[_]) = + tpe in withQueryResult(rowList) { implicit con => + try { + SQL("SELECT v").as(scalar[UppercaseDummy].single) + fail(s"Must not be successful: $tpe") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException( + TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message) + } + } + + spec("float", RowLists.floatList :+ 0.1F) + spec("int", RowLists.intList :+ 1) + } + } +} diff --git a/enumeratum-anorm/src/test/scala/AnormToStatementSpec.scala b/enumeratum-anorm/src/test/scala/AnormToStatementSpec.scala new file mode 100644 index 00000000..9b172c38 --- /dev/null +++ b/enumeratum-anorm/src/test/scala/AnormToStatementSpec.scala @@ -0,0 +1,102 @@ +package enumeratum + +import scala.util.control.NonFatal + +import anorm._ + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import acolyte.jdbc.{DefinedParameter => DParam, ParameterMetaData => ParamMeta, UpdateExecution} +import acolyte.jdbc.AcolyteDSL.{connection, handleStatement} +import acolyte.jdbc.Implicits._ + +final class AnormToStatementSpec extends AnyWordSpec with Matchers { + "Sensitive enum" should { + "successfully passed as parameter" when { + def spec(value: Dummy, repr: String) = + repr in withConnection(repr) { implicit c => + SQL"set-str ${value}" match { + case q @ SimpleSql(_, _, _, _) => + // execute = false: update ok but returns no resultset + // see java.sql.PreparedStatement#execute + q.execute() mustEqual false + } + } + + spec(Dummy.A, "A") + spec(Dummy.B, "B") + spec(Dummy.c, "c") + } + } + + "Insensitive enum" should { + "successfully passed as parameter" when { + def spec(value: InsensitiveDummy, repr: String) = + repr in withConnection(repr) { implicit c => + SQL"set-str ${value}" match { + case q @ SimpleSql(_, _, _, _) => + // execute = false: update ok but returns no resultset + // see java.sql.PreparedStatement#execute + q.execute() mustEqual false + } + } + + spec(InsensitiveDummy.A, "A") + spec(InsensitiveDummy.B, "B") + spec(InsensitiveDummy.c, "c") + } + } + + "Lowercase enum" should { + "successfully passed as parameter" when { + def spec(value: LowercaseDummy, repr: String) = + repr in withConnection(repr) { implicit c => + SQL"set-str ${value}" match { + case q @ SimpleSql(_, _, _, _) => + // execute = false: update ok but returns no resultset + // see java.sql.PreparedStatement#execute + q.execute() mustEqual false + } + } + + spec(LowercaseDummy.Apple, "apple") + spec(LowercaseDummy.Banana, "banana") + spec(LowercaseDummy.Cherry, "cherry") + } + } + + "Uppercase enum" should { + "successfully passed as parameter" when { + def spec(value: UppercaseDummy, repr: String) = + repr in withConnection(repr) { implicit c => + SQL"set-str ${value}" match { + case q @ SimpleSql(_, _, _, _) => + // execute = false: update ok but returns no resultset + // see java.sql.PreparedStatement#execute + q.execute() mustEqual false + } + } + + spec(UppercaseDummy.Apple, "APPLE") + spec(UppercaseDummy.Banana, "BANANA") + spec(UppercaseDummy.Cherry, "CHERRY") + } + } + + // --- + + private val SqlStr = ParamMeta.Str + + private def withConnection[A](repr: String)(f: java.sql.Connection => A): A = + f( + connection( + handleStatement withUpdateHandler { + case UpdateExecution("set-str ?", DParam(`repr`, SqlStr) :: Nil) => 1 /* case ok */ + + case _ => + throw new Exception("Unexpected execution") + + } + )) +} diff --git a/enumeratum-anorm/src/test/scala/Dummy.scala b/enumeratum-anorm/src/test/scala/Dummy.scala new file mode 100644 index 00000000..977018a6 --- /dev/null +++ b/enumeratum-anorm/src/test/scala/Dummy.scala @@ -0,0 +1,39 @@ +package enumeratum + +/** + * Created by Lloyd on 2/4/15. + */ +sealed trait Dummy extends EnumEntry +object Dummy extends Enum[Dummy] with AnormEnum[Dummy] { + case object A extends Dummy + case object B extends Dummy + case object c extends Dummy + val values = findValues +} + +/** + * Created by dbuschman on 03/20/2018 + */ +sealed trait InsensitiveDummy extends EnumEntry +object InsensitiveDummy extends Enum[InsensitiveDummy] with AnormInsensitiveEnum[InsensitiveDummy] { + case object A extends InsensitiveDummy + case object B extends InsensitiveDummy + case object c extends InsensitiveDummy + val values = findValues +} + +sealed trait LowercaseDummy extends EnumEntry +object LowercaseDummy extends Enum[LowercaseDummy] with AnormLowercaseEnum[LowercaseDummy] { + case object Apple extends LowercaseDummy + case object Banana extends LowercaseDummy + case object Cherry extends LowercaseDummy + val values = findValues +} + +sealed trait UppercaseDummy extends EnumEntry +object UppercaseDummy extends Enum[UppercaseDummy] with AnormUppercaseEnum[UppercaseDummy] { + case object Apple extends UppercaseDummy + case object Banana extends UppercaseDummy + case object Cherry extends UppercaseDummy + val values = findValues +} diff --git a/enumeratum-anorm/src/test/scala/values/AnormColumnSpec.scala b/enumeratum-anorm/src/test/scala/values/AnormColumnSpec.scala new file mode 100644 index 00000000..f66ae771 --- /dev/null +++ b/enumeratum-anorm/src/test/scala/values/AnormColumnSpec.scala @@ -0,0 +1,52 @@ +package enumeratum.values + +import scala.util.control.NonFatal + +import anorm.{AnormException, SQL, SqlParser, TypeDoesNotMatch}, SqlParser.scalar + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import acolyte.jdbc.AcolyteDSL.withQueryResult +import acolyte.jdbc.Implicits._ +import acolyte.jdbc.{RowList, RowLists} + +final class AnormColumnSpec extends AnyWordSpec with Matchers { + "ValueEnum" should { + "successfully parsed as Column" when { + def spec[T <: Drink](repr: Short, expected: T) = + repr.toString in withQueryResult(RowLists.shortList :+ repr) { implicit con => + SQL("SELECT v").as(scalar[Drink].single) mustEqual expected + } + + spec(1, Drink.OrangeJuice) + spec(2, Drink.AppleJuice) + spec(3, Drink.Cola) + spec(4, Drink.Beer) + } + + "not be parsed as Column from invalid Short representation" in { + withQueryResult(RowLists.shortList :+ 0.toShort) { implicit con => + try { + SQL("SELECT v").as(scalar[Drink].single) + fail("Must not be successful") + } catch { + case NonFatal(cause) => + cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: 0").message) + } + } + } + + "not be parsed as Column from non-Short values" when { + def spec(tpe: String, rowList: RowList[_]) = + tpe in withQueryResult(rowList) { implicit con => + assertThrows[AnormException] { + SQL("SELECT v").as(scalar[Drink].single) + } + } + + spec("float", RowLists.floatList :+ 0.12F) + spec("String", RowLists.stringList :+ "foo") + } + } +} diff --git a/enumeratum-anorm/src/test/scala/values/AnormToStatementSpec.scala b/enumeratum-anorm/src/test/scala/values/AnormToStatementSpec.scala new file mode 100644 index 00000000..8394c1b7 --- /dev/null +++ b/enumeratum-anorm/src/test/scala/values/AnormToStatementSpec.scala @@ -0,0 +1,49 @@ +package enumeratum.values + +import scala.util.control.NonFatal + +import anorm._ + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import acolyte.jdbc.{DefinedParameter => DParam, ParameterMetaData => ParamMeta, UpdateExecution} +import acolyte.jdbc.AcolyteDSL.{connection, handleStatement} +import acolyte.jdbc.Implicits._ + +final class AnormToStatementSpec extends AnyWordSpec with Matchers { + "Sensitive enum" should { + "successfully passed as parameter" when { + def spec(value: Drink, repr: Short) = + value.toString in withConnection(repr) { implicit c => + SQL"set-short ${value}" match { + case q @ SimpleSql(_, _, _, _) => + // execute = false: update ok but returns no resultset + // see java.sql.PreparedStatement#execute + q.execute() mustEqual false + } + } + + spec(Drink.OrangeJuice, 1) + spec(Drink.AppleJuice, 2) + spec(Drink.Cola, 3) + spec(Drink.Beer, 4) + } + } + + // --- + + private val SqlShort = ParamMeta.Short + + private def withConnection[A](repr: Short)(f: java.sql.Connection => A): A = + f( + connection( + handleStatement withUpdateHandler { + case UpdateExecution("set-short ?", DParam(`repr`, SqlShort) :: Nil) => 1 /* case ok */ + + case _ => + throw new Exception("Unexpected execution") + + } + )) +} diff --git a/enumeratum-anorm/src/test/scala/values/Drink.scala b/enumeratum-anorm/src/test/scala/values/Drink.scala new file mode 100644 index 00000000..318f0ad6 --- /dev/null +++ b/enumeratum-anorm/src/test/scala/values/Drink.scala @@ -0,0 +1,12 @@ +package enumeratum.values + +sealed abstract class Drink(val value: Short, name: String) extends ShortEnumEntry + +case object Drink extends ShortEnum[Drink] with ShortAnormValueEnum[Drink] { + case object OrangeJuice extends Drink(value = 1, name = "oj") + case object AppleJuice extends Drink(value = 2, name = "aj") + case object Cola extends Drink(value = 3, name = "cola") + case object Beer extends Drink(value = 4, name = "beer") + + val values = findValues +} diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index 524b2958..02c4b284 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -13,11 +13,13 @@ object EnumFormats { * @param enum The enum * @param insensitive bind in a case-insensitive way, defaults to false */ - def reads[A <: EnumEntry](enum: Enum[A], insensitive: Boolean = false): Reads[A] = - readsAndExtracts[A](enum) { s => - if (insensitive) enum.withNameInsensitiveOption(s) - else enum.withNameOption(s) + def reads[A <: EnumEntry](enum: Enum[A], insensitive: Boolean = false): Reads[A] = { + if (insensitive) { + readsAndExtracts[A](enum)(enum.withNameInsensitiveOption) + } else { + readsAndExtracts[A](enum)(enum.withNameOption) } + } def readsLowercaseOnly[A <: EnumEntry](enum: Enum[A]): Reads[A] = readsAndExtracts[A](enum)(enum.withNameLowercaseOnlyOption) diff --git a/enumeratum-play-json/src/main/scala/enumeratum/values/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/values/EnumFormats.scala index 9e4a6d67..f1aeff4d 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/values/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/values/EnumFormats.scala @@ -16,17 +16,14 @@ object EnumFormats { enum: ValueEnum[ValueType, EntryType] )( implicit baseReads: Reads[ValueType] - ): Reads[EntryType] = - new Reads[EntryType] { - def reads(json: JsValue): JsResult[EntryType] = - baseReads.reads(json).flatMap { s => - val maybeBound = enum.withValueOpt(s) - maybeBound match { - case Some(obj) => JsSuccess(obj) - case None => JsError("error.expected.validenumvalue") - } - } + ): Reads[EntryType] = Reads[EntryType] { json => + baseReads.reads(json).flatMap { s => + enum.withValueOpt(s) match { + case Some(obj) => JsSuccess(obj) + case None => JsError("error.expected.validenumvalue") + } } + } /** * Returns a Writes for the provided ValueEnum based on the given base Writes for the Enum's value type @@ -35,19 +32,17 @@ object EnumFormats { enum: ValueEnum[ValueType, EntryType] )( implicit baseWrites: Writes[ValueType] - ): Writes[EntryType] = - new Writes[EntryType] { - def writes(o: EntryType): JsValue = baseWrites.writes(o.value) - } + ): Writes[EntryType] = Writes[EntryType] { o => + baseWrites.writes(o.value) + } /** * Returns a Formats for the provided ValueEnum based on the given base Reads and Writes for the Enum's value type */ def formats[ValueType, EntryType <: ValueEnumEntry[ValueType]]( enum: ValueEnum[ValueType, EntryType] - )(implicit baseReads: Reads[ValueType], baseWrites: Writes[ValueType]): Format[EntryType] = { + )(implicit baseReads: Reads[ValueType], baseWrites: Writes[ValueType]): Format[EntryType] = Format(reads(enum), writes(enum)) - } /** * Format for Char @@ -60,5 +55,4 @@ object EnumFormats { case _ => JsError("error.expected.singleChar") } } - }