Comment Sequel et Sinatra résolvent le problème de l'API de Ruby
Publié: 2022-03-11introduction
Au cours des dernières années, le nombre de frameworks d'applications JavaScript à page unique et d'applications mobiles a considérablement augmenté. Cela impose une demande accrue d'API côté serveur. Ruby on Rails étant l'un des frameworks de développement Web les plus populaires d'aujourd'hui, c'est un choix naturel parmi de nombreux développeurs pour créer des applications API back-end.
Pourtant, alors que le paradigme architectural Ruby on Rails facilite la création d'applications API back-end, l'utilisation de Rails uniquement pour l'API est exagérée. En fait, c'est exagéré au point que même l'équipe Rails l'a reconnu et a donc introduit un nouveau mode API uniquement dans la version 5. Avec cette nouvelle fonctionnalité de Ruby on Rails, créer des applications uniquement API dans Rails est devenu encore plus facile. et une option plus viable.
Mais il y a aussi d'autres options. Les plus notables sont deux joyaux très matures et puissants, qui, combinés, fournissent des outils puissants pour créer des API côté serveur. Ce sont Sinatra et Sequel.
Ces deux joyaux ont un ensemble de fonctionnalités très riche : Sinatra sert de langage spécifique au domaine (DSL) pour les applications Web, et Sequel sert de couche de mappage objet-relationnel (ORM). Alors, jetons un bref coup d'œil à chacun d'eux.
Sinatra
Sinatra est un framework d'application Web basé sur Rack. Le Rack est une interface de serveur Web Ruby bien connue. Il est utilisé par de nombreux frameworks, comme Ruby on Rails, par exemple, et prend en charge de nombreux serveurs Web, comme WEBrick, Thin ou Puma. Sinatra fournit une interface minimale pour écrire des applications Web en Ruby, et l'une de ses fonctionnalités les plus intéressantes est la prise en charge des composants middleware. Ces composants se situent entre l'application et le serveur Web et peuvent surveiller et manipuler les demandes et les réponses.
Pour utiliser cette fonctionnalité Rack, Sinatra définit le DSL interne pour la création d'applications Web. Sa philosophie est très simple : les routes sont représentées par des méthodes HTTP, suivies d'une route correspondant à un motif. Un bloc Ruby dans lequel la demande est traitée et la réponse est formée.
get '/' do 'Hello from sinatra' end
Le modèle de correspondance de route peut également inclure un paramètre nommé. Lorsque le bloc route est exécuté, une valeur de paramètre est transmise au bloc via la variable params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
Les modèles de correspondance peuvent utiliser l'opérateur splat *
qui rend les valeurs de paramètre disponibles via params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
Ce n'est pas la fin des possibilités de Sinatra liées à l'appariement des routes. Il peut utiliser une logique de correspondance plus complexe via des expressions régulières, ainsi que des correspondances personnalisées.
Sinatra comprend tous les verbes HTTP standard nécessaires à la création d'une API REST : Get, Post, Put, Patch, Delete et Options. Les priorités de route sont déterminées par l'ordre dans lequel elles sont définies, et la première route qui correspond à une demande est celle qui dessert cette demande.
Les applications Sinatra peuvent être écrites de deux manières ; en utilisant un style classique ou modulaire. La principale différence entre eux est que, avec le style classique, nous ne pouvons avoir qu'une seule application Sinatra par processus Ruby. Les autres différences sont suffisamment mineures pour que, dans la plupart des cas, elles puissent être ignorées et les paramètres par défaut peuvent être utilisés.
Approche classique
La mise en œuvre d'une application classique est simple. Il suffit de charger Sinatra et d'implémenter les gestionnaires de route :
require 'sinatra' get '/' do 'Hello from Sinatra' end
En enregistrant ce code dans le fichier demo_api_classic.rb
, nous pouvons démarrer l'application directement en exécutant la commande suivante :
ruby demo_api_classic.rb
Cependant, si l'application doit être déployée avec des gestionnaires de rack, comme Passenger, il est préférable de la démarrer avec le fichier config.ru
de configuration du rack.
require './demo_api_classic' run Sinatra::Application
Une fois le fichier config.ru
en place, l'application est lancée avec la commande suivante :
rackup config.ru
Approche modulaire
Les applications modulaires Sinatra sont créées en sous-classant Sinatra::Base
ou Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
La déclaration commençant par run!
est utilisé pour démarrer l'application directement, avec ruby demo_api.rb
, comme avec l'application classique. D'autre part, si l'application doit être déployée avec Rack, le contenu des gestionnaires de rackup.ru
doit être :
require './demo_api' run DemoApi
Suite
Sequel est le deuxième outil de cet ensemble. Contrairement à ActiveRecord, qui fait partie de Ruby on Rails, les dépendances de Sequel sont très faibles. En même temps, il est assez riche en fonctionnalités et peut être utilisé pour toutes sortes de tâches de manipulation de bases de données. Avec son langage simple spécifique à un domaine, Sequel soulage le développeur de tous les problèmes liés au maintien des connexions, à la construction de requêtes SQL, à la récupération de données (et au renvoi de données vers) la base de données.
Par exemple, établir une connexion avec la base de données est très simple :
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
La méthode connect renvoie un objet de base de données, dans ce cas, Sequel::Postgres::Database
, qui peut ensuite être utilisé pour exécuter du SQL brut.
DB['select count(*) from players']
Sinon, pour créer un nouvel objet d'ensemble de données :
DB[:players]
Ces deux instructions créent un objet d'ensemble de données, qui est une entité Sequel de base.
L'une des fonctionnalités les plus importantes du jeu de données Sequel est qu'il n'exécute pas les requêtes immédiatement. Cela permet de stocker des ensembles de données pour une utilisation ultérieure et, dans la plupart des cas, de les enchaîner.
users = DB[:players].where(sport: 'tennis')
Donc, si un ensemble de données n'atteint pas la base de données immédiatement, la question est, quand le fait-il ? Sequel exécute SQL sur la base de données lorsque des « méthodes exécutables » sont utilisées. Ces méthodes sont, pour n'en nommer que quelques-unes, all
, each
, map
, first
et last
.
Sequel est extensible, et son extensibilité est le résultat d'une décision architecturale fondamentale de construire un petit noyau complété par un système de plugin. Les fonctionnalités sont facilement ajoutées via des plugins qui sont, en fait, des modules Ruby. Le plugin le plus important est le plugin Model
. C'est un plugin vide qui ne définit aucune méthode de classe ou d'instance par lui-même. Au lieu de cela, il inclut d'autres plugins (sous-modules) qui définissent une classe, une instance ou des méthodes de jeu de données de modèle. Le plugin Model permet d'utiliser Sequel comme outil de mappage objet-relationnel (ORM) et est souvent appelé "plugin de base".
class Player < Sequel::Model end
Le modèle Sequel analyse automatiquement le schéma de la base de données et configure toutes les méthodes d'accès nécessaires pour toutes les colonnes. Il suppose que le nom de la table est au pluriel et est une version soulignée du nom du modèle. S'il est nécessaire de travailler avec des bases de données qui ne respectent pas cette convention de dénomination, le nom de la table peut être explicitement défini lors de la définition du modèle.
class Player < Sequel::Model(:player) end
Nous avons donc maintenant tout ce dont nous avons besoin pour commencer à créer l'API back-end.
Construire l'API
Structure des codes
Contrairement à Rails, Sinatra n'impose aucune structure de projet. Cependant, comme c'est toujours une bonne pratique d'organiser le code pour faciliter la maintenance et le développement, nous le ferons ici aussi, avec la structure de répertoires suivante :

