Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/case insensitive #6

Merged
merged 3 commits into from
Mar 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion enumeratum-core/src/main/scala/enumeratum/Enum.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,29 @@ trait Enum[A] {
*/
lazy final val namesToValuesMap: Map[String, A] = values map (v => v.toString -> v) toMap

/**
* 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.toString.toLowerCase -> v) toMap

/**
* Optionally returns an [[A]] for a given name.
*/
def withNameOption(name: String): Option[A] = namesToValuesMap get name

/**
* Optionally returns an [[A]] for a given name, disregarding case
*/
def withNameInsensitiveOption(name: String): Option[A] = lowerCaseNamesToValuesMap get name.toLowerCase

/**
* Tries to get an [[A]] by the supplied name. The name corresponds to the .toString
* of the case objects implementing [[A]]
*
* Like [[Enumeration]]'s `withName`, this method will throw if the name does not match any of the values'
* .toString names.
*/
def withName(name: String): A =
namesToValuesMap getOrElse (name, throw new IllegalArgumentException(s"$name is not a member of Enum $this"))
withNameOption(name) getOrElse (throw new NoSuchElementException(s"$name is not a member of Enum $this"))

}
39 changes: 36 additions & 3 deletions enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package enumeratum

import org.scalatest.{ Matchers, FunSpec }
import org.scalatest.OptionValues._

class EnumSpec extends FunSpec with Matchers {

Expand All @@ -25,13 +26,45 @@ class EnumSpec extends FunSpec with Matchers {
}

it("should throw an error otherwise") {
intercept[IllegalArgumentException] {
intercept[NoSuchElementException] {
DummyEnum.withName("hello")
}
}

}

describe("#withNameOption") {

it("should return the proper object when passed the proper string") {
DummyEnum.withNameOption("Hello").value should be(Hello)
DummyEnum.withNameOption("GoodBye").value should be(GoodBye)
DummyEnum.withNameOption("Hi").value should be(Hi)
}

it("should return None otherwise") {
DummyEnum.withNameOption("hello") shouldBe None
}

}

describe("#withNameInsensitiveOption") {

it("should return the proper object when passed the proper string, disregarding cases") {
DummyEnum.withNameInsensitiveOption("Hello").value should be(Hello)
DummyEnum.withNameInsensitiveOption("hello").value should be(Hello)
DummyEnum.withNameInsensitiveOption("GoodBye").value should be(GoodBye)
DummyEnum.withNameInsensitiveOption("goodBye").value should be(GoodBye)
DummyEnum.withNameInsensitiveOption("gOodbye").value should be(GoodBye)
DummyEnum.withNameInsensitiveOption("Hi").value should be(Hi)
DummyEnum.withNameInsensitiveOption("hI").value should be(Hi)
}

it("should return None otherwise") {
DummyEnum.withNameInsensitiveOption("bbeeeech") shouldBe None
}

}

}

