Как стать автором
Обновить

Валидация полей формы в андроид приложении и не только

Время на прочтение15 мин
Количество просмотров926

Регулярно возникают задачи проверять, что пользователь вводит в поля и сообщать ему если он что-то сделал не правильно.

Ничего в этом сложного нет, напишем парочку регулярных выражений

так

   const val SNILS_PATTERN = "[0-9]{3}-[0-9]{3}-[0-9]{3}\\s[0-9]{2}"

и так

const val SPEC_SYMBOLS = "—−–„““”‘’„”«»"
const val UPPER_RUS_LETTERS = "А-ЯЁЙ"
const val LOWER_RUS_LETTERS = "а-яёй"
const val RUS_LETTERS = "$UPPER_RUS_LETTERS$LOWER_RUS_LETTERS"
const val RUS_NAME_PATTERN = "[${RUS_LETTERS}IVX0-9\\-`'.()\\s]*"
const val RUS_NAME_PATTERN_WITH_COMMA = "[${RUS_LETTERS}IVX0-9\\-`'.,()\\s]*"
const val LATIN_LETTERS = "A-Za-z"

еще добавим маски

const val MASK_MOBILE_PHONE = "+7 [000] [000]-[00]-[00]"     
const val SNILS_MASK = "[000]-[000]-[000] [00]"

и будет норм...

Если вы тоже так считаете, то дальше можно не читать

Мы пойдём другим путём....


Теоретическая часть

Для обработки текста введенного в поле будем применять такую концепцию.Значение введенное в поле проходит последовательно три этапа:

  • фильтрация – удаляем все недопустимые символы

  • валидация – проверяем соответствует ли значение определенным правилам

  • форматирование – форматируем значение для вывода

Проверим как это работает, например, на поле для ввода СНИЛС.

СНИЛС - это

  • 11 цифр

  • две последние - контрольная сумма

  • при выводе они форматируются вот так ХХХ-ХХХ-ХХХ ХХ

в поле значение 123-45, пользователь нажимает цифру 7

  • фильтрация – т.к. СИНЛС это только цифры то удаляем все не цифровые символы. получаем 123457. передаем его на валидацию

  • валидация – СНИЛС это 11 цифр значит значение не валидное. передаем дальше «Error(123457)»

  • форматирование – после форматирования получим «Error(123-457)»

после всех этих манипуляций, отображаем в поле 123-457. Отображать ошибку или нет для этого поля решает «логика отображения». В данном случае, пока фокус в поле ввода, ошибку не отображаем.

Теперь перейдём к написанию кода

Этап 1. Фильтрация

Создадим интерфейс Filter. У него один метод filter: String -> String

interface Filter {
    fun filter(data: String): String
}

И чтобы не возвращаться, сразу сделаем тривиальную реализацию фильтра. Эта реализация возвращает данные без изменения.

object SimpleFilter : Filter {
    override fun filter(data: String) = data
}

и тут в голову приходит мысль, скорее всего нужно иметь возможность соединять фильтры в цепочку.для этого сделаем ComplexFilter

open class ComplexFilter private constructor(private val filters: List<Filter>) 
: Filter {
    override fun filter(data: String): String =
        filters.fold(data) { res, filter -> filter.filter(res) }

    companion object {
        fun build(filters: List<Filter>): ComplexFilter {
            return ComplexFilter(filters)
        }
    }
}

И сразу напишем DSL для построения фильтров

class ComplexFilterBuilder {
    private val filters: MutableList<Filter> = mutableListOf()

    fun build(): ComplexFilter {
        return ComplexFilter.build(filters)
    }

    operator fun Filter.unaryPlus(): ComplexFilterBuilder {
        filters.add(this)
        return this@ComplexFilterBuilder
    }
}

fun filter(lambda: ComplexFilterBuilder.() -> ComplexFilterBuilder): ComplexFilter {
    return ComplexFilterBuilder().lambda().build()
}

теперь фильтр для поля СНИЛС будет выглядеть вот так

