From a1d2263434c16750f94268ff3bd1af90536ddde3 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sat, 16 Apr 2016 02:18:18 +0900 Subject: [PATCH] Feature/value enums (#29) * Remove most usage of deprecated stuff in macros (enclosingClass is going to be tough) * Add ValueEnum macro and ValueEnum traits for 2.11 in separate directories * Bump scoverage and coveralls * Improve build definition * Improve Readme * Add Maven version badge. YAY * Simplify intro * Add Circe integration * Update Travis to run coverage on more projects * Fix names * Bump Play to 2.5 for Scala 2.11.x and stay at 2.4 for 2.10.x --- .travis.yml | 6 +- README.md | 380 +++++++++++++++--- .../scala-2.11/enumeratum/values/Circe.scala | 39 ++ .../enumeratum/values/CirceValueEnum.scala | 47 +++ .../values/CirceValueEnumSpec.scala | 112 ++++++ .../src/main/scala/enumeratum/Circe.scala | 37 ++ .../src/main/scala/enumeratum/CirceEnum.scala | 25 ++ .../src/test/scala/enumeratum/CirceSpec.scala | 40 ++ .../src/test/scala/enumeratum/ShirtSize.scala | 19 + .../test/scala/enumeratum/EnumJVMSpec.scala | 28 +- .../src/test/scala/enumeratum/Eval.scala | 5 +- .../enumeratum/values/ValueEnum.scala | 95 +++++ .../enumeratum/values/ValueEnumEntry.scala | 31 ++ .../enumeratum/values/ContentType.scala | 21 + .../scala-2.11/enumeratum/values/Drinks.scala | 21 + .../enumeratum/values/LibraryItem.scala | 26 ++ .../enumeratum/values/MovieGenre.scala | 24 ++ .../enumeratum/values/ValueEnumHelpers.scala | 54 +++ .../enumeratum/values/ValueEnumSpec.scala | 114 ++++++ .../src/main/scala/enumeratum/Enum.scala | 60 +-- .../src/test/scala/enumeratum/EnumSpec.scala | 2 +- .../enumeratum/values/EnumFormats.scala | 39 ++ .../enumeratum/values/PlayJsonValueEnum.scala | 39 ++ .../enumeratum/values/EnumFormatsSpec.scala | 37 ++ .../values/EnumJsonFormatHelpers.scala | 48 +++ .../enumeratum/values/JsonDrinks.scala | 20 + .../scala/enumeratum/PlayJsonEnumSpec.scala | 2 +- .../scala-2.11/enumeratum/values/Forms.scala | 34 ++ .../enumeratum/values/PlayFormValueEnum.scala | 47 +++ .../values/PlayPathBindableValueEnum.scala | 57 +++ .../values/PlayQueryBindableValueEnum.scala | 39 ++ .../enumeratum/values/PlayValueEnums.scala | 61 +++ .../enumeratum/values/UrlBinders.scala | 44 ++ .../values/PlayValueEnumHelpers.scala | 169 ++++++++ .../enumeratum/values/PlayValueEnumSpec.scala | 76 ++++ .../scala/enumeratum/PlayFormFieldEnum.scala | 4 + .../enumeratum/PlayPathBindableEnum.scala | 36 +- .../main/scala/enumeratum/UrlBinders.scala | 4 +- .../test/scala/enumeratum/PlayEnumSpec.scala | 7 +- .../enumeratum/values/UPickleValueEnum.scala | 41 ++ .../enumeratum/values/UPickler.scala | 33 ++ .../enumeratum/values/UPicklerSpec.scala | 118 ++++++ .../enumeratum/EnrichedPartialFunction.scala | 18 + .../main/scala/enumeratum/UPickleEnum.scala | 6 +- .../src/main/scala/enumeratum/UPickler.scala | 53 +-- .../src/test/scala/enumeratum/Dummy.scala | 4 +- .../test/scala/enumeratum/UPickleSpec.scala | 4 +- .../scala-2.10/enumeratum/ContextUtils.scala | 14 + .../scala-2.11/enumeratum/ContextUtils.scala | 14 + .../enumeratum/ValueEnumMacros.scala | 149 +++++++ .../main/scala/enumeratum/EnumMacros.scala | 88 ++-- project/Build.scala | 88 +++- project/build.properties | 2 +- project/plugins.sbt | 19 +- 54 files changed, 2367 insertions(+), 233 deletions(-) create mode 100644 enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/Circe.scala create mode 100644 enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/CirceValueEnum.scala create mode 100644 enumeratum-circe/compat/src/test/scala-2.11/enumeratum/values/CirceValueEnumSpec.scala create mode 100644 enumeratum-circe/src/main/scala/enumeratum/Circe.scala create mode 100644 enumeratum-circe/src/main/scala/enumeratum/CirceEnum.scala create mode 100644 enumeratum-circe/src/test/scala/enumeratum/CirceSpec.scala create mode 100644 enumeratum-circe/src/test/scala/enumeratum/ShirtSize.scala create mode 100644 enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnum.scala create mode 100644 enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnumEntry.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ContentType.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/Drinks.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/LibraryItem.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/MovieGenre.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumHelpers.scala create mode 100644 enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumSpec.scala create mode 100644 enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/EnumFormats.scala create mode 100644 enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/PlayJsonValueEnum.scala create mode 100644 enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumFormatsSpec.scala create mode 100644 enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumJsonFormatHelpers.scala create mode 100644 enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/JsonDrinks.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/Forms.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayFormValueEnum.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayPathBindableValueEnum.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayQueryBindableValueEnum.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayValueEnums.scala create mode 100644 enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/UrlBinders.scala create mode 100644 enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumHelpers.scala create mode 100644 enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumSpec.scala create mode 100644 enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickleValueEnum.scala create mode 100644 enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickler.scala create mode 100644 enumeratum-upickle/compat/src/test/scala-2.11/enumeratum/values/UPicklerSpec.scala create mode 100644 enumeratum-upickle/src/main/scala/enumeratum/EnrichedPartialFunction.scala create mode 100644 macros/compat/src/main/scala-2.10/enumeratum/ContextUtils.scala create mode 100644 macros/compat/src/main/scala-2.11/enumeratum/ContextUtils.scala create mode 100644 macros/compat/src/main/scala-2.11/enumeratum/ValueEnumMacros.scala diff --git a/.travis.yml b/.travis.yml index 8cf37981..d96cdfb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,12 @@ language: scala jdk: - oraclejdk8 scala: - - 2.11.7 + - 2.11.8 script: - sbt clean +test:compile - sbt +test # Manually select the JVM projects to run coverage on until Scoverage is compatible with ScalaJS - - sbt coverage coreJVM/test enumeratum-play/test enumeratum-play-json/test + - sbt coverage coreJVM/test enumeratum-play/test enumeratum-play-json/test enumeratumCirceJVM/test enumeratumUPickleJVM/test after_success: - sbt coverageReport coverageAggregate coveralls cache: @@ -22,4 +22,4 @@ before_cache: - du -h -d 1 $HOME/.ivy2/ - du -h -d 2 $HOME/.sbt/ - find $HOME/.sbt -name "*.lock" -type f -delete - - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete \ No newline at end of file + - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete -print \ No newline at end of file diff --git a/README.md b/README.md index 3c911177..b8c61581 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,70 @@ -# Enumeratum [![Build Status](https://travis-ci.org/lloydmeta/enumeratum.svg?branch=master)](https://travis-ci.org/lloydmeta/enumeratum) [![Coverage Status](https://coveralls.io/repos/lloydmeta/enumeratum/badge.svg?branch=master)](https://coveralls.io/r/lloydmeta/enumeratum?branch=master) [![Codacy Badge](https://www.codacy.com/project/badge/a71a20d8678f4ed3a5b74b0659c1bc4c)](https://www.codacy.com/public/lloydmeta/enumeratum) +# Enumeratum [![Build Status](https://travis-ci.org/lloydmeta/enumeratum.svg?branch=master)](https://travis-ci.org/lloydmeta/enumeratum) [![Coverage Status](https://coveralls.io/repos/lloydmeta/enumeratum/badge.svg?branch=master)](https://coveralls.io/r/lloydmeta/enumeratum?branch=master) [![Codacy Badge](https://www.codacy.com/project/badge/a71a20d8678f4ed3a5b74b0659c1bc4c)](https://www.codacy.com/public/lloydmeta/enumeratum) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum_2.11) -A type-safe and powerful enumeration implementation for Scala with exhaustive pattern match warnings, Enumeratum is -an implementation based on a single Scala macro that searches for implementations of a sealed trait or class. - -Enumeratum aims to be similar enough to Scala's built in `Enumeration` to be easy-to-use and understand while offering -more flexibility, safety, and power. It also has **zero** dependencies, which means it's light-weight, but more importantly, -won't clutter your (or your dependants') namespace. - -Using Enumeratum allows you to use your own `sealed` traits/classes without having to maintain your own collection of -values, which not only means you get exhaustive pattern match warnings, but also richer enum values, and methods that -can take your enum values as arguments without having to worry about erasure (for more info, see [this blog post on Scala's -`Enumeration`](http://underscore.io/blog/posts/2014/09/03/enumerations.html)) +Enumeratum is a type-safe and powerful enumeration implementation for Scala that offers exhaustive pattern match warnings, +integrations with popular Scala libraries, and idiomatic usage that won't break your IDE. It aims to be similar enough +to Scala's built in `Enumeration` to be easy-to-use and understand while offering more flexibility, type-safety (see [this blog +post describing erasure on Scala's `Enumeration`](http://underscore.io/blog/posts/2014/09/03/enumerations.html)), and +richer enum values without having to maintain your own collection of values. Enumeratum has the following niceties: - Zero dependencies -- Simplicity; most of the complexity in this lib is in the macro, and the macro is fairly simple conceptually -- As idiomatic as possible: you're very clearly still writing Scala, and no funny colours in your IDE means less cognitive overhead for your team -- No usage of `synchronized`, which may help with performance and deadlocks prevention +- Allows your Enum members to be full-fledged normal objects with methods, values, inheritance, etc. +- Idiomatic: you're very clearly still writing Scala, and no funny colours in your IDE means less cognitive overhead for your team +- Simplicity; most of the complexity in this lib is in its macro, and the macro is fairly simple conceptually - No usage of reflection at run time. This may also help with performance but it means Enumeratum is compatible with ScalaJS and other - environments where reflection is a best effort. + environments where reflection is a best effort (such as Android) +- No usage of `synchronized`, which may help with performance and deadlocks prevention - All magic happens at compile-time so you know right away when things go awry +Compatible with Scala 2.11+ and 2.10 as well as ScalaJS. -Compatible with Scala 2.10.x and 2.11.x +Integrations are available for: -[Scaladocs](https://beachape.com/enumeratum/latest/api) +- [Play](https://www.playframework.com/): JVM only +- [Play JSON](https://www.playframework.com/documentation/2.5.x/ScalaJson): JVM only (included in Play integration but also available separately) +- [Circe](https://github.com/travisbrown/circe): JVM and ScalaJS +- [UPickle](http://www.lihaoyi.com/upickle-pprint/upickle/): JVM and ScalaJS -## SBT / Installation basics +### Table of Contents -Set the Enumeratum version in a variable (for the latest version, use `val enumeratumVersion = "1.3.7"`). +1. [Quick start](#quick-start) + 1. [SBT](#sbt) + 2. [Usage](#usage) +2. [More examples](#more-examples) + 1. [Enum](#enum) + 1. [Mixins](#mixins) + 2. [ValueEnum](#valueenum) +2. [ScalaJS](#scalajs) +3. [Play integration](#play-integration) +4. [Play JSON integration](#play-json) +5. [Circe integration](#circe) +6. [UPickle integration](#upickle) +7. [Scala 2.10](#scala-210) +8. [Licence](#licence) -For basic enumeratum (with no Play support): -```scala -libraryDependencies ++= Seq( - "com.beachape" %% "enumeratum" % enumeratumVersion -) -``` -For enumeratum with full Play support: -```scala -libraryDependencies ++= Seq( - "com.beachape" %% "enumeratum" % enumeratumVersion, - "com.beachape" %% "enumeratum-play" % enumeratumVersion -) -``` +## Quick start -#### ScalaJs +### SBT -In a ScalaJS project, add the following: +In `build.sbt`, set the Enumeratum version in a variable (for the latest version, set `val enumeratumVersion = ` the version you +in the Maven badge above). ```scala libraryDependencies ++= Seq( - "com.beachape" %%% "enumeratum" % enumeratumVersion + "com.beachape" %% "enumeratum" % enumeratumVersion ) ``` +Enumeratum has different integrations that can be added to your build a la cart. For more info, see the respective secions in +[the Table of Contents](#table-of-contents) -There are other ways to use Enumeratum (e.g. a la carte), [see here](#sbt-a-la-carte). - -## How-to + example - -Using Enumeratum is simple. Simply declare your own sealed trait or class `A`, and implement it as case objects inside -an object that extends from `Enum[A]` as follows. +### Usage -Note that by default, `findValues` will return a `Seq` with the enum members listed in written-order (relevant if you want to -use the `indexOf` method). +Using Enumeratum is simple. Just declare your own sealed trait or class `A` that extends `EnumEntry` and implement it as case objects inside +an object that extends from `Enum[A]` as shown below. ```scala @@ -76,6 +74,11 @@ sealed trait Greeting extends EnumEntry object Greeting extends Enum[Greeting] { + /* + `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum` + + You use it to implement the `val values` member + */ val values = findValues case object Hello extends Greeting @@ -93,6 +96,19 @@ Greeting.withName("Hello") Greeting.withName("Haro") // => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye) +``` + +Note that by default, `findValues` will return a `Seq` with the enum members listed in written-order (relevant if you want to +use the `indexOf` method). + + +## More examples + +### Enum + +Continuing from the enum declared in [the quick-start section](#usage): + +```scala import Greeting._ def tryMatching(v: Greeting): Unit = v match { @@ -140,6 +156,8 @@ State.withName("AL") ``` +#### Mixins + The second is to mixin the stackable traits provided for common string conversions, `Snakecase`, `Uppercase`, and `Lowercase`. @@ -166,15 +184,81 @@ Greeting.withName("SHOUT_GOOD_BYE") ``` +### ValueEnum + +Asides from enumerations that resolve members from `String` names, Enumeratum also supports `ValueEnum`s, enums that resolve +members from various primitive types like `Int`, `Long` and`Short`. In order to ensure at compile-time that multiple members +do not share the same value, these enums are powered by a separate macro and exposed through a different set of traits. + +```scala +import enumeratum.values._ + +sealed abstract class LibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object LibraryItem extends IntEnum[LibraryItem] { + + case object Book extends LibraryItem(value = 1, name = "book") + case object Movie extends LibraryItem(name = "movie", value = 2) + case object Magazine extends LibraryItem(3, "magazine") + case object CD extends LibraryItem(4, name = "cd") + // case object Newspaper extends LibraryItem(4, name = "cd") <-- will fail to compile because the value 4 is shared + + /* + val five = 5 + case object Article extends LibraryItem(five, name = "five") <-- will fail to compile because the value is not a literal + */ + + val values = findValues + +} + +assert(LibraryItem.withValue(1) == LibraryItem.Book) + +LibraryItem.withValue(10) // => java.util.NoSuchElementException: +``` + +** Restrictions ** +- `ValueEnum`s must have their value members implemented as literal values. +- The macro behind this enum does not work within the REPL, but works in normally compiled code. +- `ValueEnums` are not available for Scala 2.10 projects. + + +## ScalaJS + +In a ScalaJS project, add the following to `build.sbt`: + +```scala +libraryDependencies ++= Seq( + "com.beachape" %%% "enumeratum" % enumeratumVersion +) +``` + +As expected, usage is exactly the same as normal Scala. -### Play 2 +## Play Integration The `enumeratum-play` project is published separately and gives you access to various tools to help you avoid boilerplate in your Play project. +### SBT + +For enumeratum with full Play support: +```scala +libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum" % enumeratumVersion, + "com.beachape" %% "enumeratum-play" % enumeratumVersion +) +``` + +Note that as of version 1.4.0, `enumeratum-play` for Scala 2.11 is compatible with Play 2.5+ while 2.10 is compatible with +Play 2.4.x. Versions prior to 1.4.0 are compatible with 2.4.x. + +### Usage + +#### PlayEnum + The included `PlayEnum` trait is probably going to be the most interesting as it includes a bunch -of built-in implicits like Json formats, Path bindables, Query string bindables, -and form field support. +of built-in implicits like Json formats, Path bindables, Query string bindables, and Form field support. For example: @@ -216,16 +300,60 @@ Router.from { } ``` -### Play-JSON + +#### PlayValueEnums + +There are `IntPlayEnum`, `LongPlayEnum`, and `ShortPlayEnum` traits for use with `IntEnumEntry`, `LongEnumEntry`, and +`ShortEnumEntry` respectively that provide Play-specific implicits as with normal `PlayEnum`. For example: + +```scala +import enumeratum.values._ + +sealed abstract class PlayLibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object PlayLibraryItem extends IntPlayEnum[PlayLibraryItem] { + + // A good mix of named, unnamed, named + unordered args + case object Book extends PlayLibraryItem(value = 1, name = "book") + case object Movie extends PlayLibraryItem(name = "movie", value = 2) + case object Magazine extends PlayLibraryItem(3, "magazine") + case object CD extends PlayLibraryItem(4, name = "cd") + + val values = findValues + +} + +import play.api.libs.json.{ JsNumber, JsString, Json => PlayJson } +PlayLibraryItem.values.foreach { item => + assert(PlayJson.toJson(item) == JsNumber(item.value)) +} +``` + + +## Play JSON The `enumeratum-play-json` project is published separately and gives you access to Play's auto-generated boilerplate for JSON serialization in your Enum's. -For example: +### SBT ```scala -package enums._ +libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum" % enumeratumVersion, + "com.beachape" %% "enumeratum-play-json" % enumeratumVersion +) +``` + +Note that as of version 1.4.0, `enumeratum-play` for Scala 2.11 is compatible with Play 2.5+ while 2.10 is compatible with +Play 2.4.x. Versions prior to 1.4.0 are compatible with 2.4.x. + +### Usage + +#### PlayJsonEnum +For example: + +```scala import enumeratum.{ PlayJsonEnum, Enum, EnumEntry } sealed trait Greeting extends EnumEntry @@ -240,45 +368,131 @@ object Greeting extends Enum[Greeting] with PlayJsonEnum[Greeting] { case object Bye extends Greeting } + ``` -## SBT a la carte +#### PlayJsonValueEnum -If you just want to use the macro that finds implementations of a sealed trait within an enclosed -tree: +There are `IntPlayJsonEnum`, `LongPlayJsonEnum`, and `ShortPlayJsonEnum` traits for use with `IntEnumEntry`, `LongEnumEntry`, and +`ShortEnumEntry` respectively. For example: ```scala -libraryDependencies ++= Seq("com.beachape" %% "enumeratum-macros" % enumeratumVersion) +import enumeratum.values._ + +sealed abstract class JsonDrinks(val value: Short, name: String) extends ShortEnumEntry + +case object JsonDrinks extends ShortEnum[JsonDrinks] with ShortPlayJsonValueEnum[JsonDrinks] { + + case object OrangeJuice extends JsonDrinks(value = 1, name = "oj") + case object AppleJuice extends JsonDrinks(value = 2, name = "aj") + case object Cola extends JsonDrinks(value = 3, name = "cola") + case object Beer extends JsonDrinks(value = 4, name = "beer") + + val values = findValues + +} + +import play.api.libs.json.{ JsNumber, JsString, Json => PlayJson, JsSuccess } + +// Use to deserialise numbers to enum members directly +JsonDrinks.values.foreach { drink => + assert(PlayJson.toJson(drink) == JsNumber(drink.value)) +} +assert(PlayJson.fromJson[JsonDrinks](JsNumber(3)) == JsSuccess(JsonDrinks.Cola)) +assert(PlayJson.fromJson[JsonDrinks](JsNumber(19)).isError) ``` -For enumeratum with [uPickle](http://lihaoyi.github.io/upickle/): +## Circe + +### SBT + +To use enumeratum with [Circe](https://github.com/travisbrown/circe): ```scala libraryDependencies ++= Seq( "com.beachape" %% "enumeratum" % enumeratumVersion, - "com.beachape" %% "enumeratum-upickle" % enumeratumVersion + "com.beachape" %% "enumeratum-circe" % enumeratumVersion ) ``` -For enumeratum with Play JSON: +To use with ScalaJS: + ```scala libraryDependencies ++= Seq( - "com.beachape" %% "enumeratum" % enumeratumVersion, - "com.beachape" %% "enumeratum-play-json" % enumeratumVersion + "com.beachape" %%% "enumeratum" % enumeratumVersion, + "com.beachape" %%% "enumeratum-circe" % enumeratumVersion ) ``` -### ScalaJs +### Usage -There is support for ScalaJs, though only for the core lib and the UPickle helper lib. +#### Enum + +```scala +import enumeratum._ + +sealed trait ShirtSize extends EnumEntry + +case object ShirtSize extends CirceEnum[ShirtSize] with Enum[ShirtSize] { + + case object Small extends ShirtSize + case object Medium extends ShirtSize + case object Large extends ShirtSize + + val values = findValues + +} + +import io.circe.Json +import io.circe.syntax._ + +ShirtSize.values.foreach { size => + assert(size.asJson == Json.fromString(size.entryName)) +} + +``` + +#### ValueEnum + +```scala +import enumeratum.values._ + +sealed abstract class CirceLibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object CirceLibraryItem extends IntEnum[CirceLibraryItem] with IntCirceEnum[CirceLibraryItem] { + + // A good mix of named, unnamed, named + unordered args + case object Book extends CirceLibraryItem(value = 1, name = "book") + case object Movie extends CirceLibraryItem(name = "movie", value = 2) + case object Magazine extends CirceLibraryItem(3, "magazine") + case object CD extends CirceLibraryItem(4, name = "cd") + + val values = findValues + +} + +import io.circe.Json +import io.circe.syntax._ + +CirceLibraryItem.values.foreach { item => + assert(item.asJson == Json.fromInt(item.value)) +} +``` + +## UPickle + +### SBT + +To use enumeratum with [uPickle](http://lihaoyi.github.io/upickle/): ```scala libraryDependencies ++= Seq( - "com.beachape" %%% "enumeratum" % enumeratumVersion + "com.beachape" %% "enumeratum" % enumeratumVersion, + "com.beachape" %% "enumeratum-upickle" % enumeratumVersion ) ``` -To use with uPickle: +To use with ScalaJS: ```scala libraryDependencies ++= Seq( @@ -287,11 +501,49 @@ libraryDependencies ++= Seq( ) ``` +### Usage + +`CirceEnum` works pretty much the same as `CirceEnum` and `PlayJsonEnum` variants, so we'll skip straight to the +`ValueEnum` integration. + +```scala +import enumeratum.values._ + +sealed abstract class ContentType(val value: Long, name: String) extends LongEnumEntry + +case object ContentType + extends LongEnum[ContentType] + with LongUPickleEnum[ContentType] { + + val values = findValues + + case object Text extends ContentType(value = 1L, name = "text") + case object Image extends ContentType(value = 2L, name = "image") + case object Video extends ContentType(value = 3L, name = "video") + case object Audio extends ContentType(value = 4L, name = "audio") + +} + +import upickle.default.{ readJs, writeJs, Reader, Writer } +enum.values.foreach { entry => + val written = writeJs(entry) + assert(readJs(written) == entry) +} + +``` + +## Scala 2.10 + +Scala's Macro API is experimental and has changed quite a bit between 2.10 and 2.11, so some features of Enumeratum are +not available in 2.10 (though PRs making them available are welcome): + +- [Value Enums](#valueenum): The `.tpe` of constructor functions are not resolved yet during the macro phase, so we can't resolve `value` arguments + ## Licence The MIT License (MIT) -Copyright (c) 2015 by Lloyd Chan +Copyright (c) 2016 by Lloyd Chan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/Circe.scala b/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/Circe.scala new file mode 100644 index 00000000..3ef81158 --- /dev/null +++ b/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/Circe.scala @@ -0,0 +1,39 @@ +package enumeratum.values + +import cats.data.Xor +import io.circe.Decoder.Result +import io.circe._ + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +object Circe { + + /** + * Returns an Encoder for the provided ValueEnum + */ + def encoder[ValueType <: AnyVal: Encoder, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType]): Encoder[EntryType] = { + new Encoder[EntryType] { + private val valueEncoder = implicitly[Encoder[ValueType]] + def apply(a: EntryType): Json = valueEncoder.apply(a.value) + } + } + + /** + * Returns a Decoder for the provided ValueEnum + */ + def decoder[ValueType <: AnyVal: Decoder, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType]): Decoder[EntryType] = { + new Decoder[EntryType] { + private val valueDecoder = implicitly[Decoder[ValueType]] + def apply(c: HCursor): Result[EntryType] = valueDecoder.apply(c).flatMap { v => + val maybeBound: Option[EntryType] = enum.withValueOpt(v) + maybeBound match { + case Some(member) => Xor.Right(member) + case _ => Xor.Left(DecodingFailure(s"$v is not a member of enum $enum", c.history)) + } + } + } + } +} diff --git a/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/CirceValueEnum.scala b/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/CirceValueEnum.scala new file mode 100644 index 00000000..80ed30b5 --- /dev/null +++ b/enumeratum-circe/compat/src/main/scala-2.11/enumeratum/values/CirceValueEnum.scala @@ -0,0 +1,47 @@ +package enumeratum.values + +import io.circe.{ Decoder, Encoder } + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ + +sealed trait CirceValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { + this: ValueEnum[ValueType, EntryType] => + + /** + * Implicit Encoder for this enum + */ + implicit def circeEncoder: Encoder[EntryType] + + /** + * Implicit Decoder for this enum + */ + implicit def circeDecoder: Decoder[EntryType] +} + +/** + * CirceEnum for IntEnumEntry + */ +trait IntCirceEnum[EntryType <: IntEnumEntry] extends CirceValueEnum[Int, EntryType] { this: ValueEnum[Int, EntryType] => + implicit val circeEncoder = Circe.encoder(this) + implicit val circeDecoder = Circe.decoder(this) +} + +/** + * CirceEnum for LongEnumEntry + */ +trait LongCirceEnum[EntryType <: LongEnumEntry] extends CirceValueEnum[Long, EntryType] { this: ValueEnum[Long, EntryType] => + implicit val circeEncoder = Circe.encoder(this) + implicit val circeDecoder = Circe.decoder(this) +} + +/** + * CirceEnum for ShortEnumEntry + */ +trait ShortCirceEnum[EntryType <: ShortEnumEntry] extends CirceValueEnum[Short, EntryType] { this: ValueEnum[Short, EntryType] => + implicit val circeEncoder = Circe.encoder(this) + implicit val circeDecoder = Circe.decoder(this) +} diff --git a/enumeratum-circe/compat/src/test/scala-2.11/enumeratum/values/CirceValueEnumSpec.scala b/enumeratum-circe/compat/src/test/scala-2.11/enumeratum/values/CirceValueEnumSpec.scala new file mode 100644 index 00000000..b0156f43 --- /dev/null +++ b/enumeratum-circe/compat/src/test/scala-2.11/enumeratum/values/CirceValueEnumSpec.scala @@ -0,0 +1,112 @@ +package enumeratum.values + +import org.scalatest.{ FunSpec, Matchers } +import cats.data.Xor +import io.circe.{ Decoder, Encoder, Json } +import io.circe.syntax._ + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +class CirceValueEnumSpec extends FunSpec with Matchers { + + testCirceEnum("LongCirceEnum", CirceContentType) + testCirceEnum("ShortCirceEnum", CirceDrinks) + testCirceEnum("IntCirceEnum", CirceLibraryItem) + testCirceEnum("IntCirceEnum with val value members", CirceMovieGenre) + + // Test method that generates tests for most primitve-based ValueEnums when given a simple descriptor and the enum + private def testCirceEnum[ValueType <: AnyVal: Encoder: Decoder, EntryType <: ValueEnumEntry[ValueType]: Encoder: Decoder](enumKind: String, enum: ValueEnum[ValueType, EntryType] with CirceValueEnum[ValueType, EntryType]): Unit = { + describe(enumKind) { + + describe("to JSON") { + + it("should work") { + enum.values.foreach { entry => + entry.asJson shouldBe entry.value.asJson + } + } + + } + + describe("from Json") { + + it("should parse to members when given proper JSON") { + enum.values.foreach { entry => + entry.value.asJson.as[EntryType] shouldBe Xor.Right(entry) + } + } + + it("should fail to parse random JSON to members") { + Json.fromString("GOBBLYGOOKITY").as[EntryType].isLeft shouldBe true + Json.fromInt(Int.MaxValue).as[EntryType].isLeft shouldBe true + } + + } + + } + } + +} + +sealed abstract class CirceContentType(val value: Long, name: String) extends LongEnumEntry + +case object CirceContentType + extends LongEnum[CirceContentType] + with LongCirceEnum[CirceContentType] { + + val values = findValues + + case object Text extends CirceContentType(value = 1L, name = "text") + case object Image extends CirceContentType(value = 2L, name = "image") + case object Video extends CirceContentType(value = 3L, name = "video") + case object Audio extends CirceContentType(value = 4L, name = "audio") + +} + +sealed abstract class CirceDrinks(val value: Short, name: String) extends ShortEnumEntry + +case object CirceDrinks extends ShortEnum[CirceDrinks] with ShortCirceEnum[CirceDrinks] { + + case object OrangeJuice extends CirceDrinks(value = 1, name = "oj") + case object AppleJuice extends CirceDrinks(value = 2, name = "aj") + case object Cola extends CirceDrinks(value = 3, name = "cola") + case object Beer extends CirceDrinks(value = 4, name = "beer") + + val values = findValues + +} + +sealed abstract class CirceLibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object CirceLibraryItem extends IntEnum[CirceLibraryItem] with IntCirceEnum[CirceLibraryItem] { + + // A good mix of named, unnamed, named + unordered args + case object Book extends CirceLibraryItem(value = 1, name = "book") + case object Movie extends CirceLibraryItem(name = "movie", value = 2) + case object Magazine extends CirceLibraryItem(3, "magazine") + case object CD extends CirceLibraryItem(4, name = "cd") + + val values = findValues + +} + +sealed abstract class CirceMovieGenre extends IntEnumEntry + +case object CirceMovieGenre extends IntEnum[CirceMovieGenre] with IntCirceEnum[CirceMovieGenre] { + + case object Action extends CirceMovieGenre { + val value = 1 + } + case object Comedy extends CirceMovieGenre { + val value: Int = 2 + } + case object Romance extends CirceMovieGenre { + val value = 3 + } + + val values = findValues + +} diff --git a/enumeratum-circe/src/main/scala/enumeratum/Circe.scala b/enumeratum-circe/src/main/scala/enumeratum/Circe.scala new file mode 100644 index 00000000..54f4550a --- /dev/null +++ b/enumeratum-circe/src/main/scala/enumeratum/Circe.scala @@ -0,0 +1,37 @@ +package enumeratum + +import cats.data.Xor +import io.circe.Decoder.Result +import io.circe._ + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +object Circe { + + /** + * Returns an Encoder for the given enum + */ + def encoder[A <: EnumEntry](enum: Enum[A]): Encoder[A] = new Encoder[A] { + final def apply(a: A): Json = stringEncoder.apply(a.entryName) + } + + /** + * Returns a Decoder for the given enum + */ + def decoder[A <: EnumEntry](enum: Enum[A]): Decoder[A] = new Decoder[A] { + final def apply(c: HCursor): Result[A] = stringDecoder.apply(c).flatMap { s => + val maybeMember = enum.withNameOption(s) + maybeMember match { + case Some(member) => Xor.right(member) + case None => Xor.left(DecodingFailure(s"$s' is not a member of enum $enum", c.history)) + } + } + } + + private val stringEncoder = implicitly[Encoder[String]] + private val stringDecoder = implicitly[Decoder[String]] + +} \ No newline at end of file diff --git a/enumeratum-circe/src/main/scala/enumeratum/CirceEnum.scala b/enumeratum-circe/src/main/scala/enumeratum/CirceEnum.scala new file mode 100644 index 00000000..e6e3678b --- /dev/null +++ b/enumeratum-circe/src/main/scala/enumeratum/CirceEnum.scala @@ -0,0 +1,25 @@ +package enumeratum +import io.circe.{ Decoder, Encoder } + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ + +/** + * Helper trait that adds implicit Circe encoders and decoders for an [[Enum]]'s members + */ +trait CirceEnum[A <: EnumEntry] { this: Enum[A] => + + /** + * Implicit Encoder for this enum + */ + implicit val circeEncoder: Encoder[A] = Circe.encoder(this) + + /** + * Implicit Decoder for this enum + */ + implicit val circeDecoder: Decoder[A] = Circe.decoder(this) + +} diff --git a/enumeratum-circe/src/test/scala/enumeratum/CirceSpec.scala b/enumeratum-circe/src/test/scala/enumeratum/CirceSpec.scala new file mode 100644 index 00000000..819017a4 --- /dev/null +++ b/enumeratum-circe/src/test/scala/enumeratum/CirceSpec.scala @@ -0,0 +1,40 @@ +package enumeratum + +import org.scalatest.{ FunSpec, Matchers } +import cats.data.Xor +import io.circe.Json +import io.circe.syntax._ + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +class CirceSpec extends FunSpec with Matchers { + + describe("to JSON") { + + it("should work") { + ShirtSize.values.foreach { entry => + entry.asJson shouldBe Json.fromString(entry.entryName) + } + } + + } + + describe("from Json") { + + it("should parse to members when given proper JSON") { + ShirtSize.values.foreach { entry => + Json.fromString(entry.entryName).as[ShirtSize] shouldBe Xor.Right(entry) + } + } + + it("should fail to parse random JSON to members") { + Json.fromString("XXL").as[ShirtSize].isLeft shouldBe true + Json.fromInt(Int.MaxValue).as[ShirtSize].isLeft shouldBe true + } + + } + +} diff --git a/enumeratum-circe/src/test/scala/enumeratum/ShirtSize.scala b/enumeratum-circe/src/test/scala/enumeratum/ShirtSize.scala new file mode 100644 index 00000000..ce7442f9 --- /dev/null +++ b/enumeratum-circe/src/test/scala/enumeratum/ShirtSize.scala @@ -0,0 +1,19 @@ +package enumeratum + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ + +sealed trait ShirtSize extends EnumEntry + +case object ShirtSize extends CirceEnum[ShirtSize] with Enum[ShirtSize] { + + case object Small extends ShirtSize + case object Medium extends ShirtSize + case object Large extends ShirtSize + + val values = findValues + +} \ No newline at end of file diff --git a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala index 0a2371ff..f9387fab 100644 --- a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala +++ b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala @@ -4,16 +4,16 @@ import org.scalatest.{ FunSpec, Matchers } class EnumJVMSpec extends FunSpec with Matchers { - describe("findValues Vector") { - - // This is a fairly intense test. - it("should be in the same order that the objects were declared in") { - import scala.util._ - (1 to 100).foreach { i => - val members = Random.shuffle((1 to Random.nextInt(20)).map { m => s"Member$m" }) - val membersDefs = members.map { m => s"case object $m extends Enum$i" }.mkString("\n\n") - val objDefinition = - s""" + describe("findValues Vector") { + + // This is a fairly intense test. + it("should be in the same order that the objects were declared in") { + import scala.util._ + (1 to 100).foreach { i => + val members = Random.shuffle((1 to Random.nextInt(20)).map { m => s"Member$m" }) + val membersDefs = members.map { m => s"case object $m extends Enum$i" }.mkString("\n\n") + val objDefinition = + s""" import enumeratum._ sealed trait Enum$i extends EnumEntry @@ -24,11 +24,11 @@ class EnumJVMSpec extends FunSpec with Matchers { Enum$i """ - val obj = Eval.apply[Enum[_ <: EnumEntry]](objDefinition) - obj.values.map(_.entryName) shouldBe members - } + val obj = Eval.apply[Enum[_ <: EnumEntry]](objDefinition) + obj.values.map(_.entryName) shouldBe members } - } + } + } diff --git a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala index 38940175..9a09f018 100644 --- a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala +++ b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala @@ -8,8 +8,9 @@ import scala.tools.reflect.ToolBox object Eval { def apply[A]( - string: String, - compileOptions: String = s"-cp ${macroToolboxClassPath.mkString(";")}"): A = { + string: String, + compileOptions: String = s"-cp ${macroToolboxClassPath.mkString(";")}" + ): A = { import scala.reflect.runtime.currentMirror val toolbox = currentMirror.mkToolBox(options = compileOptions) val tree = toolbox.parse(string) diff --git a/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnum.scala b/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnum.scala new file mode 100644 index 00000000..76f14a27 --- /dev/null +++ b/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnum.scala @@ -0,0 +1,95 @@ +package enumeratum.values + +import enumeratum.ValueEnumMacros + +import scala.language.experimental.macros + +sealed trait ValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { + + /** + * Map of [[EntryType]] object names to [[EntryType]]s + */ + lazy final val intToValuesMap: Map[ValueType, EntryType] = values.map(v => v.value -> v).toMap + + /** + * The sequence of values for your [[Enum]]. You will typically want + * to implement this in your extending class as a `val` so that `withName` + * and friends are as efficient as possible. + * + * Feel free to implement this however you'd like (including messing around with ordering, etc) if that + * fits your needs better. + */ + def values: Seq[EntryType] + + /** + * Tries to get an [[EntryType]] by the supplied value. The name corresponds to the .value + * of the case objects implementing [[EntryType]] + * + * Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values' + * .entryName values. + */ + def withValue(i: ValueType): EntryType = withValueOpt(i).getOrElse(throw new NoSuchElementException(buildNotFoundMessage(i))) + + /** + * Optionally returns an [[EntryType]] for a given value. + */ + def withValueOpt(i: ValueType): Option[EntryType] = intToValuesMap.get(i) + + private lazy val existingEntriesString = values.map(_.value).mkString(", ") + + private def buildNotFoundMessage(i: ValueType): String = { + s"$i is not a member of ValueEnum ($existingEntriesString)" + } + +} + +/* + * For the sake of keeping implementations of ValueEnums constrainted to a subset that we have tested to work relatively well, + * the following traits are implementations of the sealed trait. + * + * There is a bit of repetition in order to supply the findValues method (esp in the comments) because we are using a macro + * and macro invocations cannot provide implementations for a super class's abstract method + */ + +/** + * Value enum with [[IntEnumEntry]] entries + */ +trait IntEnum[A <: IntEnumEntry] extends ValueEnum[Int, A] { + + /** + * Method that returns a Seq of [[A]] objects that the macro was able to find. + * + * You will want to use this in some way to implement your [[values]] method. In fact, + * if you aren't using this method...why are you even bothering with this lib? + */ + protected def findValues: IndexedSeq[A] = macro ValueEnumMacros.findIntValueEntriesImpl[A] + +} + +/** + * Value enum with [[LongEnumEntry]] entries + */ +trait LongEnum[A <: LongEnumEntry] extends ValueEnum[Long, A] { + + /** + * Method that returns a Seq of [[A]] objects that the macro was able to find. + * + * You will want to use this in some way to implement your [[values]] method. In fact, + * if you aren't using this method...why are you even bothering with this lib? + */ + final protected def findValues: IndexedSeq[A] = macro ValueEnumMacros.findLongValueEntriesImpl[A] +} + +/** + * Value enum with [[ShortEnumEntry]] entries + */ +trait ShortEnum[A <: ShortEnumEntry] extends ValueEnum[Short, A] { + + /** + * Method that returns a Seq of [[A]] objects that the macro was able to find. + * + * You will want to use this in some way to implement your [[values]] method. In fact, + * if you aren't using this method...why are you even bothering with this lib? + */ + final protected def findValues: IndexedSeq[A] = macro ValueEnumMacros.findShortValueEntriesImpl[A] +} \ No newline at end of file diff --git a/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnumEntry.scala b/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnumEntry.scala new file mode 100644 index 00000000..a4eaf8f8 --- /dev/null +++ b/enumeratum-core/compat/src/main/scala-2.11/enumeratum/values/ValueEnumEntry.scala @@ -0,0 +1,31 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/11/16. + * + * Copyright 2016 + */ + +sealed trait ValueEnumEntry[ValueType <: AnyVal] { + + /** + * Value of this entry + */ + def value: ValueType + +} + +/** + * Value Enum Entry parent class for [[Int]] valued entries + */ +abstract class IntEnumEntry extends ValueEnumEntry[Int] + +/** + * Value Enum Entry parent class for [[Long]] valued entries + */ +abstract class LongEnumEntry extends ValueEnumEntry[Long] + +/** + * Value Enum Entry parent class for [[Short]] valued entries + */ +abstract class ShortEnumEntry extends ValueEnumEntry[Short] \ No newline at end of file diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ContentType.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ContentType.scala new file mode 100644 index 00000000..03b19bfb --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ContentType.scala @@ -0,0 +1,21 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/12/16. + * + * Copyright 2016 + */ +sealed abstract class ContentType(val value: Long, name: String) extends LongEnumEntry + +case object ContentType extends LongEnum[ContentType] { + + case object Text extends ContentType(value = 1L, name = "text") + case object Image extends ContentType(value = 2L, name = "image") + case object Video extends ContentType(value = 3L, name = "video") + case object Audio extends ContentType(value = 4L, name = "audio") + + val values = findValues + +} + +case object Papyrus extends ContentType(5, "papyrus") \ No newline at end of file diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/Drinks.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/Drinks.scala new file mode 100644 index 00000000..8c57ad7b --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/Drinks.scala @@ -0,0 +1,21 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/12/16. + * + * Copyright 2016 + */ +sealed abstract class Drinks(val value: Short, name: String) extends ShortEnumEntry + +case object Drinks extends ShortEnum[Drinks] { + + case object OrangeJuice extends Drinks(value = 1, name = "oj") + case object AppleJuice extends Drinks(value = 2, name = "aj") + case object Cola extends Drinks(value = 3, name = "cola") + case object Beer extends Drinks(value = 4, name = "beer") + + val values = findValues + +} + +case object CoughSyrup extends Drinks(5, "cough-syrup") \ No newline at end of file diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/LibraryItem.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/LibraryItem.scala new file mode 100644 index 00000000..a0cf2c17 --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/LibraryItem.scala @@ -0,0 +1,26 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/11/16. + * + * Copyright 2016 + */ + +sealed abstract class LibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object LibraryItem extends IntEnum[LibraryItem] { + + /* + - A good mix of named, unnamed, named + unordered args + - Values are not in ordered consecutive order + */ + case object Movie extends LibraryItem(name = "movie", value = 2) + case object Book extends LibraryItem(value = 1, name = "book") + case object Magazine extends LibraryItem(10, "magazine") + case object CD extends LibraryItem(14, name = "cd") + + val values = findValues + +} + +case object Newspaper extends LibraryItem(5, "Zeitung") \ No newline at end of file diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/MovieGenre.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/MovieGenre.scala new file mode 100644 index 00000000..5caf9fd3 --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/MovieGenre.scala @@ -0,0 +1,24 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +sealed abstract class MovieGenre extends IntEnumEntry + +case object MovieGenre extends IntEnum[MovieGenre] { + + case object Action extends MovieGenre { + val value = 1 + } + case object Comedy extends MovieGenre { + val value: Int = 2 + } + case object Romance extends MovieGenre { + val value = 3 + } + + val values = findValues + +} diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumHelpers.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumHelpers.scala new file mode 100644 index 00000000..21cdda68 --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumHelpers.scala @@ -0,0 +1,54 @@ +package enumeratum.values + +import java.util.NoSuchElementException + +import org.scalatest._ + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +trait ValueEnumHelpers { this: FunSpec with Matchers => + + /* + * Generates tests for a given enum and groups the tests inside the given enumKind descriptor + */ + def testEnum[EntryType <: ValueEnumEntry[ValueType], ValueType <: AnyVal: Numeric](enumKind: String, enum: ValueEnum[ValueType, EntryType]): Unit = { + val numeric = implicitly[Numeric[ValueType]] + describe(enumKind) { + + describe("withValue") { + + it("should return entries that match the value") { + enum.values.foreach { entry => + enum.withValue(entry.value) shouldBe entry + } + } + + it("should throw on values that don't map to any entries") { + intercept[NoSuchElementException] { + enum.withValue(numeric.fromInt(Int.MaxValue)) + } + } + + } + + describe("withValueOpt") { + + it("should return Some(entry) that match the value") { + enum.values.foreach { entry => + enum.withValueOpt(entry.value) shouldBe Some(entry) + } + } + + it("should return None when given values that do not map to any entries") { + enum.withValueOpt(numeric.fromInt(Int.MaxValue)) shouldBe None + } + + } + + } + } + +} diff --git a/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumSpec.scala b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumSpec.scala new file mode 100644 index 00000000..9cbbe3d2 --- /dev/null +++ b/enumeratum-core/compat/src/test/scala-2.11/enumeratum/values/ValueEnumSpec.scala @@ -0,0 +1,114 @@ +package enumeratum.values + +import org.scalatest.{ FunSpec, Matchers } + +/** + * Created by Lloyd on 4/12/16. + * + * Copyright 2016 + */ +class ValueEnumSpec extends FunSpec with Matchers with ValueEnumHelpers { + + describe("basic sanity check") { + + it("should have the proper values") { + LibraryItem.withValue(1) shouldBe LibraryItem.Book + LibraryItem.withValue(2) shouldBe LibraryItem.Movie + LibraryItem.withValue(10) shouldBe LibraryItem.Magazine + LibraryItem.withValue(14) shouldBe LibraryItem.CD + } + + } + + testEnum("IntEnum", LibraryItem) + testEnum("ShortEnum", Drinks) + testEnum("LongEnum", ContentType) + testEnum("when using val members in the body", MovieGenre) + + describe("compilation failures") { + + describe("problematic values") { + + it("should fail to compile when values are repeated") { + """ + sealed abstract class ContentTypeRepeated(val value: Long, name: String) extends LongEnumEntry + + case object ContentTypeRepeated extends LongEnum[ContentTypeRepeated] { + + case object Text extends ContentTypeRepeated(value = 1L, name = "text") + case object Image extends ContentTypeRepeated(value = 2L, name = "image") + case object Video extends ContentTypeRepeated(value = 2L, name = "video") + case object Audio extends ContentTypeRepeated(value = 4L, name = "audio") + + val values = findValues + + } + """ shouldNot compile + } + + it("should fail to compile when there are non literal values") { + """ + sealed abstract class ContentTypeRepeated(val value: Long, name: String) extends LongEnumEntry + + case object ContentTypeRepeated extends LongEnum[ContentTypeRepeated] { + val one = 1L + + case object Text extends ContentTypeRepeated(value = one, name = "text") + case object Image extends ContentTypeRepeated(value = 2L, name = "image") + case object Video extends ContentTypeRepeated(value = 2L, name = "video") + case object Audio extends ContentTypeRepeated(value = 4L, name = "audio") + + val values = findValues + + } + """ shouldNot compile + } + + } + + describe("trying to use with improper types") { + + it("should fail to compile for unsealed traits") { + """ + trait NotSealed extends IntEnumEntry + + object NotSealed extends IntEnum[NotSealed] { + val values = findValues + } + """ shouldNot compile + } + + it("should fail to compile for unsealed abstract classes") { + """ + abstract class NotSealed(val value: Int) extends IntEnumEntry + + object NotSealed extends IntEnum[NotSealed] { + val values = findValues + } + """ shouldNot compile + } + + it("should fail to compile for classes") { + """ + class Class(val value: Int) extends IntEnumEntry + + object Class extends IntEnum[Class] { + val values = findValues + } + """ shouldNot compile + } + + it("should fail to compile if the enum is not an object") { + """ + sealed abstract class Sealed(val value: Int) extends IntEnumEntry + + class SealedEnum extends IntEnum[Sealed] { + val values = findValues + } + """ shouldNot compile + } + } + + } + +} \ No newline at end of file diff --git a/enumeratum-core/src/main/scala/enumeratum/Enum.scala b/enumeratum-core/src/main/scala/enumeratum/Enum.scala index 374681ad..a7f7c6a7 100644 --- a/enumeratum-core/src/main/scala/enumeratum/Enum.scala +++ b/enumeratum-core/src/main/scala/enumeratum/Enum.scala @@ -34,37 +34,37 @@ import scala.language.postfixOps trait Enum[A <: EnumEntry] { /** - * Map of [[A]] object names to [[A]]s + * Map of [[A]] object names to [[A]]s */ lazy final val namesToValuesMap: Map[String, A] = values map (v => v.entryName -> v) toMap /** - * Map of [[A]] object names in lower case to [[A]]s for case-insensitive comparison + * Map of [[A]] object names in lower case to [[A]]s for case-insensitive comparison */ lazy final val lowerCaseNamesToValuesMap: Map[String, A] = values map (v => v.entryName.toLowerCase -> v) toMap /** - * Map of [[A]] to their index in the values sequence. - * - * A performance optimisation so that indexOf can be found in constant time. - */ + * Map of [[A]] to their index in the values sequence. + * + * A performance optimisation so that indexOf can be found in constant time. + */ lazy final val valuesToIndex: Map[A, Int] = values.zipWithIndex.toMap /** - * The sequence of values for your [[Enum]]. You will typically want - * to implement this in your extending class as a `val` so that `withName` - * and friends are as efficient as possible. - * - * Feel free to implement this however you'd like (including messing around with ordering, etc) if that - * fits your needs better. + * The sequence of values for your [[Enum]]. You will typically want + * to implement this in your extending class as a `val` so that `withName` + * and friends are as efficient as possible. + * + * Feel free to implement this however you'd like (including messing around with ordering, etc) if that + * fits your needs better. */ def values: Seq[A] /** - * Tries to get an [[A]] by the supplied name. The name corresponds to the .name - * of the case objects implementing [[A]] - * - * Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values' - * .entryName values. - */ + * Tries to get an [[A]] by the supplied name. The name corresponds to the .name + * of the case objects implementing [[A]] + * + * Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values' + * .entryName values. + */ def withName(name: String): A = withNameOption(name) getOrElse (throw new NoSuchElementException(buildNotFoundMessage(name))) @@ -75,34 +75,34 @@ trait Enum[A <: EnumEntry] { def withNameOption(name: String): Option[A] = namesToValuesMap get name /** - * Tries to get an [[A]] by the supplied name. The name corresponds to the .name - * of the case objects implementing [[A]], disregarding case - * - * Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values' - * .entryName values. + * Tries to get an [[A]] by the supplied name. The name corresponds to the .name + * of the case objects implementing [[A]], disregarding case + * + * Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values' + * .entryName values. */ def withNameInsensitive(name: String): A = withNameInsensitiveOption(name) getOrElse (throw new NoSuchElementException(buildNotFoundMessage(name))) /** - * Optionally returns an [[A]] for a given name, disregarding case + * Optionally returns an [[A]] for a given name, disregarding case */ def withNameInsensitiveOption(name: String): Option[A] = lowerCaseNamesToValuesMap get name.toLowerCase /** - * Returns the index number of the member passed in the values picked up by this enum + * Returns the index number of the member passed in the values picked up by this enum * - * @param member the member you want to check the index of - * @return the index of the first element of values that is equal (as determined by ==) to member, or -1, if none exists. + * @param member the member you want to check the index of + * @return the index of the first element of values that is equal (as determined by ==) to member, or -1, if none exists. */ def indexOf(member: A): Int = valuesToIndex.getOrElse(member, -1) /** - * Method that returns a Seq of [[A]] objects that the macro was able to find. + * Method that returns a Seq of [[A]] objects that the macro was able to find. * - * You will want to use this in some way to implement your [[values]] method. In fact, - * if you aren't using this method...why are you even bothering with this lib? + * You will want to use this in some way to implement your [[values]] method. In fact, + * if you aren't using this method...why are you even bothering with this lib? */ protected def findValues: Seq[A] = macro EnumMacros.findValuesImpl[A] diff --git a/enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala b/enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala index f5838822..f6e35f47 100644 --- a/enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala +++ b/enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala @@ -1,7 +1,7 @@ package enumeratum import org.scalatest.OptionValues._ -import org.scalatest.{FunSpec, Matchers} +import org.scalatest.{ FunSpec, Matchers } class EnumSpec extends FunSpec with Matchers { diff --git a/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/EnumFormats.scala b/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/EnumFormats.scala new file mode 100644 index 00000000..b9a4a434 --- /dev/null +++ b/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/EnumFormats.scala @@ -0,0 +1,39 @@ +package enumeratum.values + +import play.api.libs.json._ + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +object EnumFormats { + + /** + * Returns a Reads for the provided ValueEnum based on the given base Reads for the Enum's value type + */ + def reads[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]](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(s"Enumeration expected of type: '$enum', but it does not appear to contain the value: '$s'") + } + } + } + + /** + * Returns a Writes for the provided ValueEnum based on the given base Writes for the Enum's value type + */ + def writes[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType])(implicit baseWrites: Writes[ValueType]): Writes[EntryType] = new Writes[EntryType] { + def writes(o: EntryType): JsValue = 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 <: AnyVal, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType])(implicit baseReads: Reads[ValueType], baseWrites: Writes[ValueType]): Format[EntryType] = { + Format(reads(enum), writes(enum)) + } + +} \ No newline at end of file diff --git a/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/PlayJsonValueEnum.scala b/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/PlayJsonValueEnum.scala new file mode 100644 index 00000000..40bcef4a --- /dev/null +++ b/enumeratum-play-json/compat/src/main/scala-2.11/enumeratum/values/PlayJsonValueEnum.scala @@ -0,0 +1,39 @@ +package enumeratum.values + +import play.api.libs.json.Format + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ + +trait PlayJsonValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { enum: ValueEnum[ValueType, EntryType] => + + /** + * Implicit JSON format for the entries of this enum + */ + implicit def format: Format[EntryType] + +} + +/** + * Enum implementation for Int enum members that contains an implicit Play JSON Format + */ +trait IntPlayJsonValueEnum[EntryType <: IntEnumEntry] extends PlayJsonValueEnum[Int, EntryType] { this: IntEnum[EntryType] => + implicit val format: Format[EntryType] = EnumFormats.formats(this) +} + +/** + * Enum implementation for Long enum members that contains an implicit Play JSON Format + */ +trait LongPlayJsonValueEnum[EntryType <: LongEnumEntry] extends PlayJsonValueEnum[Long, EntryType] { this: LongEnum[EntryType] => + implicit val format: Format[EntryType] = EnumFormats.formats(this) +} + +/** + * Enum implementation for Short enum members that contains an implicit Play JSON Format + */ +trait ShortPlayJsonValueEnum[EntryType <: ShortEnumEntry] extends PlayJsonValueEnum[Short, EntryType] { this: ShortEnum[EntryType] => + implicit val format: Format[EntryType] = EnumFormats.formats(this) +} \ No newline at end of file diff --git a/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumFormatsSpec.scala b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumFormatsSpec.scala new file mode 100644 index 00000000..6a069822 --- /dev/null +++ b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumFormatsSpec.scala @@ -0,0 +1,37 @@ +package enumeratum.values + +import org.scalatest._ + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +class EnumFormatsSpec extends FunSpec with Matchers with EnumJsonFormatHelpers { + + describe(".reads") { + + testReads("IntEnum", LibraryItem) + testReads("LongEnum", ContentType) + testReads("ShortEnum", Drinks) + + } + + describe(".writes") { + + testWrites("IntEnum", LibraryItem) + testWrites("LongEnum", ContentType) + testWrites("ShortEnum", Drinks) + + } + + describe(".formats") { + + testFormats("IntEnum", LibraryItem) + testFormats("LongEnum", ContentType) + testFormats("ShortEnum", Drinks) + testFormats("PlayJsonValueEnum", JsonDrinks, Some(JsonDrinks.format)) + + } + +} \ No newline at end of file diff --git a/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumJsonFormatHelpers.scala b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumJsonFormatHelpers.scala new file mode 100644 index 00000000..caf2b85a --- /dev/null +++ b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/EnumJsonFormatHelpers.scala @@ -0,0 +1,48 @@ +package enumeratum.values + +import org.scalatest._ +import play.api.libs.json._ +import org.scalatest.OptionValues._ + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +trait EnumJsonFormatHelpers { this: FunSpec with Matchers => + + def testWrites[EntryType <: ValueEnumEntry[ValueType], ValueType <: AnyVal: Numeric: Writes](enumKind: String, enum: ValueEnum[ValueType, EntryType], providedWrites: Option[Writes[EntryType]] = None): Unit = { + val numeric = implicitly[Numeric[ValueType]] + val writes = providedWrites.getOrElse(EnumFormats.writes(enum)) + describe(enumKind) { + it("should write proper JsValues") { + enum.values.foreach { entry => + writes.writes(entry) shouldBe JsNumber(numeric.toInt(entry.value)) + } + } + } + } + + def testReads[EntryType <: ValueEnumEntry[ValueType], ValueType <: AnyVal: Numeric: Reads](enumKind: String, enum: ValueEnum[ValueType, EntryType], providedReads: Option[Reads[EntryType]] = None): Unit = { + val numeric = implicitly[Numeric[ValueType]] + val reads = providedReads.getOrElse(EnumFormats.reads(enum)) + describe(enumKind) { + it("should read valid values") { + enum.values.foreach { entry => + reads.reads(JsNumber(numeric.toInt(entry.value))).asOpt.value shouldBe entry + } + } + it("should fail to read with invalid values") { + reads.reads(JsNumber(Int.MaxValue)) shouldBe 'error + reads.reads(JsString("boon")) shouldBe 'error + } + } + } + + def testFormats[EntryType <: ValueEnumEntry[ValueType], ValueType <: AnyVal: Numeric: Reads: Writes](enumKind: String, enum: ValueEnum[ValueType, EntryType], providedFormat: Option[Format[EntryType]] = None): Unit = { + val format = providedFormat.getOrElse(EnumFormats.formats(enum)) + testReads(enumKind, enum, Some(format)) + testWrites(enumKind, enum, Some(format)) + } + +} diff --git a/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/JsonDrinks.scala b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/JsonDrinks.scala new file mode 100644 index 00000000..19ba7d8e --- /dev/null +++ b/enumeratum-play-json/compat/src/test/scala-2.11/enumeratum/values/JsonDrinks.scala @@ -0,0 +1,20 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ + +sealed abstract class JsonDrinks(val value: Short, name: String) extends ShortEnumEntry + +case object JsonDrinks extends ShortEnum[JsonDrinks] with ShortPlayJsonValueEnum[JsonDrinks] { + + case object OrangeJuice extends JsonDrinks(value = 1, name = "oj") + case object AppleJuice extends JsonDrinks(value = 2, name = "aj") + case object Cola extends JsonDrinks(value = 3, name = "cola") + case object Beer extends JsonDrinks(value = 4, name = "beer") + + val values = findValues + +} \ No newline at end of file diff --git a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala index c79a9aaa..cda8a4a2 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala @@ -23,7 +23,7 @@ class PlayJsonEnumSpec extends FunSpec with Matchers { describe("serialisation") { it("should serialise values to JsString") { - PlayJson.toJson(Dummy.A) shouldBe (JsString("A")) + PlayJson.toJson(Dummy.A) shouldBe JsString("A") } } diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/Forms.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/Forms.scala new file mode 100644 index 00000000..06367bbd --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/Forms.scala @@ -0,0 +1,34 @@ +package enumeratum.values + +import play.api.data.format.Formatter +import play.api.data.{ FormError, Mapping, Forms => PlayForms } + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +object Forms { + + /** + * Returns a [[ValueEnum]] mapping for Play form fields + */ + def enum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType], EnumType <: ValueEnum[ValueType, EntryType]](baseFormatter: Formatter[ValueType])(enum: EnumType): Mapping[EntryType] = { + PlayForms.of(formatter(baseFormatter)(enum)) + } + + private[this] def formatter[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType], EnumType <: ValueEnum[ValueType, EntryType]](baseFormatter: Formatter[ValueType])(enum: EnumType) = { + new Formatter[EntryType] { + def bind(key: String, data: Map[String, String]): Either[Seq[FormError], EntryType] = baseFormatter.bind(key, data).right.flatMap { s => + val maybeBound = enum.withValueOpt(s) + maybeBound match { + case Some(obj) => Right(obj) + case None => Left(Seq(FormError(key, "error.enum", Nil))) + } + } + + def unbind(key: String, value: EntryType): Map[String, String] = Map(key -> value.value.toString) + } + } + +} diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayFormValueEnum.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayFormValueEnum.scala new file mode 100644 index 00000000..9ca7c18b --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayFormValueEnum.scala @@ -0,0 +1,47 @@ +package enumeratum.values + +import play.api.data.format.{ Formatter, Formats } +import play.api.data.Mapping + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ + +sealed trait PlayFormValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { enum: ValueEnum[ValueType, EntryType] => + + /** + * The [[Formmater]] for binding the ValueType of this ValueEnum. + * + * Used for building the [[Formmater]] for the entries + */ + protected def baseFormatter: Formatter[ValueType] + + /** + * Field for mapping this enum in Forms + */ + lazy val formField: Mapping[EntryType] = Forms.enum(baseFormatter)(enum) + +} + +/** + * Form Bindable implicits for IntEnum + */ +trait IntPlayFormValueEnum[EntryType <: IntEnumEntry] extends PlayFormValueEnum[Int, EntryType] { this: IntEnum[EntryType] => + protected val baseFormatter: Formatter[Int] = Formats.intFormat +} + +/** + * Form Bindable implicits for LongEnum + */ +trait LongPlayFormValueEnum[EntryType <: LongEnumEntry] extends PlayFormValueEnum[Long, EntryType] { this: LongEnum[EntryType] => + protected val baseFormatter: Formatter[Long] = Formats.longFormat +} + +/** + * Form Bindable implicits for ShortEnum + */ +trait ShortPlayFormValueEnum[EntryType <: ShortEnumEntry] extends PlayFormValueEnum[Short, EntryType] { this: ShortEnum[EntryType] => + protected val baseFormatter: Formatter[Short] = Formats.shortFormat +} \ No newline at end of file diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayPathBindableValueEnum.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayPathBindableValueEnum.scala new file mode 100644 index 00000000..22a9c269 --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayPathBindableValueEnum.scala @@ -0,0 +1,57 @@ +package enumeratum.values + +import play.api.mvc.PathBindable +import play.api.routing.sird.PathBindableExtractor + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +sealed trait PlayPathBindableValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { enum: ValueEnum[ValueType, EntryType] => + + /** + * Implicit path binder for Play's default router + */ + implicit def pathBindable: PathBindable[EntryType] + + /** + * Binder for [[play.api.routing.sird]] router + * + * Example: + * + * {{{ + * import play.api.routing.sird._ + * import play.api.routing._ + * import play.api.mvc._ + * + * Router.from { + * case GET(p"/hello/${Greeting.fromPath(greeting)}") => Action { + * Results.Ok(s"$greeting") + * } + * } + * }}} + */ + lazy val fromPath = new PathBindableExtractor[EntryType] +} + +/** + * Path Bindable implicits for IntEnum + */ +trait IntPlayPathBindableValueEnum[EntryType <: IntEnumEntry] extends PlayPathBindableValueEnum[Int, EntryType] { this: IntEnum[EntryType] => + implicit val pathBindable: PathBindable[EntryType] = UrlBinders.pathBinder(this) +} + +/** + * Path Bindable implicits for LongEnum + */ +trait LongPlayPathBindableValueEnum[EntryType <: LongEnumEntry] extends PlayPathBindableValueEnum[Long, EntryType] { this: LongEnum[EntryType] => + implicit val pathBindable: PathBindable[EntryType] = UrlBinders.pathBinder(this) +} + +/** + * Path Bindable implicits for ShortEnum + */ +trait ShortPlayPathBindableValueEnum[EntryType <: ShortEnumEntry] extends PlayPathBindableValueEnum[Short, EntryType] { this: ShortEnum[EntryType] => + implicit val pathBindable: PathBindable[EntryType] = UrlBinders.pathBinder(this)(PathBindable.bindableInt.transform(_.toShort, _.toInt)) +} \ No newline at end of file diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayQueryBindableValueEnum.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayQueryBindableValueEnum.scala new file mode 100644 index 00000000..833985ac --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayQueryBindableValueEnum.scala @@ -0,0 +1,39 @@ +package enumeratum.values + +import play.api.mvc.QueryStringBindable +import play.api.routing.sird.PathBindableExtractor + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ + +sealed trait PlayQueryBindableValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { enum: ValueEnum[ValueType, EntryType] => + + /** + * Implicit path binder for Play's default router + */ + implicit def queryBindable: QueryStringBindable[EntryType] +} + +/** + * Query Bindable implicits for IntEnum + */ +trait IntPlayQueryBindableValueEnum[EntryType <: IntEnumEntry] extends PlayQueryBindableValueEnum[Int, EntryType] { this: IntEnum[EntryType] => + implicit val queryBindable: QueryStringBindable[EntryType] = UrlBinders.queryBinder(this) +} + +/** + * Query Bindable implicits for LongEnum + */ +trait LongPlayQueryBindableValueEnum[EntryType <: LongEnumEntry] extends PlayQueryBindableValueEnum[Long, EntryType] { this: LongEnum[EntryType] => + implicit val queryBindable: QueryStringBindable[EntryType] = UrlBinders.queryBinder(this) +} + +/** + * Query Bindable implicits for ShortEnum + */ +trait ShortPlayQueryBindableValueEnum[EntryType <: ShortEnumEntry] extends PlayQueryBindableValueEnum[Short, EntryType] { this: ShortEnum[EntryType] => + implicit val queryBindable: QueryStringBindable[EntryType] = UrlBinders.queryBinder(this)(QueryStringBindable.bindableInt.transform(_.toShort, _.toInt)) +} \ No newline at end of file diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayValueEnums.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayValueEnums.scala new file mode 100644 index 00000000..d0d5a317 --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/PlayValueEnums.scala @@ -0,0 +1,61 @@ +package enumeratum.values + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ + +/** + * An IntEnum that has a lot of the Play-related implicits built-in so you can avoid + * boilerplate. + * + * Things included are: + * + * - implicit PathBindable (for binding from request path) + * - implicit QueryStringBindable (for binding from query strings) + * - formField for doing things like `Form("hello" -> MyEnum.formField)` + * - implicit Json format + * + */ +trait IntPlayEnum[EnumEntry <: IntEnumEntry] extends IntEnum[EnumEntry] + with IntPlayPathBindableValueEnum[EnumEntry] + with IntPlayQueryBindableValueEnum[EnumEntry] + with IntPlayFormValueEnum[EnumEntry] + with IntPlayJsonValueEnum[EnumEntry] + +/** + * An LongEnum that has a lot of the Play-related implicits built-in so you can avoid + * boilerplate. + * + * Things included are: + * + * - implicit PathBindable (for binding from request path) + * - implicit QueryStringBindable (for binding from query strings) + * - formField for doing things like `Form("hello" -> MyEnum.formField)` + * - implicit Json format + * + */ +trait LongPlayEnum[EnumEntry <: LongEnumEntry] extends LongEnum[EnumEntry] + with LongPlayPathBindableValueEnum[EnumEntry] + with LongPlayQueryBindableValueEnum[EnumEntry] + with LongPlayFormValueEnum[EnumEntry] + with LongPlayJsonValueEnum[EnumEntry] + +/** + * An ShortEnum that has a lot of the Play-related implicits built-in so you can avoid + * boilerplate. + * + * Things included are: + * + * - implicit PathBindable (for binding from request path) + * - implicit QueryStringBindable (for binding from query strings) + * - formField for doing things like `Form("hello" -> MyEnum.formField)` + * - implicit Json format + * + */ +trait ShortPlayEnum[EnumEntry <: ShortEnumEntry] extends ShortEnum[EnumEntry] + with ShortPlayPathBindableValueEnum[EnumEntry] + with ShortPlayQueryBindableValueEnum[EnumEntry] + with ShortPlayFormValueEnum[EnumEntry] + with ShortPlayJsonValueEnum[EnumEntry] \ No newline at end of file diff --git a/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/UrlBinders.scala b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/UrlBinders.scala new file mode 100644 index 00000000..6a053d59 --- /dev/null +++ b/enumeratum-play/compat/src/main/scala-2.11/enumeratum/values/UrlBinders.scala @@ -0,0 +1,44 @@ +package enumeratum.values + +import play.api.mvc.{ PathBindable, QueryStringBindable } + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +object UrlBinders { + + /** + * Returns a [[PathBindable]] for the provided ValueEnum and base [[PathBindable]] + */ + def pathBinder[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType])(implicit baseBindable: PathBindable[ValueType]): PathBindable[EntryType] = new PathBindable[EntryType] { + def bind(key: String, value: String): Either[String, EntryType] = baseBindable.bind(key, value).right.flatMap { b => + val maybeBound = enum.withValueOpt(b) + maybeBound match { + case Some(obj) => Right(obj) + case None => Left(s"Unknown value supplied for $enum '" + value + "'") + } + } + + def unbind(key: String, value: EntryType): String = value.value.toString + } + + /** + * Returns a [[QueryStringBindable]] for the provided ValueEnum and base [[PathBindable]] + */ + def queryBinder[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType])(implicit baseBindable: QueryStringBindable[ValueType]): QueryStringBindable[EntryType] = new QueryStringBindable[EntryType] { + def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, EntryType]] = { + baseBindable.bind(key, params).map(_.right.flatMap { s => + val maybeBound = enum.withValueOpt(s) + maybeBound match { + case Some(obj) => Right(obj) + case None => Left(s"Unknown value supplied for $enum '$s'") + } + }) + } + + def unbind(key: String, entry: EntryType): String = s"$key=${entry.value}" + } + +} diff --git a/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumHelpers.scala b/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumHelpers.scala new file mode 100644 index 00000000..01ee3614 --- /dev/null +++ b/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumHelpers.scala @@ -0,0 +1,169 @@ +package enumeratum.values + +import java.security.cert.X509Certificate + +import org.scalatest._ +import play.api.data.Form +import play.api.http.HttpVerbs +import play.api.mvc.{ Headers, RequestHeader } +import org.scalatest.OptionValues._ +import org.scalatest.EitherValues._ +import play.api.libs.json.Format + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +trait PlayValueEnumHelpers extends EnumJsonFormatHelpers { this: FunSpec with Matchers => + + def testPlayEnum[EntryType <: ValueEnumEntry[ValueType], ValueType <: AnyVal: Numeric: Format]( + enumKind: String, + enum: ValueEnum[ValueType, EntryType] with PlayFormValueEnum[ValueType, EntryType] with PlayPathBindableValueEnum[ValueType, EntryType] with PlayQueryBindableValueEnum[ValueType, EntryType] with PlayJsonValueEnum[ValueType, EntryType] + ) = { + + describe(enumKind) { + + describe("Form binding") { + + val subject = Form("hello" -> enum.formField) + + it("should bind proper strings into an Enum value") { + enum.values.foreach { entry => + val r = subject.bind(Map("hello" -> s"${entry.value}")) + r.value.value shouldBe entry + } + } + + it("should fail to bind random strings") { + val r1 = subject.bind(Map("hello" -> "AARS143515123E")) + val r2 = subject.bind(Map("hello" -> s"${Int.MaxValue}")) + r1.value shouldBe None + r2.value shouldBe None + } + + it("should unbind") { + enum.values.foreach { entry => + val r = subject.mapping.unbind(entry) + r shouldBe Map("hello" -> s"${entry.value}") + } + } + + } + + describe("URL binding") { + + describe("PathBindable") { + + val subject = enum.pathBindable + + it("should bind strings corresponding to enum values") { + enum.values.foreach { entry => + subject.bind("hello", s"${entry.value}").right.value shouldBe entry + } + } + + it("should not bind strings not found as values in the enumeration") { + subject.bind("hello", s"s${Int.MaxValue}").isLeft shouldBe true + subject.bind("hello", "Z").isLeft shouldBe true + } + + it("should unbind values") { + enum.values.foreach { entry => + subject.unbind("hello", entry) shouldBe entry.value.toString + } + } + + } + + describe("PathBindableExtractor") { + + val subject = enum.fromPath + + it("should extract strings corresponding to enum values") { + enum.values.foreach { entry => + subject.unapply(s"${entry.value}") shouldBe Some(entry) + } + } + + it("should not extract strings that are not found as valuesin the enumeration") { + subject.unapply("Z") shouldBe None + subject.unapply(s"${Int.MaxValue}") shouldBe None + } + + it("should allow me to build an SIRD router") { + import play.api.routing.sird._ + import play.api.routing._ + import play.api.mvc._ + enum.values.foreach { entry => + + val router = Router.from { + case GET(p"/${ enum.fromPath(greeting) }") => Action { + Results.Ok(s"$greeting") + } + } + router.routes.isDefinedAt(reqHeaderAt(HttpVerbs.GET, s"/${entry.value}")) shouldBe true + router.routes.isDefinedAt(reqHeaderAt(HttpVerbs.GET, s"/${Int.MaxValue}")) shouldBe false + } + } + + } + + describe("QueryStringBindable") { + + val subject = enum.queryBindable + + it("should bind strings corresponding to enum values regardless of case") { + enum.values.foreach { entry => + subject.bind("hello", Map("hello" -> Seq(s"${entry.value}"))).value.right.value shouldBe entry + } + } + + it("should not bind strings not found as values in the enumeration") { + subject.bind("hello", Map("hello" -> Seq("Z"))).value shouldBe 'left + subject.bind("hello", Map("hello" -> Seq(s"${Int.MaxValue}"))).value shouldBe 'left + subject.bind("hello", Map("helloz" -> Seq("1"))) shouldBe None + } + + it("should unbind values") { + enum.values.foreach { entry => + subject.unbind("hello", entry) shouldBe s"hello=${entry.value}" + } + } + + } + + describe("JSON formats") { + testFormats(enumKind, enum, Some(enum.format)) + } + + } + } + + } + + def reqHeaderAt(theMethod: String, theUri: String) = + new RequestHeader { + def clientCertificateChain: Option[Seq[X509Certificate]] = ??? + + def secure: Boolean = ??? + + def uri: String = theUri + + def remoteAddress: String = ??? + + def queryString: Map[String, Seq[String]] = ??? + + def method: String = theMethod + + def headers: Headers = ??? + + def path: String = uri + + def version: String = ??? + + def tags: Map[String, String] = ??? + + def id: Long = ??? + } +} diff --git a/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumSpec.scala b/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumSpec.scala new file mode 100644 index 00000000..7f85e176 --- /dev/null +++ b/enumeratum-play/compat/src/test/scala-2.11/enumeratum/values/PlayValueEnumSpec.scala @@ -0,0 +1,76 @@ +package enumeratum.values + +import org.scalatest.{ FunSpec, Matchers } + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +class PlayValueEnumSpec extends FunSpec with Matchers with PlayValueEnumHelpers { + + testPlayEnum("LongPlayEnum", PlayContentType) + testPlayEnum("ShortPlayEnum", PlayDrinks) + testPlayEnum("IntPlayEnum", PlayLibraryItem) + testPlayEnum("IntPlayEnum with values declared as members", PlayMovieGenre) + +} + +sealed abstract class PlayContentType(val value: Long, name: String) extends LongEnumEntry + +case object PlayContentType + extends LongPlayEnum[PlayContentType] { + + val values = findValues + + case object Text extends PlayContentType(value = 1L, name = "text") + case object Image extends PlayContentType(value = 2L, name = "image") + case object Video extends PlayContentType(value = 3L, name = "video") + case object Audio extends PlayContentType(value = 4L, name = "audio") + +} + +sealed abstract class PlayDrinks(val value: Short, name: String) extends ShortEnumEntry + +case object PlayDrinks extends ShortPlayEnum[PlayDrinks] { + + case object OrangeJuice extends PlayDrinks(value = 1, name = "oj") + case object AppleJuice extends PlayDrinks(value = 2, name = "aj") + case object Cola extends PlayDrinks(value = 3, name = "cola") + case object Beer extends PlayDrinks(value = 4, name = "beer") + + val values = findValues + +} + +sealed abstract class PlayLibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object PlayLibraryItem extends IntPlayEnum[PlayLibraryItem] { + + // A good mix of named, unnamed, named + unordered args + case object Book extends PlayLibraryItem(value = 1, name = "book") + case object Movie extends PlayLibraryItem(name = "movie", value = 2) + case object Magazine extends PlayLibraryItem(3, "magazine") + case object CD extends PlayLibraryItem(4, name = "cd") + + val values = findValues + +} + +sealed abstract class PlayMovieGenre extends IntEnumEntry + +case object PlayMovieGenre extends IntPlayEnum[PlayMovieGenre] { + + case object Action extends PlayMovieGenre { + val value = 1 + } + case object Comedy extends PlayMovieGenre { + val value: Int = 2 + } + case object Romance extends PlayMovieGenre { + val value = 3 + } + + val values = findValues + +} diff --git a/enumeratum-play/src/main/scala/enumeratum/PlayFormFieldEnum.scala b/enumeratum-play/src/main/scala/enumeratum/PlayFormFieldEnum.scala index 8fbe62d1..fa6f073f 100644 --- a/enumeratum-play/src/main/scala/enumeratum/PlayFormFieldEnum.scala +++ b/enumeratum-play/src/main/scala/enumeratum/PlayFormFieldEnum.scala @@ -3,5 +3,9 @@ package enumeratum import play.api.data.Mapping trait PlayFormFieldEnum[A <: EnumEntry] { self: Enum[A] => + + /** + * Form field for this enum + */ val formField: Mapping[A] = Forms.enum(self) } diff --git a/enumeratum-play/src/main/scala/enumeratum/PlayPathBindableEnum.scala b/enumeratum-play/src/main/scala/enumeratum/PlayPathBindableEnum.scala index 47f0a3b6..6778f2a5 100644 --- a/enumeratum-play/src/main/scala/enumeratum/PlayPathBindableEnum.scala +++ b/enumeratum-play/src/main/scala/enumeratum/PlayPathBindableEnum.scala @@ -6,26 +6,26 @@ import play.api.routing.sird.PathBindableExtractor trait PlayPathBindableEnum[A <: EnumEntry] { self: Enum[A] => /** - * Implicit path binder for Play's default router - */ + * Implicit path binder for Play's default router + */ implicit val pathBindable: PathBindable[A] = UrlBinders.pathBinder(self) /** - * Binder for [[play.api.routing.sird]] router - * - * Example: - * - * {{{ - * import play.api.routing.sird._ - * import play.api.routing._ - * import play.api.mvc._ - * - * Router.from { - * case GET(p"/hello/${Greeting.fromPath(greeting)}") => Action { - * Results.Ok(s"$greeting") - * } - * } - * }}} - */ + * Binder for [[play.api.routing.sird]] router + * + * Example: + * + * {{{ + * import play.api.routing.sird._ + * import play.api.routing._ + * import play.api.mvc._ + * + * Router.from { + * case GET(p"/hello/${Greeting.fromPath(greeting)}") => Action { + * Results.Ok(s"$greeting") + * } + * } + * }}} + */ lazy val fromPath = new PathBindableExtractor[A] } \ No newline at end of file diff --git a/enumeratum-play/src/main/scala/enumeratum/UrlBinders.scala b/enumeratum-play/src/main/scala/enumeratum/UrlBinders.scala index 911ae416..d737ca5f 100644 --- a/enumeratum-play/src/main/scala/enumeratum/UrlBinders.scala +++ b/enumeratum-play/src/main/scala/enumeratum/UrlBinders.scala @@ -20,7 +20,7 @@ object UrlBinders { val maybeBound = if (insensitive) enum.withNameInsensitiveOption(value) else enum.withNameOption(value) maybeBound match { case Some(v) => Right(v) - case _ => Left(s"Unknown value supplied for $enum '" + value + "'") + case _ => Left(s"Unknown value supplied for $enum '$value'") } } } @@ -34,7 +34,7 @@ object UrlBinders { def queryBinder[A <: EnumEntry](enum: Enum[A], insensitive: Boolean = false): QueryStringBindable[A] = new QueryStringBindable[A] { - def unbind(key: String, value: A): String = key + "=" + value.entryName + def unbind(key: String, value: A): String = s"$key=${value.entryName}" def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, A]] = { params.get(key).flatMap(_.headOption).map { p => diff --git a/enumeratum-play/src/test/scala/enumeratum/PlayEnumSpec.scala b/enumeratum-play/src/test/scala/enumeratum/PlayEnumSpec.scala index 123394b7..2c8abdc6 100644 --- a/enumeratum-play/src/test/scala/enumeratum/PlayEnumSpec.scala +++ b/enumeratum-play/src/test/scala/enumeratum/PlayEnumSpec.scala @@ -1,6 +1,8 @@ package enumeratum -import org.scalatest.{ Matchers, FunSpec } +import java.security.cert.X509Certificate + +import org.scalatest.{ FunSpec, Matchers } import play.api.data.Form import play.api.http.HttpVerbs import play.api.libs.json.{ JsNumber, JsString, Json => PlayJson } @@ -127,6 +129,9 @@ class PlayEnumSpec extends FunSpec with Matchers { private def reqHeaderAt(theMethod: String, theUri: String) = new RequestHeader { + + def clientCertificateChain: Option[Seq[X509Certificate]] = ??? + def secure: Boolean = ??? def uri: String = theUri diff --git a/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickleValueEnum.scala b/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickleValueEnum.scala new file mode 100644 index 00000000..7c252085 --- /dev/null +++ b/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickleValueEnum.scala @@ -0,0 +1,41 @@ +package enumeratum.values + +import upickle.default.Aliases.RW +import upickle.default.ReadWriter +import UPickler._ + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ + +sealed trait UPickleValueEnum[ValueType <: AnyVal, EntryType <: ValueEnumEntry[ValueType]] { this: ValueEnum[ValueType, EntryType] => + + /** + * Implicit UPickle ReadWriter + */ + implicit def uPickleReadWriter: RW[EntryType] + +} + +/** + * Enum implementation for Int enum members that contains an implicit UPickle ReadWriter + */ +trait IntUPickleEnum[EntryType <: IntEnumEntry] extends UPickleValueEnum[Int, EntryType] { this: ValueEnum[Int, EntryType] => + implicit val uPickleReadWriter: RW[EntryType] = ReadWriter(writer(this).write, reader(this).read) +} + +/** + * Enum implementation for Long enum members that contains an implicit UPickle ReadWriter + */ +trait LongUPickleEnum[EntryType <: LongEnumEntry] extends UPickleValueEnum[Long, EntryType] { this: ValueEnum[Long, EntryType] => + implicit val uPickleReadWriter: RW[EntryType] = ReadWriter(writer(this).write, reader(this).read) +} + +/** + * Enum implementation for Short enum members that contains an implicit UPickle ReadWriter + */ +trait ShortUPickleEnum[EntryType <: ShortEnumEntry] extends UPickleValueEnum[Short, EntryType] { this: ValueEnum[Short, EntryType] => + implicit val uPickleReadWriter: RW[EntryType] = ReadWriter(writer(this).write, reader(this).read) +} diff --git a/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickler.scala b/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickler.scala new file mode 100644 index 00000000..1581ee31 --- /dev/null +++ b/enumeratum-upickle/compat/src/main/scala-2.11/enumeratum/values/UPickler.scala @@ -0,0 +1,33 @@ +package enumeratum.values + +import enumeratum.EnrichedPartialFunction._ +import upickle.default.{ Reader, Writer } + +/** + * Created by Lloyd on 4/13/16. + * + * Copyright 2016 + */ +object UPickler { + + /** + * Returns a Reader for the given ValueEnum + */ + def reader[ValueType <: AnyVal: Reader, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType]): Reader[EntryType] = { + val valueReader = implicitly[Reader[ValueType]] + Reader[EntryType] { + valueReader.read.andThenPartial { case v if enum.withValueOpt(v).isDefined => enum.withValue(v) } + } + } + + /** + * Returns a Writer for the given ValueEnum + */ + def writer[ValueType <: AnyVal: Writer, EntryType <: ValueEnumEntry[ValueType]](enum: ValueEnum[ValueType, EntryType]): Writer[EntryType] = { + val valueWriter = implicitly[Writer[ValueType]] + Writer[EntryType] { + case member => valueWriter.write(member.value) + } + } + +} diff --git a/enumeratum-upickle/compat/src/test/scala-2.11/enumeratum/values/UPicklerSpec.scala b/enumeratum-upickle/compat/src/test/scala-2.11/enumeratum/values/UPicklerSpec.scala new file mode 100644 index 00000000..a3005fc3 --- /dev/null +++ b/enumeratum-upickle/compat/src/test/scala-2.11/enumeratum/values/UPicklerSpec.scala @@ -0,0 +1,118 @@ +package enumeratum.values + +import org.scalatest._ +import upickle.Js +import upickle.default.{ readJs, writeJs, Reader, Writer } + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +class UPicklerSpec extends FunSpec with Matchers { + + testPickling("LongUPickleEnum", UPickleContentType) + testPickling("ShortUPickleEnum", UPickleDrinks) + testPickling("IntUPickleEnum", UPickleLibraryItem) + testPickling("IntUPickleEnum with values declared as members", UPickleMovieGenre) + + /** + * Given an enum, tests its JSON reading and writing behaviour, grouping the test results under the given enumKind descriptor + */ + private def testPickling[ValueType <: AnyVal: Writer: Numeric, EntryType <: ValueEnumEntry[ValueType]: Reader: Writer](enumKind: String, enum: UPickleValueEnum[ValueType, EntryType] with ValueEnum[ValueType, EntryType]) = { + describe(enumKind) { + describe("Reader") { + + it("should work with valid values") { + enum.values.foreach { entry => + val written = writeJs(entry) + readJs(written) shouldBe entry + } + } + + it("should fail with invalid values") { + intercept[Exception] { + readJs(Js.Str("D")) + } + intercept[Exception] { + readJs(Js.Num(Int.MaxValue)) + } + } + + } + + describe("Writer") { + + it("should write enum values to JS") { + val numeric = implicitly[Numeric[ValueType]] + val valueTypeWriter = implicitly[Writer[ValueType]] + enum.values.foreach { entry => + writeJs(entry) shouldBe valueTypeWriter.write(entry.value) + } + } + + } + } + } + +} + +sealed abstract class UPickleContentType(val value: Long, name: String) extends LongEnumEntry + +case object UPickleContentType + extends LongEnum[UPickleContentType] + with LongUPickleEnum[UPickleContentType] { + + val values = findValues + + case object Text extends UPickleContentType(value = 1L, name = "text") + case object Image extends UPickleContentType(value = 2L, name = "image") + case object Video extends UPickleContentType(value = 3L, name = "video") + case object Audio extends UPickleContentType(value = 4L, name = "audio") + +} + +sealed abstract class UPickleDrinks(val value: Short, name: String) extends ShortEnumEntry + +case object UPickleDrinks extends ShortEnum[UPickleDrinks] with ShortUPickleEnum[UPickleDrinks] { + + case object OrangeJuice extends UPickleDrinks(value = 1, name = "oj") + case object AppleJuice extends UPickleDrinks(value = 2, name = "aj") + case object Cola extends UPickleDrinks(value = 3, name = "cola") + case object Beer extends UPickleDrinks(value = 4, name = "beer") + + val values = findValues + +} + +sealed abstract class UPickleLibraryItem(val value: Int, val name: String) extends IntEnumEntry + +case object UPickleLibraryItem extends IntEnum[UPickleLibraryItem] with IntUPickleEnum[UPickleLibraryItem] { + + // A good mix of named, unnamed, named + unordered args + case object Book extends UPickleLibraryItem(value = 1, name = "book") + case object Movie extends UPickleLibraryItem(name = "movie", value = 2) + case object Magazine extends UPickleLibraryItem(3, "magazine") + case object CD extends UPickleLibraryItem(4, name = "cd") + + val values = findValues + +} + +sealed abstract class UPickleMovieGenre extends IntEnumEntry + +case object UPickleMovieGenre extends IntEnum[UPickleMovieGenre] with IntUPickleEnum[UPickleMovieGenre] { + + case object Action extends UPickleMovieGenre { + val value = 1 + } + case object Comedy extends UPickleMovieGenre { + val value: Int = 2 + } + case object Romance extends UPickleMovieGenre { + val value = 3 + } + + val values = findValues + +} diff --git a/enumeratum-upickle/src/main/scala/enumeratum/EnrichedPartialFunction.scala b/enumeratum-upickle/src/main/scala/enumeratum/EnrichedPartialFunction.scala new file mode 100644 index 00000000..8c854007 --- /dev/null +++ b/enumeratum-upickle/src/main/scala/enumeratum/EnrichedPartialFunction.scala @@ -0,0 +1,18 @@ +package enumeratum + +/** + * Created by Lloyd on 4/14/16. + * + * Copyright 2016 + */ +object EnrichedPartialFunction { + + /** + * From http://stackoverflow.com/questions/23024626/compose-partial-functions + */ + implicit class PartialFunctionOps[A, B](val pf: PartialFunction[A, B]) extends AnyVal { + def andThenPartial[C](that: PartialFunction[B, C]): PartialFunction[A, C] = + Function.unlift(pf.lift(_) flatMap that.lift) + } + +} diff --git a/enumeratum-upickle/src/main/scala/enumeratum/UPickleEnum.scala b/enumeratum-upickle/src/main/scala/enumeratum/UPickleEnum.scala index 1e604db8..b60d317a 100644 --- a/enumeratum-upickle/src/main/scala/enumeratum/UPickleEnum.scala +++ b/enumeratum-upickle/src/main/scala/enumeratum/UPickleEnum.scala @@ -4,12 +4,12 @@ import upickle.default.Aliases.RW import upickle.default.ReadWriter /** - * Enum mix-in with default Reader and Writers defined (case sensitive) - */ + * Enum mix-in with default Reader and Writers defined (case sensitive) + */ trait UPickleEnum[A <: EnumEntry] { self: Enum[A] => import UPickler._ - implicit val uPickleReadWriter: RW[A] = ReadWriter(writer(this).write, reader(this, false).read) + implicit val uPickleReadWriter: RW[A] = ReadWriter(writer(this).write, reader(enum = this, insensitive = false).read) } \ No newline at end of file diff --git a/enumeratum-upickle/src/main/scala/enumeratum/UPickler.scala b/enumeratum-upickle/src/main/scala/enumeratum/UPickler.scala index bfc2a6d0..c33e87fd 100644 --- a/enumeratum-upickle/src/main/scala/enumeratum/UPickler.scala +++ b/enumeratum-upickle/src/main/scala/enumeratum/UPickler.scala @@ -1,46 +1,35 @@ package enumeratum -import upickle.Js import upickle.default.{ Writer, Reader } +import EnrichedPartialFunction._ object UPickler { /** - * Returns a UPickle [[Reader]] for a given [[Enum]] - * - * @param enum the enum you wish to make a Reader for - * @param insensitive whether or not to match case-insensitively - */ - def reader[A <: EnumEntry](enum: Enum[A], insensitive: Boolean = false): Reader[A] = { - Reader[A] { - val memberFinder: String => Option[A] = if (insensitive) enum.withNameInsensitiveOption else enum.withNameOption - val pfIfJsStr: PartialFunction[Js.Value, String] = { - case Js.Str(s) => s - } - val pfMaybeMember = pfIfJsStr.andThen(memberFinder) - val pfMaybeMemberToMember: PartialFunction[Option[A], A] = { - case Some(a) => a - } - andThenPartial(pfMaybeMember, pfMaybeMemberToMember) + * Returns a UPickle [[Reader]] for a given [[Enum]] + * + * @param enum the enum you wish to make a Reader for + * @param insensitive whether or not to match case-insensitively + */ + def reader[A <: EnumEntry](enum: Enum[A], insensitive: Boolean = false): Reader[A] = Reader[A] { + val stringReader = implicitly[Reader[String]] + val memberFinder: String => Option[A] = if (insensitive) enum.withNameInsensitiveOption else enum.withNameOption + val pfMaybeMemberToMember: PartialFunction[Option[A], A] = { + case Some(a) => a } + stringReader.read.andThen(memberFinder).andThenPartial(pfMaybeMemberToMember) } /** - * Returns a [[Writer]] for a given [[Enum]] - * - * @param enum [[Enum]] to make a [[Writer]] for - */ - def writer[A <: EnumEntry](enum: Enum[A]): Writer[A] = Writer[A] { - case member => Js.Str(member.entryName) - } - - /** - * Private helper for composing PartialFunctions - * - * Stolen from http://stackoverflow.com/questions/23024626/compose-partial-functions - */ - private def andThenPartial[A, B, C](pf1: PartialFunction[A, B], pf2: PartialFunction[B, C]): PartialFunction[A, C] = { - Function.unlift(pf1.lift(_) flatMap pf2.lift) + * Returns a [[Writer]] for a given [[Enum]] + * + * @param enum [[Enum]] to make a [[Writer]] for + */ + def writer[A <: EnumEntry](enum: Enum[A]): Writer[A] = { + val stringWriter = implicitly[Writer[String]] + Writer[A] { + case member => stringWriter.write(member.entryName) + } } } \ No newline at end of file diff --git a/enumeratum-upickle/src/test/scala/enumeratum/Dummy.scala b/enumeratum-upickle/src/test/scala/enumeratum/Dummy.scala index 22c6c25e..9e6b2d32 100644 --- a/enumeratum-upickle/src/test/scala/enumeratum/Dummy.scala +++ b/enumeratum-upickle/src/test/scala/enumeratum/Dummy.scala @@ -1,8 +1,8 @@ package enumeratum /** - * Created by Lloyd on 2/4/15. - */ + * Created by Lloyd on 2/4/15. + */ sealed trait Dummy extends EnumEntry object Dummy extends Enum[Dummy] with UPickleEnum[Dummy] { case object A extends Dummy diff --git a/enumeratum-upickle/src/test/scala/enumeratum/UPickleSpec.scala b/enumeratum-upickle/src/test/scala/enumeratum/UPickleSpec.scala index 328c9899..c9803114 100644 --- a/enumeratum-upickle/src/test/scala/enumeratum/UPickleSpec.scala +++ b/enumeratum-upickle/src/test/scala/enumeratum/UPickleSpec.scala @@ -4,8 +4,8 @@ import org.scalatest._ import upickle.Js /** - * Created by Lloyd on 12/12/15. - */ + * Created by Lloyd on 12/12/15. + */ class UPickleSpec extends FunSpec with Matchers { import Dummy._ diff --git a/macros/compat/src/main/scala-2.10/enumeratum/ContextUtils.scala b/macros/compat/src/main/scala-2.10/enumeratum/ContextUtils.scala new file mode 100644 index 00000000..db63e06f --- /dev/null +++ b/macros/compat/src/main/scala-2.10/enumeratum/ContextUtils.scala @@ -0,0 +1,14 @@ +package enumeratum + +object ContextUtils { + + type Context = scala.reflect.macros.Context + + /** + * Returns a TermName + */ + def termName(c: Context)(name: String): c.universe.TermName = { + c.universe.newTermName(name) + } + +} \ No newline at end of file diff --git a/macros/compat/src/main/scala-2.11/enumeratum/ContextUtils.scala b/macros/compat/src/main/scala-2.11/enumeratum/ContextUtils.scala new file mode 100644 index 00000000..04b85c5e --- /dev/null +++ b/macros/compat/src/main/scala-2.11/enumeratum/ContextUtils.scala @@ -0,0 +1,14 @@ +package enumeratum + +object ContextUtils { + + type Context = scala.reflect.macros.blackbox.Context + + /** + * Returns a TermName + */ + def termName(c: Context)(name: String): c.universe.TermName = { + c.universe.TermName(name) + } + +} \ No newline at end of file diff --git a/macros/compat/src/main/scala-2.11/enumeratum/ValueEnumMacros.scala b/macros/compat/src/main/scala-2.11/enumeratum/ValueEnumMacros.scala new file mode 100644 index 00000000..e0bad3b5 --- /dev/null +++ b/macros/compat/src/main/scala-2.11/enumeratum/ValueEnumMacros.scala @@ -0,0 +1,149 @@ +package enumeratum + +import scala.reflect.ClassTag +import ContextUtils.Context + +object ValueEnumMacros { + + /** + * Finds ValueEntryType-typed objects in scope that have literal value:Int implementations + * + * Note, requires the ValueEntryType to have a 'value' member that has a literal value + */ + def findIntValueEntriesImpl[ValueEntryType: c.WeakTypeTag](c: Context): c.Expr[IndexedSeq[ValueEntryType]] = { + findValueEntriesImpl[ValueEntryType, Int, Int](c)(identity) + } + + /** + * Finds ValueEntryType-typed objects in scope that have literal value:Long implementations + * + * Note, requires the ValueEntryType to have a 'value' member that has a literal value + */ + def findLongValueEntriesImpl[ValueEntryType: c.WeakTypeTag](c: Context): c.Expr[IndexedSeq[ValueEntryType]] = { + findValueEntriesImpl[ValueEntryType, Long, Long](c)(identity) + } + + /** + * Finds ValueEntryType-typed objects in scope that have literal value:Short implementations + * + * Note + * + * - requires the ValueEntryType to have a 'value' member that has a literal value + * - the Short value should be a literal Int (do no need to cast .toShort). + */ + def findShortValueEntriesImpl[ValueEntryType: c.WeakTypeTag](c: Context): c.Expr[IndexedSeq[ValueEntryType]] = { + findValueEntriesImpl[ValueEntryType, Int, Short](c)(_.toShort) // do a transform because there is no such thing as Short literals + } + + /** + * The method that does the heavy lifting. + */ + private[this] def findValueEntriesImpl[ValueEntryType: c.WeakTypeTag, ValueType <: AnyVal: ClassTag, ProcessedValue](c: Context)(processFoundValues: ValueType => ProcessedValue): c.Expr[IndexedSeq[ValueEntryType]] = { + import c.universe._ + val typeSymbol = weakTypeOf[ValueEntryType].typeSymbol + EnumMacros.validateType(c)(typeSymbol) + // Find the trees in the enclosing object that match the given ValueEntryType + val subclassTrees = EnumMacros.enclosedSubClassTrees(c)(typeSymbol) + // Identify the value:ValueType implementations for each of the trees we found and process them if required + val treeWithVals = findValuesForSubclassTrees[ValueType, ProcessedValue](c)(subclassTrees, processFoundValues) + // Make sure the processed found value implementations are unique + ensureUnique[ProcessedValue](c)(treeWithVals) + // Finish by building our Sequence + val subclassSymbols = treeWithVals.map(_.tree.symbol) + EnumMacros.buildSeqExpr[ValueEntryType](c)(subclassSymbols) + } + + /** + * Returns a list of TreeWithVal (tree with value of type ProcessedValueType) for the given trees and transformation + * + * Will abort compilation if not all the trees provided have a literal value member/constructor argument + */ + private[this] def findValuesForSubclassTrees[ValueType: ClassTag, ProcessedValueType](c: Context)(memberTrees: Seq[c.universe.Tree], processFoundValues: ValueType => ProcessedValueType): Seq[TreeWithVal[c.universe.Tree, ProcessedValueType]] = { + val treeWithValues = toTreeWithMaybeVals[ValueType, ProcessedValueType](c)(memberTrees, processFoundValues) + val (hasValueMember, lacksValueMember) = treeWithValues.partition(_.maybeValue.isDefined) + if (lacksValueMember.nonEmpty) { + val classTag = implicitly[ClassTag[ValueType]] + val lacksValueMemberStr = lacksValueMember.map(_.tree.symbol).mkString(", ") + c.abort( + c.enclosingPosition, + s"It looks like not all of the members have a literal/constant 'value:${classTag.runtimeClass}' declaration, namely: $lacksValueMemberStr." + ) + } + hasValueMember.collect { + case TreeWithMaybeVal(tree, Some(v)) => TreeWithVal(tree, v) + } + } + + /** + * Looks through the given trees and tries to find the proper value declaration/constructor argument. + * + * Aborts compilation if the value declaration/constructor is of the wrong type, + */ + private[this] def toTreeWithMaybeVals[ValueType: ClassTag, ProcessedValueType](c: Context)(memberTrees: Seq[c.universe.Tree], processFoundValues: ValueType => ProcessedValueType): Seq[TreeWithMaybeVal[c.universe.Tree, ProcessedValueType]] = { + import c.universe._ + val classTag = implicitly[ClassTag[ValueType]] + val valueTerm = ContextUtils.termName(c)("value") + // go through all the trees + memberTrees.map { declTree => + val values = declTree.collect { + // The tree has a value declaration with a constant value. + case ValDef(_, termName, _, Literal(Constant(i: ValueType))) if termName == valueTerm => Some(i) + // The tree has a method + case Apply(fun, args) if fun.tpe != null => { + // resolve the type of the constructor method and find the parameter terms and arguments provided + val funTpe = fun.tpe + val members: c.universe.MemberScope = funTpe.members + val valueArguments: Iterable[Option[ValueType]] = members.collect { + case constr if constr.isConstructor => { + val asMethod = constr.asMethod + val paramTermNames = asMethod.paramLists.flatten.map(_.asTerm.name) + val paramsWithArg = paramTermNames.zip(args) + paramsWithArg.collectFirst { + // found a (paramName, argument) parameter-argument pair where paramName is "value", and argument is a constant with the right type + case (`valueTerm`, Literal(Constant(i: ValueType))) => i + // found a (paramName, argument) parameter-argument pair where paramName is "value", and argument is a constant with the wrong type + case (`valueTerm`, Literal(Constant(i))) => c.abort(c.enclosingPosition, s"${declTree.symbol} has a value with the wrong type: $i:${i.getClass}, instead of ${classTag.runtimeClass}.") + /* + * found a (_, NamedArgument(argName, argument)) parameter-named pair where the argument is named "value" and the argument itself is of the right type + * + * Note: Can't match without using Ident(ContextUtils.termName(c)(" ")) extractor ??! + */ + case (_, AssignOrNamedArg(Ident(TermName("value")), Literal(Constant(i: ValueType)))) => i + /* + * found a (_, NamedArgument(argName, argument)) parameter-named pair where the argument is named "value" and the argument itself is of the wrong type + */ + case (_, AssignOrNamedArg(Ident(TermName("value")), Literal(Constant(i)))) => c.abort(c.enclosingPosition, s"${declTree.symbol} has a value with the wrong type: $i:${i.getClass}, instead of ${classTag.runtimeClass}") + } + } + } + // We only want the first such constructor argument + valueArguments.collectFirst { case Some(v) => v } + } + } + val processedValue = values.collectFirst { case Some(v) => processFoundValues(v) } + TreeWithMaybeVal(declTree, processedValue) + } + } + + /** + * Ensures that we have unique values for trees, aborting otherwise with a message indicating which trees have the same symbol + */ + private[this] def ensureUnique[A](c: Context)(treeWithVals: Seq[TreeWithVal[c.universe.Tree, A]]): Unit = { + val membersWithValues = treeWithVals.map { treeWithVal => + treeWithVal.tree.symbol -> treeWithVal.value + } + val groupedByValue = membersWithValues.groupBy(_._2).mapValues(_.map(_._1)) + val (valuesWithOneSymbol, valuesWithMoreThanOneSymbol) = groupedByValue.partition(_._2.size <= 1) + if (valuesWithOneSymbol.size != membersWithValues.toMap.keys.size) { + c.abort( + c.enclosingPosition, + s"It does not look like you have unique values. Found the following values correspond to more than one members: $valuesWithMoreThanOneSymbol" + ) + } + } + + // Helper case classes + private[this] case class TreeWithMaybeVal[CTree, T](tree: CTree, maybeValue: Option[T]) + private[this] case class TreeWithVal[CTree, T](tree: CTree, value: T) + +} \ No newline at end of file diff --git a/macros/src/main/scala/enumeratum/EnumMacros.scala b/macros/src/main/scala/enumeratum/EnumMacros.scala index cad3e4cb..5409f3d3 100644 --- a/macros/src/main/scala/enumeratum/EnumMacros.scala +++ b/macros/src/main/scala/enumeratum/EnumMacros.scala @@ -1,34 +1,27 @@ package enumeratum -import scala.reflect.macros.Context +import ContextUtils.Context import scala.util.control.NonFatal -// TODO switch to blackbox.Context when dropping support for 2.10.x - object EnumMacros { + /** + * Finds any [A] in the current scope and returns an expression for a list of them + */ def findValuesImpl[A: c.WeakTypeTag](c: Context): c.Expr[IndexedSeq[A]] = { import c.universe._ - val resultType = implicitly[c.WeakTypeTag[A]].tpe val typeSymbol = weakTypeOf[A].typeSymbol validateType(c)(typeSymbol) val subclassSymbols = enclosedSubClasses(c)(typeSymbol) - if (subclassSymbols.isEmpty) { - c.Expr[IndexedSeq[A]](reify(IndexedSeq.empty[A]).tree) - } else { - c.Expr[IndexedSeq[A]]( - Apply( - TypeApply( - Select(reify(IndexedSeq).tree, newTermName("apply")), - List(TypeTree(resultType)) - ), - subclassSymbols.map(Ident(_)).toList - ) - ) - } + buildSeqExpr[A](c)(subclassSymbols) } - private[this] def validateType(c: Context)(typeSymbol: c.universe.Symbol): Unit = { + /** + * Makes sure that we can work with the given type as an enum: + * + * Aborts if the type is not sealed + */ + private[enumeratum] def validateType(c: Context)(typeSymbol: c.universe.Symbol): Unit = { if (!typeSymbol.asClass.isSealed) c.abort( c.enclosingPosition, @@ -36,19 +29,27 @@ object EnumMacros { ) } - private[this] def enclosedSubClasses(c: Context)(typeSymbol: c.universe.Symbol): Seq[c.universe.Symbol] = { + /** + * Finds the actual trees in the current scope that implement objects of the given type + * + * aborts compilation if: + * + * - the implementations are not all objects + * - the current scope is not an object + */ + private[enumeratum] def enclosedSubClassTrees(c: Context)(typeSymbol: c.universe.Symbol): Seq[c.universe.Tree] = { import c.universe._ - val enclosingBodySubclasses: List[Symbol] = try { + val enclosingBodySubClassTrees: List[Tree] = try { /* - When moving beyond 2.11, we should use this instead, because enclosingClass will be deprecated. + When moving beyond 2.11, we should use this instead, because enclosingClass will be deprecated. - val enclosingModuleMembers = c.internal.enclosingOwner.owner.typeSignature.decls.toList - enclosingModuleMembers.filter { x => - try (x.asModule.moduleClass.asClass.baseClasses.contains(typeSymbol)) catch { case _: Throwable => false } - } + val enclosingModuleMembers = c.internal.enclosingOwner.owner.typeSignature.decls.toList + enclosingModuleMembers.filter { x => + try (x.asModule.moduleClass.asClass.baseClasses.contains(typeSymbol)) catch { case _: Throwable => false } + } - Unfortunately, 2.10.x does not support .enclosingOwner :P - */ + Unfortunately, 2.10.x does not support .enclosingOwner :P + */ val enclosingModule = c.enclosingClass match { case md @ ModuleDef(_, _, _) => md case _ => c.abort( @@ -67,11 +68,38 @@ object EnumMacros { c.warning(c.enclosingPosition, s"Got an exception, indicating a possible bug in Enumeratum. Message: ${e.getMessage}") false } - }.map(_.symbol) + } } catch { case NonFatal(e) => c.abort(c.enclosingPosition, s"Unexpected error: ${e.getMessage}") } - if (!enclosingBodySubclasses.forall(x => x.isModule)) + if (!enclosingBodySubClassTrees.forall(x => x.symbol.isModule)) c.abort(c.enclosingPosition, "All subclasses must be objects.") - else enclosingBodySubclasses + else enclosingBodySubClassTrees } + /** + * Returns a sequence of symbols for objects that implement the given type + */ + private[enumeratum] def enclosedSubClasses(c: Context)(typeSymbol: c.universe.Symbol): Seq[c.universe.Symbol] = { + enclosedSubClassTrees(c)(typeSymbol).map(_.symbol) + } + + /** + * Builds and returns an expression for an IndexedSeq containing the given symbols + */ + private[enumeratum] def buildSeqExpr[A: c.WeakTypeTag](c: Context)(subclassSymbols: Seq[c.universe.Symbol]) = { + import c.universe._ + val resultType = implicitly[c.WeakTypeTag[A]].tpe + if (subclassSymbols.isEmpty) { + c.Expr[IndexedSeq[A]](reify(IndexedSeq.empty[A]).tree) + } else { + c.Expr[IndexedSeq[A]]( + Apply( + TypeApply( + Select(reify(IndexedSeq).tree, ContextUtils.termName(c)("apply")), + List(TypeTree(resultType)) + ), + subclassSymbols.map(Ident(_)).toList + ) + ) + } + } } diff --git a/project/Build.scala b/project/Build.scala index 6d30b0b5..70ae84df 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2,22 +2,26 @@ import sbt._ import sbt.Keys._ import com.typesafe.sbt.SbtScalariform._ import scalariform.formatter.preferences._ -import scoverage.ScoverageSbtPlugin.ScoverageKeys._ +import scoverage.ScoverageKeys._ import com.typesafe.sbt.SbtGhPages.ghpages import com.typesafe.sbt.SbtSite.site import sbtunidoc.Plugin.UnidocKeys._ import sbtunidoc.Plugin._ -import com.typesafe.sbt.SbtGit.{GitKeys => git} +import com.typesafe.sbt.SbtGit.{ GitKeys => git } import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ object Enumeratum extends Build { - lazy val theVersion = "1.3.8-SNAPSHOT" - lazy val theScalaVersion = "2.11.7" - lazy val scalaVersions = Seq("2.10.6", "2.11.7") - lazy val thePlayVersion = "2.4.6" - lazy val scalaTestVersion = "3.0.0-M14" + lazy val theVersion = "1.4.0-SNAPSHOT" + lazy val theScalaVersion = "2.11.8" + lazy val scalaVersions = Seq("2.10.6", "2.11.8") + def thePlayVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { + case Some((2, scalaMajor)) if scalaMajor >= 11 => "2.5.2" + case Some((2, scalaMajor)) if scalaMajor == 10 => "2.4.6" + case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion") + } + lazy val scalaTestVersion = "3.0.0-M16-SNAP3" lazy val root = Project(id = "enumeratum-root", base = file("."), settings = commonWithPublishSettings) .settings( @@ -39,12 +43,13 @@ object Enumeratum extends Build { publishArtifact := false, publishLocal := {} ) - .aggregate(macrosJs, macrosJvm, coreJs, coreJvm, coreJVMTests, enumeratumPlay, enumeratumPlayJson, enumeratumUPickleJs, enumeratumUPickleJvm) + .aggregate(macrosJs, macrosJvm, coreJs, coreJvm, coreJVMTests, enumeratumPlay, enumeratumPlayJson, enumeratumUPickleJs, enumeratumUPickleJvm, enumeratumCirceJs, enumeratumCirceJvm) lazy val core = crossProject.crossType(CrossType.Pure).in(file("enumeratum-core")) .settings( name := "enumeratum" ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = true, include_210Dir = false, includeTestSrcs = true):_*) .settings(testSettings:_*) .settings(commonWithPublishSettings:_*) .dependsOn(macros) @@ -71,6 +76,7 @@ object Enumeratum extends Build { "org.scala-lang" % "scala-reflect" % scalaVersion.value ) ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = true, include_210Dir = true, includeTestSrcs = false):_*) .settings(testSettings:_*) lazy val macrosJs = macros.js lazy val macrosJvm = macros.jvm @@ -78,20 +84,22 @@ object Enumeratum extends Build { lazy val enumeratumPlayJson = Project(id = "enumeratum-play-json", base = file("enumeratum-play-json"), settings = commonWithPublishSettings) .settings( libraryDependencies ++= Seq( - "com.typesafe.play" %% "play-json" % thePlayVersion % "provided" + "com.typesafe.play" %% "play-json" % thePlayVersion(scalaVersion.value) ) ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = false, include_210Dir = false, includeTestSrcs = true):_*) .settings(testSettings:_*) - .dependsOn(coreJvm) + .dependsOn(coreJvm % "test->test;compile->compile") lazy val enumeratumPlay = Project(id = "enumeratum-play", base = file("enumeratum-play"), settings = commonWithPublishSettings) .settings( libraryDependencies ++= Seq( - "com.typesafe.play" %% "play" % thePlayVersion % Provided + "com.typesafe.play" %% "play" % thePlayVersion(scalaVersion.value) ) ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = false, include_210Dir = false, includeTestSrcs = true):_*) .settings(testSettings:_*) - .dependsOn(coreJvm, enumeratumPlayJson) + .dependsOn(coreJvm, enumeratumPlayJson % "test->test;compile->compile") lazy val enumeratumUPickle = crossProject.crossType(CrossType.Pure).in(file("enumeratum-upickle")) .settings(commonWithPublishSettings:_*) @@ -105,7 +113,7 @@ object Enumeratum extends Build { else CrossVersion.binary } - Seq(impl.ScalaJSGroupID.withCross("com.lihaoyi", "upickle", cross) % "0.3.6") + Seq(impl.ScalaJSGroupID.withCross("com.lihaoyi", "upickle", cross) % "0.3.9") } ++ { val additionalMacroDeps = CrossVersion.partialVersion(scalaVersion.value) match { // if scala 2.11+ is used, quasiquotes are merged into scala-reflect @@ -119,11 +127,33 @@ object Enumeratum extends Build { additionalMacroDeps } ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = true, include_210Dir = false, includeTestSrcs = true):_*) .settings(testSettings:_*) - .dependsOn(core) + .dependsOn(core % "test->test;compile->compile") lazy val enumeratumUPickleJs = enumeratumUPickle.js lazy val enumeratumUPickleJvm = enumeratumUPickle.jvm + lazy val enumeratumCirce = crossProject.crossType(CrossType.Pure).in(file("enumeratum-circe")) + .settings(commonWithPublishSettings:_*) + .settings( + name := "enumeratum-circe", + libraryDependencies ++= { + import org.scalajs.sbtplugin._ + val cross = { + if (ScalaJSPlugin.autoImport.jsDependencies.?.value.isDefined) + ScalaJSCrossVersion.binary + else + CrossVersion.binary + } + Seq(impl.ScalaJSGroupID.withCross("io.circe", "circe-core", cross) % "0.4.1") + } + ) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = true, include_210Dir = false, includeTestSrcs = true):_*) + .settings(testSettings:_*) + .dependsOn(core % "test->test;compile->compile") + lazy val enumeratumCirceJs = enumeratumCirce.js + lazy val enumeratumCirceJvm = enumeratumCirce.jvm + lazy val commonSettings = Seq( organization := "com.beachape", @@ -164,7 +194,7 @@ object Enumeratum extends Build { ) lazy val scoverageSettings = Seq( - coverageExcludedPackages := """enumeratum\.EnumMacros""", + coverageExcludedPackages := """enumeratum\.EnumMacros;enumeratum\.ContextUtils;enumeratum\.ValueEnumMacros""", coverageHighlighting := true ) @@ -216,4 +246,32 @@ object Enumeratum extends Build { ) } + /** + * Helper function to add unmanaged source compat directories for different scala versions + */ + private def withCompatUnmanagedSources(jsJvmCrossProject: Boolean, include_210Dir: Boolean, includeTestSrcs: Boolean): Seq[Setting[_]] = { + def compatDirs(projectbase: File, scalaVersion: String, isMain: Boolean) = { + val base = if (jsJvmCrossProject ) projectbase / ".." else projectbase + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, scalaMajor)) if scalaMajor >= 11 => Seq(base / "compat" / "src" / (if (isMain) "main" else "test") / "scala-2.11").map(_.getCanonicalFile) + case Some((2, scalaMajor)) if scalaMajor == 10 && include_210Dir => Seq(base / "compat" / "src" / (if (isMain) "main" else "test") / "scala-2.10").map(_.getCanonicalFile) + case _ => Nil + } + } + val unmanagedMainDirsSetting = Seq( + unmanagedSourceDirectories in Compile ++= { + compatDirs(projectbase = baseDirectory.value, scalaVersion = scalaVersion.value, isMain = true) + } + ) + if (includeTestSrcs) { + unmanagedMainDirsSetting ++ { + unmanagedSourceDirectories in Test ++= { + compatDirs(projectbase = baseDirectory.value, scalaVersion = scalaVersion.value, isMain = false) + } + } + } else { + unmanagedMainDirsSetting + } + } + } diff --git a/project/build.properties b/project/build.properties index da4bbedd..e6a37396 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.9 \ No newline at end of file +sbt.version = 0.13.11 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index f46f7848..1c90ab40 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,18 +1,13 @@ -// The Typesafe repository -resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" - -resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" - -resolvers += Classpaths.sbtPluginReleases +resolvers ++= Seq( + Classpaths.sbtPluginReleases +) // for code formatting -addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") +addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") -// SBT-Scoverage version must be compatible with SBT-coveralls version below -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.1") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.5") -// Upgrade when this issue is solved https://github.com/scoverage/sbt-coveralls/issues/73 -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.0.0") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.1.0") // Provides the ability to generate unifed documentation for multiple projects addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.1") @@ -23,4 +18,4 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") // Provides auto-generating and publishing a gh-pages site addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") \ No newline at end of file +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.8") \ No newline at end of file