AlpineLab blog technique

Des helpers pour vérifier la version de Rails

En travaillant sur la gem de Locale, et comme ça arrive souvent quand on développe une gem, on a eu besoin de charger du code spécifique pour certaines versions de Rails. Les petites fonctions toutes bêtes qui suivent servent à savoir dans quelle version de Rails on se trouve au moment de l'exécution du code.

TL;DR : les helpers sont disponibles dans ce Gist

En l'occurrence, il fallait s'adapter à un comportement de Rails que l'on observe dans Rails 3.2.16+ et Rails 4.0.2+ (suite à un patch pour une n-ième faille de sécu dans la gestion des fichiers YAML).

Déjà, il faut savoir que les numéros de version de Rails sont stockés dans le module ::Rails::VERSION qui contient 4 constantes :

  • MAJOR: le numéro de version majeur (pour 4.0.2, c'est 4)
  • MINOR: le numéro de version mineur (pour 4.0.2, c'est 0)
  • TINY: le numéro de patch (pour 4.0.2, c'est 2)
  • STRING: le numéro de version complet sous forme de string (donc pour 4.0.2, c'est “4.0.2”)

La façon bourrine de vérifier qu'on est dans un de ces deux cas est la suivante :

if (::Rails::VERSION::MAJOR == 4 && (::Rails::VERSION::MINOR > 0 || (::Rails::VERSION::MINOR == 0 && ::Rails::VERSION::TINY >= 2)))
|| (::Rails::VERSION::MAJOR == 3 && (::Rails::VERSION::MINOR > 2 || (::Rails::VERSION::MINOR == 2 && ::Rails::VERSION::TINY >= 16)))
  # votre code va ici
end

C'est moche, hein ? Non, je n'en suis pas fier.

C'est con parce qu'il suffit de savoir qu'il y a deux classes Ruby vachement utiles pour comparer les numéros de versions qui sont Gem::Requirement et Gem::Version.

Gem::Requirement permet de créer des matchers, genre ‘~> 4.0.2’ (ça vous dit quelque chose ?). Gem::Version, comme son nom l'indique intelligemment, ça stocke un numéro de version. Et le lien utile entre les deux, c'est la méthode satisfied_by? de Gem::Requirement qui renvoie vrai ou faux selon si l'objet Gem::Version qu'on lui passe en paramètre est conforme au matcher.

Quelques exemples :

Gem::Requirement.new('>= 4.0.2').satisfied_by? Gem::Version.new('3.2')
# => false
Gem::Requirement.new('>= 4.0.2').satisfied_by? Gem::Version.new('4.1')
# => true
Gem::Requirement.new('~> 4.0.2').satisfied_by? Gem::Version.new('4.0.3')
# => true
Gem::Requirement.new('~> 4.0.2').satisfied_by? Gem::Version.new('4.1')
# => false

Voilà, à partir de là, tout est très simple, on créé une fonction qui fait la même chose - non pas par rapport à un numéro de version arbitraire - mais par rapport au numéro de version de Rails (dans lequel le code est exécuté) :

def rails_version_matches?(requirement)
  Gem::Requirement.new(requirement).satisfied_by? Gem::Version::new(::Rails::VERSION::STRING)
end

Il nous suffit dans le code de faire:

if rails_version_matches? '~> 4.0.2'
  # du code custom ici
end

Classe, non ?

Oui, mais nous on veut vérifier plusieurs versions de Rails (souvenez-vous : “3.2.16+ et 4.0.2+”).

Pour ça, on va utiliser, des fonctions similaires mais qui prennent plusieurs matchers en paramètre, et qui vérifie que la version courante de Rails satisfait au moins un de ces matchers (pour la version _any) ou tous ces matchers à la fois (pour la version _all) en utilisant les opérateurs booléens & et | entre leurs résultats :

def rails_version_matches_any?(*requirements)
  requirements.map{ |r| rails_version_matches?(r) }.reduce(:|)
end

def rails_version_matches_all?(*requirements)
  requirements.map{ |r| rails_version_matches?(r) }.reduce(:&)
end

Au final, je teste que ma version courante de Rails est 3.2.16+ ou 4.0.2+ comme ça :

if rails_version_matches_any? '~> 3.2.16', '~> 4.0.2'
  # mon code qui va bien
end

Bien plus propre qu'avant. Là, ça me plait.

Publié par Michael Baudino michael.baudino@alpine-lab.com
Alpine Lab