2
votes

I am struggling with using the extractor pattern in a certain use case where it seems that it could be very powerful.

I start with an input of Map[String, String] coming from a web request. This is either a searchRequest or a countRequest to our api.

searchRequest has keys

  • query(required)
  • fromDate(optional-defaulted)
  • toDate(optional-defaulted)
  • nextToken(optional)
  • maxResults(optional-defaulted)

countRequest has keys

  • query(required)
  • fromDate(optional-defaulted)
  • toDate(optional-defaulted)
  • bucket(optional-defaulted)

Then, I want to convert both of these to a composition type structure like so

protected case class CommonQueryRequest(
  originalQuery: String,
  fromDate: DateTime,
  toDate: DateTime
)

case class SearchQueryRequest(
  commonRequest: CommonQueryRequest,
  maxResults: Int,
  nextToken: Option[Long])

case class CountRequest(commonRequest: CommonQueryRequest, bucket: String)

As you can see, I am sort of converting Strings to DateTimes and Int, Long, etc. My issue is that I really need errors for invalid fromDate vs. invalid toDate format vs. invalid maxResults vs. invalid next token IF available.

At the same time, I need to stick in defaults(which vary depending on if it is a search or count request).

Naturally, with the Map being passed in, you can tell search vs. count so in my first go at this, I added a key="type" with value of search or count so that I could match at least on that.

Am I even going down the correct path? I thought perhaps using matching could be cleaner than our existing implementation but the further I go down this path, it seems to be getting a bit uglier.

thanks, Dean

2

2 Answers

2
votes

I would suggest you to take a look at scalaz.Validation and ValidationNel. It's super nice way to collect validation errors, perfect fit for input request validation.

You can learn more about Validation here: http://eed3si9n.com/learning-scalaz/Validation.html. However in my example I use scalaz 7.1 and it can be a little bit different from what described in this article. However main idea remains the same.

Heres small example for your use case:

  import java.util.NoSuchElementException

  import org.joda.time.DateTime
  import org.joda.time.format.DateTimeFormat

  import scala.util.Try

  import scalaz.ValidationNel
  import scalaz.syntax.applicative._
  import scalaz.syntax.validation._

  type Input = Map[String, String]
  type Error = String

  case class CommonQueryRequest(originalQuery: String,
                                fromDate: DateTime,
                                toDate: DateTime)

  case class SearchQueryRequest(commonRequest: CommonQueryRequest,
                                maxResults: Int,
                                nextToken: Option[Long])

  case class CountRequest(commonRequest: CommonQueryRequest, bucket: String)

  def stringField(field: String)(input: Input): ValidationNel[Error, String] =
    input.get(field) match {
      case None => s"Field $field is not defined".failureNel
      case Some(value) => value.successNel
    }


  val dateTimeFormat = DateTimeFormat.fullTime()

  def dateTimeField(field: String)(input: Input): ValidationNel[Error, DateTime] =
    Try(dateTimeFormat.parseDateTime(input(field))) recover {
      case _: NoSuchElementException => DateTime.now()
    }  match {
      case scala.util.Success(dt) => dt.successNel
      case scala.util.Failure(err) => err.toString.failureNel
    }

  def intField(field: String)(input: Input): ValidationNel[Error, Int] =
    Try(input(field).toInt) match {
      case scala.util.Success(i) => i.successNel
      case scala.util.Failure(err) => err.toString.failureNel
    }

  def countRequest(input: Input): ValidationNel[Error, CountRequest] =
    (
      stringField  ("query")   (input) |@|
      dateTimeField("fromDate")(input) |@|
      dateTimeField("toDate")  (input) |@|
      stringField  ("bucket")  (input)
    ) { (query, from, to, bucket) =>
        CountRequest(CommonQueryRequest(query, from, to), bucket)
    }


  val validCountReq = Map("query" -> "a", "bucket" -> "c")
  val badCountReq = Map("fromDate" -> "invalid format", "bucket" -> "c")

  println(countRequest(validCountReq))
  println(countRequest(badCountReq))
0
votes

scalactic looks pretty cool as well and I may go that route (though not sure if we can use that lib or not but I think I will just proceed forward until someone says no).