How to use

provides a strictly-typed, syntactically concise, extensible set of data validators.

Characteristics:

  • Simple and concise usage syntax.
  • Complex validators are constructed by chaining (piping) primitive ones.
  • Strict typization leveraged: the compatibility of sibling validators in chains is checked by compiler.
  • As a consequence, validator's output is of correct type. For example, if you check value to be an integer, you'll get Int.
  • Flexible error messages support.
  • Validators are stateless and thus thread-safe.
  • Creating your own validators is easy.

Contents of this page:

Basics: validator chains

contains many primitive validators as building blocks and allows to combine (chain) them into more complex validators, like this:

					import ru.dimgel.lib.validator._
					
					val v = VInt & VRange(0, 100)
				

This validator chain does two things:

  1. Checks that its input contains integer in range [0, 100], inclusive.
  2. Returns that integer if validation was successful.

So, validators are also data converters. Each validator in chain takes output of its leftside sibling as its own input. It's like Unix shell pipes. Of course, chained validators must have compatible types, or you'll get compilation error: that is, leftside output must be of the same type or subtype of rightside input.

Not all validators actually convert data. For example, VInt does: its input type is String and output type is Int. But VRange performs only range test without conversion, and its both input and output types are the same: T <: Ordered[T].

Validator class's constructors are protected, so the only way you can instantiate validator is by using companion object. Just a matter of enforcing concise code style; if it prevents you, please let me know.

Now let's use it:

					import Validator.{Data, Failure}
					
					val s = "42"  // String
					val r = v.validate(s)  // Validator.Result[Int]
					r.match {
						case Data(i) => ... // Here we are, i == 42
						case Failure(msg) => throw new Exception(msg)
					}
				

You can also check r.ok boolean property if you don't want neither i nor msg.

Class Param. Special "chain-head" validators: VRequired* and VOptional*

In the web (which this library was initially created for) HTTP request parameters and headers may be multi-valued. Class Param encapsulates list of parameter values:

					val p0  = Param(Nil)  // missing parameter
					val p0a = Param()     // missing parameter
					val p1  = Param("42")  // single-valued
					val p2  = Param("42" :: "43" :: Nil)  // multi-valued
				

There is a special group of validators with names starting with VRequired and VOptional. They are subtypes of VChain.Head, take Param as their input and always are in the beginning of the chain.

  • VRequired* test their input for emptiness (emptiness semantics differs among classes with different name suffixes). It returns error if value is empty, and passes on to the rest of the chain otherwise.
  • VOptional* differ from VRequired* only in that in the case of empty value they return successful Empty result.

An updated example:

					val p = Param(" 42 ")
					val v = VOptionalTrim & VInt & VRange(0, 100)
					val r = v.validate(p)
					r.match {
						case Empty => ...
						case Data(i) => ... // Here we are, i == 42
						case Failure(msg) => throw new Exception(msg)
					}
				

What is important here to notice:

  • Parameter value " 42 " contains spaces around number. If you pass this to VInt, it will fail, but VOptionalTrim trims value before checking it for emptiness and passing on.
  • Inside r.match {...} block there is one more option added: case Empty. This is successful result (and its ok property returns true) that can be returned only by VOptional* validators.

A few other examples for better understanding:

					(VOptionalTrim & VInt).validate(Param("  "))
					// Returns Empty, since VOptionalTrim trims value before checking for emptiness.
				
					(VOptional & VInt & VRange(0, 100)).validate(Param("  "))
					(VRequired & VInt & VRange(0, 100)).validate(Param("  "))
					// Returns "not an integer" error in both cases, since VRequired/VOptional 
					// considers value non-empty and passes it on to VInt. 
				
					(VOptional & VInt & VRange(0, 100)).validate(Param(""))
					(VOptional & VInt & VRange(0, 100)).validate(Param(Nil))
					(VOptional & VInt & VRange(0, 100)).validate(Param(null :: Nil))
					// Returns Empty in all three cases.
				
					(VRequired & VInt & VRange(0, 100)).validate(Param(""))
					// Returns "missing value" error, since VRequired expects non-empty value.
				
					(VRequiredTrim & VInt & VRange(0, 100)).validate(Param(" 123 "))
					// Returns "not in range" error, since VRequiredTrim trims value, so VInt
					// passes successfully and VRange fails. 
				

The point for making two different Data[T](data: T) and Empty subtypes of Validator.Result instead of one Successful[T](data: Option[T]) is to make validator signatures more explicit. As I mentioned above, Empty may be returned only by VOptional*. That is, I encode business logic rule into type system. This is useful when you both use and create validators.

And the point for making these chain-head validators at all is to follow the concept "each next validator takes output of its leftside sibling as its input" and thus to prevent code duplication: since VOptionalTrim has already performed trimming and emptiness test, its followers don't need to do it themselves. Just like VRange does not need to convert its input to Ordered[T] since it's already done by VInt.

