All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.api.data.mapping.DefaultRules.scala Maven / Gradle / Ivy

The newest version!
package play.api.data.mapping

/**
 * This trait provides default Rule implementations,
 * from String to various date types and format
 */
trait DateRules {

  /**
   * Rule for the `java.util.Date` type.
   *
   * @param pattern a date pattern, as specified in `java.text.SimpleDateFormat`.
   * @param corrector a simple string transformation function that can be used to transform input String before parsing. Useful when standards are not exactly respected and require a few tweaks
   */
  def date(format: String = "yyyy-MM-dd", corrector: String => String = identity) = Rule.fromMapping[String, java.util.Date] { s =>
    def parseDate(input: String): Option[java.util.Date] = {
      // REMEMBER THAT SIMPLEDATEFORMAT IS NOT THREADSAFE
      val df = new java.text.SimpleDateFormat(format)
      df.setLenient(false)
      try { Some(df.parse(input)) } catch {
        case _: java.text.ParseException => None
      }
    }

    parseDate(corrector(s)) match {
      case Some(d) => Success(d)
      case None => Failure(Seq(ValidationError("error.expected.date", format)))
    }
  }

  /**
   * default Rule for the `java.util.Date` type.
   * It uses the default date format: `yyyy-MM-dd`
   */
  implicit val date: Rule[String, java.util.Date] = date()

  /**
   * Rule for the `org.joda.time.DateTime` type.
   *
   * @param pattern a date pattern, as specified in `java.text.SimpleDateFormat`.
   * @param corrector a simple string transformation function that can be used to transform input String before parsing. Useful when standards are not exactly respected and require a few tweaks
   */
  def jodaDateRule(pattern: String, corrector: String => String = identity) = Rule.fromMapping[String, org.joda.time.DateTime] { s =>
    import scala.util.Try
    import org.joda.time.DateTime

    val df = org.joda.time.format.DateTimeFormat.forPattern(pattern)
    Try(df.parseDateTime(corrector(s)))
      .map(Success.apply)
      .getOrElse(Failure(Seq(ValidationError("error.expected.jodadate.format", pattern))))
  }

  /**
   * default Rule for the `java.util.DateTime` type.
   */
  implicit def jodaTime = Rule.fromMapping[Long, org.joda.time.DateTime] { d =>
    import org.joda.time.DateTime
    Success(new DateTime(d.toLong))
  }

  /**
   * the default implicit JodaDate reads
   * It uses the default date format: `yyyy-MM-dd`
   */
  implicit val jodaDate = jodaDateRule("yyyy-MM-dd")

  implicit def jodaLocalDateRule(pattern: String, corrector: String => String = identity) = Rule.fromMapping[String, org.joda.time.LocalDate] { s =>
    import scala.util.Try
    import org.joda.time.LocalDate
    import org.joda.time.format.{ DateTimeFormat, ISODateTimeFormat }

    val df = if (pattern == "") ISODateTimeFormat.localDateParser else DateTimeFormat.forPattern(pattern)
    Try(LocalDate.parse(corrector(s), df))
      .map(Success.apply)
      .getOrElse(Failure(Seq(ValidationError("error.expected.jodadate.format", pattern))))
  }
  /**
   * the default implicit Rule for `org.joda.time.LocalDate`
   */
  implicit val jodaLocalDate = jodaLocalDateRule("")

  /**
   * ISO 8601 Reads
   */
  val isoDate = Rule.fromMapping[String, java.util.Date] { s =>
    import scala.util.Try
    import java.util.Date
    import org.joda.time.format.ISODateTimeFormat
    val parser = ISODateTimeFormat.dateOptionalTimeParser()
    Try(parser.parseDateTime(s).toDate())
      .map(Success.apply)
      .getOrElse(Failure(Seq(ValidationError("error.expected.date.isoformat"))))
  }

  /**
   * Rule for the `java.sql.Date` type.
   *
   * @param pattern a date pattern, as specified in `java.text.SimpleDateFormat`.
   * @param corrector a simple string transformation function that can be used to transform input String before parsing. Useful when standards are not exactly respected and require a few tweaks
   */
  def sqlDateRule(pattern: String, corrector: String => String = identity): Rule[String, java.sql.Date] =
    date(pattern, corrector).fmap(d => new java.sql.Date(d.getTime))

  /**
   * the default implicit Rule for `java.sql.Date`
   */
  implicit val sqlDate = sqlDateRule("yyyy-MM-dd")
}

/**
 * GenericRules provides basic constraints, utility methods on Rules, and completely generic Rules.
 * Extends this trait if your implementing a new set of Rules.
 */
trait GenericRules {

  /**
   * Create a new constraint, verifying that the provided predicate is satisfied.
   * {{{
   *   def notEmpty = validateWith[String]("validation.nonemptytext"){ !_.isEmpty }
   * }}}
   * @param msg The error message to return if predicate `pred` is not satisfied
   * @param args Arguments for the `ValidationError`
   * @param pred A predicate to satify
   * @return A new Rule validating data of type `I` against a predicate `p`
   */
  def validateWith[I](msg: String, args: Any*)(pred: I => Boolean) = Rule.fromMapping[I, I] {
    v => if (!pred(v)) Failure(Seq(ValidationError(msg, args: _*))) else Success(v)
  }