val snilsFilter = filter {
     +FilterOnlyDigits
     +FilterMaxLength(11)
}
Напишем несколько фильтров
/**
 * Удаляет из строки все символы пробела
 */
object FilterSpacesSymbols: Filter {
    override fun filter(data: String): String = data.filterNot { it.isWhitespace() }
}

/**
 * Удаляет из строки все символы из кирииллицы
 */
object FilterNonLatinsSymbols: Filter {
    override fun filter(data: String): String = data.filterNot { it.isCyrillic() }
}

/**
 * Удаляет из строки указанные символы
 */
class FilterSymbols(private val filteredSymbols: String): Filter {
    override fun filter(data: String): String = data.filterNot { it in filteredSymbols }
}

/**
 * Оставляет строку длиной не более maxLength символов
 */
class FilterMaxLength(private val maxLength: Int) : Filter {
    override fun filter(data: String): String =
        data.take(maxLength)
}

/**
 * Оставляет в строке только цифры
 */
object FilterOnlyDigits : Filter {
    override fun filter(data: String): String = data.filter { it.isDigit() }
} 

И теперь проверим, что snilsFilter ведёт себя правильно

тесты для snilsFilter
class SnilsFilterTest : FunSpec({
    context("Snils filter") {

        val snilsFilter = filter {
            +FilterOnlyDigits
            +FilterMaxLength(11)
        }

        withData(
            listOf(
                "            " to "",
                "sd fasdf as fsd a fas asd f" to "",
                "s6d84f65sd46s5d4f" to "684654654",
                "123-456-789" to "123456789",
                "123-456-789 11" to "12345678911",
                "123-456" to "123456",
                "123-456-789-123-456-789" to "12345678912",
                "123456789123456789" to "12345678912",
            )
        ) { (data, res) ->
            snilsFilter.filter(data) should be(res)
        }
    }
})

c фильтрами закончили, теперь перейдем к валидаторам

Этап 2. Валидация

С валидацией чуть сложнее.

Создадим интерфейс Validator. Для него нужен метод, который принимает String возвращает ValidationResult (результат валидации)

interface Validator {
    fun validate(data: String): ValidationResult
}

ValidationResult это sealed класс.

  • У него может быть два варианта Valid и Error

  • Valid и Error содержать строку с данными

  • Error дополнительно содержит список ошибок : List<ValidationError>

  • ValidationError - базовый интерфейс для ошибок валидации

interface ValidationError

sealed class ValidationResult {
    abstract val data: String
    abstract fun isValid(): Boolean

    class Valid(override val data: String) : ValidationResult() {
        override fun isValid(): Boolean = true
    }

    class Error(override val data: String,  val errors: List<ValidationError>) : ValidationResult() {
        override fun isValid(): Boolean = false
    }

	companion object {
        fun valid(value: String): ValidationResult = Valid(value)
        fun invalid(value: String, errors: List<ValidationError>): ValidationResult {
            assert(errors.isNotEmpty())
            return Error(value, errors)
        }

        fun invalid(value: String, error: ValidationError): ValidationResult {
            return Error(value, listOf(error))
        }
    }
}

fun String.asValid() : ValidationResult = ValidationResult.valid(this)

Тривиальный валидатор будет выглядеть так. Он всегда считает данные валидными

class SimpleValidator: Validator {
    override fun validate(data: String): ValidationResult  = data.asValid()
}

и снова надо соединять валидаторы в цепочки, т.е. прогонять строку через несколько валидаций для этого

open class ComplexValidator private constructor(private val validators: List<Validator>) :
    Validator {

    override fun validate(data: String) =
        validators.fold(valid(data)) { res, validator -> res.andThen(validator) }

    companion object {
        fun build(validators: List<Validator>): ComplexValidator {
            return ComplexValidator(validators)
        }
    }
}

для того чтобы последовательно применять валидации добавим несколько методов в ValidationResult

    fun bind(anotherValidationFunction: (String) -> ValidationResult): ValidationResult {
        return when (this) {
            is Error -> {
                when(val res = anotherValidationFunction(data)) {
                    is Error -> invalid(res.data, this.errors + res.errors)
                    is Valid -> invalid(res.data, this.errors)
                }
            }

            is Valid -> anotherValidationFunction(data)
        }
    }

    fun andThen(anotherValidator: Validator): ValidationResult =
        bind { str: String -> anotherValidator.validate(str) }

