Como Sequel e Sinatra resolvem o problema da API do Ruby

Publicados: 2022-03-11

Introdução

Nos últimos anos, o número de estruturas de aplicativos de página única JavaScript e aplicativos móveis aumentou substancialmente. Isso impõe uma demanda correspondentemente aumentada por APIs do lado do servidor. Com Ruby on Rails sendo uma das estruturas de desenvolvimento web mais populares da atualidade, é uma escolha natural entre muitos desenvolvedores para criar aplicativos de API de back-end.

No entanto, embora o paradigma de arquitetura Ruby on Rails facilite a criação de aplicativos de API de back-end, usar Rails apenas para a API é um exagero. Na verdade, é um exagero a ponto de até mesmo a equipe do Rails reconhecer isso e, portanto, introduzir um novo modo somente API na versão 5. Com esse novo recurso no Ruby on Rails, criar aplicativos somente API no Rails ficou ainda mais fácil e opção mais viável.

Mas há outras opções também. As mais notáveis ​​são duas gemas muito maduras e poderosas, que em combinação fornecem ferramentas poderosas para criar APIs do lado do servidor. Eles são Sinatra e Sequel.

Ambas as gemas têm um conjunto de recursos muito rico: o Sinatra serve como a linguagem específica de domínio (DSL) para aplicativos da Web, e o Sequel serve como a camada de mapeamento relacional de objeto (ORM). Então, vamos dar uma breve olhada em cada um deles.

API com Sinatra e Sequel: Tutorial Ruby

API Ruby na dieta: apresentando Sequel e Sinatra.
Tweet

Sinatra

Sinatra é um framework de aplicação web baseado em Rack. O Rack é uma interface de servidor web Ruby bem conhecida. Ele é usado por muitos frameworks, como Ruby on Rails, por exemplo, e suporta muitos servidores web, como WEBrick, Thin ou Puma. O Sinatra fornece uma interface mínima para escrever aplicativos da Web em Ruby, e um de seus recursos mais atraentes é o suporte a componentes de middleware. Esses componentes ficam entre o aplicativo e o servidor web e podem monitorar e manipular solicitações e respostas.

Para utilizar este recurso de Rack, o Sinatra define DSL interno para criação de aplicações web. Sua filosofia é muito simples: as rotas são representadas por métodos HTTP, seguidos por uma rota que corresponde a um padrão. Um bloco Ruby dentro do qual a solicitação é processada e a resposta é formada.

 get '/' do 'Hello from sinatra' end

O padrão de correspondência de rota também pode incluir um parâmetro nomeado. Quando o bloco de rota é executado, um valor de parâmetro é passado para o bloco através da variável params .

 get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end

Padrões de correspondência podem usar o operador splat * que disponibiliza os valores dos parâmetros por meio de params[:splat] .

 get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end

Este não é o fim das possibilidades de Sinatra relacionadas à correspondência de rotas. Ele pode usar uma lógica de correspondência mais complexa por meio de expressões regulares, bem como correspondências personalizadas.

Sinatra entende todos os verbos HTTP padrão necessários para criar uma API REST: Get, Post, Put, Patch, Delete e Options. As prioridades de rota são determinadas pela ordem em que são definidas e a primeira rota que corresponde a uma solicitação é aquela que atende a essa solicitação.

Os aplicativos Sinatra podem ser escritos de duas maneiras; usando estilo clássico ou modular. A principal diferença entre eles é que, com o estilo clássico, podemos ter apenas uma aplicação Sinatra por processo Ruby. Outras diferenças são pequenas o suficiente para que, na maioria dos casos, possam ser ignoradas e as configurações padrão possam ser usadas.

Abordagem Clássica

A implementação do aplicativo clássico é simples. Basta carregar o Sinatra e implementar os manipuladores de rotas:

 require 'sinatra' get '/' do 'Hello from Sinatra' end

Ao salvar este código no arquivo demo_api_classic.rb , podemos iniciar o aplicativo diretamente executando o seguinte comando:

 ruby demo_api_classic.rb

No entanto, se o aplicativo for implantado com manipuladores de rack, como o Passenger, é melhor iniciá-lo com o arquivo config.ru de configuração do rack.

 require './demo_api_classic' run Sinatra::Application

Com o arquivo config.ru instalado, o aplicativo é iniciado com o seguinte comando:

 rackup config.ru

Abordagem Modular

Os aplicativos Sinatra modulares são criados subclassificando Sinatra::Base ou Sinatra::Application :

 require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end

