├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── form-binder-desc.png ├── integrations ├── README.md ├── playframework │ ├── MyFormBindSupport.scala │ ├── README.md │ └── SampleController.scala └── scalatra │ ├── MyFormBindSupport.scala │ ├── README.md │ └── SampleServlet.scala ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── bind-messages.properties └── scala │ └── com │ └── github │ └── tminglei │ └── bind │ ├── Constraints.scala │ ├── Framework.scala │ ├── FrameworkUtils.scala │ ├── Mappings.scala │ ├── Processors.scala │ └── package.scala └── test └── scala └── com └── github └── tminglei └── bind ├── AttachmentSpec.scala ├── ConstraintsSpec.scala ├── FieldMappingsSpec.scala ├── FormBinderSpec.scala ├── GeneralMappingsSpec.scala ├── GroupMappingsSpec.scala └── ProcessorsSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /project/project 3 | /project/target 4 | /.idea 5 | /.idea_modules 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: scala 3 | scala: 4 | - 2.10.6 5 | - 2.11.8 6 | jdk: 7 | - oraclejdk8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Minglei Tu (tmlneu@gmail.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | form-binder 2 | =========== 3 | [![Build Status](https://travis-ci.org/tminglei/form-binder.svg?branch=master)](https://travis-ci.org/tminglei/form-binder) 4 | 5 | 6 | Form-binder is a micro data binding and validating framework, very easy to use and hack. 7 | 8 | > _It was initially created for my [`Scalatra`](https://github.com/scalatra/scalatra)-based project, but you can easily integrate it with other frameworks._ 9 | 10 | 11 | 12 | Features 13 | ------------- 14 | - very lightweight, only ~900 lines codes (framework + built-in extensions) 15 | - easy use, no verbose codes, and what you see is what you get 16 | - high customizable, you can extend almost every executing point 17 | - easily extensible, every extension interface is an alias of `FunctionN` 18 | - immutable, you can share mapping definition object safely 19 | 20 | 21 | 22 | Usage 23 | ------------- 24 | ![form-binder description](https://github.com/tminglei/form-binder/raw/master/form-binder-desc.png) 25 | 26 | Steps: 27 | 1. define your binder 28 | 2. define your mappings 29 | 3. prepare your data 30 | 4. bind and consume 31 | 32 | 33 | > _p.s. every points above (1)/(2)/(3)/(4)/ are all extendable and you can easily customize it._ 34 | 35 | 36 | 37 | Install & Integrate 38 | -------------------- 39 | To use `form-binder`, pls add the dependency to your [sbt](http://www.scala-sbt.org/ "slick-sbt") project file: 40 | ```scala 41 | libraryDependencies += "com.github.tminglei" %% "form-binder" % "0.12.2" 42 | ``` 43 | 44 | Then you can integrate it with your framework to simplify normal usage. 45 | 46 | Here's the way in my `Scalatra` project: 47 | 48 | First, I defined a `FormBindSupport` trait, 49 | ```scala 50 | trait MyFormBindSupport extends I18nSupport { self: ScalatraBase => 51 | import MyFormBindSupport._ 52 | 53 | before() { 54 | request(BindMessagesKey) = Messages(locale, bundlePath = "bind-messages") 55 | } 56 | 57 | def binder(implicit request: HttpServletRequest) = FormBinder(bindMessages.get, errsTree()) 58 | 59 | /// 60 | private def bindMessages(implicit request: HttpServletRequest): Messages = if (request == null) { 61 | throw new ScalatraException("There needs to be a request in scope to call bindMessages") 62 | } else { 63 | request.get(BindMessagesKey).map(_.asInstanceOf[Messages]).orNull 64 | } 65 | } 66 | ``` 67 | Then mix it to my xxxServlet, and use it like this, 68 | ```scala 69 | import com.github.tminglei.bind.simple._ 70 | 71 | class SampleServlet extends ScalatraServlet with MyFormBindSupport { 72 | 73 | get("/:id") { 74 | val mappings = tmapping( 75 | "id" -> long() 76 | ) 77 | binder.bind(mappings, params).fold( 78 | errors => holt(400, errors), 79 | { case (id) => 80 | Ok(toJson(repos.features.get(id))) 81 | } 82 | ) 83 | } 84 | } 85 | ``` 86 | 87 | _p.s. you can check more integration sample codes under [/integrations](https://github.com/tminglei/form-binder/tree/master/integrations)._ 88 | 89 | 90 | How it works 91 | -------------------- 92 | ### Principle 93 | The core of `form-binder` is `Mapping`, **tree structure** mappings. With **depth-first** algorithm, it was used to validate data and construct the result value object. 94 | 95 | ### Details 96 | 97 | ![form-binder description](https://github.com/tminglei/form-binder/raw/master/form-binder-desc.png) 98 | 99 | #### Major Components: 100 | [1] **binder**: facade, used to bind and trigger processing, two major methods: `bind`, `validate` 101 | [2] **messages**: used to provide error messages 102 | [3] **mapping**: holding constraints, processors, and maybe child mapping, etc. used to validate/convert data, two types of mappings: `field` and `group` 103 | [4] **data**: inputting data map 104 | 105 | > _Check [here](https://github.com/tminglei/form-binder/blob/master/src/main/scala/com/github/tminglei/bind/package.scala) and [here](https://github.com/tminglei/form-binder/blob/master/src/main/scala/com/github/tminglei/bind/Framework.scala) for framework details._ 106 | 107 | binder **bind** method signature (return an `Either` and let user to continue processing): 108 | ```scala 109 | //bind mappings to data, and return an either, holding validation errors (left) or converted value (right) 110 | def bind[T](mapping: Mapping[T], data: Map[String, String], root: String = ""): Either[R, T] 111 | ``` 112 | 113 | binder **validate** method signature (_validate only_ and not consume converted data): 114 | ```scala 115 | //return (maybe processed) errors 116 | def validate[T](mapping: Mapping[T], data: Map[String, String], root: String = "") 117 | ``` 118 | 119 | > _Check [here](https://github.com/tminglei/form-binder/blob/master/src/main/scala/com/github/tminglei/bind/Mappings.scala) for built-in **mapping**s._ 120 | 121 | #### Extension Types: 122 | (1) **ErrProcessor**: used to process error seq, like converting it to json 123 | (2) **PreProcessor**: used to pre-process data, like omitting `$` from `$3,013` 124 | (3) **Constraint**: used to validate raw string data 125 | (4) **ExtraConstraint**: used to validate converted value 126 | 127 | > _* Check [here](https://github.com/tminglei/form-binder/blob/master/src/main/scala/com/github/tminglei/bind/Processors.scala) for built-in `PreProcessor`/`ErrProcessor`._ 128 | > _**Check [here](https://github.com/tminglei/form-binder/blob/master/src/main/scala/com/github/tminglei/bind/Constraints.scala) for built-in `Constraint`/`ExtraConstraint`._ 129 | 130 | #### Options/Features: 131 | 1) **label**: `feature`, readable name for current group/field 132 | 2) **mapTo**: `feature`, map converted value to another type 133 | 3) **i18n**: `feature`, labels starting with `@` will be used as a message key to fetch a i18n value from `messages` 134 | 4) **eagerCheck**: `option`, check errors as more as possible; default `false`, return right after a validation error found 135 | 5) **skipUntouched**: `option`, skip checking untouched empty field/values; default `false`, won't skip untouched 136 | 6) **touchedChecker**: `function`, check whether a field was touched by user; if yes, required fields can't be empty 137 | 138 | 139 | _p.s. for more dev and usage details, pls check the [source codes](https://github.com/tminglei/form-binder/tree/master/src/main/scala/com/github/tminglei/bind) and [test cases](https://github.com/tminglei/form-binder/tree/master/src/test/scala/com/github/tminglei/bind)._ 140 | 141 | 142 | 143 | How to 144 | -------------------- 145 | [TODO] 146 | 147 | 148 | 149 | Build & Test 150 | ------------------- 151 | To hack it and make your contribution, you can setup it like this: 152 | ```bash 153 | $ git clone https://github.com/tminglei/form-binder.git 154 | $ cd form-binder 155 | $ sbt 156 | ... 157 | ``` 158 | To run the tests, pls execute: 159 | ```bash 160 | $ sbt test 161 | ``` 162 | 163 | 164 | 165 | Acknowledgements 166 | ----------------- 167 | - [`Play!`](https://github.com/playframework/playframework) framework development team, for the original idea and implementation; 168 | - [Naoki Takezoe](https://github.com/takezoe) and his [`scalatra-forms`](https://github.com/takezoe/scalatra-forms), a `play-data` implementation for scalatra. 169 | 170 | 171 | License 172 | --------- 173 | The BSD License, Minglei Tu <tmlneu@gmail.com> 174 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "form-binder" 2 | 3 | version := "0.12.2" 4 | 5 | scalaVersion := "2.11.8" 6 | 7 | crossScalaVersions := Seq("2.11.8", "2.10.6") 8 | 9 | organizationName := "form-binder" 10 | 11 | organization := "com.github.tminglei" 12 | 13 | libraryDependencies ++= Seq( 14 | "io.spray" %% "spray-json" % "1.3.2", 15 | "org.slf4j" % "slf4j-api" % "1.7.12", 16 | "org.slf4j" % "slf4j-simple" % "1.7.12" % "provided", 17 | "org.scalatest" %% "scalatest" % "2.2.4" % "test" 18 | ) 19 | 20 | ///////////////// for publish/release /////////////////////////////// 21 | publishTo <<= version { (v: String) => 22 | val nexus = "https://oss.sonatype.org/" 23 | if (v.trim.toUpperCase.endsWith("SNAPSHOT")) 24 | Some("snapshots" at nexus + "content/repositories/snapshots") 25 | else 26 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 27 | } 28 | 29 | publishMavenStyle := true 30 | 31 | publishArtifact in Test := false 32 | 33 | pomIncludeRepository := { _ => false } 34 | 35 | makePomConfiguration ~= { _.copy(configurations = Some(Seq(Compile, Runtime, Optional))) } 36 | 37 | pomExtra := 38 | https://github.com/tminglei/form-binder 39 | 40 | 41 | BSD-style 42 | http://www.opensource.org/licenses/bsd-license.php 43 | repo 44 | 45 | 46 | 47 | git@github.com:tminglei/form-binder.git 48 | scm:git:git@github.com:tminglei/form-binder.git 49 | 50 | 51 | 52 | tminglei 53 | Minglei Tu 54 | +8 55 | 56 | 57 | -------------------------------------------------------------------------------- /form-binder-desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tminglei/form-binder/b1b5fca41e0d185b79bc126210f4e9c58dc87c6f/form-binder-desc.png -------------------------------------------------------------------------------- /integrations/README.md: -------------------------------------------------------------------------------- 1 | To integrate with 3rd framework, two key points: 2 | 1. prepare data, type of `Map[String, String]` 3 | 2. prepare messages, type of `(String) => Option[String]` -------------------------------------------------------------------------------- /integrations/playframework/MyFormBindSupport.scala: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import play.api.mvc._ 4 | import com.github.tminglei.bind.Messages 5 | import com.github.tminglei.bind.simple._ 6 | 7 | trait MyFormBindSupport { self => 8 | /** 9 | * Extracted and copied from [[play.api.data.Form]] 10 | */ 11 | def params(implicit request: Request[_]): Map[String, String] = { 12 | // convert to `Map[String, Seq[String]]` 13 | ((request.body match { 14 | case body: play.api.mvc.AnyContent if body.asFormUrlEncoded.isDefined => body.asFormUrlEncoded.get 15 | case body: play.api.mvc.AnyContent if body.asMultipartFormData.isDefined => body.asMultipartFormData.get.asFormUrlEncoded 16 | case body: play.api.mvc.AnyContent if body.asJson.isDefined => FormUtils.fromJson(js = body.asJson.get).mapValues(Seq(_)) 17 | case body: Map[_, _] => body.asInstanceOf[Map[String, Seq[String]]] 18 | case body: play.api.mvc.MultipartFormData[_] => body.asFormUrlEncoded 19 | case body: play.api.libs.json.JsValue => data.FormUtils.fromJson(js = body).mapValues(Seq(_)) 20 | case _ => Map.empty[String, Seq[String]] 21 | }) ++ request.queryString) 22 | // continue to convert to `Map[String, String]` 23 | .foldLeft(Map.empty[String, String]) { 24 | case (s, (key, values)) if key.endsWith("[]") => s ++ values.zipWithIndex.map { case (v, i) => (key.dropRight(2) + "[" + i + "]") -> v } 25 | case (s, (key, values)) => s + (key -> values.headOption.getOrElse("")) 26 | } 27 | } 28 | 29 | def binder(implicit request: Request) = FormBinder(getMessages(), errsTree()) 30 | 31 | ///TODO prepare your i18n Messages 32 | private def getMessages(implicit request: Request): Messages = ??? 33 | } 34 | -------------------------------------------------------------------------------- /integrations/playframework/README.md: -------------------------------------------------------------------------------- 1 | Integrate with `Play` 2 | ======================== 3 | 4 | Firstly, create your FormBindSupport.scala 5 | 6 | Then, use it in your xxxxxxxxController.scala 7 | -------------------------------------------------------------------------------- /integrations/playframework/SampleController.scala: -------------------------------------------------------------------------------- 1 | import play.api.mvc._ 2 | import com.github.tminglei.bind.simple._ 3 | 4 | object SampleController extends Controller with MyFormBindSupport { 5 | 6 | def find() = Action { implicit request => 7 | val mappings = tmapping( 8 | "cond" -> text() 9 | ) 10 | binder.bind(mappings, data(request.getParameterMap)).fold( 11 | errors => status(400, errors), 12 | { case (cond) => 13 | ok(repos.features.find(cond)) 14 | } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /integrations/scalatra/MyFormBindSupport.scala: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import javax.servlet.http.HttpServletRequest 4 | 5 | import org.scalatra.{ScalatraException, ScalatraBase} 6 | import org.scalatra.i18n.{Messages, I18nSupport} 7 | import com.github.tminglei.bind.simple._ 8 | 9 | object MyFormBindSupport { 10 | val BindMessagesKey = "bind-messages" 11 | } 12 | 13 | trait MyFormBindSupport extends I18nSupport { self: ScalatraBase => 14 | import MyFormBindSupport._ 15 | 16 | before() { 17 | request(BindMessagesKey) = Messages(locale, bundlePath = "bind-messages") 18 | } 19 | 20 | def binder(implicit request: HttpServletRequest) = FormBinder(bindMessages.get, errsTree()) 21 | 22 | /// 23 | private def bindMessages(implicit request: HttpServletRequest): Messages = if (request == null) { 24 | throw new ScalatraException("There needs to be a request in scope to call bindMessages") 25 | } else { 26 | request.get(BindMessagesKey).map(_.asInstanceOf[Messages]).orNull 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /integrations/scalatra/README.md: -------------------------------------------------------------------------------- 1 | Integrate with `Scalatra` 2 | ======================== 3 | 4 | Firstly, create your FormBindSupport.scala 5 | 6 | Then, use it in your xxxxxxxxServlet.scala 7 | -------------------------------------------------------------------------------- /integrations/scalatra/SampleServlet.scala: -------------------------------------------------------------------------------- 1 | import org.scalatra.ScalatraServlet 2 | import com.github.tminglei.bind.simple._ 3 | 4 | class SampleServlet extends ScalatraServlet with MyFormBindSupport { 5 | 6 | get("/:id") { 7 | val mappings = tmapping( 8 | "id" -> long() 9 | ) 10 | binder.bind(mappings, data(multiParams)).fold( 11 | errors => holt(400, errors), 12 | { case (id) => 13 | Ok(toJson(repos.features.get(id))) 14 | } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Warn 3 | 4 | // The Typesafe repository 5 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 6 | 7 | // Add sbt idea plugin 8 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 9 | 10 | // Add sbt eclipse plugin 11 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") 12 | 13 | // Add sbt PGP Plugin 14 | addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.1") -------------------------------------------------------------------------------- /src/main/resources/bind-messages.properties: -------------------------------------------------------------------------------- 1 | error.boolean='%s' must be a boolean 2 | error.number='%s' must be a number 3 | error.double='%s' must be a number 4 | error.float='%s' must be a number 5 | error.long='%s' must be a number 6 | error.bigdecimal='%s' must be a number 7 | error.bigint='%s' must be a number 8 | error.uuid='%s' missing or not a valid uuid 9 | error.required='%s' is required 10 | error.maxlength='%s' cannot be longer than %d characters 11 | error.minlength='%s' cannot be shorter than %d characters 12 | error.max='%s' cannot be greater than %s 13 | error.min='%s' cannot be lower than %s 14 | error.length='%s' must be %d characters 15 | error.pattern='%s' must be '%s' 16 | error.patternnot='%s' mustn't be '%s' 17 | error.arrayindex='%s' contains illegal array index 18 | error.oneof='%s' must be one of %s 19 | error.wronginput=required %s value, but found %s value 20 | error.anypassed='%s' must satisfy any of following: %s -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/Constraints.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import java.time.{LocalDate, LocalDateTime} 4 | import org.slf4j.LoggerFactory 5 | import scala.util.matching.Regex 6 | 7 | trait Constraints { 8 | import FrameworkUtils._ 9 | 10 | private val logger = LoggerFactory.getLogger(Constraints.getClass) 11 | 12 | ///////////////////////////////////////// pre-defined constraints /////////////////////////////// 13 | 14 | def required(message: String = ""): Constraint with Metable[ExtensionMeta] = 15 | mkConstraintWithMeta((name, data, messages, options) => { 16 | logger.debug(s"checking required for $name") 17 | 18 | if (isEmptyInput(name, data, options._inputMode)) { 19 | val errMsg = 20 | if (!isEmptyInput(name, data, PolyInput)) { 21 | val msgTemplate = messages("error.wronginput").get 22 | val simple = getLabel("simple", messages, options) 23 | val compound = getLabel("compound", messages, options) 24 | 25 | if (options._inputMode == SoloInput) 26 | msgTemplate.format(simple, compound) 27 | else 28 | msgTemplate.format(compound, simple) 29 | } else { 30 | (if (message.isEmpty) messages("error.required") else Some(message)) 31 | .get.format(getLabel(name, messages, options)) 32 | } 33 | 34 | Seq((name, errMsg)) 35 | } else Nil 36 | }, meta = mkExtensionMeta("required")) 37 | 38 | def maxLength(length: Int, message: String = ""): Constraint with Metable[ExtensionMeta] = 39 | mkSimpleConstraint((label, vString, messages) => { 40 | logger.debug(s"checking max-length ($length) for '$vString'") 41 | 42 | if (vString != null && vString.length > length) { 43 | Some( (if (message.isEmpty) messages("error.maxlength") else Some(message)) 44 | .get.format(vString, length)) 45 | } else None 46 | }, meta = mkExtensionMeta("maxLength", length)) 47 | 48 | def minLength(length: Int, message: String = ""): Constraint with Metable[ExtensionMeta] = 49 | mkSimpleConstraint((label, vString, messages) => { 50 | logger.debug(s"checking min-length ($length) for '$vString'") 51 | 52 | if (vString != null && vString.length < length) { 53 | Some( (if (message.isEmpty) messages("error.minlength") else Some(message)) 54 | .get.format(vString, length)) 55 | } else None 56 | }, meta = mkExtensionMeta("minLength", length)) 57 | 58 | def length(length: Int, message: String = ""): Constraint with Metable[ExtensionMeta] = 59 | mkSimpleConstraint((label, vString, messages) => { 60 | logger.debug(s"checking length ($length) for '$vString'") 61 | 62 | if (vString != null && vString.length != length) { 63 | Some( (if (message.isEmpty) messages("error.length") else Some(message)) 64 | .get.format(vString, length)) 65 | } else None 66 | }, meta = mkExtensionMeta("length", length)) 67 | 68 | def oneOf(values: Seq[String], message: String = ""): Constraint with Metable[ExtensionMeta] = 69 | mkSimpleConstraint((label, vString, messages) => { 70 | logger.debug(s"checking one of $values for '$vString'") 71 | 72 | if (!values.contains(vString)) { 73 | Some( (if (message.isEmpty) messages("error.oneof") else Some(message)) 74 | .get.format(vString, values.map("'" + _ + "'").mkString(", ")) ) 75 | } else None 76 | }, meta = mkExtensionMeta("oneOf", values)) 77 | 78 | def email(message: String = ""): Constraint with Metable[ExtensionMeta] = pattern(EMAIL_REGEX, message) 79 | 80 | def pattern(regex: Regex, message: String = ""): Constraint with Metable[ExtensionMeta] = 81 | mkSimpleConstraint((label, vString, messages) => { 82 | logger.debug(s"checking pattern '$regex' for '$vString'") 83 | 84 | if (vString != null && regex.findFirstIn(vString).isEmpty) { 85 | Some( (if (message.isEmpty) messages("error.pattern") else Some(message)) 86 | .get.format(vString, regex.toString)) 87 | } else None 88 | }, meta = mkExtensionMeta("pattern", regex)) 89 | 90 | def patternNot(regex: Regex, message: String = ""): Constraint with Metable[ExtensionMeta] = 91 | mkSimpleConstraint((label, vString, messages) => { 92 | logger.debug(s"checking pattern-not '$regex' for '$vString'") 93 | 94 | if (vString != null && regex.findFirstIn(vString).isDefined) { 95 | Some( (if (message.isEmpty) messages("error.patternnot") else Some(message)) 96 | .get.format(vString, regex.toString)) 97 | } else None 98 | }, meta = mkExtensionMeta("patternNot", regex)) 99 | 100 | def indexInKeys(message: String = ""): Constraint with Metable[ExtensionMeta] = 101 | mkConstraintWithMeta((name, data, messages, options) => { 102 | logger.debug(s"checking index in keys for '$name'") 103 | 104 | data.filter(_._1.startsWith(name)).map { case (key, value) => 105 | ILLEGAL_ARRAY_INDEX.findFirstIn(key).map { m => 106 | (key -> (if (message.isEmpty) messages("error.arrayindex") else Some(message)).get.format(key)) 107 | } 108 | }.filterNot(_.isEmpty).map(_.get).toSeq 109 | }, meta = mkExtensionMeta("indexInKeys")) 110 | 111 | /////////////////////////////////////// pre-defined extra constraints //////////////////////////// 112 | 113 | implicit object LocalDateOrdering extends Ordering[LocalDate] { 114 | override def compare(x: LocalDate, y: LocalDate): Int = x.compareTo(y) 115 | } 116 | implicit object LocalDateTimeOrdering extends Ordering[LocalDateTime] { 117 | override def compare(x: LocalDateTime, y: LocalDateTime): Int = x.compareTo(y) 118 | } 119 | 120 | def min[T: Ordering](minVal: T, message: String = ""): ExtraConstraint[T] with Metable[ExtensionMeta] = 121 | mkExtraConstraintWithMeta((label, value, messages) => { 122 | logger.debug(s"checking min value ($minVal) for $value") 123 | 124 | val ord = Ordering[T]; import ord._ 125 | if (value < minVal) { 126 | Seq((if (message.isEmpty) messages("error.min") else Some(message)) 127 | .get.format(value, minVal)) 128 | } else Nil 129 | }, meta = mkExtensionMeta("min", minVal)) 130 | 131 | def max[T: Ordering](maxVal: T, message: String = ""): ExtraConstraint[T] with Metable[ExtensionMeta] = 132 | mkExtraConstraintWithMeta((label, value, messages) => { 133 | logger.debug(s"checking max value ($maxVal) for $value") 134 | 135 | val ord = Ordering[T]; import ord._ 136 | if (value > maxVal) { 137 | Seq((if (message.isEmpty) messages("error.max") else Some(message)) 138 | .get.format(value, maxVal)) 139 | } else Nil 140 | }, meta = mkExtensionMeta("max", maxVal)) 141 | } 142 | 143 | object Constraints extends Constraints -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/Framework.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import FrameworkUtils._ 4 | import org.slf4j.LoggerFactory 5 | 6 | /** 7 | * A mapping, w/ constraints/processors/options, was used to validate/convert input data 8 | */ 9 | trait Mapping[T] extends Metable[MappingMeta] { 10 | 11 | /** 12 | * get the mapping's options 13 | */ 14 | def options: Options = Options.apply() 15 | 16 | /** 17 | * set the mapping's options 18 | */ 19 | def options(setting: Options => Options) = this 20 | 21 | /** 22 | * associate a label the mapping, which will be used when generating error message 23 | */ 24 | def label(label: String) = options(_.copy(_label = Option(label))) 25 | 26 | /** 27 | * prepend some [[PreProcessor]]s to the mapping, which will be used to pre-process inputting data strings 28 | */ 29 | def >-:(newProcessors: PreProcessor*) = options(_.copy(_processors = newProcessors ++: options._processors)) 30 | 31 | /** 32 | * prepend some [[Constraint]]s to the mapping, which will be used to validate data string before it was converted 33 | */ 34 | def >+:(newConstraints: Constraint*) = options(_.copy(_constraints = newConstraints ++: options._constraints)) 35 | 36 | /** 37 | * append some [[ExtraConstraint]]s to the mapping, which will be used to validate data after it was converted 38 | */ 39 | def verifying(validates: ExtraConstraint[T]*) = options(_.copy(_extraConstraints = options._extraConstraints ++ validates)) 40 | 41 | ///--- 42 | /** 43 | * do the converting and return the result object if successful 44 | * (p.s. to prevent failure, [[validate]] should be called before this) 45 | * 46 | * @param name the full name of the data node 47 | * @param data the data map 48 | * @return result object 49 | */ 50 | def convert(name: String, data: Map[String, String]): T 51 | 52 | /** 53 | * do the validating and return the error message list 54 | * (p.s. if successful, the returned list is empty) 55 | * 56 | * @param name the full name of the data node 57 | * @param data the data map 58 | * @param messages the messages object 59 | * @param parentOptions the parent mapping's options 60 | * @return error message list 61 | */ 62 | def validate(name: String, data: Map[String, String], messages: Messages, parentOptions: Options): Seq[(String, String)] 63 | 64 | /** 65 | * used to map/transform result object to another 66 | * 67 | * @param transform the transform function 68 | * @tparam R target result type 69 | * @return a new wrapper mapping 70 | */ 71 | def map[R](transform: T => R): Mapping[R] = new TransformMapping[T, R](this, transform) 72 | } 73 | 74 | ///////////////////////////////////////// core mapping implementations ///////////////////////////////// 75 | /** 76 | * A wrapper mapping, used to transform converted value to another 77 | */ 78 | private 79 | case class TransformMapping[T, R](base: Mapping[T], transform: T => R, 80 | extraConstraints: List[ExtraConstraint[R]] = Nil) extends Mapping[R] { 81 | private val logger = LoggerFactory.getLogger(TransformMapping.getClass) 82 | 83 | override def _meta = base._meta 84 | override def options = base.options 85 | override def options(setting: Options => Options) = copy(base = base.options(setting)) 86 | override def verifying(validates: ExtraConstraint[R]*) = copy(extraConstraints = extraConstraints ++ validates) 87 | 88 | def convert(name: String, data: Map[String, String]): R = { 89 | logger.debug(s"transforming $name") 90 | transform(base.convert(name, data)) 91 | } 92 | 93 | def validate(name: String, data: Map[String, String], messages: Messages, parentOptions: Options): Seq[(String, String)] = { 94 | val errors = base.validate(name, data, messages, parentOptions) 95 | if (errors.isEmpty) 96 | Option(convert(name, data)).map { v => 97 | extraValidateRec(name, v, messages, base.options.merge(parentOptions), extraConstraints) 98 | }.getOrElse(Nil) 99 | else errors 100 | } 101 | 102 | override def toString = _meta.name 103 | } 104 | 105 | /** 106 | * A field mapping is an atomic mapping, which doesn't contain other mappings 107 | */ 108 | case class FieldMapping[T](inputMode: InputMode = SoloInput, doConvert: (String, Map[String, String]) => T, 109 | moreValidate: Constraint = PassValidating, meta: MappingMeta, 110 | override val options: Options = Options.apply()) extends Mapping[T] { 111 | private val logger = LoggerFactory.getLogger(FieldMapping.getClass) 112 | 113 | override val _meta = meta 114 | override def options(setting: Options => Options) = copy(options = setting(options)) 115 | 116 | def convert(name: String, data: Map[String, String]): T = { 117 | logger.debug(s"converting $name") 118 | val newData = processDataRec(name, data, options.copy(_inputMode = inputMode), options._processors) 119 | doConvert(name, newData) 120 | } 121 | 122 | def validate(name: String, data: Map[String, String], messages: Messages, parentOptions: Options): Seq[(String, String)] = { 123 | logger.debug(s"validating $name") 124 | 125 | val theOptions = options.merge(parentOptions).copy(_inputMode = inputMode) 126 | val newData = processDataRec(name, data, theOptions, theOptions._processors) 127 | 128 | if (isUntouchedEmpty(name, newData, theOptions)) Nil 129 | else { 130 | val validates = (if (theOptions._ignoreConstraints) Nil else theOptions._constraints) :+ 131 | moreValidate 132 | val errors = validateRec(name, newData, messages, theOptions, validates) 133 | if (errors.isEmpty) { 134 | Option(doConvert(name, newData)).map { v => 135 | extraValidateRec(name, v, messages, theOptions, theOptions.$extraConstraints) 136 | }.getOrElse(Nil) 137 | } else errors 138 | } 139 | } 140 | 141 | override def toString = _meta.name 142 | } 143 | 144 | /** 145 | * A group mapping is a compound mapping, and is used to construct a complex/nested mapping 146 | */ 147 | case class GroupMapping[T](fields: Seq[(String, Mapping[_])], doConvert: (String, Map[String, String]) => T, 148 | override val options: Options = Options.apply(_inputMode = BulkInput)) extends Mapping[T] { 149 | private val logger = LoggerFactory.getLogger(GroupMapping.getClass) 150 | 151 | override val _meta = MappingMeta("object", reflect.classTag[Product], Nil) 152 | override def options(setting: Options => Options) = copy(options = setting(options)) 153 | 154 | def convert(name: String, data: Map[String, String]): T = { 155 | logger.debug(s"converting $name") 156 | 157 | val newData = processDataRec(name, data, options, options._processors) 158 | if (isEmptyInput(name, newData, options._inputMode)) null.asInstanceOf[T] 159 | else doConvert(name, newData) 160 | } 161 | 162 | def validate(name: String, data: Map[String, String], messages: Messages, parentOptions: Options): Seq[(String, String)] = { 163 | logger.debug(s"validating $name") 164 | 165 | val theOptions = options.merge(parentOptions) 166 | val newData = processDataRec(name, data, theOptions, theOptions._processors) 167 | 168 | if (isUntouchedEmpty(name, newData, theOptions)) Nil 169 | else { 170 | val validates = theOptions._constraints :+ 171 | { (name: String, data: Map[String, String], messages: Messages, options: Options) => 172 | if (isEmptyInput(name, data, options._inputMode)) Nil 173 | else { 174 | fields.flatMap { case (fieldName, binding) => 175 | val fullName = if (name.isEmpty) fieldName else name + "." + fieldName 176 | binding.validate(fullName, data, messages, options) 177 | } 178 | } 179 | } 180 | 181 | val errors = validateRec(name, newData, messages, theOptions, validates) 182 | if (errors.isEmpty) { 183 | if (isEmptyInput(name, newData, options._inputMode)) Nil 184 | else { 185 | extraValidateRec(name, doConvert(name, newData), messages, theOptions, theOptions.$extraConstraints) 186 | } 187 | } else errors 188 | } 189 | } 190 | 191 | override def toString = _meta.name 192 | } 193 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/FrameworkUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import java.util.regex.Pattern 4 | import org.slf4j.LoggerFactory 5 | 6 | import scala.collection.mutable.{ListBuffer, HashMap} 7 | import spray.json._ 8 | 9 | /** 10 | * Framework internal used util methods (!!!NOTE: be careful if using it externally) 11 | */ 12 | object FrameworkUtils { 13 | private val logger = LoggerFactory.getLogger(FrameworkUtils.getClass) 14 | 15 | val MAYBE_QUOTED_STRING = """^"?([^"]*)"?$""".r 16 | val ILLEGAL_ARRAY_INDEX = """.*\[(\d*[^\d\[\]]+\d*)+\].*""".r 17 | /** copied from Play! form/mapping */ 18 | val EMAIL_REGEX = """^(?!\.)("([^"\r\\]|\\["\r\\])*"|([-a-zA-Z0-9!#$%&'*+/=?^_`{|}~]|(? Nil 22 | 23 | def isEmptyStr(str: String): Boolean = 24 | str == null || (str.trim == "") || str.equalsIgnoreCase("null") 25 | 26 | def isEmptyInput(name: String, data: Map[String, String], inputMode: InputMode): Boolean = { 27 | logger.trace(s"checking empty input for $name") 28 | 29 | val prefix1 = if (isEmptyStr(name)) "" else name + "." 30 | val prefix2 = if (isEmptyStr(name)) "" else name + "[" 31 | 32 | def subInput(kv: (String, String)) = kv match { 33 | case (k, v) => (k.startsWith(prefix1) || k.startsWith(prefix2)) && k.length > name.length 34 | } 35 | 36 | inputMode match { 37 | case SoloInput => data.get(name).filterNot(isEmptyStr).isEmpty 38 | case BulkInput => data.find(subInput).isEmpty 39 | case _ => data.get(name).filterNot(isEmptyStr).isEmpty && data.find(subInput).isEmpty 40 | } 41 | } 42 | 43 | // split a dot separated path name to parent part and self part, 44 | // with indicating whether it's an array path 45 | private val OBJECT_ELEM_NAME = "^(.*)\\.([^\\.]+)$".r 46 | private val ARRAY_ELEM_NAME = "^(.*)\\[([\\d]+)\\]$".r 47 | def splitName(name: String): (String, String, Boolean) = { 48 | logger.trace(s"splitting name: '$name'") 49 | 50 | name match { 51 | case ARRAY_ELEM_NAME (name, index) => (name, index, true) 52 | case OBJECT_ELEM_NAME(parent, name) => (parent, name, false) 53 | case _ => ("", name, false) 54 | } 55 | } 56 | 57 | // Find a workObject from map tree workList; create one if not exist 58 | def workObject(workList: HashMap[String, Any], name: String, isArray: Boolean): Any = { 59 | logger.trace(s"get working object for $name") 60 | 61 | workList.get(name) match { 62 | case Some(theObj) => theObj 63 | case None => { 64 | val (parent, self, isArray1) = splitName(name) 65 | val parentObj = workObject(workList, parent, isArray1).asInstanceOf[HashMap[String, Any]] 66 | val theObj = if (isArray) ListBuffer[String]() else HashMap[String, Any]() 67 | parentObj += (self -> theObj) 68 | workList += (name -> theObj) 69 | theObj 70 | } 71 | } 72 | } 73 | 74 | //----------------------------------------------------------------------------- 75 | def mkExtensionMeta(name: String, params: Any*): ExtensionMeta = { 76 | val paramsStr = params.map(Option(_).map(_.toString).getOrElse("")).mkString(", ") 77 | ExtensionMeta(name, s"$name($paramsStr)", params.toList) 78 | } 79 | 80 | // make an internal converter from `(vString) => value` 81 | def mkSimpleConverter[T](convert: String => T) = 82 | (name: String, data: Map[String, String]) => { 83 | convert(data.get(name).orNull) 84 | } 85 | 86 | // make a constraint from `(label, vString, messages) => [error]` (ps: vString may be NULL/EMPTY) 87 | def mkSimpleConstraint(validate: (String, String, Messages) => Option[String], meta: ExtensionMeta): Constraint with Metable[ExtensionMeta] = 88 | mkConstraintWithMeta( 89 | (name, data, messages, options) => { 90 | if (options._inputMode != SoloInput) { 91 | throw new IllegalArgumentException("The constraint should only be used to SINGLE INPUT mapping!") 92 | } else { 93 | validate(getLabel(name, messages, options), data.get(name).orNull, messages) 94 | .map { error => Seq(name -> error) }.getOrElse(Nil) 95 | } 96 | }, meta) 97 | 98 | def mkConstraintWithMeta(validate: (String, Map[String, String], Messages, Options) => Seq[(String, String)], meta: ExtensionMeta) = 99 | new Constraint with Metable[ExtensionMeta] { 100 | def apply(name: String, data: Map[String, String], messages: Messages, options: Options) = 101 | validate.apply(name, data, messages, options) 102 | override def _meta: ExtensionMeta = meta 103 | override def toString = meta.desc 104 | } 105 | 106 | def mkExtraConstraintWithMeta[T](validate: (String, T, Messages) => Seq[String], meta: ExtensionMeta) = 107 | new ExtraConstraint[T] with Metable[ExtensionMeta] { 108 | def apply(label: String, vObj: T, messages: Messages) = validate.apply(label, vObj, messages) 109 | override def _meta: ExtensionMeta = meta 110 | override def toString = meta.desc 111 | } 112 | 113 | def mkPreProcessorWithMeta(process: (String, Map[String, String], Options) => Map[String, String], meta: ExtensionMeta) = 114 | new PreProcessor with Metable[ExtensionMeta] { 115 | def apply(prefix: String, data: Map[String, String], options: Options) = process.apply(prefix, data, options) 116 | override def _meta: ExtensionMeta = meta 117 | override def toString = meta.desc 118 | } 119 | 120 | def isUntouchedEmpty(name: String, data: Map[String, String], options: Options) = 121 | isEmptyInput(name, data, options._inputMode) && 122 | options.skipUntouched.getOrElse(false) && 123 | (options.touchedChecker.isEmpty || ! options.touchedChecker.get.apply(name, data)) 124 | 125 | @scala.annotation.tailrec 126 | def processDataRec(prefix: String, data: Map[String,String], options: Options, 127 | processors: List[PreProcessor]): Map[String,String] = 128 | processors match { 129 | case (process :: rest) => { 130 | val newData = process(prefix, data, options) 131 | processDataRec(prefix, newData, options, rest) 132 | } 133 | case _ => data 134 | } 135 | 136 | def validateRec(name: String, data: Map[String, String], messages: Messages, options: Options, 137 | constraints: List[Constraint]): Seq[(String, String)] = { 138 | if (options.eagerCheck.getOrElse(false)) 139 | constraints.flatMap(_.apply(name, data, messages, options)) 140 | else { 141 | constraints match { 142 | case (constraint :: rest) => 143 | constraint.apply(name, data, messages, options) match { 144 | case Nil => validateRec(name, data, messages, options, rest) 145 | case errors => errors 146 | } 147 | case _ => Nil 148 | } 149 | } 150 | } 151 | 152 | def extraValidateRec[T](name: String, value: => T, messages: Messages, options: Options, 153 | constraints: List[ExtraConstraint[T]]): Seq[(String, String)] = { 154 | val label = getLabel(name, messages, options) 155 | if (options.eagerCheck.getOrElse(false)) 156 | constraints.flatMap(_.apply(label, value, messages).map((name, _))) 157 | else { 158 | constraints match { 159 | case (constraint :: rest) => 160 | constraint.apply(label, value, messages) match { 161 | case Nil => extraValidateRec(name, value, messages, options, rest) 162 | case errors => errors.map { case (message) => (name, message) } 163 | } 164 | case _ => Nil 165 | } 166 | } 167 | } 168 | 169 | ///--- 170 | 171 | // if label starts with '@', use it as key; else use label; else use last field name from full name 172 | def getLabel(fullName: String, messages: Messages, options: Options): String = { 173 | logger.trace(s"getting label for '$fullName' with label: ${options._label})") 174 | 175 | val (parent, name, isArray) = splitName(fullName) 176 | val default = if (isArray) (splitName(parent)._2 + "[" + name + "]") else name 177 | options._label.map { 178 | l => if (l.startsWith("@")) messages(l.substring(1)).get else l 179 | }.getOrElse(default) 180 | } 181 | 182 | // make a Constraint which will try to check and collect errors 183 | def checking[T](check: String => T, messageOrKey: Either[String, String], extraMessageArgs: String*): Constraint = 184 | mkSimpleConstraint( 185 | (label, vString, messages) => { 186 | logger.debug(s"checking for '$vString'") 187 | 188 | vString match { 189 | case null|"" => None 190 | case x => { 191 | try { check(x); None } 192 | catch { 193 | case e: Exception => { 194 | val msgTemplate = messageOrKey.fold(s => s, messages(_).get) 195 | Some(msgTemplate.format((vString +: extraMessageArgs): _*)) 196 | } 197 | } 198 | } 199 | } 200 | }, mkExtensionMeta("checking")) 201 | 202 | // make a compound Constraint, which checks whether any inputting constraints passed 203 | def anyPassed(constraints: Constraint*): Constraint with Metable[ExtensionMeta] = mkConstraintWithMeta( 204 | (name, data, messages, options) => { 205 | logger.debug(s"checking any passed for $name") 206 | 207 | var errErrors: List[(String, String)] = Nil 208 | val found = constraints.find(c => { 209 | val errors = c.apply(name, data, messages, options) 210 | errErrors ++= errors 211 | errors.isEmpty 212 | }) 213 | 214 | if (found.isDefined) Nil 215 | else { 216 | val label = getLabel(name, messages, options) 217 | val errStr = errErrors.map(_._2).mkString("[", ", ", "]") 218 | Seq(name -> messages("error.anypassed").get.format(label, errStr)) 219 | } 220 | }, meta = mkExtensionMeta("anyPassed")) 221 | 222 | // Computes the available indexes for the given key in this set of data. 223 | def indexes(name: String, data: Map[String, String]): Seq[Int] = { 224 | logger.debug(s"get indexes for $name") 225 | // matches: 'prefix[index]...' 226 | val KeyPattern = ("^" + Pattern.quote(name) + """\[(\d+)\].*$""").r 227 | data.toSeq.collect { case (KeyPattern(index), _) => index.toInt }.sorted.distinct 228 | } 229 | 230 | // Computes the available keys for the given prefix in this set of data. 231 | def keys(prefix: String, data: Map[String, String]): Seq[String] = { 232 | logger.debug(s"get keys for $prefix") 233 | // matches: 'prefix.xxx...' | 'prefix."xxx.t"...' 234 | val KeyPattern = ("^" + Pattern.quote(prefix) + """\.("[^"]+"|[^.]+).*$""").r 235 | data.toSeq.collect { case (KeyPattern(key), _) => key }.distinct 236 | } 237 | 238 | // Construct data map from inputting spray json object 239 | def json2map(prefix: String, json: JsValue): Map[String, String] = { 240 | logger.trace(s"json to map - prefix: $prefix") 241 | 242 | json match { 243 | case JsArray(values) => values.zipWithIndex.map { 244 | case (value, i) => json2map(prefix + "[" + i + "]", value) 245 | }.foldLeft(Map.empty[String, String])(_ ++ _) 246 | case JsObject(fields) => fields.map { case (key, value) => 247 | json2map((if (prefix.isEmpty) "" else prefix + ".") + key, value) 248 | }.foldLeft(Map.empty[String, String])(_ ++ _) 249 | case JsNull => Map.empty 250 | case v => Map(prefix -> MAYBE_QUOTED_STRING.replaceAllIn(v.toString, "$1")) 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/Mappings.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import java.time._ 4 | import java.time.format.DateTimeFormatter 5 | import java.util.{Date, UUID} 6 | import org.slf4j.LoggerFactory 7 | 8 | trait Mappings { 9 | import FrameworkUtils._ 10 | import scala.reflect._ 11 | 12 | private val logger = LoggerFactory.getLogger(Mappings.getClass) 13 | 14 | ///////////////////////////////////////// pre-defined field mappings ////////////////////////////// 15 | 16 | def text(constraints: Constraint*): Mapping[String] = 17 | new FieldMapping[String]( 18 | doConvert = mkSimpleConverter(identity), 19 | meta = MappingMeta("string", classTag[String]) 20 | ).>+:(constraints: _*) 21 | 22 | def boolean(constraints: Constraint*): Mapping[Boolean] = 23 | new FieldMapping[Boolean]( 24 | doConvert = mkSimpleConverter { 25 | case null|"" => false 26 | case x => x.toBoolean 27 | }, meta = MappingMeta("boolean", classTag[Boolean]) 28 | ).>+:(checking(_.toBoolean, Right("error.boolean"))) 29 | .>+:(constraints: _*) 30 | 31 | def int(constraints: Constraint*): Mapping[Int] = 32 | new FieldMapping[Int]( 33 | doConvert = mkSimpleConverter { 34 | case null|"" => 0 35 | case x => x.toInt 36 | }, meta = MappingMeta("int", classTag[Int]) 37 | ).>+:(checking(_.toInt, Right("error.number"))) 38 | .>+:(constraints: _*) 39 | 40 | def double(constraints: Constraint*): Mapping[Double] = 41 | new FieldMapping[Double]( 42 | doConvert = mkSimpleConverter { 43 | case null|"" => 0d 44 | case x => x.toDouble 45 | }, meta = MappingMeta("double", classTag[Double]) 46 | ).>+:(checking(_.toDouble, Right("error.double"))) 47 | .>+:(constraints: _*) 48 | 49 | def float(constraints: Constraint*): Mapping[Float] = 50 | new FieldMapping[Float]( 51 | doConvert = mkSimpleConverter { 52 | case null|"" => 0f 53 | case x => x.toFloat 54 | }, meta = MappingMeta("float", classTag[Float]) 55 | ).>+:(checking(_.toFloat, Right("error.float"))) 56 | .>+:(constraints: _*) 57 | 58 | def long(constraints: Constraint*): Mapping[Long] = 59 | new FieldMapping[Long]( 60 | doConvert = mkSimpleConverter { 61 | case null|"" => 0l 62 | case x => x.toLong 63 | }, meta = MappingMeta("long", classTag[Long]) 64 | ).>+:(checking(_.toLong, Right("error.long"))) 65 | .>+:(constraints: _*) 66 | 67 | def bigDecimal(constraints: Constraint*): Mapping[BigDecimal] = 68 | new FieldMapping[BigDecimal]( 69 | doConvert = mkSimpleConverter { 70 | case null|"" => 0d 71 | case x => BigDecimal(x) 72 | }, meta = MappingMeta("bigDecimal", classTag[BigDecimal]) 73 | ).>+:(checking(BigDecimal.apply, Right("error.bigdecimal"))) 74 | .>+:(constraints: _*) 75 | 76 | def bigInt(constraints: Constraint*): Mapping[BigInt] = 77 | new FieldMapping[BigInt]( 78 | doConvert = mkSimpleConverter { 79 | case null|"" => 0l 80 | case x => BigInt(x) 81 | }, meta = MappingMeta("bigInt", classTag[BigInt]) 82 | ).>+:(checking(BigInt.apply, Right("error.bigint"))) 83 | .>+:(constraints: _*) 84 | 85 | def uuid(constraints: Constraint*): Mapping[UUID] = 86 | new FieldMapping[UUID]( 87 | doConvert = mkSimpleConverter { 88 | case null|"" => null 89 | case x => UUID.fromString(x) 90 | }, meta = MappingMeta("uuid", classTag[UUID]) 91 | ).>+:(checking(UUID.fromString, Right("error.uuid"))) 92 | .>+:(constraints: _*) 93 | 94 | def date(constraints: Constraint*): Mapping[LocalDate] = 95 | date("yyyy-MM-dd", constraints: _*) 96 | def date(pattern: String, constraints: Constraint*): Mapping[LocalDate] = { 97 | val formatter = DateTimeFormatter.ofPattern(pattern) 98 | new FieldMapping[LocalDate]( 99 | doConvert = mkSimpleConverter { 100 | case null|"" => null 101 | case x => if (x.matches("^[\\d]+$")) { 102 | val instant = Instant.ofEpochMilli(x.toLong) 103 | LocalDateTime.ofInstant(instant, ZoneId.of("UTC")).toLocalDate() 104 | } else LocalDate.parse(x, formatter) 105 | }, meta = MappingMeta("date", classTag[LocalDate]) 106 | ).>+:(anyPassed( 107 | checking(s => new Date(s.toLong), Left("'%s' not a date long")), 108 | checking(formatter.parse, Right("error.pattern"), pattern) 109 | )).>+:(constraints: _*) 110 | } 111 | 112 | def datetime(constraints: Constraint*): Mapping[LocalDateTime] = 113 | datetime("yyyy-MM-dd'T'HH:mm:ss.SSS", constraints: _*) 114 | def datetime(pattern: String, constraints: Constraint*): Mapping[LocalDateTime] = { 115 | val formatter = DateTimeFormatter.ofPattern(pattern) 116 | new FieldMapping[LocalDateTime]( 117 | doConvert = mkSimpleConverter { 118 | case null|"" => null 119 | case x => if (x.matches("^[\\d]+$")) { 120 | val instant = Instant.ofEpochMilli(x.toLong) 121 | LocalDateTime.ofInstant(instant, ZoneId.of("UTC")) 122 | } else LocalDateTime.parse(x, formatter) 123 | }, meta = MappingMeta("datetime", classTag[LocalDateTime]) 124 | ).>+:(anyPassed( 125 | checking(s => new Date(s.toLong), Left("'%s' not a date long")), 126 | checking(formatter.parse, Right("error.pattern"), pattern) 127 | )).>+:(constraints: _*) 128 | } 129 | 130 | def time(constraints: Constraint*): Mapping[LocalTime] = 131 | time("HH:mm:ss.SSS", constraints: _*) 132 | def time(pattern: String, constraints: Constraint*): Mapping[LocalTime] = { 133 | val formatter = DateTimeFormatter.ofPattern(pattern) 134 | new FieldMapping[LocalTime]( 135 | doConvert = mkSimpleConverter { 136 | case null|"" => null 137 | case x => if (x.matches("^[\\d]+$")) { 138 | val instant = Instant.ofEpochMilli(x.toLong) 139 | LocalDateTime.ofInstant(instant, ZoneId.of("UTC")).toLocalTime 140 | } else LocalTime.parse(x, formatter) 141 | }, meta = MappingMeta("datetime", classTag[LocalTime]) 142 | ).>+:(anyPassed( 143 | checking(s => new Date(s.toLong), Left("'%s' not a date long")), 144 | checking(formatter.parse, Right("error.pattern"), pattern) 145 | )).>+:(constraints: _*) 146 | } 147 | 148 | ///////////////////////////////////////// pre-defined general usage mappings /////////////////////////////// 149 | 150 | def ignored[T](instead: T): Mapping[T] = 151 | FieldMapping[T]( 152 | inputMode = PolyInput, 153 | doConvert = (name, data) => instead, 154 | moreValidate = PassValidating, 155 | meta = MappingMeta(s"ignored to $instead", classTag[Ignored[T]]) 156 | ).options(_.copy(_ignoreConstraints = true)) 157 | 158 | def default[T](base: Mapping[T], value: T): Mapping[T] = 159 | optional(base).map(_.getOrElse(value)) 160 | 161 | def optional[T](base: Mapping[T]): Mapping[Option[T]] = 162 | FieldMapping[Option[T]]( 163 | inputMode = base.options._inputMode, 164 | doConvert = (name, data) => { 165 | logger.debug(s"optional - converting $name") 166 | if (isEmptyInput(name, data, base.options._inputMode)) None 167 | else Some(base.convert(name, data)) 168 | }, 169 | moreValidate = (name, data, messages, options) => { 170 | logger.debug(s"optional - validating $name") 171 | if (isEmptyInput(name, data, base.options._inputMode)) Nil 172 | else { // merge the optional's constraints/label to base mapping then do validating 173 | base.options(_.copy(_constraints = options._constraints ++ base.options._constraints)) 174 | .options(o => o.copy(_label = o._label.orElse(options._label))) 175 | .validate(name, data, messages, options) 176 | } 177 | }, 178 | meta = MappingMeta(s"optional ${base._meta.name}", classTag[Option[T]], List(base)) 179 | ).options(_.copy(_ignoreConstraints = true)) 180 | 181 | def list[T](base: Mapping[T], constraints: Constraint*): Mapping[List[T]] = 182 | seq(base, constraints: _*).map(_.toList) 183 | 184 | def seq[T](base: Mapping[T], constraints: Constraint*): Mapping[Seq[T]] = 185 | FieldMapping[Seq[T]]( 186 | inputMode = BulkInput, 187 | doConvert = (name, data) => { 188 | logger.debug(s"list - converting $name") 189 | indexes(name, data).map { i => 190 | base.convert(name + "[" + i + "]", data) 191 | } 192 | }, 193 | moreValidate = (name, data, messages, theOptions) => { 194 | logger.debug(s"list - validating $name") 195 | indexes(name, data).flatMap { i => 196 | base.validate(name + "[" + i + "]", data, messages, theOptions) 197 | } 198 | }, 199 | meta = MappingMeta(s"seq of ${base._meta.name}", classTag[Seq[T]], List(base)) 200 | ).>+:(constraints: _*) 201 | 202 | def map[V](valueBinding: Mapping[V], constraints: Constraint*): Mapping[Map[String, V]] = 203 | map(text(), valueBinding, constraints: _*) 204 | 205 | def map[K, V](keyBinding: Mapping[K], valueBinding: Mapping[V], 206 | constraints: Constraint*): Mapping[Map[K, V]] = 207 | FieldMapping[Map[K, V]]( 208 | inputMode = BulkInput, 209 | doConvert = (name, data) => { 210 | logger.debug(s"map - converting $name") 211 | Map.empty ++ keys(name, data).map { key => 212 | val keyName = if (isEmptyStr(name)) key else name + "." + key 213 | val unquotedKey = MAYBE_QUOTED_STRING.replaceAllIn(key, "$1") 214 | (keyBinding.convert(key, Map(key -> unquotedKey)), valueBinding.convert(keyName, data)) 215 | } 216 | }, 217 | moreValidate = (name, data, messages, theOptions) => { 218 | logger.debug(s"map - validating $name") 219 | keys(name, data).flatMap { key => 220 | val keyName = if (isEmptyStr(name)) key else name + "." + key 221 | val unquotedKey = MAYBE_QUOTED_STRING.replaceAllIn(key, "$1") 222 | keyBinding.validate(key, Map(key -> unquotedKey), messages, theOptions) ++ 223 | valueBinding.validate(keyName, data, messages, theOptions) 224 | } 225 | }, 226 | meta = MappingMeta(s"map of ${keyBinding._meta.name} -> ${valueBinding._meta.name}", classTag[Map[K,V]], List(keyBinding, valueBinding)) 227 | ).>+:(constraints: _*) 228 | 229 | //////////////////////////////////////////// pre-defined group mappings /////////////////////////////////// 230 | 231 | // tuple version 232 | def tmapping[P1](fm1: (String, Mapping[P1])) = mapping[(P1), P1](fm1)(identity) 233 | // normal version 234 | def mapping[T, P1](fm1: (String, Mapping[P1]))(create: (P1) => T): GroupMapping[T] = 235 | new GroupMapping[T](Seq(fm1), 236 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data)) 237 | ) 238 | 239 | // tuple version 240 | def tmapping[P1, P2](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2])) = mapping[(P1, P2), P1, P2](fm1, fm2)(Tuple2[P1,P2]) 241 | // normal version 242 | def mapping[T, P1, P2](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]))(create: (P1, P2) => T): GroupMapping[T] = 243 | new GroupMapping[T](Seq(fm1, fm2), 244 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data)) 245 | ) 246 | 247 | // tuple version 248 | def tmapping[P1, P2, P3](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3])) = mapping[(P1, P2, P3), P1, P2, P3](fm1, fm2, fm3)(Tuple3[P1,P2,P3]) 249 | // normal version 250 | def mapping[T, P1, P2, P3](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]))(create: (P1, P2, P3) => T): GroupMapping[T] = 251 | new GroupMapping[T](Seq(fm1, fm2, fm3), 252 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data)) 253 | ) 254 | 255 | // tuple version 256 | def tmapping[P1, P2, P3, P4](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4])) = mapping[(P1, P2, P3, P4), P1, P2, P3, P4](fm1, fm2, fm3, fm4)(Tuple4[P1,P2,P3,P4]) 257 | // normal version 258 | def mapping[T, P1, P2, P3, P4](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]))(create: (P1, P2, P3, P4) => T): GroupMapping[T] = 259 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4), 260 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data)) 261 | ) 262 | 263 | // tuple version 264 | def tmapping[P1, P2, P3, P4, P5](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5])) = mapping[(P1, P2, P3, P4, P5), P1, P2, P3, P4, P5](fm1, fm2, fm3, fm4, fm5)(Tuple5[P1,P2,P3,P4,P5]) 265 | // normal version 266 | def mapping[T, P1, P2, P3, P4, P5](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]))(create: (P1, P2, P3, P4, P5) => T): GroupMapping[T] = 267 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5), 268 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data)) 269 | ) 270 | 271 | // tuple version 272 | def tmapping[P1, P2, P3, P4, P5, P6](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6])) = mapping[(P1, P2, P3, P4, P5, P6), P1, P2, P3, P4, P5, P6](fm1, fm2, fm3, fm4, fm5, fm6)(Tuple6[P1, P2, P3, P4, P5, P6]) 273 | // normal version 274 | def mapping[T, P1, P2, P3, P4, P5, P6](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]))(create: (P1, P2, P3, P4, P5, P6) => T): GroupMapping[T] = 275 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6), 276 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data)) 277 | ) 278 | 279 | // tuple version 280 | def tmapping[P1, P2, P3, P4, P5, P6, P7](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7])) = mapping[(P1, P2, P3, P4, P5, P6, P7), P1, P2, P3, P4, P5, P6, P7](fm1, fm2, fm3, fm4, fm5, fm6, fm7)(Tuple7[P1, P2, P3, P4, P5, P6, P7]) 281 | // normal version 282 | def mapping[T, P1, P2, P3, P4, P5, P6, P7](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]))(create: (P1, P2, P3, P4, P5, P6, P7) => T): GroupMapping[T] = 283 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7), 284 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data)) 285 | ) 286 | 287 | // tuple version 288 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8), P1, P2, P3, P4, P5, P6, P7, P8](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8)(Tuple8[P1, P2, P3, P4, P5, P6, P7, P8]) 289 | // normal version 290 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]))(create: (P1, P2, P3, P4, P5, P6, P7, P8) => T): GroupMapping[T] = 291 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8), 292 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data)) 293 | ) 294 | 295 | // tuple version 296 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9), P1, P2, P3, P4, P5, P6, P7, P8, P9](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9)(Tuple9[P1, P2, P3, P4, P5, P6, P7, P8, P9]) 297 | // normal version 298 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T): GroupMapping[T] = 299 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9), 300 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data)) 301 | ) 302 | 303 | // tuple version 304 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10)(Tuple10[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10]) 305 | // normal version 306 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T): GroupMapping[T] = 307 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10), 308 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data)) 309 | ) 310 | 311 | // tuple version 312 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11)(Tuple11[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11]) 313 | // normal version 314 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T): GroupMapping[T] = 315 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11), 316 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data)) 317 | ) 318 | 319 | // tuple version 320 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12)(Tuple12[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12]) 321 | // normal version 322 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T): GroupMapping[T] = 323 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12), 324 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data)) 325 | ) 326 | 327 | // tuple version 328 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13)(Tuple13[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13]) 329 | // normal version 330 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13) => T): GroupMapping[T] = 331 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13), 332 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data)) 333 | ) 334 | 335 | // tuple version 336 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14)(Tuple14[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14]) 337 | // normal version 338 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14) => T): GroupMapping[T] = 339 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14), 340 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data)) 341 | ) 342 | 343 | // tuple version 344 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15)(Tuple15[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15]) 345 | // normal version 346 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15) => T): GroupMapping[T] = 347 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15), 348 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data)) 349 | ) 350 | 351 | // tuple version 352 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16)(Tuple16[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16]) 353 | // normal version 354 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16) => T): GroupMapping[T] = 355 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16), 356 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data)) 357 | ) 358 | 359 | // tuple version 360 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17)(Tuple17[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17]) 361 | // normal version 362 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17) => T): GroupMapping[T] = 363 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17), 364 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data)) 365 | ) 366 | 367 | // tuple version 368 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18)(Tuple18[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18]) 369 | // normal version 370 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18) => T): GroupMapping[T] = 371 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18), 372 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data), conv(fm18, name, data)) 373 | ) 374 | 375 | // tuple version 376 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19)(Tuple19[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19]) 377 | // normal version 378 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19) => T): GroupMapping[T] = 379 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19), 380 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data), conv(fm18, name, data), conv(fm19, name, data)) 381 | ) 382 | 383 | // tuple version 384 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20)(Tuple20[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20]) 385 | // normal version 386 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20) => T): GroupMapping[T] = 387 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20), 388 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data), conv(fm18, name, data), conv(fm19, name, data), conv(fm20, name, data)) 389 | ) 390 | 391 | // tuple version 392 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20]), fm21: (String, Mapping[P21])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20, fm21)(Tuple21[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21]) 393 | // normal version 394 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20]), fm21: (String, Mapping[P21]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21) => T): GroupMapping[T] = 395 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20, fm21), 396 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data), conv(fm18, name, data), conv(fm19, name, data), conv(fm20, name, data), conv(fm21, name, data)) 397 | ) 398 | 399 | // tuple version 400 | def tmapping[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20]), fm21: (String, Mapping[P21]), fm22: (String, Mapping[P22])) = mapping[(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22), P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22](fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20, fm21, fm22)(Tuple22[P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22]) 401 | // normal version 402 | def mapping[T, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22](fm1: (String, Mapping[P1]), fm2: (String, Mapping[P2]), fm3: (String, Mapping[P3]), fm4: (String, Mapping[P4]), fm5: (String, Mapping[P5]), fm6: (String, Mapping[P6]), fm7: (String, Mapping[P7]), fm8: (String, Mapping[P8]), fm9: (String, Mapping[P9]), fm10: (String, Mapping[P10]), fm11: (String, Mapping[P11]), fm12: (String, Mapping[P12]), fm13: (String, Mapping[P13]), fm14: (String, Mapping[P14]), fm15: (String, Mapping[P15]), fm16: (String, Mapping[P16]), fm17: (String, Mapping[P17]), fm18: (String, Mapping[P18]), fm19: (String, Mapping[P19]), fm20: (String, Mapping[P20]), fm21: (String, Mapping[P21]), fm22: (String, Mapping[P22]))(create: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22) => T): GroupMapping[T] = 403 | new GroupMapping[T](Seq(fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8, fm9, fm10, fm11, fm12, fm13, fm14, fm15, fm16, fm17, fm18, fm19, fm20, fm21, fm22), 404 | doConvert = (name: String, data: Map[String, String]) => create(conv(fm1, name, data), conv(fm2, name, data), conv(fm3, name, data), conv(fm4, name, data), conv(fm5, name, data), conv(fm6, name, data), conv(fm7, name, data), conv(fm8, name, data), conv(fm9, name, data), conv(fm10, name, data), conv(fm11, name, data), conv(fm12, name, data), conv(fm13, name, data), conv(fm14, name, data), conv(fm15, name, data), conv(fm16, name, data), conv(fm17, name, data), conv(fm18, name, data), conv(fm19, name, data), conv(fm20, name, data), conv(fm21, name, data), conv(fm22, name, data)) 405 | ) 406 | 407 | /** convert param string to value with given binding */ 408 | private def conv[T](fieldMapping: (String, Mapping[T]), name: String, data: Map[String, String]): T = 409 | fieldMapping match { case (fieldName, mapping) => 410 | val fullName = if (name.isEmpty) fieldName else name + "." + fieldName 411 | mapping.convert(fullName, data) 412 | } 413 | } 414 | 415 | object Mappings extends Mappings -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/Processors.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import java.util.regex.Pattern 4 | import org.slf4j.LoggerFactory 5 | 6 | import scala.collection.mutable.ListBuffer 7 | import scala.util.matching.Regex 8 | 9 | trait Processors { 10 | import FrameworkUtils._ 11 | 12 | private val logger = LoggerFactory.getLogger(Processors.getClass) 13 | 14 | //////////////////////////////////// pre-defined pre-processors //////////////////////////////// 15 | 16 | def trim(): PreProcessor with Metable[ExtensionMeta] = 17 | mkPreProcessorWithMeta((prefix, data, options) => { 18 | logger.debug(s"trimming '$prefix'") 19 | data.map { case (k, v) => 20 | if (!k.startsWith(prefix)) (k, v) 21 | else (k, Option(v).map(_.trim).orNull) 22 | } 23 | }, meta = mkExtensionMeta("trim")) 24 | 25 | def omit(str: String) = replacing(Pattern.quote(str).r, "", mkExtensionMeta("omit", str)) 26 | 27 | def omitLeft(str: String) = replacing(("^"+Pattern.quote(str)).r, "", mkExtensionMeta("omitLeft", str)) 28 | 29 | def omitRight(str: String) = replacing((Pattern.quote(str)+"$").r, "", mkExtensionMeta("omitRight", str)) 30 | 31 | def omitRedundant(str: String) = replacing(("["+Pattern.quote(str)+"]+").r, str, mkExtensionMeta("omitRedundant", str)) 32 | 33 | def omitMatched(regex: Regex, replacement: String = "") = replacing(regex, replacement, mkExtensionMeta("omitMatched", regex, replacement)) 34 | 35 | protected def replacing(regex: Regex, replacement: String, meta: ExtensionMeta): PreProcessor with Metable[ExtensionMeta] = 36 | mkPreProcessorWithMeta((prefix, data, options) => { 37 | logger.debug(s"replacing '$regex' with '$replacement'") 38 | data.map { case (k, v) => 39 | if (!k.startsWith(prefix)) (k, v) 40 | else (k, Option(v).map(regex.replaceAllIn(_, replacement)).orNull) 41 | } 42 | }, meta = meta) 43 | 44 | def changePrefix(from: String, to: String): PreProcessor with Metable[ExtensionMeta] = 45 | mkPreProcessorWithMeta((prefix, data, options) => { 46 | logger.debug(s"changing prefix from '$from' to '$to' at '$prefix'") 47 | data.map { case (k, v) => 48 | if (!k.startsWith(prefix)) (k, v) 49 | else { 50 | val tail = k.substring(prefix.length).replaceFirst("^[\\.]?" + Pattern.quote(from), to) 51 | .replaceFirst("^\\.", "") 52 | val newKey = if (isEmptyStr(tail)) prefix else (prefix + "." + tail) 53 | .replaceFirst("^\\.", "") 54 | (newKey, v) 55 | } 56 | } 57 | }, meta = ExtensionMeta("changePrefix", s"changePrefix(from '$from' to '$to')", List(from, to))) 58 | 59 | def expandJson(prefix: Option[String] = None): PreProcessor with Metable[ExtensionMeta] = 60 | mkPreProcessorWithMeta((prefix1, data, options) => { 61 | logger.debug(s"expanding json at '${prefix.getOrElse(prefix1)}'") 62 | val thePrefix = prefix.getOrElse(prefix1) 63 | if (!isEmptyStr(data.get(thePrefix).orNull)) { 64 | val json = spray.json.JsonParser(data(thePrefix)) 65 | (data - thePrefix) ++ json2map(thePrefix, json) 66 | } else data 67 | }, meta = mkExtensionMeta("expandJson", prefix)) 68 | 69 | def expandJsonKeys(prefix: Option[String] = None): PreProcessor with Metable[ExtensionMeta] = 70 | mkPreProcessorWithMeta((prefix1, data, options) => { 71 | logger.debug(s"expanding json keys at '${prefix.getOrElse(prefix1)}'") 72 | val data1 = expandJson(prefix).apply(prefix1, data, options) 73 | val data2 = expandListKeys(prefix).apply(prefix1, data1, options) 74 | data2 75 | }, meta = mkExtensionMeta("expandJsonKeys", prefix)) 76 | 77 | def expandListKeys(prefix: Option[String] = None): PreProcessor with Metable[ExtensionMeta] = 78 | mkPreProcessorWithMeta((prefix1, data, options) => { 79 | logger.debug(s"expanding list keys at '${prefix.getOrElse(prefix1)}'") 80 | val thePrefix = prefix.getOrElse(prefix1) 81 | val p = Pattern.compile("^" + Pattern.quote(thePrefix) + "\\[[\\d]+\\].*") 82 | data.map { case (k, v) => 83 | if (p.matcher(k).matches()) { 84 | val newKey = if (isEmptyStr(thePrefix)) v else thePrefix + "." + v 85 | (newKey, "true") 86 | } else (k, v) 87 | } 88 | }, meta = mkExtensionMeta("expandListKeys", prefix)) 89 | 90 | //////////////////////////////////// pre-defined post err-processors ///////////////////////////// 91 | import scala.collection.mutable.HashMap 92 | 93 | def foldErrs(): ErrProcessor[Map[String, List[String]]] = 94 | (errors: Seq[(String, String)]) => { 95 | logger.debug("folding errors") 96 | Map.empty ++ errors.groupBy(_._1).map { 97 | case (key, pairs) => (key, pairs.map(_._2).toList) 98 | } 99 | } 100 | 101 | def errsTree(): ErrProcessor[Map[String, Any]] = 102 | (errors: Seq[(String, String)]) => { 103 | logger.debug("converting errors list to errors tree") 104 | 105 | val root = HashMap[String, Any]() 106 | val workList = HashMap[String, Any]("" -> root) 107 | errors.map { case (name, error) => 108 | val name1 = name.replaceAll("\\[", ".").replaceAll("\\]", "") 109 | val workObj = workObject(workList, name1 + "._errors", true) 110 | .asInstanceOf[ListBuffer[String]] 111 | workObj += (error) 112 | } 113 | root.toMap 114 | } 115 | 116 | //////////////////////////////////// pre-defined touched checkers //////////////////////////////// 117 | 118 | def listTouched(touched: List[String]): TouchedChecker = 119 | (prefix, data) => { 120 | logger.debug(s"checking touched in list for '$prefix'") 121 | touched.find(_.startsWith(prefix)).isDefined 122 | } 123 | 124 | def prefixTouched(dataPrefix: String, touchedPrefix: String): TouchedChecker = 125 | (prefix, data) => { 126 | logger.debug(s"checking touched with data prefix '$dataPrefix' and touched prefix '$touchedPrefix' for '$prefix'") 127 | val prefixBeChecked = prefix.replaceAll("^" + Pattern.quote(dataPrefix), touchedPrefix) 128 | data.keys.find(_.startsWith(prefixBeChecked)).isDefined 129 | } 130 | } 131 | 132 | object Processors extends Processors 133 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tminglei/bind/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei 2 | 3 | package object bind { 4 | 5 | // (messageKey) => [message] (ps: all input parameters WON'T BE NULL/EMPTY) 6 | type Messages = (String) => Option[String] 7 | 8 | // (name, data, messages, options) => errors (ps: all input parameters WON'T BE NULL/EMPTY) 9 | type Constraint = (String, Map[String, String], Messages, Options) => Seq[(String, String)] 10 | 11 | // (label, vObject, messages) => errors (ps: all input parameters WON'T BE NULL/EMPTY) 12 | type ExtraConstraint[T] = (String, T, Messages) => Seq[String] 13 | 14 | // (prefix, data, options) => data (ps: all input parameters WON'T BE NULL/EMPTY) 15 | type PreProcessor = (String, Map[String, String], Options) => Map[String, String] 16 | 17 | // (errors) => R (ps: all inputs parameter WON'T BE NULL/EMPTY) 18 | type ErrProcessor[R] = (Seq[(String, String)]) => R 19 | 20 | // (prefix, data) => true/false (ps: all input parameters WON'T BE NULL/EMPTY) 21 | type TouchedChecker = (String, Map[String, String]) => Boolean 22 | 23 | /** 24 | * A helper object, used to simplify `form-binder` usage 25 | * 26 | * Note: add {{{import com.github.tminglei.bind.simple._}}} to your class, then 27 | * you can use form binder's built-in mappings/constraints/processors directly 28 | */ 29 | object simple extends Mappings with Constraints with Processors { 30 | import collection.convert.wrapAsScala._ 31 | 32 | type FormBinder[R] = com.github.tminglei.bind.FormBinder[R] 33 | val FormBinder = com.github.tminglei.bind.FormBinder 34 | 35 | ///-- 36 | def data(params: java.util.Map[String, Array[String]]): Map[String, String] = 37 | data(params.map { case (k, v) => (k, v.toSeq) }.toMap) 38 | 39 | private val MAYBE_TAIL_BRACKETS = "([^\\[\\]]*)\\[\\]$".r 40 | def data(params: Map[String, Seq[String]]): Map[String, String] = { 41 | params.flatMap { case (key, values) => 42 | if (values == null || values.length == 0) Nil 43 | else if (values.length == 1 && ! key.endsWith("[]")) Seq((key, values(0))) 44 | else { 45 | for(i <- 0 until values.length) yield { 46 | val cleanKey = MAYBE_TAIL_BRACKETS.replaceAllIn(key, "$1") 47 | (s"$cleanKey[$i]", values(i)) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | package bind { 56 | 57 | import org.slf4j.LoggerFactory 58 | 59 | /** 60 | * The Facade class 61 | */ 62 | case class FormBinder[R](messages: Messages, 63 | errProcessor: ErrProcessor[R] = identity[Seq[(String, String)]] _) { 64 | 65 | private val logger = LoggerFactory.getLogger(FormBinder.getClass) 66 | 67 | /** 68 | * bind mappings to data, and return an either, holding validation errors (left) or converted value (right) 69 | */ 70 | def bind[T](mapping: Mapping[T], data: Map[String, String], root: String = ""): Either[R, T] = { 71 | logger.debug(s"start binding ... from '$root'") 72 | mapping.validate(root, data, messages, Options.apply()) match { 73 | case Nil => Right(mapping.convert(root, data)) 74 | case errs => Left(errProcessor.apply(errs)) 75 | } 76 | } 77 | 78 | /** 79 | * bind and validate data, return (processed) errors 80 | */ 81 | def validate[T](mapping: Mapping[T], data: Map[String, String], root: String = ""): R = { 82 | logger.debug(s"start validating ... from '$root'") 83 | errProcessor.apply( 84 | mapping.validate(root, data, messages, Options.apply()) 85 | ) 86 | } 87 | } 88 | 89 | ///////////////////////////////////////// helper interfaces and classes ////////////////////////////////// 90 | /** 91 | * Some mark traits, used to help ensure the matching of fixtures in a data processing flow/pipe 92 | */ 93 | sealed trait InputMode 94 | object SoloInput extends InputMode 95 | object BulkInput extends InputMode 96 | object PolyInput extends InputMode 97 | 98 | /** 99 | * Trait/classes for meta support 100 | */ 101 | trait Metable[M] { 102 | def _meta: M 103 | } 104 | 105 | case class MappingMeta(name: String, targetType: reflect.ClassTag[_], baseMappings: List[Mapping[_]] = Nil) 106 | case class ExtensionMeta(name: String, desc: String, params: List[_] = Nil) 107 | 108 | final class Ignored[T] 109 | 110 | /** 111 | * Used to transfer config info in the data processing flow 112 | */ 113 | case class Options( 114 | /** whether to check errors as more as possible */ 115 | eagerCheck: Option[Boolean] = None, 116 | /** whether to skip checking untouched empty field/values */ 117 | skipUntouched: Option[Boolean] = None, 118 | /** used to check whether a field was touched by user; if yes, required fields can't be empty */ 119 | touchedChecker: Option[TouchedChecker] = None, 120 | // internal state, only applied to current mapping 121 | private[bind] val _label: Option[String] = None, 122 | private[bind] val _constraints: List[Constraint] = Nil, 123 | private[bind] val _extraConstraints: List[ExtraConstraint[_]] = Nil, 124 | private[bind] val _processors: List[PreProcessor] = Nil, 125 | private[bind] val _ignoreConstraints: Boolean = false, 126 | private[bind] val _inputMode: InputMode = SoloInput, 127 | // used to associate/hold application specific object 128 | private[bind] val _attachment: Option[Any] = None 129 | ) { 130 | def eagerCheck(check: Boolean): Options = copy(eagerCheck = Some(check)) 131 | def skipUntouched(skip: Boolean): Options = copy(skipUntouched = Some(skip)) 132 | def touchedChecker(touched: TouchedChecker): Options = copy(touchedChecker = Some(touched)) 133 | 134 | def $extraConstraints[T] = _extraConstraints.map(_.asInstanceOf[ExtraConstraint[T]]) 135 | 136 | def merge(parent: Options): Options = copy( 137 | eagerCheck = eagerCheck.orElse(parent.eagerCheck), 138 | skipUntouched = skipUntouched.orElse(parent.skipUntouched), 139 | touchedChecker = touchedChecker.orElse(parent.touchedChecker)) 140 | } 141 | } -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/AttachmentSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import simple._ 5 | 6 | class AttachmentSpec extends FunSpec with Matchers { 7 | 8 | describe("test mapping extension support") { 9 | it("simple test") { 10 | tmapping( 11 | "id" -> long().$.in("path").$$.$.desc("pet id").$$, 12 | "name" -> text().$.in("query").desc("pet name").$$ 13 | ).fields.foreach { 14 | case ("id", id) => id.options._attachment.orNull should be (Attachment(Some("path"), Some("pet id"))) 15 | case ("name", name) => name.options._attachment.orNull should be (Attachment(Some("query"), Some("pet name"))) 16 | } 17 | } 18 | } 19 | 20 | ///--- 21 | 22 | case class Attachment( 23 | _in: Option[String] = None, 24 | _desc: Option[String] = None 25 | ) 26 | 27 | case class AttachmentBuilder[T](mapping: Mapping[T], attachment: Attachment) { 28 | def in(in: String) = copy(mapping, attachment.copy(_in = Some(in))) 29 | def desc(desc: String) = copy(mapping, attachment.copy(_desc = Some(desc))) 30 | def $$ = mapping.options(_.copy(_attachment = Some(attachment))) 31 | } 32 | 33 | implicit class AttachmentImplicit[T](mapping: Mapping[T]) { 34 | def $ = AttachmentBuilder(mapping, mapping.options._attachment.getOrElse(Attachment()).asInstanceOf[Attachment]) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/ConstraintsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import java.util.ResourceBundle 5 | 6 | class ConstraintsSpec extends FunSpec with Matchers { 7 | val bundle: ResourceBundle = ResourceBundle.getBundle("bind-messages") 8 | val messages: Messages = (key) => Option(bundle.getString(key)) 9 | 10 | describe("test pre-defined constraints") { 11 | 12 | describe("required") { 13 | it("single input") { 14 | val required = Constraints.required() 15 | required("", Map("" -> null), messages, Options(_inputMode = SoloInput)).toList should be (List("" -> "'' is required")) 16 | required("", Map("" -> ""), messages, Options(_label = Some(""))).toList should be (List("" -> "'' is required")) 17 | required("", Map("" -> "test"), messages, Options(_label = Some(""))).toList should be (Nil) 18 | } 19 | 20 | it("multi input") { 21 | val required1 = Constraints.required("%s is required") 22 | required1("tt", Map("tt.a" -> "tt"), messages, Options(_label = Some("haha"), _inputMode = BulkInput)).toList should be (Nil) 23 | required1("tt", Map("tt.a" -> null), messages, Options(_label = Some("haha"), _inputMode = BulkInput)).toList should be (Nil) 24 | required1("tt", Map("tt" -> null), messages, Options(_inputMode = BulkInput)).toList should be (List("tt" -> "tt is required")) 25 | required1("tt", Map(), messages, Options(_inputMode = BulkInput)).toList should be (List("tt" -> "tt is required")) 26 | } 27 | 28 | it("poly input") { 29 | val required1 = Constraints.required("%s is required") 30 | required1("tt", Map("tt.a" -> "tt"), messages, Options(_label = Some("haha"), _inputMode = PolyInput)).toList should be (Nil) 31 | required1("tt", Map("tt.a" -> null), messages, Options(_label = Some("haha"), _inputMode = PolyInput)).toList should be (Nil) 32 | required1("tt", Map("tt" -> null), messages, Options(_inputMode = PolyInput)).toList should be (List("tt" -> "tt is required")) 33 | required1("tt.a", Map("tt.a" -> null), messages, Options(_inputMode = PolyInput)).toList should be (List("tt.a" -> "a is required")) 34 | } 35 | } 36 | 37 | describe("maxlength") { 38 | it("simple use") { 39 | val maxlength = Constraints.maxLength(10) 40 | maxlength("", Map("" -> "wetyyuu"), messages, Options(_label = Some(""))).toList should be (Nil) 41 | maxlength("", Map("" -> "wetyettyiiie"), messages, Options(_label = Some(""))).toList should be (List("" -> "'wetyettyiiie' cannot be longer than 10 characters")) 42 | maxlength("", Map("" -> "tuewerri97"), messages, Options(_label = Some(""))).toList should be (Nil) 43 | } 44 | 45 | it("with custom message") { 46 | val maxlength1 = Constraints.maxLength(10, "'%s': length > %d") 47 | maxlength1("", Map("" -> "eewryuooerjhy"), messages, Options(_label = Some("haha"))).toList should be (List("" -> "'eewryuooerjhy': length > 10")) 48 | } 49 | } 50 | 51 | describe("minlength") { 52 | it("simple use") { 53 | val minlength = Constraints.minLength(3) 54 | minlength("", Map("" -> "er"), messages, Options(_label = Some(""))).toList should be (List("" -> "'er' cannot be shorter than 3 characters")) 55 | minlength("", Map("" -> "ert6"), messages, Options(_label = Some(""))).toList should be (Nil) 56 | minlength("", Map("" -> "tee"), messages, Options(_label = Some(""))).toList should be (Nil) 57 | } 58 | 59 | it("with custom message") { 60 | val minlength1 = Constraints.minLength(3, "'%s': length cannot < %d") 61 | minlength1("", Map("" -> "te"), messages, Options(_label = Some("haha"))).toList should be (List("" -> "'te': length cannot < 3")) 62 | } 63 | } 64 | 65 | describe("length") { 66 | it("simple use") { 67 | val length = Constraints.length(9) 68 | length("", Map("" -> "123456789"), messages, Options(_label = Some(""))).toList should be (Nil) 69 | length("", Map("" -> "123"), messages, Options(_label = Some(""))).toList should be (List("" -> "'123' must be 9 characters")) 70 | length("", Map("" -> "1234567890"), messages, Options(_label = Some(""))).toList should be (List("" -> "'1234567890' must be 9 characters")) 71 | } 72 | 73 | it("with custom message") { 74 | val length1 = Constraints.length(9, "'%s': length not equal to %d") 75 | length1("", Map("" -> "123"), messages, Options(_label = Some("haha"))).toList should be (List("" -> "'123': length not equal to 9")) 76 | } 77 | } 78 | 79 | describe("oneOf") { 80 | it("simple use") { 81 | val oneof = Constraints.oneOf(Seq("a","b","c")) 82 | oneof("", Map("" -> "a"), messages, Options(_label = Some(""))).toList should be (Nil) 83 | oneof("", Map("" -> "t"), messages, Options(_label = Some(""))).toList should be (List("" -> "'t' must be one of 'a', 'b', 'c'")) 84 | oneof("", Map("" -> null), messages, Options(_label = Some(""))).toList should be (List("" -> "'null' must be one of 'a', 'b', 'c'")) 85 | } 86 | 87 | it("with custom message") { 88 | val oneof1 = Constraints.oneOf(Seq("a","b","c"), "'%s': is not one of %s") 89 | oneof1("t.a", Map("t.a" -> "ts"), messages, Options(_label = Some("haha"))).toList should be (List("t.a" -> "'ts': is not one of 'a', 'b', 'c'")) 90 | } 91 | } 92 | 93 | describe("pattern") { 94 | it("simple use") { 95 | val pattern = Constraints.pattern("^(\\d+)$".r) 96 | pattern("", Map("" -> "1234657"), messages, Options(_label = Some(""))).toList should be (Nil) 97 | pattern("", Map("" -> "32566y"), messages, Options(_label = Some(""))) 98 | .toList should be (List("" -> "'32566y' must be '^(\\d+)$'")) 99 | pattern("", Map("" -> "123,567"), messages, Options(_label = Some(""))) 100 | .toList should be (List("" -> "'123,567' must be '^(\\d+)$'")) 101 | } 102 | 103 | it("with custom message") { 104 | val pattern1 = Constraints.pattern("^(\\d+)$".r, "'%s' not match '%s'") 105 | pattern1("", Map("" -> "t4366"), messages, Options(_label = Some("haha"))).toList should be (List("" -> "'t4366' not match '^(\\d+)$'")) 106 | } 107 | } 108 | 109 | describe("patternNot") { 110 | it("simple use") { 111 | val pattern = Constraints.patternNot(""".*\[(\d*[^\d\[\]]+\d*)+\].*""".r) 112 | pattern("", Map("" -> "eree.[1234657].eee"), messages, Options(_label = Some(""))).toList should be (Nil) 113 | pattern("", Map("" -> "errr.[32566y].ereee"), messages, Options(_label = Some(""))) 114 | .toList should be (List("" -> "'errr.[32566y].ereee' mustn't be '.*\\[(\\d*[^\\d\\[\\]]+\\d*)+\\].*'")) 115 | } 116 | 117 | it("with custom message") { 118 | val pattern1 = Constraints.pattern("^(\\d+)$".r, "'%s' contains illegal array index") 119 | pattern1("", Map("" -> "ewtr.[t4366].eweee"), messages, Options(_label = Some("haha"))) 120 | .toList should be (List("" -> "'ewtr.[t4366].eweee' contains illegal array index")) 121 | } 122 | } 123 | 124 | /** 125 | * test cases copied from: 126 | * http://en.wikipedia.org/wiki/Email_address 127 | */ 128 | describe("email") { 129 | val email = Constraints.email("'%s' not valid") 130 | 131 | it("valid email addresses") { 132 | List( 133 | "niceandsimple@example.com", 134 | "very.common@example.com", 135 | "a.little.lengthy.but.fine@dept.example.com", 136 | "disposable.style.email.with+symbol@example.com", 137 | "other.email-with-dash@example.com"//, 138 | // "user@localserver", 139 | // internationalization examples 140 | // "Pelé@example.com", //Latin Alphabet (with diacritics) 141 | // "δοκιμή@παράδειγμα.δοκιμή", //Greek Alphabet 142 | // "我買@屋企.香港", //Traditional Chinese Characters 143 | // "甲斐@黒川.日本", //Japanese Characters 144 | // "чебурашка@ящик-с-апельсинами.рф" //Cyrillic Characters 145 | ).map { emailAddr => 146 | email("", Map("" -> emailAddr), messages, Options(_label = Some(""))).toList should be (Nil) 147 | } 148 | } 149 | 150 | it("invalid email addresses") { 151 | List( 152 | "Abc.example.com", //(an @ character must separate the local and domain parts) 153 | "A@b@c@example.com", //(only one @ is allowed outside quotation marks) 154 | "a\"b(c)d,e:f;gi[j\\k]l@example.com", //(none of the special characters in this local part is allowed outside quotation marks) 155 | "just\"not\"right@example.com", //(quoted strings must be dot separated or the only element making up the local-part) 156 | """this is"not\allowed@example.com""", //(spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash) 157 | """this\ still\"not\\allowed@example.com""", //(even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes) 158 | "john..doe@example.com", //(double dot before @) 159 | "john.doe@example..com" //(double dot after @) 160 | ).map { emailAddr => 161 | email("", Map("" -> emailAddr), messages, Options(_label = Some(""))).toList should be (List("" -> s"'$emailAddr' not valid")) 162 | } 163 | } 164 | } 165 | 166 | describe("IndexInKeys") { 167 | it("simple use") { 168 | val numArrayIndex = Constraints.indexInKeys() 169 | numArrayIndex("a", Map("a[0]" -> "aaa"), messages, Options(_label = Some("xx"), _inputMode = BulkInput)).toList should be (Nil) 170 | numArrayIndex("a", Map("a[t0]" -> "aaa", "a[3]" -> "tew"), messages, Options(_label = Some(""), _inputMode = BulkInput)) 171 | .toList should be (List("a[t0]" -> "'a[t0]' contains illegal array index")) 172 | numArrayIndex("a", Map("a[t1]" -> "aewr", "a[t4]" -> "ewre"), messages, Options(_label = Some("xx"), _inputMode = BulkInput)) 173 | .toList should be (List("a[t1]" -> "'a[t1]' contains illegal array index", "a[t4]" -> "'a[t4]' contains illegal array index")) 174 | } 175 | 176 | it("w/ custom message") { 177 | val numArrayIndex = Constraints.indexInKeys("illegal array index") 178 | numArrayIndex("a", Map("a[0]" -> "aaa"), messages, Options(_label = Some("xx"), _inputMode = BulkInput)).toList should be (Nil) 179 | numArrayIndex("a", Map("a[t0]" -> "aaa", "a[3]" -> "tew"), messages, Options(_label = Some(""), _inputMode = BulkInput)) 180 | .toList should be (List("a[t0]" -> "illegal array index")) 181 | numArrayIndex("a", Map("a[t1]" -> "aewr", "a[t4].er" -> "ewre"), messages, Options(_label = Some("xx"), _inputMode = BulkInput)) 182 | .toList should be (List("a[t1]" -> "illegal array index", "a[t4].er" -> "illegal array index")) 183 | } 184 | } 185 | } 186 | 187 | describe("test pre-defined extra constraints") { 188 | 189 | describe("min") { 190 | it("for int, with custom message") { 191 | val min = Constraints.min(5, "%s cannot < %s") 192 | min("xx", 6, messages) should be (Nil) 193 | min("xx", 3, messages) should be (Seq("3 cannot < 5")) 194 | } 195 | 196 | it("for double, with custom message") { 197 | val min1 = Constraints.min(5.5d, "%s cannot < %s") 198 | min1("xx", 6d, messages) should be (Nil) 199 | min1("xx", 3d, messages) should be (Seq("3.0 cannot < 5.5")) 200 | } 201 | } 202 | 203 | describe("max") { 204 | it("for int, with custom message") { 205 | val max = Constraints.max(15, "%s cannot > %s") 206 | max("xx", 6, messages) should be (Nil) 207 | max("xx", 23, messages) should be (Seq("23 cannot > 15")) 208 | } 209 | 210 | it("for double, with custom message") { 211 | val max1 = Constraints.max(35.5d, "%s cannot > %s") 212 | max1("xx", 26d, messages) should be (Nil) 213 | max1("xx", 37d, messages) should be (Seq("37.0 cannot > 35.5")) 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/FieldMappingsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import java.time._ 4 | import java.time.format.DateTimeFormatter 5 | import java.util.{ResourceBundle, UUID} 6 | import org.scalatest._ 7 | 8 | class FieldMappingsSpec extends FunSpec with Matchers with Constraints with Processors { 9 | val bundle: ResourceBundle = ResourceBundle.getBundle("bind-messages") 10 | val messages: Messages = (key) => Option(bundle.getString(key)) 11 | 12 | describe("test pre-defined field mappings") { 13 | 14 | describe("text") { 15 | val text = trim() >-: Mappings.text() 16 | 17 | it("valid data") { 18 | val data = Map("text" -> "tett ") 19 | text.validate("text", data, messages, Options.apply()) match { 20 | case Nil => text.convert("text", data) should be ("tett") 21 | case err => err should be (Nil) 22 | } 23 | } 24 | 25 | it("null data") { 26 | val nullData = Map[String, String]() 27 | text.validate("text", nullData, messages, Options.apply()) match { 28 | case Nil => text.convert("text", nullData) should be (null) 29 | case err => err should be (Nil) 30 | } 31 | } 32 | 33 | it("w/ eager check") { 34 | val text1 = Mappings.text(maxLength(20, "%s: length > %s"), email("%s: invalid email")) 35 | .options(_.eagerCheck(true)) 36 | val data = Map("text" -> "etttt.att#example-1111111.com") 37 | text1.validate("text", data, messages, Options.apply()) match { 38 | case Nil => ("invalid - shouldn't occur!") should be ("") 39 | case err => err should be (Seq( 40 | "text" -> "etttt.att#example-1111111.com: length > 20", 41 | "text" -> "etttt.att#example-1111111.com: invalid email")) 42 | } 43 | } 44 | 45 | it("w/ ignore empty") { 46 | val nullData = Map[String, String]() 47 | 48 | val text1 = Mappings.text(required("%s is required")) 49 | text1.validate("text", nullData, messages, Options.apply()) match { 50 | case Nil => ("invalid - shouldn't occur!") should be ("") 51 | case err => err should be (Seq("text" -> "text is required")) 52 | } 53 | 54 | val text2 = Mappings.text(required("%s is required")) 55 | .options(_.skipUntouched(true)) 56 | text2.validate("text", nullData, messages, Options.apply()) match { 57 | case Nil => text.convert("text", nullData) should be (null) 58 | case err => err should be (Nil) 59 | } 60 | } 61 | 62 | it("w/ ignore empty and touched") { 63 | val nullData = Map[String, String]() 64 | 65 | val text1 = Mappings.text(required("%s is required")) 66 | text1.validate("text", nullData, messages, Options.apply()) match { 67 | case Nil => ("invalid - shouldn't occur!") should be ("") 68 | case err => err should be (Seq("text" -> "text is required")) 69 | } 70 | 71 | val text2 = Mappings.text(required("%s is required")) 72 | .options(_.skipUntouched(true)) 73 | text2.validate("text", nullData, messages, Options().touchedChecker(Processors.listTouched(List("text")))) match { 74 | case Nil => ("invalid - shouldn't occur!") should be ("") 75 | case err => err should be (Seq("text" -> "text is required")) 76 | } 77 | } 78 | 79 | it("w/ eager check thru verifying") { 80 | val text1 = Mappings.text(maxLength(20, "%s: length > %s"), email("%s: invalid email")).verifying() 81 | .options(_.eagerCheck(true)) 82 | val data = Map("text" -> "etttt.att#example-1111111.com") 83 | text1.validate("text", data, messages, Options.apply()) match { 84 | case Nil => ("invalid - shouldn't occur!") should be ("") 85 | case err => err should be (Seq( 86 | "text" -> "etttt.att#example-1111111.com: length > 20", 87 | "text" -> "etttt.att#example-1111111.com: invalid email")) 88 | } 89 | } 90 | 91 | it("w/ ignore empty thru verifying") { 92 | val nullData = Map[String, String]() 93 | 94 | val text1 = Mappings.text(required("%s is required")).verifying() 95 | text1.validate("text", nullData, messages, Options.apply()) match { 96 | case Nil => ("invalid - shouldn't occur!") should be ("") 97 | case err => err should be (Seq("text" -> "text is required")) 98 | } 99 | 100 | val text2 = Mappings.text(required("%s is required")).verifying() 101 | .options(_.skipUntouched(true)) 102 | text2.validate("text", nullData, messages, Options.apply()) match { 103 | case Nil => text.convert("text", nullData) should be (null) 104 | case err => err should be (Nil) 105 | } 106 | } 107 | 108 | it("w/ ignore empty and touched thru verifying") { 109 | val nullData = Map[String, String]() 110 | 111 | val text1 = Mappings.text(required("%s is required")).verifying() 112 | text1.validate("text", nullData, messages, Options.apply()) match { 113 | case Nil => ("invalid - shouldn't occur!") should be ("") 114 | case err => err should be (Seq("text" -> "text is required")) 115 | } 116 | 117 | val text2 = Mappings.text(required("%s is required")).verifying() 118 | .options(_.skipUntouched(true)) 119 | text2.validate("text", nullData, messages, Options().touchedChecker(Processors.listTouched(List("text")))) match { 120 | case Nil => ("invalid - shouldn't occur!") should be ("") 121 | case err => err should be (Seq("text" -> "text is required")) 122 | } 123 | } 124 | 125 | it("w/ eager check + transform (mapTo)") { 126 | val text1 = Mappings.text(maxLength(20, "%s: length > %s"), email("invalid email")).map(identity) 127 | .options(_.eagerCheck(true)) 128 | val data = Map("text" -> "etttt.att#example-1111111.com") 129 | text1.validate("text", data, messages, Options.apply()) match { 130 | case Nil => ("invalid - shouldn't occur!") should be ("") 131 | case err => err should be (Seq( 132 | "text" -> "etttt.att#example-1111111.com: length > 20", 133 | "text" -> "invalid email")) 134 | } 135 | } 136 | 137 | it("w/ ignore empty + transform (mapTo)") { 138 | val nullData = Map[String, String]() 139 | 140 | val text1 = Mappings.text(required("%s is required")).map(identity) 141 | text1.validate("text", nullData, messages, Options.apply()) match { 142 | case Nil => ("invalid - shouldn't occur!") should be ("") 143 | case err => err should be (Seq("text" -> "text is required")) 144 | } 145 | 146 | val text2 = Mappings.text(required("%s is required")).map(identity) 147 | .options(_.skipUntouched(true)) 148 | text2.validate("text", nullData, messages, Options.apply()) match { 149 | case Nil => text.convert("text", nullData) should be (null) 150 | case err => err should be (Nil) 151 | } 152 | } 153 | 154 | it("w/ ignore empty and touched + transform (mapTo)") { 155 | val nullData = Map[String, String]() 156 | 157 | val text1 = Mappings.text(required("%s is required")).map(identity) 158 | text1.validate("text", nullData, messages, Options.apply()) match { 159 | case Nil => ("invalid - shouldn't occur!") should be ("") 160 | case err => err should be (Seq("text" -> "text is required")) 161 | } 162 | 163 | val text2 = Mappings.text(required("%s is required")).map(identity) 164 | .options(_.skipUntouched(true)) 165 | text2.validate("text", nullData, messages, Options().touchedChecker(Processors.listTouched(List("text")))) match { 166 | case Nil => ("invalid - shouldn't occur!") should be ("") 167 | case err => err should be (Seq("text" -> "text is required")) 168 | } 169 | } 170 | } 171 | 172 | describe("boolean") { 173 | val boolean = Mappings.boolean() 174 | 175 | it("invalid data") { 176 | val invalidData = Map("boolean" -> "teed") 177 | boolean.validate("boolean", invalidData, messages, Options.apply()) match { 178 | case Nil => ("invalid - shouldn't occur!") should be ("") 179 | case err => err should be (Seq("boolean" -> "'teed' must be a boolean")) 180 | } 181 | } 182 | 183 | it("valid data") { 184 | val validData = Map("boolean" -> "true") 185 | boolean.validate("boolean", validData, messages, Options.apply()) match { 186 | case Nil => boolean.convert("boolean", validData) should be (true) 187 | case err => err should be (Nil) 188 | } 189 | } 190 | 191 | it("null data") { 192 | val nullData = Map[String, String]() 193 | boolean.validate("boolean", nullData, messages, Options.apply()) match { 194 | case Nil => boolean.convert("boolean", nullData) should be (false) 195 | case err => err should be (Nil) 196 | } 197 | } 198 | 199 | it("empty data") { 200 | val emptyData = Map("boolean" -> "") 201 | boolean.validate("boolean", emptyData, messages, Options.apply()) match { 202 | case Nil => boolean.convert("boolean", emptyData) should be (false) 203 | case err => err should be (Nil) 204 | } 205 | } 206 | } 207 | 208 | describe("int") { 209 | val int = (omit(",") >-: Mappings.int()).verifying(min(1000), max(10000)) 210 | 211 | it("invalid data") { 212 | val int1 = Mappings.int().label("xx") 213 | val invalidData = Map("int" -> "t12345") 214 | int1.validate("int", invalidData, messages, Options.apply()) match { 215 | case Nil => ("invalid - shouldn't occur!") should be ("") 216 | case err => err should be (Seq("int" -> "'t12345' must be a number")) 217 | } 218 | } 219 | 220 | it("out-of-scope data") { 221 | val outScopeData = Map("int" -> "345") 222 | int.validate("int", outScopeData, messages, Options.apply()) match { 223 | case Nil => ("out of scope - shouldn't occur!") should be ("") 224 | case err => err should be (Seq("int" -> "'345' cannot be lower than 1000")) 225 | } 226 | } 227 | 228 | it("long number data") { 229 | val int1 = Mappings.int() 230 | val longNumberData = Map("int" -> "146894532240") 231 | int1.validate("int", longNumberData, messages, Options.apply()) match { 232 | case Nil => ("long number - shouldn't occur!") should be ("") 233 | case err => err should be (Seq("int" -> "'146894532240' must be a number")) 234 | } 235 | } 236 | 237 | it("valid data with comma") { 238 | val validData = Map("int" -> "3,549") 239 | int.validate("int", validData, messages, Options.apply()) match { 240 | case Nil => int.convert("int", validData) should be (3549) 241 | case err => err should be (Nil) 242 | } 243 | } 244 | 245 | it("null data") { 246 | val nullData = Map[String, String]() 247 | int.convert("int", nullData) should be (0) 248 | int.validate("int", nullData, messages, Options.apply()) match { 249 | case Nil => ("(null->) 0 - shouldn't occur!") should be ("") 250 | case err => err should be (Seq("int" -> "'0' cannot be lower than 1000")) 251 | } 252 | } 253 | 254 | it("empty data") { 255 | val emptyData = Map("int" -> "") 256 | int.convert("int", emptyData) should be (0) 257 | int.validate("int", emptyData, messages, Options.apply()) match { 258 | case Nil => ("(empty->) 0 - shouldn't occur!") should be ("") 259 | case err => err should be (Seq("int" -> "'0' cannot be lower than 1000")) 260 | } 261 | } 262 | } 263 | 264 | describe("double") { 265 | val double = Mappings.double() 266 | 267 | it("invalid datq") { 268 | val double1 = double.label("xx") 269 | val invalidData = Map("double" -> "tesstt") 270 | double1.validate("double", invalidData, messages, Options.apply()) match { 271 | case Nil => ("invalid - shouldn't occur!") should be ("") 272 | case err => err should be (Seq("double" -> "'tesstt' must be a number")) 273 | } 274 | } 275 | 276 | it("valid data") { 277 | val validData = Map("double" -> "23545.2355") 278 | double.validate("double", validData, messages, Options.apply()) match { 279 | case Nil => double.convert("double", validData) should be (23545.2355d) 280 | case err => err should be (Nil) 281 | } 282 | } 283 | 284 | it("null data") { 285 | val nullData = Map[String, String]() 286 | double.validate("double", nullData, messages, Options.apply()) match { 287 | case Nil => double.convert("double", nullData) should be (0d) 288 | case err => err should be (Nil) 289 | } 290 | } 291 | 292 | it("empty data") { 293 | val emptyData = Map("double" -> "") 294 | double.validate("double", emptyData, messages, Options.apply()) match { 295 | case Nil => double.convert("double", emptyData) should be (0d) 296 | case err => err should be (Nil) 297 | } 298 | } 299 | } 300 | 301 | describe("float") { 302 | val float = Mappings.float() 303 | 304 | it("invalid data") { 305 | val float1 = float.label("xx") 306 | val invalidData = Map("float" -> "tesstt") 307 | float1.validate("float", invalidData, messages, Options.apply()) match { 308 | case Nil => ("invalid - shouldn't occur!") should be ("") 309 | case err => err should be (Seq("float" -> "'tesstt' must be a number")) 310 | } 311 | } 312 | 313 | it("valid data") { 314 | val validData = Map("float" -> "23545.2355") 315 | float.validate("float", validData, messages, Options.apply()) match { 316 | case Nil => float.convert("float", validData) should be (23545.2355f) 317 | case err => err should be (Nil) 318 | } 319 | } 320 | 321 | it("null data") { 322 | val nullData = Map[String, String]() 323 | float.validate("float", nullData, messages, Options.apply()) match { 324 | case Nil => float.convert("float", nullData) should be (0f) 325 | case err => err should be (Nil) 326 | } 327 | } 328 | 329 | it("empty data") { 330 | val emptyData = Map("float" -> "") 331 | float.validate("float", emptyData, messages, Options.apply()) match { 332 | case Nil => float.convert("float", emptyData) should be (0f) 333 | case err => err should be (Nil) 334 | } 335 | } 336 | } 337 | 338 | describe("long") { 339 | val long = Mappings.long() 340 | 341 | it("invalid data") { 342 | val invalidData = Map("long" -> "tesstt") 343 | long.validate("long", invalidData, messages, Options.apply()) match { 344 | case Nil => ("invalid - shouldn't occur!") should be ("") 345 | case err => err should be (Seq("long" -> "'tesstt' must be a number")) 346 | } 347 | } 348 | 349 | it("valid data") { 350 | val validData = Map("long" -> "235452355") 351 | long.validate("long", validData, messages, Options.apply()) match { 352 | case Nil => long.convert("long", validData) should be (235452355L) 353 | case err => err should be (Nil) 354 | } 355 | } 356 | 357 | it("null data") { 358 | val nullData = Map[String, String]() 359 | long.validate("long", nullData, messages, Options.apply()) match { 360 | case Nil => long.convert("long", nullData) should be (0L) 361 | case err => err should be (Nil) 362 | } 363 | } 364 | 365 | it("empty data") { 366 | val emptyData = Map("long" -> "") 367 | long.validate("long", emptyData, messages, Options.apply()) match { 368 | case Nil => long.convert("long", emptyData) should be (0L) 369 | case err => err should be (Nil) 370 | } 371 | } 372 | } 373 | 374 | describe("bigDecimal") { 375 | val bigDecimal = Mappings.bigDecimal() 376 | 377 | it("invalid data") { 378 | val invalidData = Map("bigDecimal" -> "tesstt") 379 | bigDecimal.validate("bigDecimal", invalidData, messages, Options.apply()) match { 380 | case Nil => ("invalid - shouldn't occur!") should be ("") 381 | case err => err should be (Seq("bigDecimal" -> "'tesstt' must be a number")) 382 | } 383 | } 384 | 385 | it("valid data") { 386 | val validData = Map("bigDecimal" -> "23545.2355") 387 | bigDecimal.validate("bigDecimal", validData, messages, Options.apply()) match { 388 | case Nil => bigDecimal.convert("bigDecimal", validData) should be (BigDecimal("23545.2355")) 389 | case err => err should be (Nil) 390 | } 391 | } 392 | 393 | it("null data") { 394 | val nullData = Map[String, String]() 395 | bigDecimal.validate("bigDecimal", nullData, messages, Options.apply()) match { 396 | case Nil => bigDecimal.convert("bigDecimal", nullData) should be (BigDecimal("0.0")) 397 | case err => err should be (Nil) 398 | } 399 | } 400 | 401 | it("empty data") { 402 | val emptyData = Map("bigDecimal" -> "") 403 | bigDecimal.validate("bigDecimal", emptyData, messages, Options.apply()) match { 404 | case Nil => bigDecimal.convert("bigDecimal", emptyData) should be (BigDecimal("0.0")) 405 | case err => err should be (Nil) 406 | } 407 | } 408 | } 409 | 410 | describe("bigInt") { 411 | val bigInt = Mappings.bigInt() 412 | 413 | it("invalid data") { 414 | val invalidData = Map("bigInt" -> "tesstt") 415 | bigInt.validate("bigInt", invalidData, messages, Options.apply()) match { 416 | case Nil => ("invalid - shouldn't occur!") should be ("") 417 | case err => err should be (Seq("bigInt" -> "'tesstt' must be a number")) 418 | } 419 | } 420 | 421 | it("valid data") { 422 | val validData = Map("bigInt" -> "235452355") 423 | bigInt.validate("bigInt", validData, messages, Options.apply()) match { 424 | case Nil => bigInt.convert("bigInt", validData) should be (BigInt("235452355")) 425 | case err => err should be (Nil) 426 | } 427 | } 428 | 429 | it("null data") { 430 | val nullData = Map[String, String]() 431 | bigInt.validate("bigInt", nullData, messages, Options.apply()) match { 432 | case Nil => bigInt.convert("bigInt", nullData) should be (BigInt("0")) 433 | case err => err should be (Nil) 434 | } 435 | } 436 | 437 | it("empty data") { 438 | val emptyData = Map("bigInt" -> "") 439 | bigInt.validate("bigInt", emptyData, messages, Options.apply()) match { 440 | case Nil => bigInt.convert("bigInt", emptyData) should be (BigInt("0")) 441 | case err => err should be (Nil) 442 | } 443 | } 444 | } 445 | 446 | describe("uuid") { 447 | val uuid = Mappings.uuid() 448 | 449 | it("invalid data") { 450 | val invalidData = Map("uuid" -> "tesstt") 451 | uuid.validate("uuid", invalidData, messages, Options.apply()) match { 452 | case Nil => ("invalid - shouldn't occur!") should be ("") 453 | case err => err should be (Seq("uuid" -> "'tesstt' missing or not a valid uuid")) 454 | } 455 | } 456 | 457 | it("valid data") { 458 | val uuidObj = UUID.randomUUID() 459 | val validData = Map("uuid" -> uuidObj.toString) 460 | uuid.validate("uuid", validData, messages, Options.apply()) match { 461 | case Nil => uuid.convert("uuid", validData) should be (uuidObj) 462 | case err => err should be (Nil) 463 | } 464 | } 465 | 466 | it("null data") { 467 | val nullData = Map[String, String]() 468 | uuid.validate("uuid", nullData, messages, Options.apply()) match { 469 | case Nil => uuid.convert("uuid", nullData) should be (null) 470 | case err => err should be (Nil) 471 | } 472 | } 473 | 474 | it("empty data") { 475 | val emptyData = Map("uuid" -> "") 476 | uuid.validate("uuid", emptyData, messages, Options.apply()) match { 477 | case Nil => uuid.convert("uuid", emptyData) should be (null) 478 | case err => err should be (Nil) 479 | } 480 | } 481 | } 482 | 483 | describe("date") { 484 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 485 | val date = Mappings.date("yyyy-MM-dd").verifying( 486 | min(LocalDate.parse("2000-01-01", formatter), "min failed"), 487 | max(LocalDate.parse("2015-01-01", formatter), "max failed")) 488 | 489 | it("invalid data") { 490 | val date1 = Mappings.date("yyyy-MM-dd").label("xx") 491 | val invalidData = Map("date" -> "5/3/2003") 492 | date1.validate("date", invalidData, messages, Options.apply()) match { 493 | case Nil => ("invalid - shouldn't occur!") should be ("") 494 | case err => err should be (Seq("date" -> "'xx' must satisfy any of following: ['5/3/2003' not a date long, '5/3/2003' must be 'yyyy-MM-dd']")) 495 | } 496 | } 497 | 498 | it("out-of-scope data") { 499 | val outScopeData = Map("date" -> "1998-07-01") 500 | date.validate("date", outScopeData, messages, Options.apply()) match { 501 | case Nil => ("invalid - shouldn't occur!") should be ("") 502 | case err => err should be (Seq("date" -> "min failed")) 503 | } 504 | } 505 | 506 | it("valid data") { 507 | val validData = Map("date" -> "2007-08-03") 508 | date.validate("date", validData, messages, Options.apply()) match { 509 | case Nil => date.convert("date", validData) should be (LocalDate.parse("2007-08-03", formatter)) 510 | case err => err should be (Nil) 511 | } 512 | } 513 | 514 | it("valid data - long") { 515 | val dateMapping = Mappings.date() 516 | val ts = System.currentTimeMillis() 517 | val dateObj = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.of("UTC")).toLocalDate 518 | val validData = Map("date" -> ts.toString) 519 | dateMapping.validate("date", validData, messages, Options.apply()) match { 520 | case Nil => dateMapping.convert("date", validData) should be (dateObj) 521 | case err => err should be (Nil) 522 | } 523 | } 524 | 525 | it("valid data - default format") { 526 | val dateMapping = Mappings.date() 527 | val dateObj = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toLocalDate 528 | val validData = Map("date" -> dateObj.toString) 529 | dateMapping.validate("date", validData, messages, Options.apply()) match { 530 | case Nil => dateMapping.convert("date", validData) should be (dateObj) 531 | case err => err should be (Nil) 532 | } 533 | } 534 | 535 | it("null data") { 536 | val date1 = Mappings.date("yyyy-MM-dd") 537 | val nullData = Map[String, String]() 538 | date1.validate("date", nullData, messages, Options.apply()) match { 539 | case Nil => date1.convert("date", nullData) should be (null) 540 | case err => err should be (Nil) 541 | } 542 | } 543 | 544 | it("empty data") { 545 | val date1 = Mappings.date("yyyy-MM-dd") 546 | val emptyData = Map("date" -> "") 547 | date1.validate("date", emptyData, messages, Options.apply()) match { 548 | case Nil => date1.convert("date", emptyData) should be (null) 549 | case err => err should be (Nil) 550 | } 551 | } 552 | } 553 | 554 | describe("datetime") { 555 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") 556 | val datetime = Mappings.datetime("yyyy-MM-dd'T'HH:mm:ss.SSS").verifying( 557 | min(LocalDateTime.parse("2001-01-03T13:21:00.223", formatter), "min failed"), 558 | max(LocalDateTime.parse("2012-01-03T13:21:00.102", formatter), "max failed")) 559 | 560 | it("invalid data") { 561 | val datetime1 = Mappings.datetime("yyyy-MM-dd'T'HH:mm:ss.SSS").label("xx") 562 | val invalidData = Map("datetime" -> "5/3/2003 13:21:00") 563 | datetime1.validate("datetime", invalidData, messages, Options.apply()) match { 564 | case Nil => ("invalid - shouldn't occur!") should be ("") 565 | case err => err should be (Seq("datetime" -> "'xx' must satisfy any of following: ['5/3/2003 13:21:00' not a date long, '5/3/2003 13:21:00' must be 'yyyy-MM-dd'T'HH:mm:ss.SSS']")) 566 | } 567 | } 568 | 569 | it("out-of-scope data") { 570 | val outScopeData = Map("datetime" -> "1998-07-01T13:21:00.223") 571 | datetime.validate("datetime", outScopeData, messages, Options.apply()) match { 572 | case Nil => ("invalid - shouldn't occur!") should be ("") 573 | case err => err should be (Seq("datetime" -> "min failed")) 574 | } 575 | } 576 | 577 | it("valid data") { 578 | val validData = Map("datetime" -> "2007-08-03T13:21:00.223") 579 | datetime.validate("datetime", validData, messages, Options.apply()) match { 580 | case Nil => datetime.convert("datetime", validData) should be (LocalDateTime.parse("2007-08-03T13:21:00.223", formatter)) 581 | case err => err should be (Nil) 582 | } 583 | } 584 | 585 | it("valid data - long") { 586 | val dateMapping = Mappings.datetime() 587 | val ts = System.currentTimeMillis() 588 | val dateObj = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.of("UTC")) 589 | val validData = Map("datetime" -> ts.toString) 590 | dateMapping.validate("datetime", validData, messages, Options.apply()) match { 591 | case Nil => dateMapping.convert("datetime", validData) should be (dateObj) 592 | case err => err should be (Nil) 593 | } 594 | } 595 | 596 | it("valid data - default format") { 597 | val dateMapping = Mappings.datetime() 598 | val dateObj = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")) 599 | val validData = Map("datetime" -> dateObj.toString) 600 | dateMapping.validate("datetime", validData, messages, Options.apply()) match { 601 | case Nil => dateMapping.convert("datetime", validData) should be (dateObj) 602 | case err => err should be (Nil) 603 | } 604 | } 605 | 606 | it("null data") { 607 | val datetime1 = Mappings.datetime("yyyy-MM-dd'T'HH:mm:ss.SSS") 608 | val nullData = Map[String, String]() 609 | datetime1.validate("datetime", nullData, messages, Options.apply()) match { 610 | case Nil => datetime1.convert("datetime", nullData) should be (null) 611 | case err => err should be (Nil) 612 | } 613 | } 614 | 615 | it("empty data") { 616 | val datetime1 = Mappings.datetime("yyyy-MM-dd'T'HH:mm:ss.SSS") 617 | val emptyData = Map("datetime" -> "") 618 | datetime1.validate("datetime", emptyData, messages, Options.apply()) match { 619 | case Nil => datetime1.convert("datetime", emptyData) should be (null) 620 | case err => err should be (Nil) 621 | } 622 | } 623 | } 624 | 625 | describe("time") { 626 | val formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") 627 | val time = Mappings.time("HH:mm:ss.SSS").verifying( 628 | min(LocalTime.parse("02:33:01.101", formatter), "min failed"), 629 | max(LocalTime.parse("12:33:01.101", formatter), "max failed")) 630 | 631 | it("invalid data") { 632 | val time1 = Mappings.time("HH:mm:ss.SSS").label("xx") 633 | val invalidData = Map("time" -> "13:21:00") 634 | time1.validate("time", invalidData, messages, Options.apply()) match { 635 | case Nil => ("invalid - shouldn't occur!") should be ("") 636 | case err => err should be (Seq("time" -> "'xx' must satisfy any of following: ['13:21:00' not a date long, '13:21:00' must be 'HH:mm:ss.SSS']")) 637 | } 638 | } 639 | 640 | it("out-of-scope data") { 641 | val outScopeData = Map("time" -> "13:21:00.333") 642 | time.validate("time", outScopeData, messages, Options.apply()) match { 643 | case Nil => ("invalid - shouldn't occur!") should be ("") 644 | case err => err should be (Seq("time" -> "max failed")) 645 | } 646 | } 647 | 648 | it("valid data") { 649 | val validData = Map("time" -> "11:21:00.213") 650 | time.validate("time", validData, messages, Options.apply()) match { 651 | case Nil => time.convert("time", validData) should be (LocalTime.parse("11:21:00.213", formatter)) 652 | case err => err should be (Nil) 653 | } 654 | } 655 | 656 | it("valid data - long") { 657 | val dateMapping = Mappings.time() 658 | val ts = System.currentTimeMillis() 659 | val dateObj = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.of("UTC")).toLocalTime 660 | val validData = Map("time" -> ts.toString) 661 | dateMapping.validate("time", validData, messages, Options.apply()) match { 662 | case Nil => dateMapping.convert("time", validData) should be (dateObj) 663 | case err => err should be (Nil) 664 | } 665 | } 666 | 667 | it("valid data - default format") { 668 | val dateMapping = Mappings.time() 669 | val dateObj = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toLocalTime 670 | val validData = Map("time" -> dateObj.toString) 671 | dateMapping.validate("time", validData, messages, Options.apply()) match { 672 | case Nil => dateMapping.convert("time", validData) should be (dateObj) 673 | case err => err should be (Nil) 674 | } 675 | } 676 | 677 | it("null data") { 678 | val time1 = Mappings.time("HH:mm:ss.SSS") 679 | val nullData = Map[String, String]() 680 | time1.validate("time", nullData, messages, Options.apply()) match { 681 | case Nil => time1.convert("time", nullData) should be (null) 682 | case err => err should be (Nil) 683 | } 684 | } 685 | 686 | it("empty data") { 687 | val time1 = Mappings.date("HH:mm:ss.SSS") 688 | val emptyData = Map("time" -> "") 689 | time1.validate("time", emptyData, messages, Options.apply()) match { 690 | case Nil => time1.convert("time", emptyData) should be (null) 691 | case err => err should be (Nil) 692 | } 693 | } 694 | } 695 | } 696 | } 697 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/FormBinderSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import java.util.ResourceBundle 5 | 6 | import scala.collection.mutable.ListBuffer 7 | 8 | class FormBinderSpec extends FunSpec with Matchers { 9 | import com.github.tminglei.bind.simple._ 10 | 11 | val bundle: ResourceBundle = ResourceBundle.getBundle("bind-messages") 12 | val messages: Messages = (key) => Option(bundle.getString(key)) 13 | 14 | describe("show and check form binder") { 15 | 16 | describe("usage cases") { 17 | val mappings = expandJson(Some("body")) >-: tmapping( 18 | "id" -> long(), 19 | "body" -> tmapping( 20 | "price" -> (omitLeft("$") >-: float()), 21 | "count" -> int().verifying(min(3), max(10)) 22 | ).label("xx").verifying { case (label, (price, count), messages) => 23 | if (price * count > 1000) { 24 | Seq(s"$label: $price * $count = ${price * count}, too much") 25 | } else Nil 26 | } 27 | ) 28 | 29 | it("w/ valid data") { 30 | val binder = FormBinder(messages) 31 | val validData = Map( 32 | "id" -> "133", 33 | "body" -> """{"price":"$137.5", "count":5}""" 34 | ) 35 | binder.bind(mappings, validData).fold( 36 | errors => "shouldn't happen!!!", 37 | { case (id, (price, count)) => 38 | id should be (133L) 39 | price should be (137.5f) 40 | count should be (5) 41 | (">> bind successful!") 42 | } 43 | ) should be (">> bind successful!") 44 | } 45 | 46 | it("w/ invalid data") { 47 | val binder = FormBinder(messages) 48 | val invalidData = Map( 49 | "id" -> "133", 50 | "body" -> """{"price":337.5, "count":5}""" 51 | ) 52 | binder.bind(mappings, invalidData).fold( 53 | errors => errors, 54 | { case (id, (price, count)) => 55 | ("invalid - shouldn't occur!") should be ("") 56 | } 57 | ) should be (List("body" -> "xx: 337.5 * 5 = 1687.5, too much")) 58 | } 59 | 60 | it("w/ invalid data + errors processor") { 61 | val binder = FormBinder(messages, errsTree()) 62 | val invalidData = Map( 63 | "id" -> "133", 64 | "body" -> """{"price":337.5, "count":5}""" 65 | ) 66 | binder.bind(mappings, invalidData).fold( 67 | errors => errors, 68 | { case (id, (price, count)) => 69 | ("invalid - shouldn't occur!") should be("") 70 | } 71 | ) should be(Map("body" -> Map("_errors" -> ListBuffer("xx: 337.5 * 5 = 1687.5, too much")))) 72 | } 73 | } 74 | 75 | describe("w/ options") { 76 | val mappings = expandJson(Some("body")) >-: tmapping( 77 | "id" -> long(), 78 | "body" -> tmapping( 79 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 80 | "price" -> (omitLeft("$") >-: float()), 81 | "count" -> int().verifying(min(3), max(10)) 82 | ).label("xx").verifying { case (label, (email, price, count), messages) => 83 | if (price * count > 1000) { 84 | Seq(s"$label: $price * $count = ${price * count}, too much") 85 | } else Nil 86 | } 87 | ) 88 | 89 | it("w/ eager check") { 90 | val binder = FormBinder(messages) 91 | val invalidData = Map( 92 | "id" -> "133", 93 | "body" -> """{"email":"etttt.att#example-1111111.com", "price":337.5, "count":5}""" 94 | ) 95 | 96 | binder.bind(mappings, invalidData).fold( 97 | errors => errors, 98 | { case (id, (email, price, count)) => 99 | ("invalid - shouldn't occur!") should be ("") 100 | } 101 | ) should be (List("body.email" -> "etttt.att#example-1111111.com: length > 20")) 102 | /// 103 | binder.bind(mappings.options(_.eagerCheck(true)), invalidData).fold( 104 | errors => errors, 105 | { case (id, (email, price, count)) => 106 | ("invalid - shouldn't occur!") should be ("") 107 | } 108 | ) should be (List( 109 | "body.email" -> "etttt.att#example-1111111.com: length > 20", 110 | "body.email" -> "etttt.att#example-1111111.com: invalid email" 111 | )) 112 | /// 113 | binder.validate(mappings.options(_.eagerCheck(true)), invalidData) 114 | .asInstanceOf[Seq[(String, String)]] should be (List( 115 | "body.email" -> "etttt.att#example-1111111.com: length > 20", 116 | "body.email" -> "etttt.att#example-1111111.com: invalid email" 117 | )) 118 | } 119 | 120 | it("w/ ignore empty") { 121 | val binder = FormBinder(messages) 122 | val invalidData = Map( 123 | "id" -> "133", 124 | "body" -> """{"email":null, "price":337.5, "count":5}""" 125 | ) 126 | 127 | binder.bind(mappings, invalidData).fold( 128 | errors => errors, 129 | { case (id, (email, price, count)) => 130 | ("invalid - shouldn't occur!") should be ("") 131 | } 132 | ) should be (List("body.email" -> "email is required")) 133 | /// 134 | binder.bind(mappings.options(_.skipUntouched(true)), invalidData).fold( 135 | errors => errors, 136 | { case (id, (email, price, count)) => 137 | ("invalid - shouldn't occur!") should be ("") 138 | } 139 | ) should be (List("body" -> "xx: 337.5 * 5 = 1687.5, too much")) 140 | } 141 | 142 | it("w/ ignore empty and touched") { 143 | val binder = FormBinder(messages) 144 | val invalidData = Map( 145 | "id" -> "133", 146 | "body" -> """{"email":null, "price":337.5, "count":5}""" 147 | ) 148 | 149 | binder.bind(mappings, invalidData).fold( 150 | errors => errors, 151 | { case (id, (email, price, count)) => 152 | ("invalid - shouldn't occur!") should be ("") 153 | } 154 | ) should be (List("body.email" -> "email is required")) 155 | /// 156 | binder.bind(mappings.options(_.skipUntouched(true).touchedChecker(listTouched(List("body.email")))), invalidData).fold( 157 | errors => errors, 158 | { case (id, (email, price, count)) => 159 | ("invalid - shouldn't occur!") should be ("") 160 | } 161 | ) should be (List("body.email" -> "email is required")) 162 | /// 163 | binder.validate(mappings.options(_.skipUntouched(true).touchedChecker(listTouched(List("body.email")))), invalidData) 164 | .asInstanceOf[Seq[(String, String)]] should be (List("body.email" -> "email is required")) 165 | } 166 | 167 | it("w/ ignore empty and touched (+)") { 168 | val binder1 = FormBinder(messages) 169 | //>>> group mapping with bulk pre-processor 170 | val mappings1 = expandJsonKeys(Some("touched")) >-: tmapping( 171 | "id" -> long(), 172 | "data" -> (expandJson() >-: tmapping( 173 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 174 | "price" -> (omitLeft("$") >-: float()), 175 | "count" -> int().verifying(min(3), max(10)) 176 | )).label("xx").verifying { case (label, (email, price, count), messages) => 177 | if (price * count > 1000) { 178 | Seq(s"$label: $price * $count = ${price * count}, too much") 179 | } else Nil 180 | } 181 | ) 182 | val invalidData = Map( 183 | "id" -> "133", 184 | "data" -> """{"email":null, "price":337.5, "count":5}""", 185 | "touched" -> """["email", "price"]""" 186 | ) 187 | 188 | binder1.validate(mappings1.options(_.skipUntouched(true).touchedChecker(prefixTouched("data", "touched"))), invalidData) 189 | .asInstanceOf[Seq[(String, String)]] should be (List("data.email" -> "email is required")) 190 | } 191 | 192 | it("w/ ignore empty and touched (combined)") { 193 | val binder = FormBinder(messages) 194 | val mappingx = expandJson(Some("body")) >-: expandListKeys(Some("body.touched")) >-: 195 | changePrefix("body.data", "body") >-: changePrefix("body.touched", "touched") >-: mappings 196 | val invalidData = Map( 197 | "id" -> "133", 198 | "body" -> """{"data": {"email":null, "price":337.5, "count":5}, "touched": ["email", "price"]}""" 199 | ) 200 | 201 | binder.validate(mappingx.options(_.skipUntouched(true).touchedChecker(prefixTouched("body", "touched"))), invalidData) 202 | .asInstanceOf[Seq[(String, String)]] should be (List("body.email" -> "email is required")) 203 | } 204 | 205 | it("w/ i18n and label") { 206 | val messages1 = (key: String) => if (key == "xx") Some("haha") else Some("dummy") 207 | val binder = FormBinder(messages1) 208 | val mappings = tmapping( 209 | "id" -> long(), 210 | "body" -> (expandJson() >-: tmapping( 211 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 212 | "price" -> (omitLeft("$") >-: float()), 213 | "count" -> int().verifying(min(3), max(10)) 214 | ).label("@xx").verifying { case (label, (email, price, count), messages) => 215 | if (price * count > 1000) { 216 | Seq(s"$label: $price * $count = ${price * count}, too much") 217 | } else Nil 218 | }) 219 | ) 220 | 221 | val invalidData = Map( 222 | "id" -> "133", 223 | "body" -> """{"email":"example@123.com", "price":337.5, "count":5}""" 224 | ) 225 | 226 | binder.bind(mappings, invalidData).fold( 227 | errors => errors, 228 | { case (id, (email, price, count)) => 229 | ("invalid - shouldn't occur!") should be ("") 230 | } 231 | ) should be (List("body" -> "haha: 337.5 * 5 = 1687.5, too much")) 232 | } 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/GeneralMappingsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import java.util.ResourceBundle 5 | 6 | class GeneralMappingsSpec extends FunSpec with Matchers with Constraints { 7 | val bundle: ResourceBundle = ResourceBundle.getBundle("bind-messages") 8 | val messages: Messages = (key) => Option(bundle.getString(key)) 9 | 10 | case class TestBean(id: Long, name: String, desc: Option[String] = None) 11 | 12 | describe("test pre-defined general usage mappings") { 13 | 14 | describe("ignored-simple") { 15 | val ignored = Mappings.ignored(35) 16 | 17 | it("invalid data") { 18 | val invalidData = Map("number" -> "t135") 19 | ignored.validate("number", invalidData, messages, Options.apply()) match { 20 | case Nil => ignored.convert("number", invalidData) should be (35) 21 | case err => err should be (Nil) 22 | } 23 | } 24 | 25 | it("valid data") { 26 | val validData = Map("number" -> "135") 27 | ignored.validate("number", validData, messages, Options.apply()) match { 28 | case Nil => ignored.convert("number", validData) should be (35) 29 | case err => err should be (Nil) 30 | } 31 | } 32 | 33 | it("null data") { 34 | val nullData = Map[String, String]() 35 | ignored.validate("number", nullData, messages, Options.apply()) match { 36 | case Nil => ignored.convert("number", nullData) should be (35) 37 | case err => err should be (Nil) 38 | } 39 | } 40 | 41 | it("empty data") { 42 | val emptyData = Map("number" -> "") 43 | ignored.validate("number", emptyData, messages, Options.apply()) match { 44 | case Nil => ignored.convert("number", emptyData) should be (35) 45 | case err => err should be (Nil) 46 | } 47 | } 48 | } 49 | 50 | describe("optional-simple") { 51 | val base = Mappings.int() 52 | val optional = Mappings.optional(base) 53 | 54 | it("invalid data") { 55 | val invalidData = Map("number" -> "t122345") 56 | optional.validate("number", invalidData, messages, Options.apply()) match { 57 | case Nil => ("invalid - shouldn't occur!") should be ("") 58 | case err => { 59 | err should be (Seq("number" -> "'t122345' must be a number")) 60 | base.validate("number", invalidData, messages, Options.apply()) should be (Seq("number" -> "'t122345' must be a number")) 61 | } 62 | } 63 | } 64 | 65 | it("valid data") { 66 | val validData = Map("number" -> "122345") 67 | optional.validate("number", validData, messages, Options.apply()) match { 68 | case Nil => { 69 | base.validate("number", validData, messages, Options.apply()) should be (Nil) 70 | optional.convert("number", validData) should be (Some(122345)) 71 | } 72 | case err => err should be (Nil) 73 | } 74 | } 75 | 76 | it("null data") { 77 | val nullData = Map[String, String]() 78 | optional.validate("number", nullData, messages, Options.apply()) match { 79 | case Nil => { 80 | base.validate("number", nullData, messages, Options.apply()) should be (Nil) 81 | optional.convert("number", nullData) should be (None) 82 | } 83 | case err => err should be (Nil) 84 | } 85 | } 86 | 87 | it("empty data") { 88 | val emptyData = Map("number" -> "") 89 | optional.validate("number", emptyData, messages, Options.apply()) match { 90 | case Nil => { 91 | base.validate("number", emptyData, messages, Options.apply()) should be (Nil) 92 | optional.convert("number", emptyData) should be (None) 93 | } 94 | case err => err should be (Nil) 95 | } 96 | } 97 | 98 | it("delegate pre-processors") { 99 | val optional1 = Processors.omitLeft("$") >-: optional 100 | val validData = Map("number" -> "$12453") 101 | optional1.validate("number", validData, messages, Options.apply()) match { 102 | case Nil => { 103 | base.validate("number", validData, messages, Options.apply()) should be (Seq("number" -> "'$12453' must be a number")) 104 | optional1.convert("number", validData) should be (Some(12453)) 105 | } 106 | case err => err should be (Nil) 107 | } 108 | } 109 | 110 | it("delegate constraints") { 111 | val optional1 = Constraints.maxLength(8) >+: optional 112 | val invalidData = Map("number" -> "146896540") 113 | optional1.validate("number", invalidData, messages, Options.apply()) match { 114 | case Nil => ("invalid - shouldn't occur!") should be ("") 115 | case err => { 116 | base.validate("number", invalidData, messages, Options.apply()) should be (Nil) 117 | err should be (Seq("number" -> "'146896540' cannot be longer than 8 characters")) 118 | } 119 | } 120 | } 121 | } 122 | 123 | describe("default-simple") { 124 | val base = Mappings.int() 125 | val default = Mappings.default(base, 101) 126 | 127 | it("invalid data") { 128 | val invalidData = Map("number" -> "t122345") 129 | default.validate("number", invalidData, messages, Options.apply()) match { 130 | case Nil => ("invalid - shouldn't occur!") should be ("") 131 | case err => { 132 | err should be (Seq("number" -> "'t122345' must be a number")) 133 | base.validate("number", invalidData, messages, Options.apply()) should be (Seq("number" -> "'t122345' must be a number")) 134 | } 135 | } 136 | } 137 | 138 | it("valid data") { 139 | val validData = Map("number" -> "122345") 140 | default.validate("number", validData, messages, Options.apply()) match { 141 | case Nil => { 142 | base.validate("number", validData, messages, Options.apply()) should be (Nil) 143 | default.convert("number", validData) should be (122345) 144 | } 145 | case err => err should be (Nil) 146 | } 147 | } 148 | 149 | it("null data") { 150 | val nullData = Map[String, String]() 151 | default.validate("number", nullData, messages, Options.apply()) match { 152 | case Nil => { 153 | base.validate("number", nullData, messages, Options.apply()) should be (Nil) 154 | default.convert("number", nullData) should be (101) 155 | } 156 | case err => err should be (Nil) 157 | } 158 | } 159 | 160 | it("empty data") { 161 | val emptyData = Map("number" -> "") 162 | default.validate("number", emptyData, messages, Options.apply()) match { 163 | case Nil => { 164 | base.validate("number", emptyData, messages, Options.apply()) should be (Nil) 165 | default.convert("number", emptyData) should be (101) 166 | } 167 | case err => err should be (Nil) 168 | } 169 | } 170 | } 171 | 172 | describe("list-simple") { 173 | val base = Mappings.int() 174 | val list = Constraints.required() >+: Mappings.list(base).label("xx") 175 | 176 | it("invalid data") { 177 | val invalidData = Map("number[0]" -> "t122345", "number[1]" -> "t11345") 178 | list.validate("number", invalidData, messages, Options.apply()) match { 179 | case Nil => ("invalid - shouldn't occur!") should be ("") 180 | case err => { 181 | base.validate("number[0]", invalidData, messages, Options.apply()) should be (Seq("number[0]" -> "'t122345' must be a number")) 182 | base.validate("number[1]", invalidData, messages, Options.apply()) should be (Seq("number[1]" -> "'t11345' must be a number")) 183 | err should be (Seq("number[0]" -> "'t122345' must be a number", "number[1]" -> "'t11345' must be a number")) 184 | } 185 | } 186 | } 187 | 188 | it("valid data") { 189 | val validData = Map("number[0]" -> "122345", "number[1]" -> "754") 190 | list.validate("number", validData, messages, Options.apply()) match { 191 | case Nil => { 192 | base.validate("number[0]", validData, messages, Options.apply()) should be (Nil) 193 | base.validate("number[1]", validData, messages, Options.apply()) should be (Nil) 194 | list.convert("number", validData) should be (List(122345, 754)) 195 | } 196 | case err => err should be (Nil) 197 | } 198 | } 199 | 200 | it("null data") { 201 | val nullData = Map[String, String]() 202 | list.validate("number", nullData, (key) => Some("%s is required"), Options.apply()) match { 203 | case Nil => ("invalid - shouldn't occur!") should be ("") 204 | case err => err.toList should be (List("number" -> "xx is required")) 205 | } 206 | } 207 | 208 | it("empty data") { 209 | val emptyData = Map("number[0]" -> "", "number[1]" -> "133") 210 | list.validate("number", emptyData, (key) => Some("%s is required"), Options.apply()) match { 211 | case Nil => { 212 | base.validate("number[0]", emptyData, messages, Options.apply()) should be (Nil) 213 | base.validate("number[1]", emptyData, messages, Options.apply()) should be (Nil) 214 | list.convert("number", emptyData) should be (List(0, 133)) 215 | } 216 | case err => err should be (Nil) 217 | } 218 | } 219 | } 220 | 221 | describe("map-simple") { 222 | val base = Mappings.int() 223 | val map = Mappings.map(base, Constraints.required()).label("xx") 224 | 225 | it("invalid data") { 226 | val invalidData = Map("map.aa" -> "t122345", "map.\"b-1\"" -> "t11345") 227 | map.validate("map", invalidData, messages, Options.apply()) match { 228 | case Nil => ("invalid - shouldn't occur!") should be ("") 229 | case err => { 230 | base.validate("map.aa", invalidData, messages, Options.apply()) should be (Seq("map.aa" -> "'t122345' must be a number")) 231 | base.validate("map.\"b-1\"", invalidData, messages, Options.apply()) should be (Seq("map.\"b-1\"" -> "'t11345' must be a number")) 232 | err should be (Seq("map.aa" -> "'t122345' must be a number", "map.\"b-1\"" -> "'t11345' must be a number")) 233 | } 234 | } 235 | } 236 | 237 | it("valid data") { 238 | val validData = Map("map.aa" -> "122345", "map.\"b-1\"" -> "754") 239 | map.validate("map", validData, messages, Options.apply()) match { 240 | case Nil => { 241 | base.validate("map.aa", validData, messages, Options.apply()) should be (Nil) 242 | base.validate("map.\"b-1\"", validData, messages, Options.apply()) should be (Nil) 243 | map.convert("map", validData) should be (Map("aa" -> 122345, "b-1" -> 754)) 244 | } 245 | case err => err should be (Nil) 246 | } 247 | } 248 | 249 | it("null data") { 250 | val nullData = Map[String, String]() 251 | map.validate("map", nullData, (key) => Some("%s is required"), Options.apply()) match { 252 | case Nil => ("invalid - shouldn't occur!") should be ("") 253 | case err => err.toList should be (List("map" -> "xx is required")) 254 | } 255 | } 256 | 257 | it("empty data") { 258 | val emptyData = Map("map.aa" -> "", "map.\"b-1\"" -> "133") 259 | map.validate("map", emptyData, (key) => Some("%s is required"), Options.apply()) match { 260 | case Nil => { 261 | base.validate("map.aa", emptyData, messages, Options.apply()) should be (Nil) 262 | base.validate("map.\"b-1\"", emptyData, messages, Options.apply()) should be (Nil) 263 | map.convert("map", emptyData) should be (Map("aa" -> 0, "b-1" -> 133)) 264 | } 265 | case err => err should be (Nil) 266 | } 267 | } 268 | } 269 | 270 | describe("ignored-compound") { 271 | val testBean = TestBean(101, "test") 272 | val ignored = Mappings.ignored(testBean) 273 | 274 | it("invalid data") { 275 | val invalidData = Map( 276 | "test.id" -> "t135", 277 | "test.name" -> "test", 278 | "test.desc" -> "" 279 | ) 280 | ignored.validate("", invalidData, messages, Options.apply()) match { 281 | case Nil => ignored.convert("", invalidData) should be (testBean) 282 | case err => err should be (Nil) 283 | } 284 | } 285 | 286 | it("valid data") { 287 | val validData = Map( 288 | "test.id" -> "135", 289 | "test.name" -> "test", 290 | "test.desc" -> "" 291 | ) 292 | ignored.validate("", validData, messages, Options.apply()) match { 293 | case Nil => ignored.convert("", validData) should be (testBean) 294 | case err => err should be (Nil) 295 | } 296 | } 297 | 298 | it("null data") { 299 | val nullData = Map[String, String]() 300 | ignored.validate("", nullData, messages, Options.apply()) match { 301 | case Nil => ignored.convert("", nullData) should be (testBean) 302 | case err => err should be (Nil) 303 | } 304 | } 305 | 306 | it("empty data (wrong way)") { 307 | val emptyData = Map("test" -> "") 308 | ignored.validate("", emptyData, messages, Options.apply()) match { 309 | case Nil => ignored.convert("", emptyData) should be (testBean) 310 | case err => err should be (Nil) 311 | } 312 | } 313 | 314 | it("empty data") { 315 | val emptyData = Map("test" -> null) 316 | ignored.validate("", emptyData, messages, Options.apply()) match { 317 | case Nil => ignored.convert("", emptyData) should be (testBean) 318 | case err => err should be (Nil) 319 | } 320 | } 321 | } 322 | 323 | describe("optional-compound") { 324 | val dummyMessages1: Messages = (key: String) => Some("dummy") 325 | 326 | val base = Mappings.mapping( 327 | "id" -> Mappings.long(), 328 | "name" -> Mappings.text(), 329 | "desc" -> Mappings.optional(Mappings.text()) 330 | )(TestBean.apply) 331 | 332 | val optional = Mappings.optional(base) 333 | 334 | it("invalid data") { 335 | val invalidData = Map( 336 | "test.id" -> "t135", 337 | "test.name" -> "test", 338 | "test.desc" -> "" 339 | ) 340 | optional.validate("test", invalidData, dummyMessages1, Options.apply()) match { 341 | case Nil => ("invalid - shouldn't occur!") should be ("") 342 | case err => { 343 | err should be (Seq("test.id" -> "dummy")) 344 | base.validate("test", invalidData, dummyMessages1, Options.apply()) should be (Seq("test.id" -> "dummy")) 345 | } 346 | } 347 | } 348 | 349 | it("valid data") { 350 | val validData = Map( 351 | "test.id" -> "135", 352 | "test.name" -> "test", 353 | "test.desc" -> "" 354 | ) 355 | optional.validate("test", validData, dummyMessages1, Options.apply()) match { 356 | case Nil => { 357 | base.validate("test", validData, dummyMessages1, Options.apply()) should be (Nil) 358 | optional.convert("test", validData) should be (Some(TestBean(135, "test"))) 359 | } 360 | case err => err should be (Nil) 361 | } 362 | } 363 | 364 | it("null data") { 365 | val nullData = Map[String, String]() 366 | optional.validate("test", nullData, dummyMessages1, Options.apply()) match { 367 | case Nil => { 368 | base.validate("test", nullData, dummyMessages1, Options.apply()) should be (Nil) 369 | optional.convert("test", nullData) should be (None) 370 | } 371 | case err => err should be (Nil) 372 | } 373 | } 374 | 375 | it("empty data (wrong way)") { 376 | val emptyData = Map("test" -> "") 377 | optional.validate("test", emptyData, dummyMessages1, Options.apply()) match { 378 | case Nil => { 379 | base.validate("test", emptyData, dummyMessages1, Options.apply()) should be (Nil) 380 | optional.convert("test", emptyData) should be (None) 381 | } 382 | case err => err should be (Nil) 383 | } 384 | } 385 | 386 | it("empty data") { 387 | val emptyData = Map("test" -> null) 388 | optional.validate("test", emptyData, dummyMessages1, Options.apply()) match { 389 | case Nil => { 390 | base.validate("test", emptyData, dummyMessages1, Options.apply()) should be (Nil) 391 | optional.convert("test", emptyData) should be (None) 392 | } 393 | case err => err should be (Nil) 394 | } 395 | } 396 | } 397 | 398 | describe("default-compound") { 399 | val dummyMessages1: Messages = (key: String) => { 400 | if (key == "error.object") Some("%s missing or not valid") 401 | else Some("dummy") 402 | } 403 | 404 | val base = Mappings.mapping( 405 | "id" -> Mappings.long(), 406 | "name" -> Mappings.text(), 407 | "desc" -> Mappings.optional(Mappings.text()) 408 | )(TestBean.apply) 409 | 410 | val testBean = TestBean(35, "test1", Some("test bean")) 411 | val default = Mappings.default(base, testBean) 412 | 413 | it("invalid data") { 414 | val invalidData = Map( 415 | "test.id" -> "t135", 416 | "test.name" -> "test", 417 | "test.desc" -> "" 418 | ) 419 | default.validate("test", invalidData, dummyMessages1, Options.apply()) match { 420 | case Nil => ("invalid - shouldn't occur!") should be ("") 421 | case err => { 422 | err should be (Seq("test.id" -> "dummy")) 423 | base.validate("test", invalidData, dummyMessages1, Options.apply()) should be (Seq("test.id" -> "dummy")) 424 | } 425 | } 426 | } 427 | 428 | it("valid data") { 429 | val validData = Map( 430 | "test.id" -> "135", 431 | "test.name" -> "test", 432 | "test.desc" -> "" 433 | ) 434 | default.validate("test", validData, dummyMessages1, Options.apply()) match { 435 | case Nil => { 436 | base.validate("test", validData, dummyMessages1, Options.apply()) should be (Nil) 437 | default.convert("test", validData) should be (TestBean(135, "test")) 438 | } 439 | case err => err should be (Nil) 440 | } 441 | } 442 | 443 | it("null data") { 444 | val nullData = Map[String, String]() 445 | default.validate("test", nullData, dummyMessages1, Options.apply()) match { 446 | case Nil => { 447 | base.validate("test", nullData, dummyMessages1, Options.apply()) should be (Nil) 448 | default.convert("test", nullData) should be (testBean) 449 | } 450 | case err => err should be (Nil) 451 | } 452 | } 453 | 454 | it("empty data (wrong way)") { 455 | val emptyData = Map("test" -> "") 456 | default.validate("test", emptyData, dummyMessages1, Options.apply()) match { 457 | case Nil => { 458 | base.validate("test", emptyData, dummyMessages1, Options.apply()) should be (Nil) 459 | default.convert("test", emptyData) should be (testBean) 460 | } 461 | case err => err should be (Nil) 462 | } 463 | } 464 | 465 | it("empty data") { 466 | val emptyData = Map("test" -> null) 467 | default.validate("test", emptyData, dummyMessages1, Options.apply()) match { 468 | case Nil => { 469 | base.validate("test", emptyData, dummyMessages1, Options.apply()) should be (Nil) 470 | default.convert("test", emptyData) should be (testBean) 471 | } 472 | case err => err should be (Nil) 473 | } 474 | } 475 | } 476 | 477 | describe("list-compound") { 478 | val dummyMessages1: Messages = (key: String) => { 479 | if (key == "error.object") Some("%s missing or not valid") 480 | else Some("dummy") 481 | } 482 | 483 | val base = Mappings.mapping( 484 | "id" -> Mappings.long(), 485 | "name" -> Mappings.text(), 486 | "desc" -> Mappings.optional(Mappings.text()) 487 | )(TestBean.apply) 488 | 489 | val list = Mappings.list(base) 490 | 491 | it("invalid data") { 492 | val invalidData = Map( 493 | "test[0].id" -> "t135", 494 | "test[0].name" -> "test", 495 | "test[0].desc" -> "", 496 | "test[1].id" -> "t137", 497 | "test[1].name" -> "test", 498 | "test[1].desc" -> "tt" 499 | ) 500 | list.validate("test", invalidData, dummyMessages1, Options.apply()) match { 501 | case Nil => ("invalid - shouldn't occur!") should be ("") 502 | case err => err should be (Seq("test[0].id" -> "dummy", "test[1].id" -> "dummy")) 503 | } 504 | } 505 | 506 | it("valid data") { 507 | val validData = Map( 508 | "test[0].id" -> "135", 509 | "test[0].name" -> "test", 510 | "test[0].desc" -> "", 511 | "test[1].id" -> "137", 512 | "test[1].name" -> "test1", 513 | "test[1].desc" -> "tt" 514 | ) 515 | list.validate("test", validData, dummyMessages1, Options.apply()) match { 516 | case Nil => { 517 | base.validate("test[0]", validData, dummyMessages1, Options.apply()) should be (Nil) 518 | base.validate("test[1]", validData, dummyMessages1, Options.apply()) should be (Nil) 519 | list.convert("test", validData) should be (List(TestBean(135, "test"), TestBean(137, "test1", Some("tt")))) 520 | } 521 | case err => err should be (Nil) 522 | } 523 | } 524 | 525 | it("null data") { 526 | val nullData = Map[String, String]() 527 | list.validate("test", nullData, dummyMessages1, Options.apply()) match { 528 | case Nil => { 529 | base.validate("test[0]", nullData, dummyMessages1, Options.apply()) should be (Nil) 530 | list.convert("test", nullData) should be (Nil) 531 | } 532 | case err => err should be (Nil) 533 | } 534 | } 535 | 536 | it("empty data (wrong way)") { 537 | val emptyData = Map("test" -> "") 538 | list.validate("test", emptyData, dummyMessages1, Options.apply()) match { 539 | case Nil => { 540 | base.validate("test[0]", emptyData, dummyMessages1, Options.apply()) should be (Nil) 541 | list.convert("test", emptyData) should be (Nil) 542 | } 543 | case err => err should be (Nil) 544 | } 545 | } 546 | 547 | it("empty data") { 548 | val emptyData = Map("test" -> null) 549 | list.validate("test", emptyData, dummyMessages1, Options.apply()) match { 550 | case Nil => { 551 | base.validate("test[0]", emptyData, dummyMessages1, Options.apply()) should be (Nil) 552 | list.convert("test", emptyData) should be (Nil) 553 | } 554 | case err => err should be (Nil) 555 | } 556 | } 557 | } 558 | 559 | describe("map-compound") { 560 | val dummyMessages1: Messages = (key: String) => { 561 | if (key == "error.object") Some("%s missing or not valid") 562 | else Some("dummy") 563 | } 564 | 565 | val key = Mappings.int() 566 | val value = Mappings.mapping( 567 | "id" -> Mappings.long(), 568 | "name" -> Mappings.text(), 569 | "desc" -> Mappings.optional(Mappings.text()) 570 | )(TestBean.apply) 571 | 572 | val map = Mappings.map(key, value) 573 | 574 | it("invalid data") { 575 | val invalidData = Map( 576 | "test.101.id" -> "t135", 577 | "test.101.name" -> "test", 578 | "test.101.desc" -> "", 579 | "test.103.id" -> "t137", 580 | "test.103.name" -> "test", 581 | "test.103.desc" -> "tt" 582 | ) 583 | map.validate("test", invalidData, dummyMessages1, Options.apply()) match { 584 | case Nil => ("invalid - shouldn't occur!") should be ("") 585 | case err => err.toList should be (List("test.103.id" -> "dummy", "test.101.id" -> "dummy")) 586 | } 587 | } 588 | 589 | it("valid data") { 590 | val validData = Map( 591 | "test.101.id" -> "135", 592 | "test.101.name" -> "test", 593 | "test.101.desc" -> "", 594 | "test.103.id" -> "137", 595 | "test.103.name" -> "test1", 596 | "test.103.desc" -> "tt" 597 | ) 598 | map.validate("test", validData, dummyMessages1, Options.apply()) match { 599 | case Nil => { 600 | value.validate("test.101", validData, dummyMessages1, Options.apply()) should be (Nil) 601 | value.validate("test.101", validData, dummyMessages1, Options.apply()) should be (Nil) 602 | map.convert("test", validData) should be (Map(101 -> TestBean(135, "test"), 103 -> TestBean(137, "test1", Some("tt")))) 603 | } 604 | case err => err should be (Nil) 605 | } 606 | } 607 | 608 | it("null data") { 609 | val nullData = Map[String, String]() 610 | map.validate("test", nullData, dummyMessages1, Options.apply()) match { 611 | case Nil => { 612 | value.validate("test.101", nullData, dummyMessages1, Options.apply()) should be (Nil) 613 | map.convert("test", nullData) should be (Map()) 614 | } 615 | case err => err should be (Nil) 616 | } 617 | } 618 | 619 | it("empty data (wrong way)") { 620 | val emptyData = Map("test" -> "") 621 | map.validate("test", emptyData, dummyMessages1, Options.apply()) match { 622 | case Nil => { 623 | value.validate("test.101", emptyData, dummyMessages1, Options.apply()) should be (Nil) 624 | map.convert("test", emptyData) should be (Map()) 625 | } 626 | case err => err should be (Nil) 627 | } 628 | } 629 | 630 | it("empty data") { 631 | val emptyData = Map("test" -> null) 632 | map.validate("test", emptyData, dummyMessages1, Options.apply()) match { 633 | case Nil => { 634 | value.validate("test.101", emptyData, dummyMessages1, Options.apply()) should be (Nil) 635 | map.convert("test", emptyData) should be (Map()) 636 | } 637 | case err => err should be (Nil) 638 | } 639 | } 640 | } 641 | 642 | describe("w/ options") { 643 | 644 | it("pass thru options") { 645 | val base = Mappings.mapping( 646 | "id" -> Mappings.long(required("%s is required")).label("id"), 647 | "name" -> Mappings.text(), 648 | "desc" -> Mappings.optional(Mappings.text()) 649 | )(TestBean.apply) 650 | val list = Mappings.list(base) 651 | 652 | val data = Map( 653 | "test[0].id" -> "", 654 | "test[0].name" -> "test", 655 | "test[0].desc" -> "", 656 | "test[1].id" -> "137", 657 | "test[1].name" -> "test1", 658 | "test[1].desc" -> "tt" 659 | ) 660 | 661 | list.validate("test", data, messages, Options.apply()) match { 662 | case Nil => ("invalid - shouldn't occur!") should be ("") 663 | case err => err.toList should be (List("test[0].id" -> "id is required")) 664 | } 665 | list.validate("test", data, messages, Options().skipUntouched(true)) match { 666 | case Nil => list.convert("test", data) should be ( 667 | List(TestBean(0, "test"), TestBean(137, "test1", Some("tt")))) 668 | case err => err should be (Nil) 669 | } 670 | } 671 | } 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/GroupMappingsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import java.util.ResourceBundle 5 | 6 | class GroupMappingsSpec extends FunSpec with Matchers with Mappings with Constraints { 7 | val bundle: ResourceBundle = ResourceBundle.getBundle("bind-messages") 8 | val messages: Messages = (key) => Option(bundle.getString(key)) 9 | 10 | describe("test pre-defined group mappings") { 11 | 12 | describe("group-mapping1") { 13 | val mapping1 = tmapping( 14 | "count" -> int() 15 | ).label("xx") verifying { (label, v, messages) => 16 | if (v < 3) Seq(s"$v: cannot less than 3") 17 | else if (v > 10) Seq(s"$label: cannot greater than 10") 18 | else Nil 19 | } 20 | 21 | it("invalid data") { 22 | val invalidData = Map("count" -> "t5") 23 | mapping1.validate("", invalidData, messages, Options.apply()) match { 24 | case Nil => ("invalid - shouldn't occur!") should be ("") 25 | case err => err should be (Seq("count" -> "'t5' must be a number")) 26 | } 27 | } 28 | 29 | it("out-of-scope data") { 30 | val invalidData = Map("count" -> "15") 31 | mapping1.validate("", invalidData, messages, Options.apply()) match { 32 | case Nil => ("invalid - shouldn't occur!") should be ("") 33 | case err => err should be (Seq("" -> "xx: cannot greater than 10")) 34 | } 35 | } 36 | 37 | it("valid data") { 38 | val validData = Map("count" -> "5") 39 | mapping1.validate("", validData, messages, Options.apply()) match { 40 | case Nil => mapping1.convert("", validData) should be (5) 41 | case err => err should be (Nil) 42 | } 43 | } 44 | 45 | it("null data") { 46 | val nullData = Map[String, String]() 47 | mapping1.validate("", nullData, messages, Options.apply()) match { 48 | case Nil => mapping1.convert("", nullData) should be (null.asInstanceOf[(Int)]) 49 | case err => err should be (Nil) 50 | } 51 | } 52 | 53 | it("empty data") { 54 | val emptyData = Map("" -> null) 55 | mapping1.validate("", emptyData, messages, Options.apply()) match { 56 | case Nil => mapping1.convert("", emptyData) should be (null.asInstanceOf[(Int)]) 57 | case err => err should be (Nil) 58 | } 59 | } 60 | } 61 | 62 | describe("group-mapping2") { 63 | val mapping2 = tmapping( 64 | "price" -> float(), 65 | "count" -> int().verifying(min(3), max(10)) 66 | ).label("xx") verifying { case (label, (price, count), messages) => 67 | if (price * count > 1000) { 68 | Seq(s"$label: $price * $count = ${price * count}, too much") 69 | } else Nil 70 | } 71 | 72 | it("invalid data") { 73 | val invalidData = Map("price" -> "23.5f", "count" -> "t5") 74 | mapping2.validate("", invalidData, messages, Options.apply()) match { 75 | case Nil => ("invalid - shouldn't occur!") should be ("") 76 | case err => err should be (Seq("count" -> "'t5' must be a number")) 77 | } 78 | } 79 | 80 | it("out-of-scope data") { 81 | val invalidData = Map("price" -> "23.5f", "count" -> "15") 82 | mapping2.validate("", invalidData, messages, Options.apply()) match { 83 | case Nil => ("invalid - shouldn't occur!") should be ("") 84 | case err => err should be (Seq("count" -> "'15' cannot be greater than 10")) 85 | } 86 | } 87 | 88 | it("out-of-scope data1") { 89 | val invalidData = Map("price" -> "123.5f", "count" -> "9") 90 | mapping2.validate("", invalidData, messages, Options.apply()) match { 91 | case Nil => ("invalid - shouldn't occur!") should be ("") 92 | case err => err should be (Seq("" -> "xx: 123.5 * 9 = 1111.5, too much")) 93 | } 94 | } 95 | 96 | it("valid data") { 97 | val validData = Map("price" -> "23.5", "count" -> "5") 98 | mapping2.validate("", validData, messages, Options.apply()) match { 99 | case Nil => mapping2.convert("", validData) should be ((23.5f, 5)) 100 | case err => err should be (Nil) 101 | } 102 | } 103 | 104 | it("null data") { 105 | val nullData = Map[String, String]() 106 | mapping2.validate("", nullData, messages, Options.apply()) match { 107 | case Nil => mapping2.convert("", nullData) should be (null.asInstanceOf[(Float, Int)]) 108 | case err => err should be (Nil) 109 | } 110 | } 111 | 112 | it("empty data") { 113 | val emptyData = Map("" -> null) 114 | mapping2.validate("", emptyData, messages, Options.apply()) match { 115 | case Nil => mapping2.convert("", emptyData) should be (null.asInstanceOf[(Float, Int)]) 116 | case err => err should be (Nil) 117 | } 118 | } 119 | } 120 | 121 | describe("w/ options") { 122 | 123 | it("w/ eager check") { 124 | val mappingx = tmapping( 125 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email")), 126 | "count" -> int().verifying(max(10, "%s > %s"), max(15, "%s > %s")) 127 | ).options(_.eagerCheck(true)) 128 | val data = Map( 129 | "email" -> "etttt.att#example-1111111.com", 130 | "count" -> "20") 131 | 132 | mappingx.validate("", data, messages, Options.apply()) match { 133 | case Nil => ("invalid - shouldn't occur!") should be ("") 134 | case err => err should be (Seq( 135 | "email" -> "etttt.att#example-1111111.com: length > 20", 136 | "email" -> "etttt.att#example-1111111.com: invalid email", 137 | "count" -> "20 > 10", 138 | "count" -> "20 > 15")) 139 | } 140 | } 141 | 142 | it("w/ ignore empty") { 143 | val mappingx = tmapping( 144 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 145 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 146 | ) 147 | val nullData = Map[String, String]() 148 | val emptyData = Map.empty + ("count" -> "") 149 | 150 | /// 151 | mappingx.validate("", nullData, messages, Options.apply()) match { 152 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 153 | case err => err should be (Nil) 154 | } 155 | 156 | mappingx.options(_.skipUntouched(true)) 157 | .validate("", nullData, messages, Options.apply()) match { 158 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 159 | case err => err should be (Nil) 160 | } 161 | 162 | /// 163 | mappingx.validate("", emptyData, messages, Options.apply()) match { 164 | case Nil => ("invalid - shouldn't occur!") should be ("") 165 | case err => err should be (Seq("email" -> "email is required")) 166 | } 167 | 168 | mappingx.options(_.skipUntouched(true)) 169 | .validate("", emptyData, messages, Options.apply()) match { 170 | case Nil => mappingx.convert("", emptyData) should be ((null, 0)) 171 | case err => err should be (Nil) 172 | } 173 | } 174 | 175 | it("w/ ignore empty and touched") { 176 | val mappingx = tmapping( 177 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 178 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 179 | ) 180 | val emptyData = Map.empty + ("count" -> "") 181 | 182 | /// 183 | mappingx.validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 184 | case Nil => ("invalid - shouldn't occur!") should be ("") 185 | case err => err should be (Seq("email" -> "email is required")) 186 | } 187 | 188 | mappingx.options(_.skipUntouched(true)) 189 | .validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 190 | case Nil => ("invalid - shouldn't occur!") should be ("") 191 | case err => err should be (Seq("email" -> "email is required")) 192 | } 193 | } 194 | 195 | it("w/ eager check thru verifying") { 196 | val mappingx = tmapping( 197 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email")), 198 | "count" -> int().verifying(max(10, "%s > %s"), max(15, "%s > %s")) 199 | ).verifying().options(_.eagerCheck(true)) 200 | val data = Map( 201 | "email" -> "etttt.att#example-1111111.com", 202 | "count" -> "20") 203 | 204 | mappingx.validate("", data, messages, Options.apply()) match { 205 | case Nil => ("invalid - shouldn't occur!") should be ("") 206 | case err => err should be (Seq( 207 | "email" -> "etttt.att#example-1111111.com: length > 20", 208 | "email" -> "etttt.att#example-1111111.com: invalid email", 209 | "count" -> "20 > 10", 210 | "count" -> "20 > 15")) 211 | } 212 | } 213 | 214 | it("w/ ignore empty thru verifying") { 215 | val mappingx = tmapping( 216 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 217 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 218 | ).verifying() 219 | val nullData = Map[String, String]() 220 | val emptyData = Map.empty + ("count" -> "") 221 | 222 | /// 223 | mappingx.validate("", nullData, messages, Options.apply()) match { 224 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 225 | case err => err should be (Nil) 226 | } 227 | 228 | mappingx.options(_.skipUntouched(true)) 229 | .validate("", nullData, messages, Options.apply()) match { 230 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 231 | case err => err should be (Nil) 232 | } 233 | 234 | /// 235 | mappingx.validate("", emptyData, messages, Options.apply()) match { 236 | case Nil => ("invalid - shouldn't occur!") should be ("") 237 | case err => err should be (Seq("email" -> "email is required")) 238 | } 239 | 240 | mappingx.options(_.skipUntouched(true)) 241 | .validate("", emptyData, messages, Options.apply()) match { 242 | case Nil => mappingx.convert("", emptyData) should be ((null, 0)) 243 | case err => err should be (Nil) 244 | } 245 | } 246 | 247 | it("w/ ignore empty and touched thru verifying") { 248 | val mappingx = tmapping( 249 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 250 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 251 | ).verifying() 252 | val emptyData = Map.empty + ("count" -> "") 253 | 254 | /// 255 | mappingx.validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 256 | case Nil => ("invalid - shouldn't occur!") should be ("") 257 | case err => err should be (Seq("email" -> "email is required")) 258 | } 259 | 260 | mappingx.options(_.skipUntouched(true)) 261 | .validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 262 | case Nil => ("invalid - shouldn't occur!") should be ("") 263 | case err => err should be (Seq("email" -> "email is required")) 264 | } 265 | } 266 | 267 | it("w/ eager check + transform (mapTo)") { 268 | val mappingx = tmapping( 269 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email")), 270 | "count" -> int().verifying(max(10, "%s > %s"), max(15, "%s > %s")) 271 | ).map(identity).options(_.eagerCheck(true)) 272 | val data = Map( 273 | "email" -> "etttt.att#example-1111111.com", 274 | "count" -> "20") 275 | 276 | mappingx.validate("", data, messages, Options.apply()) match { 277 | case Nil => ("invalid - shouldn't occur!") should be ("") 278 | case err => err should be (Seq( 279 | "email" -> "etttt.att#example-1111111.com: length > 20", 280 | "email" -> "etttt.att#example-1111111.com: invalid email", 281 | "count" -> "20 > 10", 282 | "count" -> "20 > 15")) 283 | } 284 | } 285 | 286 | it("w/ ignore empty + transform (mapTo)") { 287 | val mappingx = tmapping( 288 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 289 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 290 | ).map(identity) 291 | val nullData = Map[String, String]() 292 | val emptyData = Map.empty + ("count" -> "") 293 | 294 | /// 295 | mappingx.validate("", nullData, messages, Options.apply()) match { 296 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 297 | case err => err should be (Nil) 298 | } 299 | 300 | mappingx.options(_.skipUntouched(true)) 301 | .validate("", nullData, messages, Options.apply()) match { 302 | case Nil => mappingx.convert("", nullData) should be (null.asInstanceOf[(String, Int)]) 303 | case err => err should be (Nil) 304 | } 305 | 306 | /// 307 | mappingx.validate("", emptyData, messages, Options.apply()) match { 308 | case Nil => ("invalid - shouldn't occur!") should be ("") 309 | case err => err should be (Seq("email" -> "email is required")) 310 | } 311 | 312 | mappingx.options(_.skipUntouched(true)) 313 | .validate("", emptyData, messages, Options.apply()) match { 314 | case Nil => mappingx.convert("", emptyData) should be ((null, 0)) 315 | case err => err should be (Nil) 316 | } 317 | } 318 | 319 | it("w/ ignore empty and touched + transform (mapTo)") { 320 | val mappingx = tmapping( 321 | "email" -> text(maxLength(20, "%s: length > %s"), email("%s: invalid email"), required("%s is required")), 322 | "count" -> int().verifying(max(10, "%s: > %s"), max(15, "%s: > %s")) 323 | ).map(identity) 324 | val emptyData = Map.empty + ("count" -> "") 325 | 326 | /// 327 | mappingx.validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 328 | case Nil => ("invalid - shouldn't occur!") should be ("") 329 | case err => err should be (Seq("email" -> "email is required")) 330 | } 331 | 332 | mappingx.options(_.skipUntouched(true)) 333 | .validate("", emptyData, messages, Options().touchedChecker(Processors.listTouched(List("email")))) match { 334 | case Nil => ("invalid - shouldn't occur!") should be ("") 335 | case err => err should be (Seq("email" -> "email is required")) 336 | } 337 | } 338 | } 339 | 340 | /// 341 | case class A(id: Long, name: String) 342 | case class B(id: Long, desc: String, children: List[A]) 343 | 344 | describe("case classes") { 345 | val mapping1 = mapping( 346 | "id" -> long(), 347 | "desc" -> text(), 348 | "children" -> list( 349 | mapping( 350 | "id" -> long(), 351 | "name" -> text() 352 | )(A.apply _) 353 | ) 354 | )(B) 355 | 356 | it("simple test") { 357 | val data = Map( 358 | "id" -> "101", 359 | "desc" -> "test", 360 | "children[0].id" -> "201", 361 | "children[0].name" -> "ch1", 362 | "children[1].id" -> "202", 363 | "children[1].name" -> "ch2" 364 | ) 365 | mapping1.validate("", data, messages, Options.apply()) match { 366 | case Nil => mapping1.convert("", data) should be (B(101, "test", List(A(201, "ch1"), A(202, "ch2")))) 367 | case err => err should be (Nil) 368 | } 369 | } 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tminglei/bind/ProcessorsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind 2 | 3 | import org.scalatest._ 4 | import scala.collection.mutable.ListBuffer 5 | 6 | class ProcessorsSpec extends FunSpec with Matchers { 7 | 8 | describe("test pre-defined pre-processors") { 9 | 10 | it("trim") { 11 | val trim = Processors.trim 12 | trim("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 13 | trim("", Map("" -> " yuu"), Options.apply()) should be (Map("" -> "yuu")) 14 | trim("a", Map("a" -> "eyuu"), Options.apply()) should be (Map("a" -> "eyuu")) 15 | } 16 | 17 | it("omit") { 18 | val omit = Processors.omit(",") 19 | omit("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 20 | omit("", Map("" -> "123,334"), Options.apply()) should be (Map("" -> "123334")) 21 | omit("a", Map("a" -> "2.345e+5"), Options.apply()) should be (Map("a" -> "2.345e+5")) 22 | } 23 | 24 | it("omit-left") { 25 | val omitLeft = Processors.omitLeft("$") 26 | omitLeft("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 27 | omitLeft("", Map("" -> "$3,567"), Options.apply()) should be (Map("" -> "3,567")) 28 | omitLeft("a", Map("a" -> "35667"), Options.apply()) should be (Map("a" -> "35667")) 29 | } 30 | 31 | it("omit-right") { 32 | val omitRight = Processors.omitRight("-tat") 33 | omitRight("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 34 | omitRight("a", Map("a" -> "tewwwtt-tat"), Options.apply()) should be (Map("a" -> "tewwwtt")) 35 | } 36 | 37 | it("omit-redundant") { 38 | val cleanRedundant = Processors.omitRedundant(" ") 39 | cleanRedundant("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 40 | cleanRedundant("a", Map("a" -> " a teee 86y"), Options.apply()) should be (Map("a" -> " a teee 86y")) 41 | cleanRedundant("", Map("" -> "te yu "), Options.apply()) should be (Map("" -> "te yu ")) 42 | } 43 | 44 | it("omit-matched") { 45 | val omitMatched = Processors.omitMatched("-\\d\\d$".r) 46 | omitMatched("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 47 | omitMatched("", Map("" -> "2342-334-12"), Options.apply()) should be (Map("" -> "2342-334")) 48 | omitMatched("a", Map("a" -> "2342-334"), Options.apply()) should be (Map("a" -> "2342-334")) 49 | } 50 | 51 | it("omit-matched w/ replacement") { 52 | val omitMatched = Processors.omitMatched("-\\d\\d$".r, "-1") 53 | omitMatched("", Map("" -> null), Options.apply()) should be (Map("" -> null)) 54 | omitMatched("", Map("" -> "2342-334-12"), Options.apply()) should be (Map("" -> "2342-334-1")) 55 | omitMatched("a", Map("a" -> "2342-334"), Options.apply()) should be (Map("a" -> "2342-334")) 56 | } 57 | } 58 | 59 | describe("test pre-defined bulk pre-processors") { 60 | 61 | describe("changePrefix") { 62 | 63 | it("simple") { 64 | val changePrefix = Processors.changePrefix("json", "data") 65 | val data = Map( 66 | "aa" -> "wett", 67 | "json.id" -> "123", 68 | "json.name" -> "tewd", 69 | "json.dr-1[0]" -> "33", 70 | "json.dr-1[1]" -> "45" 71 | ) 72 | val expected = Map( 73 | "aa" -> "wett", 74 | "data.id" -> "123", 75 | "data.name" -> "tewd", 76 | "data.dr-1[0]" -> "33", 77 | "data.dr-1[1]" -> "45" 78 | ) 79 | 80 | changePrefix("", data, Options.apply()) should be (expected) 81 | } 82 | } 83 | 84 | describe("expandJson") { 85 | 86 | it("simple") { 87 | val expandJson = Processors.expandJson(Some("json")) 88 | val data = Map( 89 | "aa" -> "wett", 90 | "json" -> """{"id":123, "name":"tewd", "dr-1":[33,45]}""" 91 | ) 92 | val expected = Map( 93 | "aa" -> "wett", 94 | "json.id" -> "123", 95 | "json.name" -> "tewd", 96 | "json.dr-1[0]" -> "33", 97 | "json.dr-1[1]" -> "45" 98 | ) 99 | 100 | expandJson("", data, Options.apply()) should be (expected) 101 | } 102 | 103 | it("null or empty") { 104 | val expandJsonData = Processors.expandJson(Some("json")) 105 | 106 | val nullData = Map("aa" -> "wett") 107 | expandJsonData("", nullData, Options.apply()) should be (nullData) 108 | 109 | val nullData1 = Map("aa" -> "wett", "json" -> null) 110 | expandJsonData("", nullData1, Options.apply()) should be (nullData1) 111 | 112 | val emptyData1 = Map("aa" -> "wett", "json" -> "") 113 | expandJsonData("", emptyData1, Options.apply()) should be (emptyData1) 114 | } 115 | 116 | it("with dest prefix") { 117 | val expandJson = Processors.expandJson(Some("body")) 118 | val data = Map( 119 | "aa" -> "wett", 120 | "body" -> """{"id":123, "name":"tewd", "dr-1":[33,45]}""" 121 | ) 122 | val expected = Map( 123 | "aa" -> "wett", 124 | "body.id" -> "123", 125 | "body.name" -> "tewd", 126 | "body.dr-1[0]" -> "33", 127 | "body.dr-1[1]" -> "45" 128 | ) 129 | 130 | expandJson("", data, Options.apply()) should be (expected) 131 | } 132 | } 133 | } 134 | 135 | describe("test pre-defined post err-processors") { 136 | 137 | describe("foldErrs") { 138 | it("simple") { 139 | val errs = Seq( 140 | "" -> "top error1", 141 | "aa" -> "error aa", 142 | "aa.ty" -> "error aa.ty", 143 | "aa" -> "error aa 1", 144 | "aa.tl[3]" -> "ewty", 145 | "aa.tl[3]" -> "ewyu7", 146 | "br-1[t0]" -> "key: eeor", 147 | "br-1[t0]" -> "tert", 148 | "br-1[1]" -> "tetty", 149 | "" -> "top error2" 150 | ) 151 | 152 | val expected = Map( 153 | "" -> List("top error1", "top error2"), 154 | "aa" -> List("error aa", "error aa 1"), 155 | "aa.ty" -> List("error aa.ty"), 156 | "aa.tl[3]" -> List("ewty", "ewyu7"), 157 | "br-1[t0]" -> List("key: eeor", "tert"), 158 | "br-1[1]" -> List("tetty") 159 | ) 160 | 161 | Processors.foldErrs()(errs) should be (expected) 162 | } 163 | } 164 | 165 | describe("errsTree") { 166 | 167 | it("simple") { 168 | val errs = Seq( 169 | "" -> "top error1", 170 | "aa" -> "error aa", 171 | "aa.ty" -> "error aa.ty", 172 | "aa" -> "error aa 1", 173 | "aa.tl[3]" -> "ewty", 174 | "aa.tl[3]" -> "ewyu7", 175 | "br-1[t0]" -> "key: eeor", 176 | "br-1[t0]" -> "tert", 177 | "br-1[1]" -> "tetty", 178 | "" -> "top error2" 179 | ) 180 | 181 | val expected = Map( 182 | "_errors" -> ListBuffer("top error1", "top error2"), 183 | "br-1" -> Map( 184 | "t0" -> Map( 185 | "_errors" -> ListBuffer("key: eeor", "tert") 186 | ), 187 | "1" -> Map( 188 | "_errors" -> ListBuffer("tetty") 189 | ) 190 | ), 191 | "aa" -> Map( 192 | "ty" -> Map( 193 | "_errors" -> ListBuffer("error aa.ty") 194 | ), 195 | "tl" -> Map( 196 | "3" -> Map( 197 | "_errors" -> ListBuffer("ewty", "ewyu7") 198 | ) 199 | ), 200 | "_errors" -> ListBuffer("error aa", "error aa 1") 201 | )) 202 | 203 | Processors.errsTree()(errs) should be (expected) 204 | } 205 | } 206 | } 207 | } 208 | --------------------------------------------------------------------------------