describe("when a sealed trait is wrapped in another object") {
Expand All @@ -56,7 +89,7 @@ class EnumSpec extends FunSpec with Matchers {
}

it("should throw an error otherwise") {
intercept[IllegalArgumentException] {
intercept[NoSuchElementException] {
SmartEnum.withName("hello")
}
}
Expand Down Expand Up @@ -86,7 +119,7 @@ class EnumSpec extends FunSpec with Matchers {
}

it("should throw an error otherwise") {
intercept[IllegalArgumentException] {
intercept[NoSuchElementException] {
withName("hello")
}
}
Expand Down
18 changes: 13 additions & 5 deletions enumeratum-play/src/main/scala/enumeratum/Forms.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@ object Forms {
* {{{
* Form("status" -> maps(Status))
* }}}
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
def enum[A](enum: Enum[A]): Mapping[A] = PlayForms.of(format(enum))
def enum[A](enum: Enum[A], insensitive: Boolean = false): Mapping[A] = PlayForms.of(format(enum, insensitive))

/**
* Returns a Formatter for [[Enum]]
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
private[enumeratum] def format[A](enum: Enum[A]): Formatter[A] = new Formatter[A] {
private[enumeratum] def format[A](enum: Enum[A], insensitive: Boolean = false): Formatter[A] = new Formatter[A] {
def bind(key: String, data: Map[String, String]) = {
play.api.data.format.Formats.stringFormat.bind(key, data).right.flatMap { s =>
scala.util.control.Exception.allCatch[A]
.either(enum.withName(s))
.left.map(e => Seq(FormError(key, "error.enum", Nil)))
val maybeBound = if (insensitive) enum.withNameInsensitiveOption(s) else enum.withNameOption(s)
maybeBound match {
case Some(obj) => Right(obj)
case None => Left(Seq(FormError(key, "error.enum", Nil)))
}
}
}
def unbind(key: String, value: A) = Map(key -> value.toString)
Expand Down
23 changes: 13 additions & 10 deletions enumeratum-play/src/main/scala/enumeratum/Json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ package enumeratum

import play.api.libs.json._

import scala.util.Try
import scala.util.control.NonFatal

/**
* Holds JSON reads and writes for [[enumeratum.Enum]]
*/
object Json {

/**
* Returns an Json Reads for a given enum [[Enum]]
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
def reads[A](enum: Enum[A]): Reads[A] = new Reads[A] {
def reads[A](enum: Enum[A], insensitive: Boolean = false): Reads[A] = new Reads[A] {
def reads(json: JsValue): JsResult[A] = json match {
case JsString(s) => {
Try {
JsSuccess(enum.withName(s))
} getOrElse {
JsError(s"Enumeration expected of type: '$enum', but it does not appear to contain the value: '$s'")
val maybeBound = if (insensitive) enum.withNameInsensitiveOption(s) else enum.withNameOption(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'")
}
}
case _ => JsError("String value expected")
Expand All @@ -35,9 +35,12 @@ object Json {

/**
* Returns a Json format for a given enum [[Enum]]
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
def formats[A](enum: Enum[A]): Format[A] = {
Format(reads(enum), writes(enum))
def formats[A](enum: Enum[A], insensitive: Boolean = false): Format[A] = {
Format(reads(enum, insensitive), writes(enum))
}

}
2 changes: 2 additions & 0 deletions enumeratum-play/src/main/scala/enumeratum/PlayEnum.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import play.api.mvc.{ QueryStringBindable, PathBindable }
* An Enum that has a lot of the Play-related implicits built-in so you can avoid
* boilerplate.
*
* Note, the binders created here are case-sensitive.
*
* Things included are:
*
* - implicit JSON format
Expand Down
34 changes: 24 additions & 10 deletions enumeratum-play/src/main/scala/enumeratum/UrlBinders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package enumeratum

import play.api.mvc.PathBindable
import play.api.mvc.QueryStringBindable
import play.api.mvc.QueryStringBindable._

import scala.util.Try

/**
* Created by Lloyd on 2/3/15.
Expand All @@ -13,11 +10,15 @@ object UrlBinders {

/**
* Builds a [[PathBindable]] A for a given Enum A
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
def pathBinder[A](enum: Enum[A]): PathBindable[A] = new PathBindable[A] {
def pathBinder[A](enum: Enum[A], insensitive: Boolean = false): PathBindable[A] = new PathBindable[A] {
def unbind(key: String, value: A): String = value.toString
def bind(key: String, value: String): Either[String, A] = {
Try(enum.withName(value)).toOption match {
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 + "'")
}
Expand All @@ -26,11 +27,24 @@ object UrlBinders {

/**
* Builds a [[QueryStringBindable]] A for a given Enum A
*
* @param enum The enum
* @param insensitive bind in a case-insensitive way, defaults to false
*/
def queryBinder[A](enum: Enum[A]): QueryStringBindable[A] = new Parsing[A](
enum.withName,
_.toString,
(key, exception) => "Cannot parse parameter %s as an Enum: %s".format(key, exception.getMessage)
)
def queryBinder[A](enum: Enum[A], insensitive: Boolean = false): QueryStringBindable[A] =
new QueryStringBindable[A] {

def unbind(key: String, value: A): String = key + "=" + value.toString

def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, A]] = {
params.get(key).flatMap(_.headOption).map { p =>
val maybeBound = if (insensitive) enum.withNameInsensitiveOption(p) else enum.withNameOption(p)
maybeBound match {
case Some(v) => Right(v)
case _ => Left(s"Cannot parse parameter $key as an Enum: $this")
}
}
}
}

}
49 changes: 49 additions & 0 deletions enumeratum-play/src/test/scala/enumeratum/FormSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ class FormSpec extends FunSpec with Matchers {

}

describe(".enum insensitive") {

val subject = Form("hello" -> enum(Dummy, true))

it("should bind proper strings into an Enum value disregarding case") {
val r1 = subject.bind(Map("hello" -> "A"))
val r2 = subject.bind(Map("hello" -> "a"))
val r3 = subject.bind(Map("hello" -> "B"))
val r4 = subject.bind(Map("hello" -> "b"))
r1.value.value shouldBe Dummy.A
r2.value.value shouldBe Dummy.A
r3.value.value shouldBe Dummy.B
r4.value.value shouldBe Dummy.B
}

it("should fail to bind random strings") {
val r = subject.bind(Map("hello" -> "AARSE"))
r.value shouldBe None
}

}

describe(".format") {

val subject = format(Dummy)
Expand All @@ -52,4 +74,31 @@ class FormSpec extends FunSpec with Matchers {

}

describe(".format case insensitive") {

val subject = format(Dummy, true)

it("should bind proper strings into an Enum value") {
val r1 = subject.bind("hello", Map("hello" -> "A"))
val r2 = subject.bind("hello", Map("hello" -> "a"))
val r3 = subject.bind("hello", Map("hello" -> "B"))
val r4 = subject.bind("hello", Map("hello" -> "b"))
r1 shouldBe Right(Dummy.A)
r2 shouldBe Right(Dummy.A)
r3 shouldBe Right(Dummy.B)
r4 shouldBe Right(Dummy.B)
}

it("should fail to bind random strings") {
val r = subject.bind("hello", Map("hello" -> "AARSE"))
r should be('left)
}

it("should unbind ") {
val r = subject.unbind("hello", Dummy.A)
r shouldBe Map("hello" -> "A")
}

}

}
14 changes: 14 additions & 0 deletions enumeratum-play/src/test/scala/enumeratum/JsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ class JsonSpec extends FunSpec with Matchers {
}
}

describe("reads insensitive") {
val reads = Json.reads(Dummy, true)

it("should create a reads that works with valid values disregarding case") {
reads.reads(JsString("A")).asOpt.value should be(Dummy.A)
reads.reads(JsString("a")).asOpt.value should be(Dummy.A)
}

it("should create a reads that fails with invalid values") {
reads.reads(JsString("D")).isError should be(true)
reads.reads(JsNumber(2)).isError should be(true)
}
}

describe("writes") {
val writer = Json.writes(Dummy)

Expand Down
43 changes: 43 additions & 0 deletions enumeratum-play/src/test/scala/enumeratum/UrlBindersSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ class UrlBindersSpec extends FunSpec with Matchers {

}

describe(".pathBinder case insensitive") {

val subject = pathBinder(Dummy, true)

it("should create an enumeration binder that can bind strings corresponding to enum strings, disregarding case") {
subject.bind("hello", "A").right.value shouldBe Dummy.A
subject.bind("hello", "a").right.value shouldBe Dummy.A
subject.bind("hello", "B").right.value shouldBe Dummy.B
subject.bind("hello", "b").right.value shouldBe Dummy.B
}

it("should create an enumeration binder that cannot bind strings not found in the enumeration") {
subject.bind("hello", "Z").isLeft shouldBe true
}

it("should create an enumeration binder that can unbind values") {
subject.unbind("hello", Dummy.A) shouldBe "A"
subject.unbind("hello", Dummy.B) shouldBe "B"
}

}

describe(".queryBinder") {

val subject = queryBinder(Dummy)
Expand All @@ -49,4 +71,25 @@ class UrlBindersSpec extends FunSpec with Matchers {

}

describe(".queryBinder case insensitive") {

val subject = queryBinder(Dummy, true)

it("should create an enumeration binder that can bind strings corresponding to enum strings regardless of case, disregarding case") {
subject.bind("hello", Map("hello" -> Seq("A"))).value.right.value should be(Dummy.A)
subject.bind("hello", Map("hello" -> Seq("a"))).value.right.value should be(Dummy.A)
}

it("should create an enumeration binder that cannot bind strings not found in the enumeration") {
subject.bind("hello", Map("hello" -> Seq("Z"))).value should be('left)
subject.bind("hello", Map("helloz" -> Seq("A"))) shouldBe None
}

it("should create an enumeration binder that can unbind values") {
subject.unbind("hello", Dummy.A) should be("hello=A")
subject.unbind("hello", Dummy.B) should be("hello=B")
}

}

}