A instrução que começa com run! é usado para iniciar o aplicativo diretamente, com ruby demo_api.rb , assim como no aplicativo clássico. Por outro lado, se o aplicativo for implantado com o Rack, o conteúdo dos manipuladores de rackup.ru deve ser:

 require './demo_api' run DemoApi

Sequência

Sequel é a segunda ferramenta neste conjunto. Ao contrário do ActiveRecord, que faz parte do Ruby on Rails, as dependências do Sequel são muito pequenas. Ao mesmo tempo, é bastante rico em recursos e pode ser usado para todos os tipos de tarefas de manipulação de banco de dados. Com sua linguagem específica de domínio simples, o Sequel alivia o desenvolvedor de todos os problemas com a manutenção de conexões, construção de consultas SQL, busca de dados (e envio de dados de volta) para o banco de dados.

Por exemplo, estabelecer uma conexão com o banco de dados é muito simples:

 DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')

O método connect retorna um objeto de banco de dados, neste caso, Sequel::Postgres::Database , que pode ser usado posteriormente para executar SQL bruto.

 DB['select count(*) from players']

Como alternativa, para criar um novo objeto de conjunto de dados:

 DB[:players]

Ambas as instruções criam um objeto de conjunto de dados, que é uma entidade Sequel básica.

Um dos recursos mais importantes do conjunto de dados do Sequel é que ele não executa consultas imediatamente. Isso possibilita armazenar conjuntos de dados para uso posterior e, na maioria dos casos, encadeá-los.

 users = DB[:players].where(sport: 'tennis')

Então, se um conjunto de dados não atinge o banco de dados imediatamente, a questão é: quando isso acontece? O Sequel executa SQL no banco de dados quando os chamados “métodos executáveis” são usados. Esses métodos são, para citar alguns, all , each , map , first e last .

Sequel é extensível, e sua extensibilidade é resultado de uma decisão arquitetural fundamental para construir um pequeno núcleo complementado com um sistema de plugins. Recursos são facilmente adicionados através de plugins que são, na verdade, módulos Ruby. O plugin mais importante é o plugin Model . É um plugin vazio que não define nenhum método de classe ou instância por si só. Em vez disso, inclui outros plugins (submódulos) que definem uma classe, instância ou métodos de conjunto de dados de modelo. O plugin Model permite o uso do Sequel como ferramenta de mapeamento relacional de objetos (ORM) e é frequentemente chamado de “plugin base”.

 class Player < Sequel::Model end

O modelo Sequel analisa automaticamente o esquema do banco de dados e configura todos os métodos de acesso necessários para todas as colunas. Ele assume que o nome da tabela é plural e é uma versão sublinhada do nome do modelo. Caso haja a necessidade de trabalhar com bancos de dados que não seguem esta convenção de nomenclatura, o nome da tabela pode ser definido explicitamente quando o modelo for definido.

 class Player < Sequel::Model(:player) end

Então, agora temos tudo o que precisamos para começar a construir a API de back-end.

Construindo a API

Estrutura do código

Ao contrário do Rails, o Sinatra não impõe nenhuma estrutura de projeto. No entanto, como é sempre uma boa prática organizar o código para facilitar a manutenção e o desenvolvimento, faremos isso aqui também, com a seguinte estrutura de diretórios:

 project root |-config |-helpers |-models |-routes

A configuração do aplicativo será carregada do arquivo de configuração YAML para o ambiente atual com:

 Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")

Por padrão, o valor de Sinatra::Applicationsettings.environment é development, e é alterado configurando a variável de ambiente RACK_ENV .

Além disso, nosso aplicativo deve carregar todos os arquivos dos outros três diretórios. Podemos fazer isso facilmente executando:

 %w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}

À primeira vista, esta forma de carregamento pode parecer conveniente. No entanto, com esta linha do código, não podemos pular arquivos facilmente, porque ela carregará todos os arquivos dos diretórios do array. É por isso que usaremos uma abordagem de carregamento de arquivo único mais eficiente, que assume que em cada pasta temos um arquivo de manifesto init.rb , que carrega todos os outros arquivos do diretório. Além disso, adicionaremos um diretório de destino ao caminho de carregamento do 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

Essa abordagem requer um pouco mais de trabalho porque temos que manter as instruções require em cada arquivo init.rb , mas, em troca, obtemos mais controle e podemos facilmente deixar um ou mais arquivos de fora removendo-os do arquivo init.rb do manifesto no diretório de destino.

Autenticação da API

A primeira coisa que precisamos em cada API é a autenticação. Vamos implementá-lo como um módulo auxiliar. A lógica de autenticação completa estará no arquivo 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

Tudo o que temos a fazer agora é carregar este arquivo adicionando uma instrução require no arquivo de manifesto auxiliar ( helpers/init.rb ) e chamar o método authenticate! método no gancho before do Sinatra que será executado antes de processar qualquer solicitação.

 before do authenticate! end

Base de dados

Em seguida, temos que preparar nosso banco de dados para o aplicativo. Existem muitas maneiras de preparar o banco de dados, mas como estamos usando o Sequel, é natural fazê-lo usando migradores. Sequel vem com dois tipos de migrador - baseado em inteiro e timestamp. Cada um tem suas vantagens e desvantagens. Em nosso exemplo, decidimos usar o timestamp migrator do Sequel, que exige que os arquivos de migração sejam prefixados com um timestamp. O migrador de carimbo de data/hora é muito flexível e pode aceitar vários formatos de carimbo de data/hora, mas usaremos apenas aquele que consiste em ano, mês, dia, hora, minuto e segundo. Aqui estão nossos dois arquivos de migração:

 # 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

Agora estamos prontos para criar um banco de dados com todas as tabelas.

 bundle exec sequel -m db/migrations sqlite://db/development.sqlite3

Finalmente, temos os arquivos de modelo sport.rb e player.rb no diretório de 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

Aqui estamos empregando uma forma Sequel de definir relacionamentos de modelo, onde o objeto Sport tem muitos jogadores e Player pode ter apenas um esporte. Além disso, cada modelo define seu método to_api , que retorna um hash com atributos que precisam ser serializados. Esta é uma abordagem geral que podemos usar para vários formatos. No entanto, se usarmos apenas um formato JSON em nossa API, poderíamos usar o to_json do Ruby com only um argumento para restringir a serialização aos atributos obrigatórios, ou seja, player.to_json(only: [:id, :name, :sport_i]) . Claro, também podemos definir um BaseModel que herda de Sequel::Model e define um método to_api padrão, do qual herdar todos os modelos podem herdar.

Agora, podemos começar a implementar os endpoints reais da API.

Pontos de extremidade da API

Manteremos a definição de todos os endpoints em arquivos dentro do diretório de routes . Como estamos usando arquivos de manifesto para carregar arquivos, agruparemos as rotas por recursos (ou seja, manteremos todas as rotas relacionadas a esportes no arquivo sports.rb , todas as rotas de jogadores em routes.rb e assim por diante).

 # 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

Rotas aninhadas, como aquela para colocar todos os jogadores em um esporte /sports/:id/players , podem ser definidas colocando-as juntas com outras rotas ou criando um arquivo de recurso separado que conterá apenas as rotas aninhadas.

Com rotas designadas, o aplicativo agora está pronto para aceitar solicitações:

 curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'

Observe que, conforme exigido pelo sistema de autenticação do aplicativo definido no arquivo helpers/authentication.rb , estamos passando as credenciais diretamente nos parâmetros de solicitação.

Relacionado: Tutorial Grape Gem: Como construir uma API do tipo REST em Ruby

Conclusão

Os princípios demonstrados neste aplicativo de exemplo simples se aplicam a qualquer aplicativo de back-end de API. Ele não é baseado na arquitetura model-view-controller (MVC), mas mantém uma clara separação de responsabilidades de maneira semelhante; a lógica de negócios completa é mantida em arquivos de modelo enquanto o tratamento de solicitações é feito nos métodos de rotas do Sinatra. Ao contrário da arquitetura MVC, onde as visualizações são usadas para renderizar as respostas, esta aplicação faz isso no mesmo local onde trata as requisições - nos métodos de rotas. Com novos arquivos auxiliares, o aplicativo pode ser facilmente estendido para enviar paginação ou, se necessário, solicitar informações de limites de volta ao usuário nos cabeçalhos de resposta.

No final, construímos uma API completa com um conjunto de ferramentas muito simples e sem perder nenhuma funcionalidade. O número limitado de dependências ajuda a garantir que o aplicativo seja carregado e inicializado muito mais rápido e tenha um espaço de memória muito menor do que um baseado em Rails teria. Portanto, da próxima vez que você começar a trabalhar em uma nova API em Ruby, considere usar Sinatra e Sequel, pois são ferramentas muito poderosas para esse caso de uso.