Comment créer une gem Ruby on Rails à partir de votre code existant

C’est presque instinctif pour les développeurs de chercher des solutions que d’autres développeurs ont utilisées avec succès face à un problème. (Encore mieux s’il existe une bibliothèque préexistante que nous pouvons brancher directement dans notre code et nous faire gagner du temps, n’est-ce pas ?)

Dans cet article de blog, je vais prendre mon article précédent (Comment utiliser FilePond avec Active Storage de Rails) et transformer le code en une gem. En lisant cet article, gardez à l’esprit que c’est mon approche. Il n’y a pas qu’une seule façon de faire cela, et vous remarquerez en lisant divers codes sources de différentes gems que les auteurs ont diverses préférences. Il y a, cependant, quelques bonnes pratiques générales que je vais décrire.

Au fait, si vous êtes nouveau dans le monde Ruby, nous appelons les bibliothèques des “gems”. Rails, par exemple, est une gem elle-même. Donc à partir de maintenant, je ne ferai référence aux bibliothèques Ruby que comme gems.

Ce que nous construisons

Si vous n’avez pas encore lu l’article précédent (Comment utiliser FilePond avec Active Storage de Rails), je vous recommande de le faire maintenant dans un nouvel onglet. Cela vous donnera du contexte sur ce que nous construisons ici.

Brièvement, voici un résumé :

  • Créer (en utilisant le code de notre article précédent) une bibliothèque d’intégration pour la bibliothèque JavaScript FilePond. Cela consistera à la fois en code Ruby pour notre contrôleur personnalisé et en code pour charger la bibliothèque JavaScript.
  • Ajouter des tests pour vérifier notre contrôleur, le chargement du code JavaScript, et des tests système pour vérifier la fonctionnalité de notre intégration.
  • Documenter notre gem pour que d’autres puissent l’utiliser.
  • Publier la gem sur RubyGems.org.

Quand créer une gem

Avoir trop de dépendances dans les projets est un moyen sûr de se compliquer la vie à long terme. Cela affecte la maintenabilité du code et rend les mises à niveau (par exemple, pour un projet Rails) plus difficiles.

Cela dit, parfois il est logique d’utiliser une gem. Voici quelques raisons :

Empaqueter du code pour la réutilisation

Si vous ajoutez le même code sur plusieurs projets — disons, dans une seule organisation ou entreprise — et qu’il y a peu ou pas de personnalisations pour cet ensemble particulier de code, alors les empaqueter dans une gem peut avoir du sens. Cela aligne cette base de code partagée qui facilite la mise à niveau et la maintenabilité.

Les scénarios typiques pourraient être :

  • Système de facturation
  • Éléments d’interface utilisateur
  • Code de déploiement répétitif

La portée de la fonctionnalité est limitée

Un scénario que vous voulez typiquement éviter est de créer une gem qui ajoute trop de fonctionnalités. Les gems qui sont limitées en portée sont plus faciles à maintenir, comprendre et utiliser.

Des exemples de cela pourraient être :

  • Wrapper d’API (par exemple, service externe, base de données, etc.)
  • Helper de test pour simuler des appels réseau
  • Méthodes utilitaires spécifiques à une fonction (par exemple, conversion de devises)

Comme tout le reste en programmation, il y a toujours des exceptions aux raisons et exemples ci-dessus. Les questions habituelles que je me pose seraient :

  • Cette gem peut-elle fonctionner seule, spécifiquement pour un petit sous-ensemble de fonctionnalités ? Ou,
  • Cette gem ajoute-t-elle une fonctionnalité — bien que couplée (comme avec Ruby on Rails) — qui est limitée en portée et a une implémentation relativement stable ?

Cela étant dit, parlons de quelques considérations générales lorsque vous créez une gem Ruby.

Considérations pour l’écriture de gems

Lorsque vous publiez du code destiné à être consommé par d’autres, il y a plusieurs choses auxquelles vous devez penser. Pour notre discussion, je suppose que vous envisagez de rendre votre gem open-source.

