Data Validation: A Different Approach

Checking the data in the application entered by the user or obtained in another way in the classical sense implies the use of only two expressions in the code: TRUE and FALSE. In another embodiment, exceptions are used that are clearly not intended for this. Is there a better option?

Validation is done by the so-called Validators (which are only part of the entire data validation process). The article Server Validation of User Data provides an interesting version of the validator implementation, but there are several nuances in the form of message localization and the error format itself .

Consider the error format first.

The proposed approach is for the validator method that checks the data to return a collection (array, list, etc.) of strings instead of boolean values ​​or throwing exceptions. Such a format will be more flexible and informative.

I will give an example in 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;
		}
		
	}
}

As you can see here, all methods return a collection of strings.

Unit test example:

        @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());
	}

Now consider the localization of error messages.

Example again in 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());
	}

Localization files are in 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.

I hope that other programmers writing in their own languages ​​will find this approach practical and convenient and will be able to apply it at home.

PS

A variant of the user authentication method that returns the implementation of the java.util.Map.Entry <K, V> interface and can contain both the user object and error data as a string (this approach can also be used to avoid returning Null from method when the user object is expected):

@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());
		}
	}
}

There is, of course, a wonderful article on the hub about validation in Java , but the approach that is suggested there can be used far from all cases and is based on annotations and exceptions.

At the end of the article a couple of useful links about testing:

  1. Antipatterns of software testing
  2. Auto Testing Concepts

Source: https://habr.com/ru/post/undefined/


All Articles