Specifying type of method argument that takes validator chains

If you have a method that takes validator chains as its argument, you should consider next rules to choose that argument's type:

  • If you want it to take any validator chains (VRequired*, VRequired* & ..., VOptional*, VOptional* & ...), declare it with type VChain[Out].
  • If you want if to take only chains starting with VRequired* (VRequired*, VRequired* & ...), declare it with type VChain.Required[Out]. By the way, this class redeclares abstract method validate() to return NonEmpty.
  • If you want if to take only chains starting with VOptional* (VOptional*, VOptional* & ...), declare it with type VChain.Optional[Out].

You can find examples in lib.web: form fields take VChain, and two overloaded versions of PARAM() helper method in class ru.dimgel.lib.web.page.AbstractPage take VChain.Required and VChain.Optional, respectively.

Validators for multi-valued parameters

There is not much done here, since all multi-valued parameter validators I needed so far were too application-specific. Mostly all validators are for single strings.

There are only two chain-head validators that take Param and return List[String]: VRequiredList and VOptionalList. The only input they consider empty is Param(Nil). If parameter has values (even they are empty strings or nulls), then it's not empty.

These two can be used as a reference implementation if you want to create your own chain-head validators. Currently there are no validators that take List[String] as input, but you can create your own if you need.

Param.apply() and Param.Result

There is a common case when default value is needed for empty optional parameters or if validation fails. This is how it's done:

					import Param._
					
					val r = Param("42")(VOptional & VInt & VRange(0, 100), 0)
					// r is of type Param.Result
					r match {
						case Success(data_or_defaultValue_?) => ...
						case Failure(defaultValue_?, message) => ...
					}
				

Class Param defines methods:

					def apply[T](validator: VChain[T], default_? : Option[T] = None): Param.Result[T]
					def apply[T](validator: VChain[T], default : T): Param.Result[T]
				

The latter one delegates to the former. Both subclasses of Param.Result - Param.Success and Param.Failure contain property data_? : Option[T]. The return value of apply() is:

  • If validator returns Validator.Data(data), then Param.Success(Some(data)) is returned.
  • If validator returns Validator.Empty, then Param.Success(default_?) is returned.
  • If validator returns Validator.Failure(message), then Param.Failure(default_?, message) is returned.

Custom error messages

All validators except VOptional* (which never fail) take optional error message as their last argument. More precisely, there are three overloaded versions of validator factory methods.

The first, most flexible version declares message argument as a function of validator's parameters and input:

					object VInt ... {
						def apply(message: String => String) = 
							new VInt(message)
						...
					}
					object VRegex {
						def apply(regex: Regex, message: (Regex, String) => String) = 
							new VRegex(regex, message)
						...
					}
					object VRange {
						def apply[T <% Ordered[T]](min: T, max: T, message: (T, T, T) => String) = 
							new VRange[T](min, max, message)
						...
					}
				

Note that message function arguments go in the same order as validator parameters, and input is always the last one. Signature is a bit confusing in case of VRange since both parameters and input have the same type T, so I added VRegex example for clearness.

The second is a convenience shortcut if you want to pass just a string instead of function, i.e. don't want to embed neither validator parameters nor input into error message:

					object VInt ... {
						def apply(message: => String) = 
							new VInt(in => message)
						// ...
					}
					object VRange {
						// ...
						def apply[T <% Ordered[T]](min: T, max: T, message: => String) = 
							new VRange[T](min, max, (min, max, in) => message)
					}
				

Note that message is by-name argument. This allows, for example, localization support as described in lib.i18n docs.

The third one (which we have used so far) uses hardcoded validator's default error message. In case of parameterless validator, a companion object itself is used instead of factory method:

					object VInt extends VInt(in => "Value is not an integer") {
						// ...
					}
					object VRange {
						// ...
						def apply[T <% Ordered[T]](min: T, max: T) = new VRange[T](
							min, 
							max, 
							(min, max, in) => "Value is not in range [" + min + ", " + max + "]"
						)
					}
				

Default messages are in English. They usually contain validator parameters but never contain input. The point is that I use validators for both request parameters and form fields, and I optimized error messages for use with forms. If request parameter validation fails, it's usually a bug and it's not a big problem to obtain the erroneous parameter value from URL or POST params using sniffer. But it would look a bit stupid if error message right above the form field duplicated that field's value. For the same reason, VRegex does not include its parameter into its default message: regexps look confusing for users.

Reusing validator chains

Since all validators are stateless (don't contain mutable state) and thus thread-safe, you can create frequently used chain instances once and reference them from different places. For example, for the common use case when web page for editing or viewing an entity requires that entity's "id" as parameter, I've got next shortcut in my web projects:

					object Util {
						val VId = VRequired & VInt & VMin(1)
					}