How to extend

Contents of this page:

How to create ordinary (non chain-head) validators

To create ordinary (non chain-head) validator, you have to subclass VImp[In,Out] and provide companion object with factory methods.

Class VImp[In,Out] is defined like this:

abstract class VImp[In, Out] extends Validator[In, Out] {
	def validate(in: In): NonEmpty[Out]
	final def &[In2 >: Out, Out2](v: VImp[In2, Out2]): VImp[In, Out2] = ...
}
				

It contains only one abstract method you have to implement - validate(). Its return type Validator.NotEmpty is a subtype of Validator.Result and has two subtypes itself: Validator.Data and Validator.Failure. You have already met them in "How to use / Basics" section.

The companion object usually contains three overloaded factory methods with different message argument signatures as was described in "How to use / Custom error messages" section: first is a function of validator parameters and input, second is by-name string, and the third without message argument at all (default message).

All validator sources are pretty short and simple, so you can just copy-paste and adopt any of them to your needs. Let's see the complete source code of VInt as example:

package ru.dimgel.lib.validator

import Validator._

class VInt protected (message: String => String) extends VImp[String, Int] {
	require(message != null, "message != null")

	def validate(in: String) =
		try {
			Data(in.toInt)
		} catch {
			case _ => Failure(message(in))
		}
}

object VInt extends VInt(in => "Value is not an integer") {
	def apply(message: String => String) = new VInt(message)
	def apply(message: => String) = new VInt(in => message)
}
				

Here you can see some common things:

  • It subclasses VImp[String,Int], thus it converts strings to integers.
  • Its constructor is protected and has no arguments except message which in turn has no arguments except validator input (again, see "How to use / Custom error messages").
  • Method validate(in) returns Data(validator's output) on successful validation, or Failure(message(validator's params, in)) on failure.
  • Since this validator is parameterless, its companion object extends its class instead of defining parameterless apply() method - to allow users write VInt instead of VInt().

And just for completeness, here's a source code of VMaxLength - validator that takes one parameter len (maximum string length) and performs only test without data conversion:

package ru.dimgel.lib.validator

import Validator._

class VMaxLength protected (len: Int, message: (Int, String) => String) extends VImp[String, String] {
	require(len >= 0, "len >= 0")
	require(message != null, "message != null")

	final def validate(in: String) =
		if (in.length <= len) Data(in)
		else Failure(message(len, in))
}

object VMaxLength {
	def apply(len: Int, message: (Int, String) => String) = new VMaxLength(len, message)
	def apply(len: Int, message: => String) = new VMaxLength(len, (len, in) => message)
	def apply(len: Int) = new VMaxLength(len, (len, in) => "String is longer than " + len + " char(s)")
}
				

Here you can see:

  • It subclasses VImp[String,String], i.e. input and output types are the same.
  • On success, method validate(in) returns Data(in), i.e. its input unchanged.
  • The message function takes two args - validator's parameters (here we have one: len) go first, and input is the last one.

You can also check source code of VRange which has a difference from examples above: its input type is not a String; thus the last message function argument is not a String, too.

NOTE. These conventions about message argument are not vital for the operation; it's just a matter of unification and convenience.

How to create chain-head validators

Chain-head validators are always implemented in pairs: VRequired* + VOptional*. Both validators in pair have same name suffix. Both perform same parameter emptiness test but act differently if parameter is empty: VRequired* returns error but VOptional* returns success. If parameter is non-empty, both validators pass on to the rest of the chain.

Here, the only thing you have to implement is emptiness test method; all other behaviour is common and encoded in base classes. In fact, it's testAndConvert() since chain-head validators may return different types. For example, VRequired / VOptional return String, but VRequiredList / VOptionalList return List[String]. By convention, testAndConvert() is implemented in class VRequired*, and VOptional*.testAndConvert() just delegates to class VRequired*.testConvert().

This is the source code for VRequiredTrim and VOptionalTrim validators:

package ru.dimgel.lib.validator

sealed class VRequiredList protected (message: Param => String) extends VChain.RequiredBase[List[String]](message) {
	def testAndConvert(in: Param) = if (!in.data.isEmpty) Some(in.data) else None
}

object VRequiredList extends VRequiredList(in => "List is empty") {
	def apply(message: Param => String) = new VRequiredList(message)
	def apply(message: => String) = new VRequiredList(in => message)
}
				
package ru.dimgel.lib.validator

object VOptionalList extends VChain.OptionalBase[List[String]] {
	def testAndConvert(in: Param) = VRequiredList.testAndConvert(in)
}
				

Besides what is already explained, other things to notice here are:

  • Method testAndConvert() returns Some[Out] if parameter is not empty, None otherwise.
  • You subclass VChain.RequiredBase[Out] and VChain.OptionalBase[Out], respectively.
  • In case of VRequired*, you implement same error message support as for ordinary validators.
  • Since VOptional* always succeeds, there's no need for class (object is enough), for error messages and for apply() factory methods.