И DSL для построения валидаторов

class ComplexValidatorBuilder() {
    private val validators: MutableList<Validator> = mutableListOf()

    fun build(): ComplexValidator {
        return ComplexValidator.build(validators)
    }

    operator fun Validator.unaryPlus(): ComplexValidatorBuilder {
        validators.add(this)
        return this@ComplexValidatorBuilder
    }
}

fun validator(lambda: ComplexValidatorBuilder.() -> ComplexValidatorBuilder): ComplexValidator {
    return ComplexValidatorBuilder().lambda().build()
}
Допишем недостающие валидаторы
object OnlyDigitsValidationError: ValidationError
object OnlyDigitsValidator: Validator {
    override fun validate(data: String): ValidationResult =
        if(data.all { it.isDigit() })
            valid(data)
        else
            invalid(data, OnlyDigitsValidationError)   
}

object ExactLengthValidationError : ValidationError
object ExactLengthValidator(val exactLength: Int) : Validator {
    override fun validate(data: String): ValidationResult =
        if (data.length == exactLength)
            valid(data)
        else
            invalid(data, ExactLengthValidationError)
}

object SnilsCheckSumValidatorError : ValidationError
object SnilsCheckSumValidator : Validator {
    override fun validate(data: String): ValidationResult {
        if (data.length != 11)
            return ValidationResult.invalid(data, SnilsCheckSumValidatorError)

        try {
            val part1 = data.substring(0, 9)
            val part2 = data.substring(9, 11)
            val checkSum = part1
                .reversed()
                .mapIndexed { index, c -> c.digitToInt() * (index + 1) }
                .sum()
                .mod(101)
                .toString()
                .padStart(2, '0')
                .takeLast(2)

            return if (checkSum == part2)
                ValidationResult.valid(data)
            else
                ValidationResult.invalid(data, SnilsCheckSumValidatorError)
        } catch (e: Exception) {
            return ValidationResult.invalid(data, SnilsCheckSumValidatorError)
        }
    }
}

Теперь можно написать валидатор для СНИЛС

val snilsValidator = validator { 
    +ExactLengthValidator(11)  // 11 символов
    +OnlyDigitsValidator     // только цифры
    +SnilsCheckSumValidator  // проверка контрольной суммы СНИЛС
}
И тесты для snilsValidator
class SnilsValidatorTest: FunSpec( {
    context("valid snils") {
        withData(
            listOf(
            "11223344595",
            "12345678964",
            "98765432183",
            "11111111145",
            "45645645617",
            "91919191943",
            "77777777712",
            "95195195147")
        ){ data ->
                snilsValidator.validate(data) should beValid()
        }
    }

    context("wrong snils") {
        withData(
            listOf(
                "777777777777777777" to SnilsCheckSumValidatorError,
                "77777777713" to SnilsCheckSumValidatorError,
                "7777777" to ExactLengthValidationError,
                "7sdfsdf7" to OnlyDigitsValidationError,
                "" to ExactLengthValidationError
            )
        ) { (data, error) ->
            with(snilsValidator.validate(data)) {
                this shouldNot beValid()
                (this as ValidationResult.Error).errors shouldContain error
            }
        }
    }
})

Этап 3. Форматирование

Создадим интерфейс Formatter

interface Formatter {
    fun format(data: String): String
}

Тривиальный форматтер будет такой. Он всегда возвращает данные без форматирования

class SimpleFormatter: Formatter {
    override fun format(data: String) = data
}

Немного поразмыслив, приходим к выводу что, скорее всего не надо выполнять несколько форматирований, для одного поля. Поэтому ComplexFormatter не будем делать.

напишем форматтер для СНИЛСа

