Weasel-Diesel
June 18, 2012
Alguns dias atrás Matt Aimonetti escreveu em seu blog um artigo sobre repensar o desenvolvimento de web services, antes de continuar lendo esse post seria interessante ler o dele.
Não vou rescrever o que ele já escreveu, vou abordar mais a parte técnica da proposta dele, o Weasel-Diesel. A primeira vista quando olhei o código da DSL pensei:
Que sintaxe horrível, como isso pode ser usável?
Mas essa normalmente é a primeira reação quando vemos algo que foge do que estamos acostumados, até das estranhesas que vemos no nosso dia-a-dia. Então deixei a idéia amadurecer, ou melhor, deixei estar, voltei para minhas atividades diárias. A noite, voltei para o artigo do Matt, li ele com mais calma e resolvi olhar o fonte, e a aplicação sinatra de exemplo disponível, então pensei em como aplicar o conceito do Weasel-Diesel em uma aplicação Rails.
Weasel-Diesel
Se você ainda não leu sobre o Weasel-Diesel no github ou no artigo do Matt, vou explicar um pouco o que ele é, se você já leu e sabe o que ele é, pode pular essa seção.
Weasel-Diesel é uma DSL para descrever serviços web, a intenção dele é ser simplista e desacoplado, ou seja, você pode usar seu framework favorito, o objetivo é prover somente uma forma de escrever o que cada serviço deve ser, qual seu input e output, e principalmente, a documentação desse serviço.
Por que a documentação é importante? Porque dificilmente você cria um serviço que ninguém poderá usar, e normalmente quem irá usar são outras pessoas, quando o serviço é pra você mesmo, ou para seus próprios projetos, não existe necessidade de documentação. Mas na maioria dos casos documentação importa e importa muito.
E a implementação? o Weasel-Diesel não tem a implementação do serviço, cabe a você escrevê-la do jeito que achar melhor e da forma que achar melhor, nas próximas seções vou mostrar alguns exemplos de como usar esse descritivo do Weasel-Diesel para validar os testes da sua implementação.
Integrando com Rails
Como a documentação do WD só mostra exemplos usando Sinatra, resolvi fazer um teste usando o Rails. Adicionar ao projeto é super simples, basta abrir seu Gemfile é adicionar:
gem 'weasel_diesel'
rodar o bundle, e pronto, já temos disponível em qualquer parte da nossa app o helper describe_service, sem arquivos de inicialização, ou qualquer configuração.
Criando um serviço
Nosso serviço vai ser simples, recuperar os produtos cadastrados. A nossa url vai ser /products e vamos simplesmente enviar um array json contendo o nome e o valor de cada produto cadastrado, a definição será algo assim:
describe_service '/products' do |service|
service.formats :json
service.http_verb :get
service.disable_auth
service.response do |response|
response.array :products do |node|
node.string :name
node.string :amount
end
end
service.documentation do |doc|
doc.overall <<-DOC
Consulta a lista de produtos cadastrados
DOC
doc.example <<-DOC
## USAGE
curl -i -H "Accept: application/json" http://localhost:3000/products
DOC
end
end
Vamos as explicações, primeiro passo é dizer qual serviço estamos descrevendo, nesse caso é /products, dizemos que o formato dele é json, o verbo http é get, e desabilitamos a autenticação, lembrando que isso é só descritivo, não quer dizer que temos a autenticação habilitada realmente. Depois definimos qual vai ser a resposta do serviço, que vai ser um array, e cada elemento desse array vai ter o nome e o valor, ambos do tipo string.
Por ultimo, mas não menos importante, definimos a documentação, dizemos o que esse serviço é e como pode ser usado, nesse caso usando curl. Pronto, nosso serviço está definido, o que precisamos é implementá-lo.
Implementando o serviço
Não vou entrar nos detalhes do nosso modelo por hora, o que nos interessa é a implementação do controller:
class ProductsController < ApplicationController
respond_to :json
def index
@products = Product.all
respond_with({:products => @products})
end
end
Esse código é bem simples, definimos um controller que responde a json, e simplesmente vai renderizar os produtos como json. Vamos escrever uma spec de request para testar a interação do usuário com o nosso serviço e ver se está tudo certo, vou usar o RSpec e Capybara pra escrever esse teste.
require 'spec_helper'
feature 'Product API' do
include Rack::Test::Methods
before do
[
{:name => "Macbook Pro",:amount => 2999},
{:name => "Macbook Air", :amount => 3299},
{:name => 'Ultrabook HP', :amount => 1799}
].each do |attributes|
Product.create attributes
end
end
scenario 'list all the products' do
header 'Accept', 'application/json'
result = get '/products'
service = WSList.all.find{|s| s.verb == :get && s.url == "/products"}
valid, errors = service.validate_hash_response(JSON(result.body))
assert valid, errors.join(" & ")
end
end
Explicando o código: primeiro o include a Rack::Test::Methods incluindo isso no nosso teste temos alguns helpers para fazer requests no nosso próprio domínio, inclusive podemos definir o header da request que estamos fazendo, veja mais sobre isso aqui.
o bloco before simplesmente inserimos alguns registros na base, e depois escrevemos nosso scenario de teste, reparem que defino no header que essa request aceita json, dessa forma não preciso escrever a url como /products.json. e fazemos uma requisição get a nossa url e recuperamos o resultado.
Precisamos encontrar a definição desse serviço para podermos testar nossa resposta, o WD disponibiliza uma classe WSList, aonde você consegue ter todos os serviços definidos, um simples find pra comparar o verbo e a url e temos nossa definição do serviço. Chamamos o método auxiliar validate_hash_response e passamos o resultado da nossa requisição, ele vai retornar um boolean, e um array de strings contento os erros caso tenha algum.
Também não podemos deixar de lado alguns ajustes a serem feitos no nosso spec/spec_helper.rb:
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
require 'json_response_verification'
require 'params_verification'
WeaselDiesel.send(:include, JSONResponseVerification)
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
Dir[Rails.root.join("app/api/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.infer_base_class_for_anonymous_controllers = false
end
O detalhe fica nos requires de json_response_verification e de params_verification, ambos disponibilizados pelo WD, e incluimos o módulo JSONResponseVerification no WeaselDiesasel, assim temos o metodo validate_hash_response disponível para os testes.
o assert no RSpec é disponibilizado pelo minitest, o que é algo que destoa um pouco do nosso teste, vamos criar um matcher pra resolver esse problema.
Extendendo o RSpec
Para extender ao RSpec precisamos criar um novo matcher, já que nenhum dos padrões do RSpec vão fazer o que o WD faz, validar o json de resultado de acordo com o que foi descrito do serviço. Criar um matcher pro RSpec é relativamente simples, o que precisamos é definir uma classe que vai trabalhar com essa validação e um metodo para chamar no assert do RSpec:
class BeApiResponse
def initialize verb, url
@service = WSList.all.find{|s| s.verb == verb.to_sym && s.url == "#{url}"}
raise "API method [#{verb}] #{url} not found" unless @service
end
def matches? response
@valid, @errors = @service.validate_hash_response(JSON(response.body))
@valid
end
def description
"be api response for [#{@service.verb}] #{@service.url}"
end
def failure_message
@errors.join(' & ')
end
end
def have_api_response verb, url
BeApiResponse.new(verb, url)
end
a classe que foi criada recebe dois parametros o verbo http e a url que estamos buscando, a responsabilidade de encontrar o serviço ficou a cargo dessa classe, que vai simplemente mandar uma mensagem de erro caso não encontre o verbo para a url informada. O truque vem no metodo matches? ele recebe um objeto e valida ele usando a validação do Weasel-Dieasel, e retorna se o elemento é válido ou não para a API, e a mensagem de falha são os erros concatenados.
por ultimo definimos o método que vamos usar pra fazer o matchers dentro do nosso teste, que vai receber o verbo e a url. E agora refatoramos nosso scenario para ficar assim:
scenario 'list all the products' do
header 'Accept', 'application/json'
result = get '/products'
result.should have_api_response(:get, '/products')
end
podemos reaproveitar o código em todos os testes.
Conclusão
Apesar de em uma primeira olhada achar o Weasel-Diesel um tanto estranho, depois que você começa a fazer alguns testes e a trabalhar com ele você passa a ver ele com outros olhos, aproveitei esse espaço para escrever sobre o uso do WD para testar usa aplicação, mas você também pode usá-lo para gerar a documentação, baixe o codigo de exemplo usado aqui e execute o comando:
rake doc:services
e você verá uma simples documentação gerada, baseada nos serviços descritos. O código usado para gerar é o mesmo da aplicação de exemplo disponibilizada pelo Matt Aimonetti, com algumas alterações pro nosso exemplo.
Se você escreve serviços usando Ruby, talvez valha a pena dar uma olhada no Weasel-Dielsel.