Проверка данных в приложении введённых пользователем или полученных другим путём в классическом понимании подразумевает использование всего лишь двух выражений в коде: TRUE и FALSE. В другом варианте используют исключения которые явно не предназначены для этого. Есть ли вариант получше?
Проверкой занимаются так называемые Валидаторы(которые являются лишь частью всего процесса проверки данных). В статье Серверная валидация пользовательских данных приводится интересный вариант реализации валидатора, но есть несколько нюансов в виде локализации сообщений и самого формата ошибок.
Рассмотрим сначала формат ошибок.
Предлагаемый подход состоит в том, чтобы метод валидатора, проверяющий данные, возвращал коллекцию(массив, список и т.д.) строк вместо булевых значений или бросания исключений. Такой формат будет более гибким и информативным.
Приведу пример на Java:
Как здесь видно все методы возвращают коллекцию строк.
Пример Unit теста:
А теперь рассмотрим локализацию сообщений ошибок.
Пример снова на Java:
Файлы локализаций лежат в src/main/resources.
errors_ru.properties:
errors.properties:
Надеюсь, что и другие программисты, пишушие на своих языках, найдут такой подход практичным и удобным и смогут его применить у себя.
P.S.
Вариант метода аутентификации пользователя, который возвращает реализацию интерфейса java.util.Map.Entry<K, V>, и может содержать в себе как объект пользователя так и данные об ошибках в виде строки(такой подход также может использоваться для избежания возврата Null из метода когда ожидается объект пользователя):
Есть, конечно, замечательная статья на хабре про валидацию в Java, но подход который там предлагается может использоваться далеко не во всех случаях и основан на аннотациях и исключениях.
В окончание статьи пару полезных ссылок про тестирование:
Проверкой занимаются так называемые Валидаторы(которые являются лишь частью всего процесса проверки данных). В статье Серверная валидация пользовательских данных приводится интересный вариант реализации валидатора, но есть несколько нюансов в виде локализации сообщений и самого формата ошибок.
Рассмотрим сначала формат ошибок.
Предлагаемый подход состоит в том, чтобы метод валидатора, проверяющий данные, возвращал коллекцию(массив, список и т.д.) строк вместо булевых значений или бросания исключений. Такой формат будет более гибким и информативным.
Приведу пример на Java:
import java.util.ArrayList;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public interface ValidateUser {
String OK = "OK:";
String FAIL= "FAIL:";
Collection<String> apply(String username, String password, String email);
default Collection<String> passwordValidate(String password){
var result = new ArrayList<String>(1);
int size = password.trim().length();
if(size < 3 || size > 20) result.add("Password error:too short value or too long name. Password must be greater or 3 characters and smaller then 20 simbols.");
return result;
}
default Collection<String> usernameValidate(String name){
var result = new ArrayList<String>(1);
int size = name.trim().length();
if(size < 3 || size > 30) result.add("Username error:too short or too long name. Name must be greater or 3 characters and smaller then 30 simbols.");
return result;
}
default Collection<String> emailValidate(String email){
var result = new ArrayList<String>(1);
String regex = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);
if (!matcher.find()) result.add("Email error:" + email + " is not valid");
return result;
}
class Default implements ValidateUser{
@Override
public Collection<String> apply(String username, String password,
String email) {
var errors = passwordValidate(password.trim());
errors.addAll(usernameValidate(username.trim()));
errors.addAll(emailValidate(email.trim()));
return errors;
}
}
}
Как здесь видно все методы возвращают коллекцию строк.
Пример Unit теста:
@Test
void validateUserTest() {
var validate = new ValidateUser2.Default();
var result = validate.apply("aaa", "qwe", "aaa@mail.ru");
assertTrue(result.isEmpty());
result = validate.apply("aaa", "qwe", "");
assertFalse(result.isEmpty());
assertEquals(1, result.size());
result = validate.apply("aa", "qwe", "aaa@mail.ru");
assertFalse(result.isEmpty());
assertEquals(1, result.size());
result = validate.apply("aaa", "qwe", "@mail.qweqwe");
assertFalse(result.isEmpty());
assertEquals(1, result.size());
result = validate.apply("aa", "qw", "");
assertFalse(result.isEmpty());
assertEquals(3, result.size());
}
А теперь рассмотрим локализацию сообщений ошибок.
Пример снова на Java:
public interface LocalizedValidation {
String OK = "OK:";
String FAIL= "FAIL:";
Collection<String> apply(String username, String password, String email, Locale locale);
default Collection<String> passwordValidate(String password, ResourceBundle bundle){
var result = new ArrayList<String>(1);
int size = password.trim().length();
if(size < 3 || size > 20) result.add(bundle.getString("password"));
return result;
}
default Collection<String> usernameValidate(String name, ResourceBundle bundle){
var result = new ArrayList<String>(1);
int size = name.trim().length();
if(size < 3 || size > 30) result.add(bundle.getString("username"));
return result;
}
default Collection<String> emailValidate(String email, ResourceBundle bundle){
var result = new ArrayList<String>(1);
String regex = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);
if (!matcher.find()) result.add(bundle.getString("username")+email);
return result;
}
class Default implements LocalizedValidation{
@Override
public Collection<String> apply(String username, String password,
String email, Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle("errors", locale);
var errors = passwordValidate(password.trim(), bundle);
errors.addAll(usernameValidate(username.trim(), bundle));
errors.addAll(emailValidate(email.trim(), bundle));
return errors;
}
}
}
@Test
void localizedUserTest() {
var validate = new LocalizedValidation.Default();
var result = validate.apply("aaa", "qwe", "aaa@mail.ru", Locale.ENGLISH);
assertTrue(result.isEmpty());
result = validate.apply("aaa", "qwe", "", Locale.ENGLISH);
assertFalse(result.isEmpty());
assertEquals(1, result.size());
System.out.println(result.iterator().next());
result = validate.apply("aaa", "qwe", "", new Locale("ru"));
assertFalse(result.isEmpty());
assertEquals(1, result.size());
System.out.println(result.iterator().next());
}
Файлы локализаций лежат в src/main/resources.
errors_ru.properties:
mail=Email ошибка: не верный Email:
username=Ошибка имени пользователя: слишком короткое или длинное имя.
password=Ошибка в длине пароля: пароль слишком длинный.
errors.properties:
mail=Email error: is not valid:
username=Username error:too short or too long name. Name must be greater or 3 characters and smaller then 30 simbols.
password=Password error:too short value or too long name. Password must be greater or 3 characters and smaller then 20 simbols.
Надеюсь, что и другие программисты, пишушие на своих языках, найдут такой подход практичным и удобным и смогут его применить у себя.
P.S.
Вариант метода аутентификации пользователя, который возвращает реализацию интерфейса java.util.Map.Entry<K, V>, и может содержать в себе как объект пользователя так и данные об ошибках в виде строки(такой подход также может использоваться для избежания возврата Null из метода когда ожидается объект пользователя):
@Override
public Entry<String, User> auth(String username, String password, String email) {
var errors = passwordValidation.apply(password.trim());
errors.addAll(userNameValidation.apply(username.trim()));
errors.addAll(emailValidation.apply(email.trim()));
if(!errors.isEmpty()) {
return REntry.ofI(null, errors.stream().collect(Collectors.joining(";")));
}else {
try {
var passwordEncrypted = new Encrypt().apply(password.trim());
var user = new User.Default(0L, username.trim(), email.trim(), passwordEncrypted, "").create(dataSource);
return REntry.ofI(user, "user is null!");
} catch (RuntimeException e) {
return REntry.ofI(null, e.getMessage());
}
}
}
Есть, конечно, замечательная статья на хабре про валидацию в Java, но подход который там предлагается может использоваться далеко не во всех случаях и основан на аннотациях и исключениях.
В окончание статьи пару полезных ссылок про тестирование:
lair
Что за "классическое понимание", и где вы его взяли? В .net
IValidatableObject.Validate
возвращает коллекциюValidationResult
, аModelState
(в ASP.net MVC) содержит коллекциюModelError
.Vadem
Так и в Java есть Jakarta Bean Validation, в котором Validator.validate() возвращает Set<ConstraintViolation>.
Автор просто решил изобрести велосипед видимо.
Moriline Автор
Не всегда эту реализацию возможно использовать в проекте. Например: в JavaSE или android. Плюс к этому там используются исключения при валидации.
Vadem
В Java SE её можно использовать:
This document is the specification of the Jakarta Bean Validation in Jakarta EE and Java SE.
Про Android я мало что знаю, но там, скорее всего, тоже есть что-то готовое.
Если вы про Exception model, то это исключения, выбрасываемые в случае неправильной конфигурации валидатора.
Сама валидация возвращает Set<ConstraintViolation>.
Moriline Автор
Нет, я имел ввиду что при проверках(под капотом этой реализации) бросаются исключения, которые потом оборачиваются в
Set<ConstraintViolation>
. ValidationException — один из основных. Вот здесь больше описано про исключения.А с JavaSE я был не прав. Но ведь такой подход применим и для других языков, а не только там где есть такие вещи как JSR-303, JSR-349, JSR-380.
ppikS
Такой подход уже сто лет как используется везде и повсеместно, особенно в вебе.
Единственные исключения это сайты, где разработчики особо не заморачиваются.
А ещё вы можете возвращать код ошибки, а описание ошибок хранить на клиенте.
Или же можно возвращать несколько кодов ошибок, а ещё лучше использовать побитовую маску и объединить все ошибки в одно значение.
Почему то ещё сразу вспомнил WSDL.
Moriline Автор
Я думаю что данная статья и поможет использовать именно такой подход для валидации данных. А с Вашей стороны было бы хорошо подсказать: какие валидаторы лучше всего использовать в вебе. Вот Вы каким пользуетесь?
ppikS
У меня есть свой, давно написанный шаблонный класс, который я дописываю под задачу. Максимально сурово контролируя все данные, с жёсткой типизацией, длинной, проверкой по спискам и тп.
Ну или использую решения из фрейморков, если он используется на проекте и может справиться с задачей. Опять же дополнительно проводя проверки где это необходимо.
А по поводу информирования пользователей, так же всё зависит от задачи и объёмов работ.
Если использую свой шаблонный класс, то он как раз возвращает код ошибок, так же есть шаблонный класс js который содержит практически тоже самое, так как не за чем гонять туда-сюда заведомо неверную информацию.
Бывает делаю и в тупую, где вся форма выдаёт ошибку или же не выдаёт. Но это опять же от проекта зависит и задач.
В данном случаи считаю нету типовых решений, есть только правила, до которых всем надо постепенно дойти, самое первое, это максимально возможно жёстко контролировать все входящие данные. А остальное придёт с опытом, как и собственные наработки.
Vadem
Я в документ, что вы привели, не вчитывался, но похоже, что там как раз наоборот.
Результат валидации оборачивается в исключение, потому что этого требует платформа CUBA:
Если вы имеете ввиду примеры из Setting validator programmatically:
То там тоже исключения кидаются потому что этого хочет CUBA:
Но это уже личные проблемы платформы CUBA.
Ни спецификация, что я привёл, ни её реализации не требуют выброса исключений для возврата результата валидации и не делают этого.
Хотя и не запрещают этого, если фреймворк требует.
Можете привести пример языка, в котором нет готовой библиотеки для валидации?
Moriline Автор
Я же имел ввиду спецификации, а не библиотеки. Библиотек много(платформа CUBA, реализация от Hibernate и т.д.), но вот стандарта единого(такого как JSR-303, JSR-349, JSR-380) нет. К тому же многие пишут свои реализации валидаторов, а не используют готовые библиотеки.
lair
Между языками? А зачем?