Как расширять

Содержание этой страницы:

Как писать обычные (не chain-head) валидаторы

Чтобы сделать обычный (не chain-head) валидатор, отнаследуйтесь из VImp[In,Out] и сделайте объект-компаньон с фабричными методами.

Класс VImp[In,Out] объявлен следующим образом:

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] = ...
}
				

Он содержит только один абстрактный метод, который вы должны реализовать - validate(). Тип его возвращаемого значения - Validator.NotEmpty является подтипом Validator.Result и сам имеет два подтипа: Validator.Data и Validator.Failure. Вы их уже встречали в разделе"Как использовать / Основы".

Объект-компаньон обычно содержит три overloaded фабричных метода с разными сигнатурами аргумента message, как было описано в разделе "Как использовать / Пользовательские сообщения об ошибках": первый как функция от параметров валидатора и входного значения, второй как by-name строка, а третий - вообще без аргумента message (сообщение по умолчанию).

Исходные тексты всех валидаторов очень короткие и простые, так что вы можете просто скопировать любой из них и подогнать под свои нужды. Ниже в качестве примера приведён исходный текст валидатора VInt:

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)
}
				

Здесь мы видим следующее:

  • Он отнаследован из VImp[String,Int], то есть он преобразует строки в числа.
  • Его конструктор - protected и не берёт аргументов кроме message, который в свою очередь не берёт аргументов кроме входного значения (опять же, см. "Как использовать / Пользовательские сообщения об ошибках").
  • Метод validate(in) возвращает Data(результат валидации) при успехе, либо Failure(message(параметры валидатора, in)) при ошибке.
  • Поскольку данный валидатор параметров не имеет, его объект-компаньон отнаследован от собственного класса-компаньона, вместо того, чтобы объявлять метод apply() без параметров - чтобы позволить пользователям писать VInt вместо VInt().

И для полноты картины, ниже приведён исходник валидатора VMaxLength, берущего один параметр len (максимальная длина строки) и выполняющего только проверку без преобразования данных:

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)")
}
				

Что мы здесь видим:

  • Он отнаследован от VImp[String,String], т.е. типы входных и выходных данных одинаковы.
  • При успехе, метод validate(in) возвращает Data(in), т.е. свои входные данные без изменений.
  • Функция message принимает два аргумента - первыми в списке аргументов идут параметры валидатора (в данном случае один: len), и в конце - входное значение.

Вы так же можете заглянуть в исходный текст валидатора VRange, в котором есть отличие от вышеприведённых примеров: его входной тип - не String; поэтому последний аргумент функции message - тоже не String.

ЗАМЕЧАНИЕ. Описанные соглашения относительно агумента message не являются необходимыми для функционирования; это просто вопрос унификации и удобства.

Как писать chain-head валидаторы

Chain-head валидаторы всегда реализуются парами: VRequired* + VOptional*. Оба валидатора в паре имеют одинаковый суффикс имени. Оба выполняют одинаковую проверку параметра на непустоту, но ведут себя по разному если параметр пуст: VRequired* возвращает ошибку, а VOptional* успех. Если параметр не пуст, оба валидатора передают управление дальше по цепи.

Единственное, что вам нужно реализовать - это метод проверки на пустоту; всё остальное поведение общее и закодировано в базовых классах. Фактически, это метод testAndConvert(), поскольку chain-head валидаторы могут возвращать различные типы данных. Например, VRequired / VOptional возвращают String, а VRequiredList / VOptionalList возвращают List[String]. По соглашению, testAndConvert() реализуется в классе VRequired*, а VOptional*.testAndConvert() просто делегирует к VRequired*.testAndConvert().

Ниже приведён исходный текст для валидаторов VRequiredTrim и VOptionalTrim:

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)
}
				

Кроме уже описанного, обратите внимание на следующие моменты:

  • Метод testAndConvert() возвращает Some[Out] если параметр не пуст, None в противном случае.
  • Наследуйте VChain.RequiredBase[Out] и VChain.OptionalBase[Out], соответственно.
  • В случае VRequired*, реализуйте поддержку сообщений об ошибках, аналогичную обычным валидаторам.
  • Поскольку VOptional* всегда успешен, нет смысла заводить для него class (достаточно одного object), равно как и нет смысла в обработке ошибок и фабричных методах apply().