16
Oct/09
0

Validation de formulaires en Erlang

Problématique

L’envoi des données de formulaire se fait par Ajax également.

Il est nécessaire de pouvoir sérialiser le formulaire afin d’envoyer les données sur le réseau, ainsi que de brancher le mécanisme côté serveur pour récupérer et valider les données.

Côté client, la sérialisation d’un formulaire se fait tout simplement en utilisant les mécanismes jQuery ; rien de spécial ici, on commencera par sérialiser un formulaire particulier avant de passer à une généralisation.

Côté serveur, nous allons mettre en oeuvre la récupération des données et leur validation. Tout ce qui vient du client doit être considéré comme non sûr et potentiellement corrompu.

La validation serveur est donc un processus absolument primordial. La pré-validation cliente est accessoire et ne sera pas traitée pour le moment.

Cette validation cliente participe à l’amélioration de l’expérience utilisateur. Pour notre part, avec l’architecture particulière mise en place où seuls les éléments mis à jour transitent entre le client et le serveur, on espère que la validation serveur ne sera pas trop pénalisante et donc suffisante dans un premier temps.

Récupération des données

On va modifier notre méthode générique de décodage des requêtes afin de lire les données depuis le corps de la requête ainsi que les paramètres d’URL.

Je pense qu’il est préférable de ne pas avoir à faire la distinction entre les deux mécanismes. On souhaite obtrenir une unique liste de tuples contenant le paramètre et sa valeur. Pour cela, mochiweb nous offre les méthodes adéquates :

decode(Req) ->
  Method = Req:get(method),
  [Accept|_] = string:tokens(Req:get_header_value('Accept'), ","),
  [_,Short] = string:tokens(Accept, "/"),
  Data = Req:parse_post() ++ Req:parse_qs(),
  {{Method, list_to_existing_atom(Short)}, Data}.

La seule différence est le remplissage de la variable Data. Cette variable est déjà passée en paramètre des méthodes des controlleurs, et contiendra une liste de ce type :

[{"login", "foo"},{"pwd", "bar"}]

Chaque clé représente le nom du champ.

Validation

Nous débuterons la validation côté serveur en implémentant les règles de validation en premier.

Ces règles prennent une valeur et essaient de la valider par rapport à une règle. On utilisera pour cela le mécanisme d’association d’Erlang :

validator(Field, Value, {min_length, MinLength}) ->
  case string:len(Value)<MinLength of
    true -> {Field, <<"min">>};
    false -> ok
  end;
 
validator(Field, Value, required) ->
  case string:len(Value)==0 of
    true -> {Field, <<"required">>};
    false -> ok
  end.

Chaque validateur prend en entrée le nom du champ et sa valeur ainsi qu’une règle à respecter. Le nom sera utilisé pour renvoyer le tuple indiquant la violation de la règle.

Le tuple ne renvoie qu’un mot clé qui sera traduit ensuite au niveau de l’interface.

Si la règle est validée, la fonction renvoie l’atome ok.

Remarque : on ne prend pas en compte encore ici les problématiques d’UTF-8. De plus, la règle min_length n’est pas parfaite car elle ne prend pas en compte un champ rempli avec des espaces.

On passe ensuite à l’écriture de ce que l’on aimerait obtenir dans la fonction, par exemple :

handle_http(Req, {'POST', json}, Data) ->
  Validation = validate("login", Data, [required, {min_length, 6}]) ++
               validate("pwd", Data, [required]),
  ...

On groupe la validation par champ, car plusieurs règles peuvent être appliquées à un même champ : ici, le login est requis et à une taille minimum.

Chaque validation renvoie une liste de violations qui sont additionnées, afin d’obtenir une liste unique.

La fonction de validation doit donc parcourir et tester l’ensemble des règles pour un couple champ / valeur et produire une liste de tuples d’invalidation :

validate(Field, Data, Constraints) ->
  Value = proplists:get_value(Field, Data),
  Errors = lists:map(fun(Constraint) -> validator(Field, Value, Constraint) end, Constraints),
  lists:filter(fun(Error) -> case Error of ok -> false; _ -> true end end, Errors).

La première ligne récupère une valeur dans une liste de tuples.

La seconde ligne représente notre analyse des contraintes. On mappe la liste des contraintes [required,{min_length,6}] vers une liste de violations ou de termes ok.

A l’issue de cette ligne, la variable Errors peut contenir ce genre de données :

[{"login",<<"min">>},ok,ok]

La dernière ligne permet donc de filtrer cette liste et de ne conserver que les tuples.

Enfin, on teste que la liste retournée par la validation est vide, sinon il y a un problème.

handle_http(Req, {'POST', json}, Data) ->
  Validation = validate("login", Data, [required, {min_length, 6}]) ++
               validate("pwd", Data, [required]),
  {_, []} = {"validation", Validation},
  ... 
  ctx:return_json_data(Req, {"success", <<"user created">>}).

Comme d’habitude, le pattern matching d’Erlang est utilisé pour s’assurer que l’on suit le flux normal au sein de la méthode. Toute incompatibilité déclenchera une erreur qui sera traitée ailleur.

Pour cela, on modifie la boucle de traitement des requêtes dans app_web.erl :

loop(Req, DocRoot) ->
  try
    routes(Req)
  catch
    error:{badmatch,unauthorized} -> Req:respond({403, [], []});
    error:{badmatch,{"validation", Constraints}} -> ctx:return_json_data(Req, {"validation", {struct, Constraints}})
  end.

Dans le cas d’une erreur de validation, on renvoie un code 200 (ok) mais avec un json représentant toutes les violations de contraintes.

Refactorisation

Le fonctionnement défini ainsi est correct, mais il n’est pas encore totalement satisfaisant, notamment sur la partie vérification des paramètres.

Le code est trop long à écrire, et l’assertion à utiliser est bien trop compliquée pour s’en rappeler une prochaine fois.

Nous allons donc refactoriser cette partie.

Le mieux est de générer la méthode qui pourra prendre en compte cette action :

handle_http(Req, {'POST', json}, Data) ->
  check_params(validate("login", Data, [required, {min_length, 6}]) ++
               validate("pwd", Data, [required])),
  ...
  ctx:return_json_data(Req, {"success", <<"user created">>}).
 
check_params(Constraints) ->
  Validation = Constraints,
  {_, []} = {"validation", Validation}.

La methode handle_http devient plus simple à écrire, ce qui est déjà bien.

Il s’agit uniquement d’un déplacement de code. Mais ce faisant, on s’aperçoit que la fonction check_params ne représente pas vraiment le comportement mis en place.

Il apparait alors qu’il est beaucoup plus compréhensible de travailler sur les conditions de réalisation de cette fonction check_params, et on réécrit donc la fonction comme suit :

check_params(Constraints) when Constraints == [] ->
  ok;
check_params(Constraints) ->
  throw({validation, Constraints}).

C’est à dire, qu’une vérification de paramètres attend obligatoirement une liste vide, sinon on génère une erreur. L’ordre de déclaration des fonctions est donc important.

la fonction de routage est modifiée en conséquence :

loop(Req, DocRoot) ->
  try
    routes(Req)
  catch
    error:{badmatch,unauthorized} -> Req:respond({403, [], []});
    throw:{validation, Constraints} -> ctx:return_json_data(Req, {"validation", {struct, Constraints}})
  end.

Il ne s’agit plus d’une erreur mais d’une exception, et le terme renvoyé est plus simple à exploiter.

Comments (0) Trackbacks (0)

No comments yet.

Leave a comment

No trackbacks yet.