Comment vais-je appeler ma gem ?

Choisir un nom est une préférence personnelle. Cependant, si vous prévoyez de publier sur RubyGems.org, vous voudrez quand même respecter l’espace de noms des organisations et individus qui ont une marque déposée existante.

Pour ma part, je préfère les noms descriptifs (c’est-à-dire ennuyeux). En consultant pour des entreprises, j’ai vu des fichiers Gemfile très volumineux. Très souvent, j’ai vu une variété de noms de gems où j’ai dû vérifier manuellement quel est leur but en tant que dépendance.

Pour notre gem d’intégration FilePond, nous l’appellerons filepond-rails.

Quelle licence allez-vous choisir ?

Il existe de nombreux types de modèles de licences open-source. Je vous recommande de consulter la page Licences et Standards de l’Open Source Initiative pour plus de détails.

ProjetLicence
Ruby on RailsMIT
BundlerMIT
DeviseMIT
SidekiqLGPLv3
aws-sdk-coreApache
NokogiriMIT
FaradayMIT
PumaBSD 3-Clause

Le type de licence que vous choisissez peut dépendre de votre philosophie sur l’open source, d’un objectif commercial spécifique, ou de quelque chose de complètement différent. Pour la gem que nous construisons dans ce tutoriel, nous utiliserons la licence MIT.

Quelle(s) version(s) de Rails votre gem supportera-t-elle ?

Si vous créez une gem qui pourrait être utilisée dans n’importe quel programme Ruby, vous devrez considérer des versions spécifiques de Ruby également. Cependant, parce que le contexte de ce que nous construisons est pour Rails, notre considération se concentrera principalement sur la version de Rails.

Lors de la publication d’une gem dans la nature, il est attendu que vous définissiez les exigences de dépendance dans le fichier gemspec (que nous passerons en revue dans une seconde). Gardez à l’esprit que plus vous voulez supporter de versions de Ruby et Rails, plus il peut y avoir de travail pour assurer les compatibilités arrière et avant.

Si vous regardez le code source de certaines gems, vous verrez du code comme ceci :

if Rails::VERSION::MAJOR < 7
  # Implémentation pour ancien Rails
else
  # Implémentation pour Rails 7+
end

Pour filepond-rails, nous supporterons Rails 7 et supérieur.

Instructions pour que d’autres développent ou contribuent

Une autre chose à considérer est si vous voulez que d’autres forkent et contribuent à votre projet. Pour les projets open-source, c’est généralement une bonne idée car cela fournit un chemin pour que d’autres développeurs offrent des pull requests et aident à corriger des bugs.

Pour faciliter l’implication des autres dans votre projet, j’aime généralement ajouter la configuration Docker et des instructions claires sur comment faire fonctionner le projet rapidement.

Pour filepond-rails, voici ce que je ferai :

# Pour exécuter la gem via l'application "dummy", exécutez :
docker compose up

# Pour entrer dans l'environnement de développement où vous pouvez exécuter bin/rails g controller et d'autres commandes :
docker compose run app bash

Échafauder votre gem

Pour créer notre gem Rails pour filepond-rails, il y a plusieurs façons de procéder.

Probablement la façon la plus simple est de suivre les instructions dans Rails Guide: Getting Started with Engines. Il existe également plusieurs templates de démarrage sur GitHub que vous pouvez utiliser et qui sont utiles pour créer des gems génériques (c’est-à-dire non spécifiques à Rails).

Pour notre projet, nous nous en remettrons à l’utilisation du Guide Rails comme ceci :

rails plugin new filepond-rails --mountable

La commande rails ci-dessus vient directement de https://guides.rubyonrails.org/engines.html#generating-an-engine.

Nous avons spécifié l’option --mountable pour pouvoir “monter” notre contrôleur personnalisé (et ses routes) sur l’application hôte. (L’application hôte est l’application Rails qui utilise notre gem.)