class SnilsFormatter: Formatter {
    override fun format(data: String): String =
        buildString {
            data.forEachIndexed { index, c ->
                when (index) {
                    0, 1, 2 -> append(c)
                    3 -> append('-').append(c)
                    4, 5 -> append(c)
                    6 -> append('-').append(c)
                    7, 8, -> append(c)
                    9 ->  append(' ').append(c)
                    10 -> append(c)
                    else -> append(c)
                }
            }
        }
}
тесты для SnilsFormatter
class SnilsFormatterTest : FunSpec({
    context("Snils formatter") {
        withData(
            listOf(
                "asdfgh" to "asd-fgh",
                "" to "",
                "1" to "1",
                "12" to "12",
                "123" to "123",
                "1234" to "123-4",
                "12345" to "123-45",
                "123456" to "123-456",
                "1234567" to "123-456-7",
                "12345678" to "123-456-78",
                "123456789" to "123-456-789",
                "1234567891" to "123-456-789 1",
                "12345678900" to "123-456-789 00",
                "123456789111111111" to "123-456-789 11",
            )
        ) { (data, res) ->
            SnilsFormatter.format(data) should be(res)
        }
    }
})

Соберём всё вместе.

добавим такую сущность FormField

  • у нее есть список фильтров

  • список валидаторов

  • форматтер (по умолчанию - SimpleFormatter)

  • поле может быть обязательным или не обязательным

class FormField private constructor(
    private val filters: List<Filter> = emptyList(),
    private val validators: List<Validator> = emptyList(),
    private val formatter: Formatter = SimpleFormatter(),
    val isOptional: Boolean = false
)

у FormField всего один метод process Алгоритм у него простой:

  • входящую строку прогоняет через фильтры

  • если поле не обязательное и полученное значение пустое, то возвращаем ValidationResult.Valid

  • то что осталось, прогоняем через валидаторы,

  • в полученном ValidationResult форматирует текст.

fun process(data: String): ValidationResult {
  val filtered = filters.fold(data) { res, filter -> filter.filter(res) }

  return if (filtered.isEmpty() && isOptional) {
    filtered.asValid()
  } else {
    validators
      .fold(ValidationResult.valid(filtered)) { res, validator ->
        res.andThen(validator)
      }
      .map {
        formatter.format(it)
      }
  }
}

Для построения FormField напишем метод build. Для того чтобы обрабатывать "обязательность" поля в этом методе, добавляем в список валидаторов NotEmptyValidator

companion object {
  fun build(
      filters: List<Filter>,
      validators: List<Validator>,
      formatter: Formatter,
      isOptional: Boolean
  ): FormField =
      if (isOptional == true)
          FormField(filters, validators, formatter, true)
      else
          FormField(filters, listOf(NotEmptyValidator) + validators, formatter, false)
}

добавим билдер для DSL.

И окончательный результат будет такой
class FormField private constructor(
  private val filters: List<Filter> = emptyList(),
  private val validators: List<Validator> = emptyList(),
  private val formatter: Formatter = SimpleFormatter,
  val isOptional: Boolean = false
) {
  fun process(data: String): ValidationResult {
    val filtered = filters.fold(data) { res, filter -> filter.filter(res) }

    return if (filtered.isEmpty() && isOptional) {
      filtered.asValid()
    } else {
      validators
        .fold(ValidationResult.valid(filtered)) { res, validator ->
          res.andThen(validator)
        }
        .map {
          formatter.format(it)
        }
    }
  }

  companion object {
    fun build(
      filters: List<Filter>,
      validators: List<Validator>,
      formatter: Formatter,
      isOptional: Boolean
    ): FormField =
      if (isOptional == true)
        FormField(filters, validators, formatter, true)
      else
        FormField(filters, listOf(NotEmptyValidator) + validators, formatter, false)
  }
}

class FieldBuilder(private val isOptional: Boolean = false) {
  private val filters: MutableList<Filter> = mutableListOf()
  private val validators: MutableList<Validator> = mutableListOf()
  private var formatter: Formatter = SimpleFormatter

  fun build(): FormField {
    return FormField.build(filters, validators, formatter, isOptional)
  }

  operator fun Filter.unaryPlus(): FieldBuilder {
    filters.add(this)
    return this@FieldBuilder
  }

