├── .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 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------