Le générateur crée plusieurs fichiers et dossiers pour nous faire démarrer. Examinons ces fichiers :

  • Répertoire app et une structure interne qui ressemble à votre application Rails habituelle (par exemple, avec assets, controllers, helpers, jobs, mailers, etc).
  • Répertoire bin pour contenir les bin stubs.
  • Répertoire config avec un seul fichier route.rb, qui définit les routes montables pour votre gem.
  • Répertoire lib qui contiendra généralement la majeure partie de tout le code personnalisé pour votre gem. Dans ce répertoire actuellement, Rails a échafaudé une constante avec namespace. Pour notre gem filepond-rails, nous avons un namespace FilePond::Rails. Il y a aussi un répertoire tasks pour contenir les tâches Rake spécifiques à la gem.
  • Répertoire test pour nos tests.
  • filepond-rails.gemspec pour décrire notre gem, ce qu’elle fait, l’auteur, la licence, les configurations du serveur de gems et les dépendances. Pour plus d’informations sur ce fichier et les différents paramètres qui peuvent y aller, consultez le Guide de Spécification sur RubyGems.org.
  • Gemfile spécifie toutes les autres gems dont vous avez besoin pour votre environnement de développement. Généralement, pour les dépendances de développement, j’utilise la directive add_development_dependency pour le fichier gemspec.
  • MIT-LICENSE est la licence par défaut créée par le générateur Rails. Modifiez-la ou changez-la selon vos besoins.
  • Rakefile charge les tâches Rake intégrées de la gem, les tâches Rake de l’application dummy et toutes les autres tâches Rake personnalisées que vous avez définies.
  • README.md devrait contenir une description de votre gem, des instructions sur l’utilisation de la gem et d’autres informations pertinentes.

Créer notre code

Cette section décrira les diverses modifications que nous apporterons à notre code échafaudé et transformera ces fichiers et dossiers génériques en une bibliothèque publiable. Commençons :

1. Définir le gemspec

Notre première étape devrait être de personnaliser le gemspec.

Vous remarquerez que la version de la gem est définie comme ceci :

spec.version = Filepond::Rails::VERSION

Le gemspec fait référence dynamiquement à ce que vous définissez comme Filepond::RAILS::VERSION. Ceci est défini dans lib/filepond/rails/version.rb :

module Filepond
  module Rails
    VERSION = "0.1.0"
  end
end

Pour une première version, je le laisse généralement à 0.1.0. Cependant, une fois que nous aurons terminé la première version de la gem, je l’augmenterai à 1.0.0 car cela représente la première vraie version.

C’est une bonne idée d’utiliser le Versionnage Sémantique (SemVer) ici. Si vous n’êtes pas familier avec cela, je vous recommande de jeter un œil à https://semver.org/.

Essentiellement, les “versions” que vous spécifiez dans votre logiciel devraient correspondre à une signification. Voici comment c’est décrit sur le site web SemVer :

  • Version MAJEURE quand vous faites des changements d’API incompatibles
  • Version MINEURE quand vous ajoutez des fonctionnalités de manière rétrocompatible
  • Version PATCH quand vous faites des corrections de bugs rétrocompatibles

Note : Rails lui-même suit un pattern SemVer dans la façon dont il publie de nouvelles versions.

Pour le reste des gemspecs, il y a déjà de bons commentaires en ligne pour vous aider à démarrer. Pour notre gem, vous pouvez voir notre gemspec terminé ici : https://github.com/Code-With-Rails/filepond-rails/blob/main/filepond-rails.gemspec.

2. Copier notre code

Parce que nous avons commencé avec une base de code déjà fonctionnelle pour notre intégration avec FilePond, nous pouvons copier notre code de là dans notre répertoire de projet échafaudé.

Si vous suivez un flux de travail de développement piloté par les tests (TDD), vous voudrez peut-être commencer par ajouter des tests. Pour ma part, au moins en ce qui concerne les gems Rails, cela vient généralement de code préexistant que je veux extraire d’un autre projet, donc mon flux de travail commence généralement par copier le code existant et ensuite ajouter des tests.

