Cet article est publié sous licence CC BY-NC-SA
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.
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.
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 :
nom
par name
;latitude
et la longitude
des coordonnées
;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.
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.
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.