À propos du problème de la validation

Le problème de la validation syntaxique simple affecte un grand nombre de types de données et crée beaucoup de redondance et d'erreurs. Prenont l'exemple de la validation d'un courriel, c'est un problème simple non ?

Solutions utilisées actuellement

Premièrement voyont un peu comment font les autres:

1. 120 scripts pour Flash 8, livre par David Tardiveau (actionscript)

yazo.net/eyrolles

etatAdMail = vAdMail.length != 0;
arobasAdMail = vAdMail.indexOf("@") != -1;
contenuPoint1AdMail = vAdMail.substr(vAdMail.length-3, 1) == ".";
contenuPoint2AdMail = vAdMail.substr(vAdMail.length-4, 1) == ".";
contenuPointAdMail = contenuPoint1AdMail || contenuPoint2AdMail;
if (etatAdMail && arobasAdMail && contenuPointAdMail) {
	...
}
		

2. Django, python web framework

djangoproject.com

class EmailField(CharField):
    def __init__(self, *args, **kwargs):
        kwargs['maxlength'] = 75
...
def isValidEmail(field_data, all_data):
    if not email_re.search(field_data):
        raise ValidationError, gettext('Enter a valid e-mail address.')
...
email_re = re.compile(
    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
			

3. Rails, ruby web framework

rubyonrails.org

#   class Person < ActiveRecord::Base
#     validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
#   end
		

4. CakePHP, php web framework

cakephp.org

define('VALID_EMAIL', '/\\A(?:^([a-z0-9][a-z0-9_\\-\\.\\+]*)@([a-z0-9][a-z0-9\\.\\-]{0,63}\\.(com|org|net|biz|info|name|net|pro|aero|coop|museum|[a-z]{2,4}))$)\\z/i');
		

5. FormEncode, python validation and form generation

formencode.org

class Email(FancyValidator):
...
    usernameRE = re.compile(r'^[^ \t\n\r@<>()]+$', re.I)
    domainRE = re.compile(r'^[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$', re.I)

    messages = {
        'empty': _('Please enter an email address'),
        'noAt': _('An email address must contain a single @'),
        'badUsername': _('The username portion of the email address is invalid (the portion before the @: %(username)s)'),
        'socketError': _('An error occured when trying to connect to the server: %(error)s'),
        'badDomain': _('The domain portion of the email address is invalid (the portion after the @: %(domain)s)'),
        'domainDoesNotExist': _('The domain of the email address does not exist (the portion after the @: %(domain)s)'),
        }
		

Problèmes

1. <mathieu@gagnon.name> ne valide pas, mais <@.!!!> si.

2. La validation limite a 75 caractères alors que seul le nom de domaine peut avoir jusqu'à 255 caractères.

3. Il est possible de faire passer plusieurs valeurs non valides.

5. Le seul qui donne des messages d'erreur un peu plus spécifique à l'usagé.

Objectif

La validation d'une donnée se fait au niveau de la syntaxe. L'objectif est simplement de s'assurer qu'elle sera traitable par le système où elle est destiné, permettant ainsi à l'utilisateur inexpérimenté de pouvoir corriger et éviter les erreurs.

Lors de la validation, deux types de problèmes peuvent arriver. Premièrement, la donnée peut être dans une syntaxe inacceptable (error), il faut alors donner le maximum d'informations possible sur les raisons de l'invalidité de la valeur afin de faciliter l'utilisateur à identifier et corriger le problème. Ces informations doivent être accessibles autant par le programme lui-même que par son utilisateur. Le deuxième type de problème survient lorsque la donnée est acceptable, mais qu'il y a des raisons de croire que le système ne la traita pas correctement (warnings). Exemple: un courriel peut sembler valide, mais s'il contient des caractères peut commun (*,&,...) ou est particulièrement long (>50) ou le nom de domaine ne répond pas à une requête DNS, il est très probable (mais impossible à certifier) que le courriel sera retourné avec une erreur par le système de messagerie. La validation des "errors" doit se faire uniquement en utilisant la valeur de l'usager (temps d'exécution constant) alors que les "warnings" peuvent utiliser différents services (temps d'exécution relatif). Ces règles de validation doivent être les mêmes à travers tous les points d'entrée d'un système (par exemple, une validation javascript côté client doit avoir les même règles qu'une validation côté serveur).

Dans un système orienté objet, la validation des données s'opère au niveau de l'objet. C'est l'objet qui encapsule les données qui est responsable de les conserver dans un état valide. La gestion des "errors" se fait grâce aux Exception. Aucun système spécial n'existe pour les "warnings". L'oo est à propos de la réutilisation du code, ceci doit être réflété dans la validation des données (par exemple, un courriel et une URL comprennent tous deux un nom de domaine).

Puisque les données sont des chaînes de caractère sous un format particulier (sous classe) toutes les opérations des chaînes doivent être disponible et révisées afin de s'assurer qu'elle soit toujours valide. Par exemple, la comparaison entre deux courriels doit se faire en ne tenant pas compte de la casse du nom de domaine.

Solutions disponibles

  1. RegEx
    pro: une ligne, très simple et léger
    con: passe ou casse (pas d'info à l'usagé), souvent difficile de "matcher" à 100%
  2. Custom (procédural et regex)
    pro: bonne balance entre la perfection de ebnf et la facilité de regex
    con: demande du codage
  3. EBNF
    pro: meilleure solution pour les problèmes de syntaxe
    con: heavyweight (crée pour les syntaxes de langage), librairie souvent non disponible

Solution proposée

Donc la solution serait de disposer de spécifications, une par syntaxe. Ceci permettant d'avoir une base commune à l'implémentation dans différents languages, cette implémentation se ferait grâce à la solution 2. Les informations requise dans une spécification sont:

Format des messages

Les messages doivent être:

accessible
Autant par le programme que par son utilisateur
Disponible dans la langue de l'utilisateur (i18n)
complete
Contient toute les causes d'une erreur
self explanatory
Donne la cause et la correction de l'erreur (si possible) sans tenir compte d'aucune connaissance à priori.

Un message doit être sur une seule ligne et peut contenir des sections variables (par exemple, pour mentionner les caractères illégaux). Les variables sont notées dans la syntaxe de printf. Les messages doivent être formatés en phrase (débute par une majuscule et se termine par un point).

Example d'implémentation

class SyntaxError(ValueError):
    """Syntax error, including information on all the broken rules."""
    
    def __init__(self):
        self.messages = dict()
    
    def end(self):
        if self.messages:
            raise self
    
    def __str__(self):
        return " ".join(self.messages.values())


class EmailAddress(unicode):
    """The Internet classic as described in rfc2822. """
    MAXLENGTH = 64 + DomainName.MAXLENGTH

    def __init__(self, value):
        e = SyntaxError()
        if value.count("@") != 1:
            e.messages['AtSign'] = _("Must have one at sign.")
            e.end()
        ...
        e.end()

    def warn(self):
        ...
        return self

    def __eq__(self, value):
        if isinstance(value, EmailAddress):
            return self.local_part == value.local_part and self.domain == value.domain
        return str(self) == value

    ...

# pour tout commentaire:
my_email_address = EmailAddress("mathieu@gagnon.name").warn()