Article écrit par Emilie Podczaszy
La manipulation de données est une tâche que nous faisons quotidiennement, renommer les paramètres d’une requête entrante dans un contrôleur, exporter des tableaux de données diverses en CSV, etc. La liste est longue et selon la complexité des structures à manipuler ou du résultat attendu, la tâche peut vite devenir ardue.
Après avoir entendu parler de la gem dry-transformer, j’ai décidé de la tester et contre toute attente, elle répond parfaitement à la problématique tout en étant simple d’utilisation. Seul point négatif, son manque de documentation et d’exemples, c’est pourquoi je vous propose de voir ensemble comment elle fonctionne et s’utilise.
Dry-transformer, qu’est-ce que c’est ?
Cette petite bibliothèque Ruby, qui fait partie de la collection des gems dry-rb, est inspirée de la programmation fonctionnelle : les données passent à travers plusieurs fonctions “stateless” qui chacune renvoie une nouvelle représentation des données d’origine à la fonction suivante. Elle propose donc une approche plus orientée donnée qu’orientée objet.
Il est aussi bon d’ajouter que la gem n’a aucune dépendance.
Exemple d’utilisation
Suite à un appel API, nous recevons cette liste de villes :
input = [ { "nom" => "Lille", "surface" => 34.8, "population" => 232741, "coordonnées" => { "latitude" => 50.62925, "longitude" => 3.057256 }, }, { "nom" => "Amiens", "surface" => 49.46, "population" => 132874, "coordonnées" => { "latitude" => 49.894067, "longitude" => 2.295753 } }, { "nom" => "Arras", "surface" => 11.63, "population" => 40721, "coordonnées" => { "latitude" => 50.291002, "longitude" => 2.777535 } }, ]
Et souhaitons travailler un peu ce tableau pour obtenir :
[ { name: "LILLE", density: 6687.96, latitude: 50.62925, longitude: 3.057256 }, { name: "ARRAS", density: 3501.38, latitude: 50.291002, longitude: 2.777535 }, { name: "AMIENS", density: 2686.49, latitude: 49.894067, longitude: 2.295753 }, ]
On veut donc appliquer les modifications suivantes :
- transformer toutes les clefs, mêmes celles imbriquées, en symboles ;
- renommer la clef
nom
parname
; - mettre en majuscule le nom des villes ;
- extraire la
latitude
et lalongitude
descoordonnées
; - calculer la densité de population ;
- ne garder que les paires clef-valeur qui nous intéresse ;
- trier le tableau par densité.
Tout d’abord, nous allons utiliser le DSL de la gem pour retranscrire chacune de ces étapes :
class Mapper < Dry::Transformer::Pipe import Dry::Transformer::ArrayTransformations import Dry::Transformer::HashTransformations define! do map_array do deep_symbolize_keys # 1. rename_keys nom: :name # 2. map_value :name, -> value { value.upcase } # 3. unwrap :coordonnees, %i[latitude longitude] # 4. accept_keys %i[name density latitude longitude] # 6. end end end
Importer les modules ArrayTransformations
et HashTransformations
mis à disposition par la gem permet d’appeler un certains nombres de fonctions de transformation. Pour notre exemple, presque tout est couvert, il ne reste que les étapes de calcul de la densité (5.) ainsi que le tri (7.).
Nous allons donc devoir créer notre propre module et pour cela rien de plus simple, il suffit d’étendre le module Registry
et d’ajouter nos méthodes :
module CustomTransformations extend Dry::Transformer::Registry def self.add_key(hash, key, fn) hash.merge(key => fn[hash]) end def self.sort_by(array, key) array.sort_by { |v| v[key] } end end
La méthode :sort_by
est somme toute assez explicite, alors voyons plus en détails :add_key
. Cette méthode prend en entrée notre hash, la clef à ajouter ainsi qu’un Proc qui sera invoqué avec :[]
comme il est d’usage dans cette gem. Le tout sera fusionné avec le hash en entrée. Cela suit le même mode de fonctionnement que map_value
utilisé plus haut.
Intégré dans notre mapper, cela donne :
define! do map_array do add_key :density, -> city { city[:population] / city[:surface] } end end
À noter qu’il y a d’autres manières d’utiliser un Proc :
compute_density = Proc.new { |city| city[:population] / city[:surface] } add_key :density, compute_density # ou encore class DensityComputation def self.to_proc proc { |city| city[:population] / city[:surface] } end end add_key :density, DensityComputation.to_proc
Un calcul, aussi simple soit-il, semble être un bon candidat pour une abstraction supplémentaire alors, gardons la classe DensityComputation
.
Nous avons toutes nos transformations, il ne reste plus qu’à les ajouter dans le mapper et voilà :
class Mapper < Dry::Transformer::Pipe import Dry::Transformer::ArrayTransformations import Dry::Transformer::HashTransformations import CustomTransformations define! do map_array do deep_symbolize_keys # 1. rename_keys nom: :name # 2. map_value :name, -> value { value.upcase } # 3. unwrap :coordonnees, %i[latitude longitude] # 4. add_key :density, DensityComputation.to_proc # 5. accept_keys %i[name density latitude longitude] # 6. end sort_by :density # 7. end end Mapper.new.call(input) => [ { name: "AMIENS", density: 2686.49, latitude: 49.894067, longitude: 2.295753 }, { name: "ARRAS", density: 3501.38, latitude: 50.291002, longitude: 2.777535 }, { name: "LILLE", density: 6687.96, latitude: 50.62925, longitude: 3.057256 }, ]
Chaque type de transformation est encapsulé dans sa méthode, ce qui rend la composition de plusieurs transformations facile et grâce au DSL le tout se lit simplement.
Un dernier pour la route
Si vous n’êtes toujours pas convaincu·e, voici un autre exemple.
On vous demande d’apporter des modifications dans un projet existant, vous préférez tomber sur cette méthode ?
def group_categories(array) array.group_by{ |h| h[:name] }.values.map do |hs| hs.first.merge({ categories: hs.map{ |h| h[:category] } }).except(:category) end end
Ou cette classe ?
class Mapper < Dry::Transformer::Pipe import Dry::Transformer::ArrayTransformations import Dry::Transformer::HashTransformations define! do group :categories, [:category] map_array do map_value :categories do extract_key :category end end end end
Personnellement le choix est vite vu, dry-transformer rend le code bien plus compréhensible et maintenable.
Ressources
- Complex Ruby Transformations made simple with dry-transformer ! par Seb Wilgosz