Grape : custom validator conditionnel

Depuis le début de l'année, j'ai passé une partie importante de mon temps à travailler sur la conception et le développement d'une API publique. C'est un vaste chantier, et, une fois passé le temps de la recherche, de la structuration et de la définition des grandes lignes, il était temps de passer à l'implémentation. Pour cela, j'ai choisi d'utiliser le gem Grape, qui fournit un DSL facilitant le développement d'API dans une application Ruby.

Mon besoin : appliquer un custom validator dans certains cas seulement

Il y a quelques semaines, j'ai eu un besoin un peu particulier : valider l'unicité d'un critère fourni en entrée par le client, et stocké dans un hstore.

Pour ce faire, je me suis appuyé sur une fonctionnalité de Grape qui permet de développer ses propres validateurs pour les données fournies dans la requête. Ce validateur est exécuté lorsque le point d'entrée est appelé, il valide (ou pas) les paramètres donnés et passe (ou pas, donc) au traitement du point d'entrée. Je l'ai déjà fait plusieurs fois sans aucun problème. Seulement dans ce cas, j'avais une contrainte supplémentaire que je n'avais pas encore rencontrée.

Mes paramètres sont en effet définis dans un helper et partagés entre différents points d'entrée. Néanmoins, cette validation ne devait s'appliquer que lors d'une requête POST. À partir de là, comment faire pour accéder dans mon validateur à la requête elle-même, et faire une "validation conditionnelle" ? Pas d'objet request disponible dans ce contexte malheureusement…

Et en creusant un peu, j'ai trouvé sur le github de Grape une issue "Access request in custom validator" qui m'a orienté vers la bonne piste. Mais comme la solution est assez peu détaillée, j'ai dû fouiller le code de Grape pour trouver la façon de faire que voici :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module PublicApi
  module Validators
    # Check that an `outer_id` does not yet exist
    # on current records. Only performed on `POST` requests
    #
    # Usage:
    #   requires :parameter, outer_id: true
    #
    class OuterId < Grape::Validations::Base
      # Only execute the validation for POST endpoints
      def validate(request)
        validate!(request.params) if request.post?
      end

      # Validation logic
      def validate_param!(attr_name, params)
        attr_value = params[ attr_name ]
        already_exists = MyModel.where("keys -> 'outer_id'=?", attr_value)
                                .exists?

        if already_exists
          fail Grape::Exceptions::Validation,
            params: [ @scope.full_name(attr_name) ],
            message: "A record with 'outer_id'=#{ attr_value } already exists."
        end
      end
    end
  end
end

La méthode validate(request) prend un paramètre request en entrée et permet de faire n'importe quelle vérification sur cet objet avant de lancer la validation des paramètres elle-même, qui est réalisée par la méthode validate!(params), qui elle-même appelle la méthode validate_param!(attr_name, params) que vous overridez dans votre validateur.

Ce n'est pas forcément un cas d'utilisation très courant, et je ne suis pas sûr qu'il soit particulièrement conseillé de multiplier les validateurs de ce type, mais dans certaines situations, ça peut sans doute s'avérer très utile !