project root |-config |-helpers |-models |-routes
La configuration de l'application sera chargée à partir du fichier de configuration YAML pour l'environnement actuel avec :
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
Par défaut, la valeur de Sinatra::Applicationsettings.environment
est development,
et elle est modifiée en définissant la variable d'environnement RACK_ENV
.
De plus, notre application doit charger tous les fichiers des trois autres répertoires. Nous pouvons le faire facilement en exécutant:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
À première vue, ce mode de chargement peut sembler pratique. Cependant, avec cette seule ligne de code, nous ne pouvons pas facilement ignorer les fichiers, car cela chargera tous les fichiers des répertoires du tableau. C'est pourquoi nous utiliserons une approche de chargement de fichier unique plus efficace, qui suppose que dans chaque dossier, nous avons un fichier manifeste init.rb
, qui charge tous les autres fichiers du répertoire. De plus, nous ajouterons un répertoire cible au chemin de chargement Ruby :
%w{helpers models routes}.each do |dir| $LOAD_PATH << File.expand_path('.', File.join(File.dirname(__FILE__), dir)) require File.join(dir, 'init') end
Cette approche nécessite un peu plus de travail car nous devons maintenir les instructions require dans chaque fichier init.rb
mais, en retour, nous obtenons plus de contrôle et nous pouvons facilement omettre un ou plusieurs fichiers en les supprimant du fichier manifeste init.rb
dans le répertoire cible.
Authentification API
La première chose dont nous avons besoin dans chaque API est l'authentification. Nous allons l'implémenter en tant que module d'assistance. La logique d'authentification complète se trouvera dans le fichier helpers/authentication.rb
.
require 'multi_json' module Sinatra module Authentication def authenticate! client_id = request['client_id'] client_secret = request['client_secret'] # Authenticate client here halt 401, MultiJson.dump({message: "You are not authorized to access this resource"}) unless authenticated? end def current_client @current_client end def authenticated? !current_client.nil? end end helpers Authentication end
Tout ce que nous avons à faire maintenant est de charger ce fichier en ajoutant une instruction require dans le fichier manifeste de l'assistant ( helpers/init.rb
) et d'appeler le fichier authenticate!
méthode dans le crochet before
de Sinatra qui sera exécutée avant de traiter toute demande.
before do authenticate! end
Base de données
Ensuite, nous devons préparer notre base de données pour l'application. Il existe de nombreuses façons de préparer la base de données, mais puisque nous utilisons Sequel, il est naturel de le faire en utilisant des migrateurs. Sequel est livré avec deux types de migrateurs - basés sur des entiers et des horodatages. Chacun a ses avantages et ses inconvénients. Dans notre exemple, nous avons décidé d'utiliser le migrateur d'horodatage de Sequel, qui nécessite que les fichiers de migration soient précédés d'un horodatage. Le migrateur d'horodatage est très flexible et peut accepter différents formats d'horodatage, mais nous n'utiliserons que celui composé de l'année, du mois, du jour, de l'heure, de la minute et de la seconde. Voici nos deux fichiers de migration :
# db/migrations/20160710094000_sports.rb Sequel.migration do change do create_table(:sports) do primary_key :id String :name, :null => false end end end # db/migrations/20160710094100_players.rb Sequel.migration do change do create_table(:players) do primary_key :id String :name, :null => false foreign_key :sport_id, :sports end end end
Nous sommes maintenant prêts à créer une base de données avec toutes les tables.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
Enfin, nous avons les fichiers modèles sport.rb
et player.rb
dans le répertoire models
.
# models/sport.rb class Sport < Sequel::Model one_to_many :players def to_api { id: id, name: name } end end # models/player.rb class Player < Sequel::Model many_to_one :sport def to_api { id: id, name: name, sport_id: sport_id } end end
Ici, nous utilisons une méthode Sequel pour définir les relations de modèle, où l'objet Sport
a de nombreux joueurs et Player
ne peut avoir qu'un seul sport. De plus, chaque modèle définit sa méthode to_api
, qui renvoie un hachage avec des attributs qui doivent être sérialisés. Il s'agit d'une approche générale que nous pouvons utiliser pour différents formats. Cependant, si nous n'utilisons qu'un format JSON dans notre API, nous pourrions utiliser le to_json
de Ruby avec un only
argument pour restreindre la sérialisation aux attributs requis, c'est-à-dire player.to_json(only: [:id, :name, :sport_i])
. Bien sûr, nous pourrions également définir un BaseModel
qui hérite de Sequel::Model
et définit une méthode to_api
par défaut, dont hériteraient tous les modèles.
Maintenant, nous pouvons commencer à implémenter les points de terminaison API réels.
Points de terminaison de l'API
Nous conserverons la définition de tous les points de terminaison dans des fichiers du répertoire routes
. Puisque nous utilisons des fichiers manifestes pour charger les fichiers, nous regrouperons les routes par ressources (c'est-à-dire, conserver toutes les routes liées au sport dans le fichier sports.rb
, toutes les routes des joueurs dans routes.rb
, etc.).
# routes/sports.rb class DemoApi < Sinatra::Application get "/sports/?" do MultiJson.dump(Sport.all.map { |s| s.to_api }) end get "/sports/:id" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.to_api : {}) end get "/sports/:id/players/?" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.players.map { |p| p.to_api } : []) end end # routes/players.rb class DemoApi < Sinatra::Application get "/players/?" do MultiJson.dump(Player.all.map { |p| s.to_api }) end get "/players/:id/?" do player = Player.where(id: params[:id]).first MultiJson.dump(player ? player.to_api : {}) end end
Les itinéraires imbriqués, comme celui qui permet d'obtenir tous les joueurs d'un sport /sports/:id/players
, peuvent être définis soit en les plaçant avec d'autres itinéraires, soit en créant un fichier de ressources séparé qui ne contiendra que les itinéraires imbriqués.
Avec les itinéraires désignés, l'application est maintenant prête à accepter les demandes :
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Notez que, comme requis par le système d'authentification de l'application défini dans le fichier helpers/authentication.rb
, nous transmettons les informations d'identification directement dans les paramètres de la requête.
Conclusion
Les principes démontrés dans cet exemple d'application simple s'appliquent à toute application API back-end. Il n'est pas basé sur l'architecture modèle-vue-contrôleur (MVC), mais il maintient une séparation claire des responsabilités de la même manière ; la logique métier complète est conservée dans les fichiers de modèle tandis que la gestion des demandes est effectuée dans les méthodes de routage de Sinatra. Contrairement à l'architecture MVC, où les vues sont utilisées pour rendre les réponses, cette application le fait au même endroit où elle gère les requêtes - dans les méthodes de routage. Avec de nouveaux fichiers d'aide, l'application peut être facilement étendue pour envoyer la pagination ou, si nécessaire, demander des informations sur les limites à l'utilisateur dans les en-têtes de réponse.
Au final, nous avons construit une API complète avec un ensemble d'outils très simple et sans perdre aucune fonctionnalité. Le nombre limité de dépendances permet de garantir que l'application se charge et démarre beaucoup plus rapidement, et a une empreinte mémoire beaucoup plus petite qu'une application basée sur Rails. Ainsi, la prochaine fois que vous commencerez à travailler sur une nouvelle API dans Ruby, envisagez d'utiliser Sinatra et Sequel car ce sont des outils très puissants pour un tel cas d'utilisation.