  /**
   * lift a `Rule[I, O]` to a Rule of `Rule[Seq[I], Array[O]]`
   * {{{
   *   (Path \ "foo").read(array(notEmpty)) // create a Rules validating that an Array contains non-empty Strings
   * }}}
   * @param r A Rule[I, O] to lift
   * @return A new Rule
   */
  implicit def arrayR[I, O: scala.reflect.ClassTag](implicit r: RuleLike[I, O]): Rule[Seq[I], Array[O]] =
    seqR[I, O](r).fmap(_.toArray)

  /**
   * lift a `Rule[I, O]` to a Rule of `Rule[Seq[I], Traversable[O]]`
   * {{{
   *   (Path \ "foo").read(traversable(notEmpty)) // create a Rules validating that an Traversable contains non-empty Strings
   * }}}
   * @param r A Rule[I, O] to lift
   * @return A new Rule
   */
  implicit def traversableR[I, O](implicit r: RuleLike[I, O]): Rule[Seq[I], Traversable[O]] =
    seqR[I, O](r).fmap(_.toTraversable)

  /**
   * lift a `Rule[I, O]` to a Rule of `Rule[Seq[I], Set[O]]`
   * {{{
   *   (Path \ "foo").read(set(notEmpty)) // create a Rules validating that a Set contains non-empty Strings
   * }}}
   * @param r A Rule[I, O] to lift
   * @return A new Rule
   */
  implicit def setR[I, O](implicit r: RuleLike[I, O]): Rule[Seq[I], Set[O]] =
    seqR[I, O](r).fmap(_.toSet)

  /**
   * lift a `Rule[I, O]` to a Rule of `Rule[Seq[I], Seq[O]]`
   * {{{
   *   (Path \ "foo").read(seq(notEmpty)) // create a Rules validating that an Seq contains non-empty Strings
   * }}}
   * @param r A Rule[I, O] to lift
   * @return A new Rule
   */
  implicit def seqR[I, O](implicit r: RuleLike[I, O]): Rule[Seq[I], Seq[O]] =
    Rule {
      case is =>
        val withI = is.zipWithIndex.map {
          case (v, i) =>
            Rule.toRule(r).repath((Path \ i) ++ _).validate(v)
        }
        Validation.sequence(withI)
    }

  /**
   * lift a `Rule[I, O]` to a Rule of `Rule[Seq[I], List[O]]`
   * {{{
   *   (Path \ "foo").read(list(notEmpty)) // create a Rules validating that an List contains non-empty Strings
   * }}}
   * @param r A Rule[I, O] to lift
   * @return A new Rule
   */
  implicit def listR[I, O](implicit r: RuleLike[I, O]): Rule[Seq[I], List[O]] =
    seqR[I, O](r).fmap(_.toList)

  /**
   * Create a Rule validation that a Seq[I] is not empty, and attempt to convert it's first element as a `O`
   * {{{
   *   (Path \ "foo").read(headAs(int))
   * }}}
   */
  implicit def headAs[I, O](implicit c: RuleLike[I, O]) = Rule.fromMapping[Seq[I], I] {
    _.headOption.map(Success[ValidationError, I](_))
      .getOrElse(Failure[ValidationError, I](Seq(ValidationError("error.required"))))
  }.compose(c)

  def not[I, O](r: RuleLike[I, O]) = Rule[I, I] { d =>
    r.validate(d) match {
      case Success(_) => Failure(Nil)
      case Failure(_) => Success(d)
    }
  }

  /**
   * Create a "constant" Rule which is always a success returning value `o`
   * (Path \ "x").read(ignored(42))
   */
  def ignored[I, O](o: O) = (_: Path) => Rule[I, O](_ => Success(o))

  /**
   * Create a Rule of equality
   * {{{
   *   (Path \ "foo").read(equalTo("bar"))
   * }}}
   */
  def equalTo[T](t: T) = validateWith[T]("error.equals", t) { _.equals(t) }

  /**
   * a Rule validating that a String is not empty.
   * @note This Rule does '''NOT''' trim the String beforehand
   * {{{
   *   (Path \ "foo").read(notEmpty)
   * }}}
   */
  def notEmpty = validateWith[String]("error.required") { !_.isEmpty }