De notre code préexistant (que vous pouvez voir à https://github.com/code-With-Rails/filepond-demo), nous copierons ce qui suit :

  • ingress_controller.rb qui gère des fonctionnalités spécifiques liées à FilePond
  • routes.rb qui définit quelques routes pour notre contrôleur ci-dessus
  • Références FilePond
  • Le code JavaScript pour instancier FilePond

Parlons rapidement de chacun de ces fichiers et comment nous pourrions les modifier pour s’adapter à notre gem :

ingress_controller.rb

Il n’y a pas grand-chose que nous devons modifier ici car le code existant fonctionne déjà. Cependant, nous devrons mettre en namespace notre constante de IngressController à Filepond::Rails::IngressController. C’est pour que nous puissions garder tout ce qui appartient à notre gem dans son propre namespace et éviter les collisions de noms possibles avec l’application hôte.

Ce fichier devrait ensuite être placé à app/controllers/filepond/rails/ingress_controller.rb.

routes.rb

Dans notre code original, nous avons simplement ajouté nos routes dans l’application principale. Dans une gem, nous devrons la “monter” dans notre application hôte comme ceci :

# config/routes.rb de notre application hôte
Rails.application.routes.draw do
  mount Filepond::Rails::Engine, at: '/filepond'
  # Et d'autres routes, etc.
end

Vous pouvez vous référer au Guide Rails pour plus d’informations sur le montage d’engines.

Dans le code source de notre propre gem, cependant, nous devons configurer quelles routes sont montées. Nous modifions notre config/routes.rb comme ceci :

Filepond::Rails::Engine.routes.draw do
  # Pour des détails concernant les endpoints FilePond, veuillez consulter https://pqina.nl/filepond/docs/api/server

  # https://pqina.nl/filepond/docs/api/server/#fetch
  post 'active_storage/fetch', to: 'ingress#fetch'

  # https://pqina.nl/filepond/docs/api/server/#remove
  delete 'active_storage/remove', to: 'ingress#remove'
end

En comparant cela à notre code original, vous pouvez voir que c’est essentiellement la même chose. Nous avons fait une amélioration ici en ajoutant active_storage pour identifier que ces routes personnalisées FilePond sont destinées à être utilisées avec ActiveStorage (ce qui était l’intention de notre intégration). Notez que c’est une préférence personnelle (la mienne).

Le message clé ici est de réfléchir clairement à la façon dont vous nommez vos routes. Parce que les routes sont (typiquement) montées dans un sous-chemin de toute façon (rappelez-vous l’option hash at dans mount Filepond::Rails::Engine, at: '/filepond'), les choses seront correctement séparées. Cependant, vous voudriez quand même être intentionnellement descriptif dans les noms des chemins.

Voir https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-mount pour des détails supplémentaires sur la méthode mount.

Ajouter les références FilePond

Cette étape est spécifique à notre gem, mais s’appliquerait à toute gem que vous créez qui dépend d’une dépendance en amont (typiquement JavaScript).

Idéalement, nous pourrions minimiser l’inclusion de tout code source FilePond dans notre gem et simplement dire à l’utilisateur final de l’ajouter lui-même. Si possible, nous pouvons éviter d’ajouter tout code.

Pour notre situation spécifique, nous utiliserons importmap qui nous permet de référencer une version spécifique de FilePond sans l’ajouter au code source de notre gem. Malheureusement, importmap-rails ne supporte pas le référencement de fichiers CSS.

Pour faciliter les choses aux utilisateurs de notre gem, nous ajouterons ces fichiers CSS à nos assets pour qu’ils puissent être inclus plus facilement. Spécifiquement, nous ajoutons filepond.css et filepond.min.css dans le dossier app/assets/stylesheets.

Nous ajoutons également des instructions à notre README.md pour que les utilisateurs sachent qu’ils doivent faire cela.

Ensuite, nous devons également nous assurer que la bibliothèque JavaScript FilePond en amont est chargée. Similaire à notre base de code originale, nous ajoutons un fichier importmap.rb au répertoire config et le modifions comme ceci :

# config/importmap.rb
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js', preload: true

Cela “verrouillera” notre version de la gem à FilePond v4.30.4. Quand FilePond sera mis à jour, nous devrons l’augmenter pour supporter la nouvelle version.

Similaire aux assets CSS, nous ajoutons une note dans le README.md pour instruire les utilisateurs d’ajouter javascript_importmap_tags à leur fichier de layout application.html.erb pour que les définitions dans importmap.rb soient chargées lors du rendu.

Ajouter notre code JavaScript

Enfin, nous devons ajouter notre code JavaScript pour tout lier ensemble. Dans notre base de code originale, nous avons placé tout ce code ici : https://github.com/Code-With-Rails/filepond-demo/blob/main/app/javascript/application.js.

Bien que le code ci-dessus puisse fonctionner pour notre application, il n’est pas approprié lorsque nous créons une bibliothèque.

Pour cela, nous supporterons l’utilisation de modules ESM. Nous modifions notre code JavaScript pour exporter des fonctions afin que les utilisateurs de notre gem puissent les importer :

// Dans le fichier JavaScript de l'application hôte...
import { FilePondRails, FilePond } from 'filepond-rails'

window.FilePond = FilePond
window.FilePondRails = FilePondRails

const input = document.querySelector('.filepond')
FilePondRails.create(input)

Notre objectif ici est de rendre aussi facile que possible l’intégration de notre bibliothèque pour les utilisateurs. Rappelez-vous que notre audience de développeurs cible sera probablement des développeurs Rails utilisant du JavaScript vanilla ou Stimulus.

Note : Rappelez-vous plus tôt que j’ai mentionné qu’en tant qu’auteur de gem vous devez considérer quelles dépendances logicielles votre bibliothèque nécessitera.

Pour filepond-rails, j’ai déclaré que les développeurs voulant utiliser cette bibliothèque devraient être sur Rails 7+ et utiliser importmap-rails. Parce que importmap-rails repose sur l’utilisation de modules ESM, c’est ce que filepond-rails supportera en termes de chargement JavaScript.

Tester avec l’application Dummy

À ce stade, vous vous demandez peut-être : Est-ce que tout cela fonctionne ?

C’est une bonne question et une à laquelle il est très facile de répondre en bootstrappant une application Rails et en suivant nos instructions de configuration. Dans le code source de notre gem, il y a une application “Dummy” avec laquelle nous pouvons faire exactement cela.

Pour le produit fini, vous pouvez voir le code source ici : https://github.com/Code-With-Rails/filepond-rails/tree/main/test/dummy.

Dans cette application, nous créons un contrôleur, une vue et du JavaScript pour instancier le JavaScript personnalisé de notre gem.

Une fois que nous faisons tout cela, nous pouvons démarrer notre serveur avec la commande suivante :

bin/rails server

Vous devriez pouvoir aller à http://localhost:3000 pour voir votre travail.

Note : Consultez filepond-rails à https://github.com/Code-With-Rails/filepond-rails si vous voulez essayer cela vous-même.

Question : Au lieu d’utiliser l’application Dummy, comment puis-je tester ma gem sur mon application hôte que je développe en même temps ?

Réponse : Pour pouvoir faire cela, vous pouvez référencer votre gem localement.

# Gemfile de l'application hôte
gem 'filepond-rails', path: '/path/to/filepond-rails-local'

N’oubliez pas d’exécuter bundle install et bundle update si vous mettez à jour le code dans le code source de la gem.

Ajouter des tests

Pour nous assurer que notre gem fonctionne et que les futures mises à jour ne cassent pas la fonctionnalité, nous devrons ajouter quelques tests.

Pour filepond-rails, nous resterons avec minitest. Vous pouvez, cependant, choisir n’importe quel framework (par exemple, rspec) si vous préférez.

Parce que notre application se compose de deux parties (notre IngressController et le code de chargement JavaScript), ce sont celles-ci que nous testerons.

Le test du contrôleur est assez simple et du test de contrôleur Rails standard : https://github.com/Code-With-Rails/filepond-rails/blob/main/test/controllers/filepond/rails/ingress_controller_test.rb.

Ensuite, nous ajoutons des tests système. Les tests système, selon le Guide Rails :

Les tests système vous permettent de tester les interactions utilisateur avec votre application, en exécutant des tests dans un navigateur réel ou headless. Les tests système utilisent Capybara sous le capot.

Pour une gem améliorant l’interface utilisateur comme filepond-rails, nous voulons nous assurer que notre code source JavaScript se charge et fonctionne correctement.

Pour créer un test système, nous tapons :

bin/rails generate system_test uploads

Cela générera le test/system/filepond/rails/uploads_test.rb correspondant. Ce test, qui est exécuté en tapant bin/rails app:test:system dans la ligne de commande (car il n’est pas exécuté par défaut avec le reste des tests), testera les choses dans un navigateur headless.

Publier notre gem

Enfin, nous avons atteint l’étape où nous pouvons publier notre gem. Avant de le faire, créons un fichier CHANGELOG. Le journal des changements devrait contenir tous les changements majeurs, mineurs et cassants que vous avez faits. Je date généralement la version aussi.

Ensuite, je recommande de taguer les versions tout comme vous le feriez pour tout autre projet logiciel.

Exemple :

git tag v1.0.0

Une fois que vous avez poussé cela sur GitHub, créez une release avec une bonne description.

Pour publier votre gem sur RubyGems.org où elle peut être accessible par d’autres utilisateurs, suivez les instructions à https://guides.rubygems.org/publishing/.

Vous devrez d’abord construire votre gem comme ceci :

gem build filepond-rails

Cela créera un fichier comme celui-ci : filepond-rails-1.0.0.gem.

Une fois que vous avez cela, vous pouvez ensuite faire ceci :

gem publish filepond-rails-1.0.0.gem

Note : Vous aurez besoin d’un compte sur RubyGems.org et devrez authentifier votre CLI gem pour pouvoir pousser des changements publiquement.

Documentation

filepond-rails est une gem simple et petite, et donc un site web de projet complet n’est pas justifié. Pour des gems plus compliquées avec une grande API publique, une documentation appropriée devrait être présente.

Conseil : Consultez https://rubydoc.info/ et ajoutez-y votre gem. C’est un outil utile pour aider à générer de la documentation à partir de vos commentaires de code et la rendre publiquement accessible aux utilisateurs développeurs.

Idées d’amélioration

Comme avec tout projet, il y a toujours place à l’amélioration. Cet article de blog n’effleure que la surface et fournit les étapes minimales pour créer un projet de gem que vous et d’autres pouvez utiliser sur plusieurs applications Rails.

D’autres choses qui peuvent être faites (sans ordre particulier) :

  • Utiliser Appraisal pour tester contre plusieurs versions de Rails
  • Ajouter une CI GitHub Action (non couvert ici, mais vous pouvez consulter le repo de filepond-rails pour voir comment nous l’avons fait là-bas)
  • Ajouter un pipeline de publication vers RubyGems.org (pour que nous puissions sauter la publication manuelle de la gem complètement)

Conclusion

Créer une gem open-source pour que d’autres l’utilisent peut être une expérience satisfaisante, à la fois techniquement et professionnellement.

Sur le plan professionnel, c’est un excellent moyen de redonner à la communauté et d’aider les autres qui pourraient être confrontés à la même situation que vous avez rencontrée.

Techniquement, la création de bibliothèque vous oblige également à réfléchir attentivement au problème que vous voulez résoudre et à bien penser votre solution pour qu’elle minimise les problèmes et ne cause pas de problèmes aux utilisateurs finaux. Pour ces raisons, j’aime généralement garder les bibliothèques de gems petites en taille avec un focus étroit et chirurgical.

J’espère que vous avez trouvé ce tutoriel utile. Encore une fois, le code source de la gem filepond-rails peut être trouvé à https://github.com/Code-With-Rails/filepond-rails. Pour la gem elle-même, consultez https://rubygems.org/gems/filepond-rails.