  operator fun Validator.unaryPlus(): FieldBuilder {
    validators.add(this)
    return this@FieldBuilder
  }

  operator fun Formatter.unaryPlus(): FieldBuilder {
    formatter = this
    return this@FieldBuilder
  }
}

/**
 * возвращает обязательное поле
 * Example
 *     val f = formField {
 *         +FilterLength(10)
 *         +SnilsValidator()
 *         +SnilsFormatter()
 *     }
 */
fun formField(lambda: FieldBuilder.() -> FieldBuilder): FormField {
  return FieldBuilder().lambda().build()
}

/**
 * возвращает не обязательное поле. т.е. если значение в поле пустое,
 * то валидация не происходит и поле считается валидным,
 * если поле не пустое, то проводятся валидации
 */
fun optionalFormField(lambda: FieldBuilder.() -> FieldBuilder): FormField {
  return FieldBuilder(isOptional = true).lambda().build()
}

Теперь поле для ввода СНИЛС можно описать так

     val snilsField = formField {
         +snilsFilter
         +snilsValidator
         +SnilsFormatter()
     }

или так

val snilsField = formField {
         +OnlyDigitsFilter()
         +MaxLengthFilter(11)
         +ExactLengthValidator(11)
         +OnlyDigitsValidator()   
         +SnilsCheckSumValidator()         
         +SnilsFormatter()
     }