  /**
   * {{{
   *   (Path \ "foo").read(min(0)) // validate that there's a positive int at (Path \ "foo")
   * }}}
   */
  def min[T](m: T)(implicit o: Ordering[T]) = validateWith[T]("error.min", m) { x => o.gteq(x, m) }
  /**
   * {{{
   *   (Path \ "foo").read(max(0)) // validate that there's a negative int at (Path \ "foo")
   * }}}
   */
  def max[T](m: T)(implicit o: Ordering[T]) = validateWith[T]("error.max", m) { x => o.lteq(x, m) }
  /**
   * {{{
   *   (Path \ "foo").read(minLength(5)) // The length of this String must be >= 5
   * }}}
   */
  def minLength(l: Int) = validateWith[String]("error.minLength", l) { _.size >= l }
  /**
   * {{{
   *   (Path \ "foo").read(maxLength(5)) // The length of this String must be <= 5
   * }}}
   */
  def maxLength(l: Int) = validateWith[String]("error.maxLength", l) { _.size <= l }
  /**
   * Validate that a String matches the provided regex
   * {{{
   *   (Path \ "foo").read(pattern("[a-z]".r)) // This String contains only letters
   * }}}
   */
  def pattern(regex: scala.util.matching.Regex) = validateWith("error.pattern", regex) { regex.unapplySeq(_: String).isDefined }
  /**
   * Validate that a String is a valid email
   * {{{
   *   (Path \ "email").read(email) // This String is an email
   * }}}
   */
  def email = Rule.fromMapping[String, String](
    pattern("""\b[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\b""".r)
      .validate(_: String)
      .fail.map(_ => Seq(ValidationError("error.email"))))

  /**
   * A Rule that always succeed
   */
  def noConstraint[From]: Constraint[From] = Success(_)

  /**
   * A Rule for HTML checkboxes
   */
  def checked[I](implicit b: RuleLike[I, Boolean]) = Rule.toRule(b) compose GenericRules.equalTo(true)
}

object GenericRules extends GenericRules

trait ParsingRules {

  self: GenericRules =>

  private def stringAs[T](f: PartialFunction[BigDecimal, Validation[ValidationError, T]])(args: Any*) =
    Rule.fromMapping[String, T] {
      val toB: PartialFunction[String, BigDecimal] = { case s if s.matches("""[-+]?[0-9]*\.?[0-9]+""") => BigDecimal(s) }
      toB.lift(_)
        .flatMap(f.lift)
        .getOrElse(Failure(Seq(ValidationError("error.number", args: _*))))
    }

  implicit def intR = stringAs {
    case s if s.isValidInt => Success(s.toInt)
  }("Int")

  implicit def shortR = stringAs {
    case s if s.isValidShort => Success(s.toShort)
  }("Short")

  implicit def booleanR = Rule.fromMapping[String, Boolean] {
    pattern("""(?iu)true|false""".r).validate(_: String)
      .map(java.lang.Boolean.parseBoolean)
      .fail.map(_ => Seq(ValidationError("error.invalid", "Boolean")))
  }

  implicit def longR = stringAs {
    case s if s.isValidLong => Success(s.toLong)
  }("Long")

  // BigDecimal.isValidFloat is buggy, see [SI-6699]
  import java.{ lang => jl }
  private def isValidFloat(bd: BigDecimal) = {
    val d = bd.toFloat
    !d.isInfinity && bd.bigDecimal.compareTo(new java.math.BigDecimal(jl.Float.toString(d), bd.mc)) == 0
  }
  implicit def floatR = stringAs {
    case s if isValidFloat(s) => Success(s.toFloat)
  }("Float")

  // BigDecimal.isValidDouble is buggy, see [SI-6699]
  private def isValidDouble(bd: BigDecimal) = {
    val d = bd.toDouble
    !d.isInfinity && bd.bigDecimal.compareTo(new java.math.BigDecimal(jl.Double.toString(d), bd.mc)) == 0
  }
  implicit def doubleR = stringAs {
    case s if isValidDouble(s) => Success(s.toDouble)
  }("Double")

  import java.{ math => jm }
  implicit def javaBigDecimalR = stringAs {
    case s => Success(s.bigDecimal)
  }("BigDecimal")

  implicit def bigDecimal = stringAs {
    case s => Success(s)
  }("BigDecimal")
}

/**
 * DefaultRules provides basic rules implementations for inputs of type `I`
 * Extends this trait if your implementing a new set of Rules for `I`.
 */
trait DefaultRules[I] extends GenericRules with DateRules {
  import scala.language.implicitConversions
  import play.api.libs.functional._

  protected def opt[J, O](r: => RuleLike[J, O], noneValues: RuleLike[I, I]*)(implicit pick: Path => RuleLike[I, I], coerce: RuleLike[I, J]) = (path: Path) =>
    Rule[I, Option[O]] {
      (d: I) =>
        val isNone = not(noneValues.foldLeft(Rule.zero[I])(_ compose not(_))).fmap(_ => None)
        val v = (pick(path).validate(d).map(Some.apply) orElse Success(None))
        v.viaEither {
          _.right.flatMap {
            case None => Right(None)
            case Some(i) => isNone.orElse(Rule.toRule(coerce).compose(r).fmap[Option[O]](Some.apply)).validate(i).asEither
          }
        }
    }

  def mapR[K, O](r: RuleLike[K, O], p: RuleLike[I, Seq[(String, K)]]): Rule[I, Map[String, O]] = {
    Rule.toRule(p).compose(Path)(
      Rule { fs =>
        val validations = fs.map { f =>
          Rule.toRule(r).repath((Path \ f._1) ++ _)
            .validate(f._2)
            .map(f._1 -> _)
        }
        Validation.sequence(validations).map(_.toMap)
      })
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy