Skip to content

Commit

Permalink
Merge pull request #6 from lloydmeta/feature/case-insensitive
Browse files Browse the repository at this point in the history
Feature/case insensitive
  • Loading branch information
lloydmeta committed Mar 6, 2015
2 parents 787a37b + 62d0db3 commit eb07294
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 29 deletions.
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")
}

}

}

0 comments on commit eb07294

Please sign in to comment.