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

от автора

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

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

так

   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 символов  */ object FilterMaxLength(private val maxLength: Int) : Filter {     override fun filter(data: String): String =         date.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) }

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

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


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

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


ссылка на оригинал статьи https://habr.com/ru/articles/897064/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *