Tutorial de Grape Gem: cómo crear una API similar a REST en Ruby
Publicado: 2022-03-11Como desarrolladores de Ruby On Rails, a menudo necesitamos ampliar nuestras aplicaciones con puntos finales de API para admitir clientes de Internet enriquecidos con gran cantidad de JavaScript o aplicaciones nativas de iPhone y Android. También hay algunos casos en los que el único propósito de la aplicación es servir las aplicaciones de iPhone/Android a través de una API JSON.
En este tutorial, demuestro cómo usar Grape, un micromarco de API similar a REST para Ruby, para crear soporte de back-end en Rails para una API JSON. Grape está diseñado para ejecutarse como un motor de rack montable para complementar nuestras aplicaciones web, sin interferir con ellas.
Caso de uso
El caso de uso en el que nos centraremos en este tutorial es una aplicación capaz de capturar y revisar sesiones de programación en pareja. La aplicación en sí se escribirá para iOS en ObjectiveC y deberá comunicarse con un servicio de back-end para almacenar y recuperar los datos. Nuestro enfoque en este tutorial es la creación de un servicio de back-end sólido y seguro que admita una API JSON.
La API admitirá métodos para:
- Iniciar sesión en el sistema
- Consulta de revisiones de sesiones de programación en pareja
NOTA: Además de proporcionar la capacidad de consultar revisiones de sesiones de programación en pareja, la API real también debería proporcionar una función para enviar revisiones de programación en pareja para su inclusión en la base de datos. Dado que admitir eso a través de la API está más allá del alcance de este tutorial, simplemente supondremos que la base de datos se ha llenado con un conjunto de muestra de revisiones de programación de pares.
Los requisitos técnicos clave incluyen:
- Cada llamada API debe devolver JSON válido
- Cada llamada API fallida debe registrarse con el contexto y la información adecuados para que luego sea reproducible y depurada si es necesario.
Además, dado que nuestra aplicación deberá servir a clientes externos, tendremos que preocuparnos por la seguridad. Hacia ese fin:
- Cada solicitud debe estar restringida a un pequeño subconjunto de desarrolladores que rastreamos
- Todas las solicitudes (que no sean de inicio de sesión/registro) deben autenticarse
Desarrollo dirigido por pruebas y RSpec
Usaremos Test Driven Development (TDD) como nuestro enfoque de desarrollo de software para ayudar a garantizar el comportamiento determinista de nuestra API.
Para fines de prueba, utilizaremos RSpec, un marco de prueba bien conocido en la comunidad de RubyOnRails. Por lo tanto, me referiré en este artículo a "especificaciones" en lugar de "pruebas".
Una metodología de prueba integral consta de pruebas "positivas" y "negativas". Las especificaciones negativas especificarán, por ejemplo, cómo se comporta el extremo de la API si faltan algunos parámetros o son incorrectos. Las especificaciones positivas cubren casos en los que la API se invoca correctamente.
Empezando
Sentemos las bases de nuestra API backend. Primero, necesitamos crear una nueva aplicación de rieles:
rails new toptal_grape_blog
A continuación, instalaremos RSpec agregando rspec-rails
a nuestro gemfile:
group :development, :test do gem 'rspec-rails', '~> 3.2' end
Luego desde nuestra línea de comando necesitamos ejecutar:
rails generate rspec:install
También podemos aprovechar algún software de código abierto existente para nuestro marco de prueba. Específicamente:
- Devise : una solución de autenticación flexible para Rails basada en Warden
- factory_girl_rails : proporciona integración de Rails para factory_girl, una biblioteca para configurar objetos de Ruby como datos de prueba
Paso 1: agregue estos en nuestro archivo de gemas:
... gem 'devise' ... group :development, :test do ... gem 'factory_girl_rails', '~> 4.5' ... end
Paso 2: Genere un modelo de usuario, inicialice la devise
del dispositivo y agréguela al modelo de usuario (esto permite que la clase de usuario se use para la autenticación).
rails g model user rails generate devise:install rails generate devise user
Paso 3: Incluya el método de sintaxis factory_girl
en nuestro archivo rails_helper.rb
para usar la versión abreviada de creación de usuarios en nuestras especificaciones:
RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods
Paso 4: Agregue la gema de uva a nuestro DSL e instálela:
gem 'grape' bundle
Caso de uso y especificaciones de inicio de sesión de usuario
Nuestro backend deberá admitir una capacidad básica de inicio de sesión. Vamos a crear el esqueleto para nuestro login_spec
, suponiendo que una solicitud de inicio de sesión válida consiste en una dirección de correo electrónico registrada y un par de contraseña:
require 'rails_helper' describe '/api/login' do context 'negative tests' do context 'missing params' do context 'password' do end context 'email' do end end context 'invalid params' do context 'incorrect password' do end context 'with a non-existent login' do end end end context 'positive tests' do context 'valid params' do end end end
Si falta alguno de los parámetros, el cliente debe recibir un código de estado de retorno HTTP de 400 (es decir, solicitud incorrecta), junto con un mensaje de error de 'falta el correo electrónico' o 'falta la contraseña'.
Para nuestra prueba, crearemos un usuario válido y estableceremos el correo electrónico y la contraseña del usuario como parámetros originales para este conjunto de pruebas. Luego, personalizaremos este hash de parámetro para cada especificación específica omitiendo la contraseña/correo electrónico o anulándola.
Vamos a crear el usuario y el parámetro hash al comienzo de la especificación. Pondremos este código después del bloque de descripción:
describe '/api/login' do let(:email) { user.email } let(:password) { user.password } let!(:user) { create :user } let(:original_params) { { email: email, password: password } } let(:params) { original_params } ...
Luego podemos extender nuestro contexto de 'parámetros faltantes'/'contraseña' de la siguiente manera:
let(:params) { original_params.except(:password) } it_behaves_like '400' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'password is missing'
Pero en lugar de repetir las expectativas en los contextos de 'correo electrónico' y 'contraseña', podemos usar los mismos ejemplos compartidos como expectativas. Para esto, necesitamos descomentar esta línea en nuestro archivo rails_helper.rb
:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
Luego, debemos agregar los 3 ejemplos compartidos de RSpec en spec/support/shared.rb
:
RSpec.shared_examples 'json result' do specify 'returns JSON' do api_call params expect { JSON.parse(response.body) }.not_to raise_error end end RSpec.shared_examples '400' do specify 'returns 400' do api_call params expect(response.status).to eq(400) end end RSpec.shared_examples 'contains error msg' do |msg| specify "error msg is #{msg}" do api_call params json = JSON.parse(response.body) expect(json['error_msg']).to eq(msg) end end
Estos ejemplos compartidos están llamando al método api_call
que nos permite definir el punto final de la API solo una vez en nuestra especificación (de acuerdo con el principio DRY). Definimos este método de la siguiente manera:
describe '/api/login' do ... def api_call *params post "/api/login", *params end ...
También necesitaremos personalizar la fábrica para nuestro usuario:
FactoryGirl.define do factory :user do password "Passw0rd" password_confirmation { |u| u.password } sequence(:email) { |n| "test#{n}@example.com" } end end
Y finalmente, antes de ejecutar nuestras especificaciones, debemos ejecutar nuestras migraciones:
rake db:migrate
Sin embargo, tenga en cuenta que las especificaciones seguirán fallando en este punto, ya que aún no hemos implementado nuestro punto final de API. Eso es lo siguiente.
Implementación del punto final de la API de inicio de sesión
Para empezar, escribiremos un esqueleto vacío para nuestra API de inicio de sesión ( app/api/login.rb
):
class Login < Grape::API format :json desc 'End-points for the Login' namespace :login do desc 'Login via email and password' params do requires :email, type: String, desc: 'email' requires :password, type: String, desc: 'password' end post do end end end
A continuación, escribiremos una clase de agregador que agregue los puntos finales de la API ( app/api/api.rb
):
class API < Grape::API prefix 'api' mount Login end
Bien, ahora podemos montar nuestra API en las rutas:
Rails.application.routes.draw do ... mount API => '/' ... end
Ahora agreguemos el código para verificar los parámetros que faltan. Podemos agregar ese código a api.rb
rescatando de Grape::Exceptions::ValidationErrors
.
rescue_from Grape::Exceptions::ValidationErrors do |e| rack_response({ status: e.status, error_msg: e.message, }.to_json, 400) end
Para la contraseña no válida, verificaremos si el código de respuesta http es 401, lo que significa acceso no autorizado. Agreguemos esto al contexto de 'contraseña incorrecta':
let(:params) { original_params.merge(password: 'invalid') } it_behaves_like '401' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'Bad Authentication Parameters'
Luego, se agrega la misma lógica al contexto 'con un inicio de sesión inexistente' también.
Luego implementamos la lógica que maneja los intentos de autenticación no válidos en nuestro login.rb
de la siguiente manera:
post do user = User.find_by_email params[:email] if user.present? && user.valid_password?(params[:password]) else error_msg = 'Bad Authentication Parameters' error!({ 'error_msg' => error_msg }, 401) end end
En este punto, todas las especificaciones negativas para la API de inicio de sesión se comportarán correctamente, pero aún debemos admitir las especificaciones positivas para nuestra API de inicio de sesión. Nuestra especificación positiva esperará que el punto final devuelva un código de respuesta HTTP de 200 (es decir, éxito) con JSON válido y un token válido:
it_behaves_like '200' it_behaves_like 'json result' specify 'returns the token as part of the response' do api_call params expect(JSON.parse(response.body)['token']).to be_present end
Agreguemos también la expectativa del código de respuesta 200 a spec/support/shared.rb
:
RSpec.shared_examples '200' do specify 'returns 200' do api_call params expect(response.status).to eq(200) end end
En caso de un inicio de sesión exitoso, devolveremos el primer token de autenticación válido junto con el correo electrónico del usuario en este formato:
{'email':<the_email_of_the_user>, 'token':<the users first valid token>}
Si aún no existe tal token, crearemos uno para el usuario actual:
... if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 else ...
Para que esto funcione, necesitaremos una clase AuthenticationToken
que pertenezca al usuario. Generaremos este modelo, luego ejecutaremos la migración correspondiente:
rails g model authentication_token token user:references expires_at:datetime rake db:migrate
También necesitamos agregar la asociación correspondiente a nuestro modelo de usuario:
class User < ActiveRecord::Base has_many :authentication_tokens end
Luego agregaremos el alcance válido a la clase AuthenticationToken
:
class AuthenticationToken < ActiveRecord::Base belongs_to :user validates :token, presence: true scope :valid, -> { where{ (expires_at == nil) | (expires_at > Time.zone.now) } } end
Tenga en cuenta que usamos la sintaxis de Ruby en la instrucción where
. Esto está habilitado por nuestro uso de la gema squeel
que habilita la compatibilidad con la sintaxis de Ruby en las consultas de ActiveRecord.
Para un usuario validado, vamos a crear una entidad a la que nos referiremos como "usuario con entidad de token", aprovechando las características de la gema grape-entity
.
Escribamos la especificación para nuestra entidad y colóquela en el archivo user_with_token_entity_spec.rb
:
require 'rails_helper' describe Entities::UserWithTokenEntity do describe 'fields' do subject(:subject) { Entities::UserWithTokenEntity } specify { expect(subject).to represent(:email)} let!(:token) { create :authentication_token } specify 'presents the first available token' do json = Entities::UserWithTokenEntity.new(token.user).as_json expect(json[:token]).to be_present end end end
A continuación, agreguemos las entidades a user_entity.rb
:
module Entities class UserEntity < Grape::Entity expose :email end end
Y finalmente, agregue otra clase a user_with_token_entity.rb
:
module Entities class UserWithTokenEntity < UserEntity expose :token do |user, options| user.authentication_tokens.valid.first.token end end end
Como no queremos que los tokens sigan siendo válidos indefinidamente, hacemos que caduquen después de un día:
FactoryGirl.define do factory :authentication_token do token "MyString" expires_at 1.day.from_now user end end
Con todo esto hecho, ahora podemos devolver el formato JSON esperado con nuestro UserWithTokenEntity
recién escrito:
... user = User.find_by_email params[:email] if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 present token.user, with: Entities::UserWithTokenEntity else ...
Frio. Ahora se cumplen todas nuestras especificaciones y se admiten los requisitos funcionales del punto final de API de inicio de sesión básico.
Punto final de API de revisión de sesión de programación de pares: Introducción
Nuestro backend deberá permitir que los desarrolladores autorizados que hayan iniciado sesión consulten las revisiones de las sesiones de programación de pares.
Nuestro nuevo extremo de API se montará en /api/pair_programming_session
y devolverá las reseñas que pertenecen a un proyecto. Comencemos escribiendo un esqueleto básico para esta especificación:
require 'rails_helper' describe '/api' do describe '/pair_programming_session' do def api_call *params get '/api/pair_programming_sessions', *params end context 'invalid params' do end context 'valid params' do end end end
También escribiremos un punto final de API vacío correspondiente ( app/api/pair_programming_sessions.rb
):
class PairProgrammingSessions < Grape::API format :json desc 'End-points for the PairProgrammingSessions' namespace :pair_programming_sessions do desc 'Retrieve the pairprogramming sessions' params do requires :token, type: String, desc: 'email' end get do end end end
Luego, montemos nuestra nueva API ( app/api/api.rb
):
... mount Login mount PairProgrammingSessions end
Ampliemos la especificación y el extremo de la API contra los requisitos uno por uno.
Extremo de API de revisión de sesión de programación de pares: validación
Uno de nuestros requisitos de seguridad no funcionales más importantes fue restringir el acceso a la API a un pequeño subconjunto de desarrolladores que rastreamos, así que especifiquemos que:

... def api_call *params get '/api/pair_programming_sessions', *params end let(:token) { create :authentication_token } let(:original_params) { { token: token.token} } let(:params) { original_params } it_behaves_like 'restricted for developers' context 'invalid params' do ...
Luego, crearemos un shared_example
en nuestro shared.rb
para confirmar que la solicitud proviene de uno de nuestros desarrolladores registrados:
RSpec.shared_examples 'restricted for developers' do context 'without developer key' do specify 'should be an unauthorized call' do api_call params expect(response.status).to eq(401) end specify 'error code is 1001' do api_call params json = JSON.parse(response.body) expect(json['error_code']).to eq(ErrorCodes::DEVELOPER_KEY_MISSING) end end end
También necesitaremos crear una clase ErrorCodes
(en app/models/error_codes.rb
):
module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end
Dado que esperamos que nuestra API se expanda en el futuro, vamos a implementar una authorization_helper
que se puede reutilizar en todos los puntos finales de la API en la aplicación para restringir el acceso solo a los desarrolladores registrados:
class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers }
Vamos a definir el método restrict_access_to_developers
en el módulo ApiHelpers::AuthenticationHerlper
( app/api/api_helpers/authentication_helper.rb
). Este método simplemente verificará si la Authorization
clave debajo de los encabezados contiene una ApiKey
válida. (Cada desarrollador que desee acceder a la API requerirá una ApiKey
válida. Esto podría proporcionarlo un administrador del sistema o mediante algún proceso de registro automatizado, pero ese mecanismo está más allá del alcance de este artículo).
module ApiHelpers module AuthenticationHelper def restrict_access_to_developers header_token = headers['Authorization'] key = ApiKey.where{ token == my{ header_token } } Rails.logger.info "API call: #{headers}\tWith params: #{params.inspect}" if ENV['DEBUG'] if key.blank? error_code = ErrorCodes::DEVELOPER_KEY_MISSING error_msg = 'please aquire a developer key' error!({ :error_msg => error_msg, :error_code => error_code }, 401) # LogAudit.new({env:env}).execute end end end end
Luego necesitamos generar el modelo ApiKey y ejecutar las migraciones: rails g model api_key token rake db:migrate
Con esto hecho, en nuestro spec/api/pair_programming_spec.rb
podemos verificar si el usuario está autenticado:
... it_behaves_like 'restricted for developers' it_behaves_like 'unauthenticated' ...
También definamos un ejemplo compartido unauthenticated
que se puede reutilizar en todas las especificaciones ( spec/support/shared.rb
):
RSpec.shared_examples 'unauthenticated' do context 'unauthenticated' do specify 'returns 401 without token' do api_call params.except(:token), developer_header expect(response.status).to eq(401) end specify 'returns JSON' do api_call params.except(:token), developer_header json = JSON.parse(response.body) end end end
Este ejemplo compartido necesita el token en el encabezado del desarrollador, así que agreguemos eso a nuestra especificación ( spec/api/pair_programming_spec.rb
):
... describe '/api' do let(:api_key) { create :api_key } let(:developer_header) { {'Authorization' => api_key.token} } ...
Ahora, en nuestra app/api/pair_programming_session.rb
, intentemos autenticar al usuario:
... class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers } before { authenticate! } ...
¡Implementemos la authenticate!
método en AuthenticationHelper
( app/api/api_helpers/authentication_helper.rb
):
... module ApiHelpers module AuthenticationHelper TOKEN_PARAM_NAME = :token def token_value_from_request(token_param = TOKEN_PARAM_NAME) params[token_param] end def current_user token = AuthenticationToken.find_by_token(token_value_from_request) return nil unless token.present? @current_user ||= token.user end def signed_in? !!current_user end def authenticate! unless signed_in? AuditLog.create data: 'unauthenticated user access' error!({ :error_msg => "authentication_error", :error_code => ErrorCodes::BAD_AUTHENTICATION_PARAMS }, 401) end end ...
(Tenga en cuenta que debemos agregar el código de error BAD_AUTHENTICATION_PARAMS
a nuestra clase ErrorCodes
).
A continuación, especifiquemos qué sucede si el desarrollador llama a la API con un token no válido. En ese caso, el código de retorno será 401, lo que indica un "acceso no autorizado". El resultado debe ser JSON y se debe crear un auditable. Entonces agregamos esto a spec/api/pair_programming_spec.rb
:
... context 'invalid params' do context 'incorrect token' do let(:params) { original_params.merge(token: 'invalid') } it_behaves_like '401' it_behaves_like 'json result' it_behaves_like 'auditable created' it_behaves_like 'contains error msg', 'authentication_error' it_behaves_like 'contains error code', ErrorCodes::BAD_AUTHENTICATION_PARAMS end end ...
Agregaremos los ejemplos compartidos "auditable creado", "contiene código de error" y "contiene mensaje de error" a spec/support/shared.rb
:
... RSpec.shared_examples 'contains error code' do |code| specify "error code is #{code}" do api_call params, developer_header json = JSON.parse(response.body) expect(json['error_code']).to eq(code) end end RSpec.shared_examples 'contains error msg' do |msg| specify "error msg is #{msg}" do api_call params, developer_header json = JSON.parse(response.body) expect(json['error_msg']).to eq(msg) end end RSpec.shared_examples 'auditable created' do specify 'creates an api call audit' do expect do api_call params, developer_header end.to change{ AuditLog.count }.by(1) end end ...
También necesitamos crear un modelo audit_log:
rails g model audit_log backtrace data user:references rake db:migrate
Punto final de API de revisión de sesión de programación de pares: resultados devueltos
Para un usuario autenticado y autorizado, una llamada a este extremo de la API debe devolver un conjunto de revisiones de sesiones de programación de pares agrupadas por proyecto. Modifiquemos nuestro spec/api/pair_programming_spec.rb
consecuencia:
... context 'valid params' do it_behaves_like '200' it_behaves_like 'json result' end ...
Esto especifica que una solicitud enviada con api_key
válida y parámetros válidos devuelve un código HTTP de 200 (es decir, éxito) y que el resultado se devuelve en forma de JSON válido.
Vamos a consultar y luego devolver en formato JSON aquellas sesiones de programación en pareja donde cualquiera de los participantes es el usuario actual ( app/api/pair_programming_sessions.rb
):
... get do sessions = PairProgrammingSession.where{(host_user == my{current_user}) | (visitor_user == my{current_user})} sessions = sessions.includes(:project, :host_user, :visitor_user, reviews: [:code_samples, :user] ) present sessions, with: Entities::PairProgrammingSessionsEntity end ...
Las sesiones de programación en pareja se modelan de la siguiente manera en la base de datos:
- Relación uno a muchos entre proyectos y sesiones de programación en pareja
- Relación de 1 a muchos entre las sesiones de programación en pareja y las revisiones
- Relación de 1 a muchos entre reseñas y muestras de código
Generemos los modelos en consecuencia y luego ejecutemos las migraciones:
rails g model project name rails g model pair_programming_session project:references host_user:references visitor_user:references rails g model review pair_programming_session:references user:references comment rails g model code_sample review:references code:text rake db:migrate
Luego necesitamos modificar nuestras clases PairProgrammingSession
y Review
para que contengan las asociaciones has_many
:
class Review < ActiveRecord::Base belongs_to :pair_programming_session belongs_to :user has_many :code_samples end class PairProgrammingSession < ActiveRecord::Base belongs_to :project belongs_to :host_user, class_name: :User belongs_to :visitor_user, class_name: 'User' has_many :reviews end
NOTA: En circunstancias normales, generaría estas clases escribiendo especificaciones para ellas primero, pero dado que eso está más allá del alcance de este artículo, omitiré ese paso.
Ahora necesitamos escribir esas clases que van a transformar nuestros modelos en sus representaciones JSON (denominadas entidades de uva en la terminología de uva). Para simplificar, utilizaremos un mapeo 1 a 1 entre los modelos y las entidades de uva.
Comenzamos exponiendo el campo de code
de CodeSampleEntity
(en api/entities/code_sample_entity.rb
):
module Entities class CodeSampleEntity < Grape::Entity expose :code end end
Luego exponemos el user
y code_samples
asociados reutilizando UserEntity
y CodeSampleEntity
ya definidos:
module Entities class ReviewEntity < Grape::Entity expose :user, using: UserEntity expose :code_samples, using: CodeSampleEntity end end
También exponemos el campo de name
de ProjectEntity
:
module Entities class ProjectEntity < Grape::Entity expose :name end end
Finalmente, ensamblamos la entidad en una nueva PairProgrammingSessionsEntity
donde exponemos el project
, el host_user
, el visitor_user
y las reviews
:
module Entities class PairProgrammingSessionsEntity < Grape::Entity expose :project, using: ProjectEntity expose :host_user, using: UserEntity expose :visitor_user, using: UserEntity expose :reviews, using: ReviewEntity end end
¡Y con eso, nuestra API está completamente implementada!
Generación de datos de prueba
Para fines de prueba, crearemos algunos datos de muestra en db/seeds.rb
. Este archivo debe contener toda la creación de registros necesaria para inicializar la base de datos con sus valores predeterminados. Luego, los datos pueden cargarse con rake db:seed
(o crearse con db cuando se invoca db:setup
). He aquí un ejemplo de lo que esto podría incluir:
user_1 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' user_2 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' user_3 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' ApiKey.create token: '12345654321' project_1 = Project.create name: 'Time Sheets' project_2 = Project.create name: 'Toptal Blog' project_3 = Project.create name: 'Hobby Project' session_1 = PairProgrammingSession.create project: project_1, host_user: user_1, visitor_user: user_2 session_2 = PairProgrammingSession.create project: project_2, host_user: user_1, visitor_user: user_3 session_3 = PairProgrammingSession.create project: project_3, host_user: user_2, visitor_user: user_3 review_1 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your code' review_2 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your specs' review_3 = session_2.reviews.create user: user_1, comment: 'Please DRY your view templates' review_4 = session_2.reviews.create user: user_1, comment: 'Please clean your N+1 queries' review_1.code_samples.create code: 'Lorem Ipsum' review_1.code_samples.create code: 'Do not abuse the single responsibility principle' review_2.code_samples.create code: 'Use some shared examples' review_2.code_samples.create code: 'Use at the beginning of specs'
Ahora nuestra aplicación está lista para usar y podemos lanzar nuestro servidor Rails.
Probando la API
Usaremos Swagger para hacer algunas pruebas manuales basadas en el navegador de nuestra API. Sin embargo, se requieren algunos pasos de configuración para que podamos usar Swagger.
Primero, necesitamos agregar un par de gemas a nuestro archivo de gemas:
... gem 'grape-swagger' gem 'grape-swagger-ui' ...
Luego ejecutamos bundle
para instalar estas gemas.
También debemos agregarlos a los activos de nuestra canalización de activos (en config/initializers/assets.rb
):
Rails.application.config.assets.precompile += %w( swagger_ui.js ) Rails.application.config.assets.precompile += %w( swagger_ui.css )
Finalmente, en app/api/api.rb
necesitamos montar el generador de swagger:
... add_swagger_documentation end ...
Ahora podemos aprovechar la agradable interfaz de usuario de Swagger para explorar nuestra API con solo ir a http://localhost:3000/api/swagger
.
Swagger presenta nuestros puntos finales de API de una manera agradablemente explorable. Si hacemos clic en un punto final, Swagger enumera sus operaciones. Si hacemos clic en una operación, Swagger muestra sus parámetros obligatorios y opcionales y sus tipos de datos.
Sin embargo, queda un detalle antes de continuar: dado que restringimos el uso de los desarrolladores de la API con una api_key
válida, no podremos acceder al extremo de la API directamente desde el navegador porque el servidor requerirá una api_key
válida en el encabezado HTTP. Podemos lograr esto con fines de prueba en Google Chrome haciendo uso del complemento Modificar encabezados para Google Chrome. Este complemento nos permitirá editar el encabezado HTTP y agregar una api_key
válida (usaremos la api_key
ficticia de 12345654321
que incluimos en el archivo semilla de nuestra base de datos).
Bien, ¡ahora estamos listos para probar!
Para llamar al extremo de la API de pair_programming_sessions
, primero debemos iniciar sesión. Solo usaremos las combinaciones de correo electrónico y contraseña de nuestro archivo semilla de la base de datos y lo enviaremos a través de Swagger al extremo de la API de inicio de sesión, como se muestra a continuación.
Como puede ver arriba, se devuelve el token que pertenece a ese usuario, lo que indica que la API de inicio de sesión funciona correctamente según lo previsto. Ahora podemos usar ese token para realizar con éxito la operación GET /api/pair_programming_sessions.json
.
Como se muestra, el resultado se devuelve como un objeto JSON jerárquico con el formato correcto. Tenga en cuenta que la estructura JSON refleja dos asociaciones anidadas de 1 a muchos, ya que el proyecto tiene varias revisiones y una revisión tiene varias muestras de código. Si no devolviéramos la estructura de esta manera, la persona que llama a nuestra API tendría que solicitar por separado las revisiones de cada proyecto, lo que requeriría enviar N consultas a nuestro punto final de API. Con esta estructura, por lo tanto, resolvemos el problema de rendimiento de la consulta N+1.
Envolver
Como se muestra en este documento, las especificaciones integrales para su API ayudan a garantizar que la API implementada aborde de manera adecuada y adecuada los casos de uso previstos (¡y no previstos!).
Si bien la API de ejemplo presentada en este tutorial es bastante básica, el enfoque y las técnicas que hemos demostrado pueden servir como base para API más sofisticadas de complejidad arbitraria utilizando la gema Grape. Esperamos que este tutorial haya demostrado que Grape es una gema útil y flexible que puede ayudar a facilitar la implementación de una API JSON en sus aplicaciones Rails. ¡Disfrutar!