тесты для snilsField
class SnilsFormFieldTest : FunSpec({
  context("required snils field") {
    val f = formField {
      +FilterOnlyDigits
      +FilterMaxLength(11)
      +OnlyDigitsValidator
      +ExactLengthValidator(11)
      +SnilsCheckSumValidator
      +SnilsFormatter
    }
    test("empty string") {
      with(f.process("")) {
        this shouldNot beValid()
        (this as ValidationResult.Error) shouldNot beValid()
      }
    }
    test("spaces") {
      with(f.process("    ")) {
        this shouldNot beValid()
        (this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
        this.errors shouldContain ExactLengthValidationError
        this.errors shouldContain SnilsCheckSumValidatorError
      }
    }
    test("123-456d") {
      with(f.process("123-456d")) {
        this shouldNot beValid()
        (this as ValidationResult.Error).errors shouldContain ExactLengthValidationError
        this.errors shouldContain SnilsCheckSumValidatorError
        this.data should be("123-456")
      }
    }
    test("123-456-789 64") {
      with(f.process("123-456-789 64")) {
        this should beValid()
        this.data should be("123-456-789 64")
      }
    }
  }

  context("optional snils field") {
    val f = optionalFormField {
      +FilterOnlyDigits
      +FilterMaxLength(11)
      +OnlyDigitsValidator
      +SnilsCheckSumValidator
      +SnilsFormatter
    }
    test("empty string") {
      with(f.process("")) {
        this should beValid()
        this.data should be("")
      }
    }
    test("spaces") {
      with(f.process("    ")) {
        this should beValid()
        this.data should be("")
      }
    }
    test("some not digit symbols") {
      with(f.process("asdfasdfasdf")) {
        this should beValid()
        this.data should be("")
      }
    }
    test("123-456d") {
      with(f.process("123-456d")) {
        this shouldNot beValid()
        (this as ValidationResult.Error).errors shouldContain SnilsCheckSumValidatorError
        this.data should be("123-456")
      }
    }
    test("123-456-789 64") {
      with(f.process("123-456-789 64")) {
        this should beValid()
        this.data should be("123-456-789 64")
      }
    }
  }
})

Внимательный читатель задаст вопрос, "Есть OnlyDigitstFilter и OnlyDigitsValidator. Выглядит это как что-то избыточное и повторяющееся...тоже самое и с длиной FilterMaxLength и ExactLengthValidator"

Краткий ответ - это принцип Single Responsibility доведенный до абсолюта.

Фильтры и валидаторы это разные сущности (фильтры занимаются фильтрацией, а валидаторы занимаются валидацией ), они разделены. И они обе нужны.

В данном случае OnlyDigitsValidator будет всегда возвращать Valid, т.к. OnlyDigitsFilter удалил все не цифровые символы. Но для сохранения целостной картины я оставляю OnlyDigitsValidator.

ExactLengthValidator позволяет определить какая именно ошибка случилась. т.е. если введено 5 символов из необходимых 11-ти, то в списке ошибок будет ExactLengthValidationError.

Можно использовать "оптимизированный" вариант поля снилс

    val f = optionalFormField {
      +FilterOnlyDigits
      +FilterMaxLength(11)
      +SnilsCheckSumValidator
      +SnilsFormatter
    }

но я предпочитаю полный вариант

     val snilsField = formField {
         +OnlyDigitsFilter()
         +MaxLengthFilter(11)
		 +ExactLengthValidator(11)
         +OnlyDigitsValidator()   
         +SnilsCheckSumValidator()         
         +SnilsFormatter()
     }

Возможны случаи, когда не надо удалять из введенной строки неправильные символы, но при этом надо выводить сообщение об ошибке (далее, если будут следующие части, будет такой пример.) - для этого также необходимо разделение фильтров и валидаторов.

И еще один вопрос "Зачем прогонять все валидации, ведь достаточно будет прекращать проверку сразу после первой неудачной валидации".

Рассмотрим такой пример - поле для составления пароля. Это поле требует много валидаторов на различные условия (содержит цифры, содержит спецсимволы, содержит заглавные буквы и т.д.) и после того как пользователь ввел пароль (или по мере ввода) надо по каждому условию выводить или не выводить ошибку. Для этого поля надо прогонять все валидации. В итоге для универсальности, я решил, что надо в любом случае прогонять все валидации.


Что в итоге получили

  • все поведение поля описано в одном методе

  • описание, отчасти похоже на ТЗ которое пишет аналитик.

  • поведение поля можно полностью протестировать с помощью Unit-тестов

Итак, с одиночным полем ввода понятно. В следующих частях соберем эти поля в форму и прикрутим их к андроиду...


А пока можно рассмотреть ещё один пример

Представим, есть такое поле, "дата рождения".

К значению в этом поле следующие требования

  • значение должно быть правильной датой

  • дата должна быть меньше текущей

  • дата отображается в формате dd.mm.yyyy

  • разрешено вводить только цифровые символы

  • если введена не верная дата, то выводим сообщение об ошибке

  • если введена дата больше текущей, то выводим сообщение об ошибке

Приступим

Сначала создадим метод dateBeforeField(maxDate: LocalDate) , который возвращает FormField с ограничением по максимальной дате.

И метод birthDateField() который уже возращает поле для ввода дня рождения

fun dateBeforeField(maxDate: LocalDate) = formField {
        +FilterOnlyDigits
        +FilterMaxLength(8)
        +ExactLengthValidator(8)
        +DateValidator()
        +DateBeforeValidator(maxDate)
        +DateFormatter
    }

fun birthDateField() = dateBeforeField(currentDate())

fun currentDate() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date

Необходимые фильтры уже написаны, Написать DateFormatter не сложно

DateValidator

DateValidator - проверяет, что введена правильная (существующая) дата. Написать его совсем не сложно (хорошо, что есть kotlinx.datetime) Строка после фильтров приходит в виде DDMMYYYY, для этого сделаем DDMMYYYYformat - нужный нам формат даты, остальное очевидно...

val DDMMYYYYformat = LocalDate.Format {
    dayOfMonth()
    monthNumber()
    year()
}

class DateValidator(val format: DateTimeFormat<LocalDate> = DDMMYYYYformat) : Validator {
    override fun validate(data: String): ValidationResult =
        try {
            LocalDate.parse(data, format)
            data.asValid()
        } catch (e: Exception) {
             ValidationResult.invalid(data, DateValidationError)
        }
}

DateBeforeValidator

Тут тоже ничего сложного, Дополнительно потребуется параметр includeBorder - включать или не включать границу в разрешенные значения

object DateBeforeValidationError : ValidationError
class DateBeforeValidator(
    val maxDate: LocalDate,
    val includeBorder: Boolean = false,
    val format: DateTimeFormat<LocalDate> = DDMMYYYYformat
) : Validator {
    override fun validate(data: String): ValidationResult =
        try {
            val localDate = LocalDate.parse(data, format)
            when {
                localDate < maxDate -> data.asValid()
                localDate == maxDate && includeBorder -> data.asValid()
                else -> ValidationResult.invalid(data, DateBeforeValidationError)
            }
        } catch (e: Exception) {
            ValidationResult.invalid(data, DateBeforeValidationError)
        }
}

и окончательно проверим, что поле ведет себя как предполагается (не получается вставить спойлер в спойлер)

class BirthDayFieldTest : FunSpec({

    val maxDate = LocalDate(2024,10,10)

    context("required birthday field") {

        val f = dateBeforeField(maxDate)

        "".let {
            test("$it must  be invalid") {
                with(f.process(it)) {
                    this shouldNot beValid()
                    (this as ValidationResult.Error) shouldNot beValid()
                }
            }
        }
        "   ".let {
            test("$it must be invalid") {
                with(f.process(it)) {
                    this shouldNot beValid()
                    (this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
                    this.errors shouldContain ExactLengthValidationError
                    this.errors shouldContain DateValidationError
                }
            }
        }
        "sadfasdf".let {
            test("$it must be invalid") {
                with(f.process(it)) {
                    this shouldNot beValid()
                    (this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
                    this.errors shouldContain ExactLengthValidationError
                    this.errors shouldContain DateValidationError
                }
            }
        }
        "1234".let {
            test("$it must be invalid") {
                with(f.process(it)) {
                    this shouldNot beValid()
                    (this as ValidationResult.Error).errors shouldContain ExactLengthValidationError
                    this.errors shouldContain DateValidationError
                    this.data should be("12.34")
                }
            }
        }

        context("valid dates") {
            listOf("12.12.2002", "29.02.2024", "09.10.2024", "01.01.2024").forEach {
                test("$it must be valid") {
                    with(f.process(it)) {
                        this should beValid()
                        this.data should be(it)
                    }
                }
            }
        }

        context("dates after max date") {
            listOf("12.12.2025", "28.02.2025", "10.10.2024", "10.10.2025").forEach {
                test("$it must be invalid") {
                    with(f.process(it)) {
                        this shouldNot beValid()
                        (this as ValidationResult.Error).errors shouldContain DateBeforeValidationError
                    }
                }
            }
        }
    }

    context("optional birthday field") {
        val f = optionalFormField {
            +FilterOnlyDigits
            +FilterMaxLength(8)
            +DateValidator()
            +DateBeforeValidator(maxDate = maxDate)
            +DateFormatter
        }

        "".let {
            test("$it must  be valid") {
                with(f.process(it)) {
                    this should beValid()
                }
            }
        }
        "   ".let {
            test("$it must be valid") {
                with(f.process(it)) {
                    this should beValid()
                }
            }
        }
        "sadfasdf".let {
            test("$it must be valid") {
                with(f.process(it)) {
                    this should beValid()
                }
            }
        }
    }
})

Пока я писал тесты (а вы их смотрели), появилась мысль, что возможно описать это поле по-другому

Изменим фильтры - разрешим вводить не только цифры но и точки, изменим формат с которым работают валидаторы и тогда не нужен будет форматтер.

val format = LocalDate.Format {
    dayOfMonth()
    char('.')
    monthNumber()
    char('.')
    year()
}

formField {
    +FilterOnlyDigitsAndDots
    +FilterMaxLength(10)
    +ExactLengthValidator(10)
    +DateValidator(format =  format)
    +DateBeforeValidator(maxDate, format = format)
}

Принципиально ничего не меняется, только теперь пользователь сам должен вводить точки в нужных местах , а в первом варианте за него это делал форматтер. И у пользователя появилось гораздо больше возможностей для ввода неправильного значения, например ......... .

Поэтому я выбираю начальный вариант..


На сегодня, точно всё.

До следующей части.

Теги:
Хабы:
+4
Комментарии6

Публикации

Работа

Ближайшие события