wip:swagger-based dsl for rspec

This commit is contained in:
domaindrivendev 2016-03-09 16:58:38 -08:00
parent fc877b4047
commit d579dab7d8
12 changed files with 246 additions and 195 deletions

View File

@ -0,0 +1,62 @@
require 'swagger_rails/test_visitor'
module SwaggerRails
module RSpec
module Adapter
def path(path_template, &block)
describe(path_template, path_template: path_template, &block)
end
def operation(method, summary=nil, &block)
operation_metadata = {
method: method,
summary: summary,
parameters: []
}
describe(method, operation: operation_metadata, &block)
end
def consumes(*mime_types)
metadata[:operation][:consumes] = mime_types
end
def produces(*mime_types)
metadata[:operation][:produces] = mime_types
end
def header(name, attributes={})
parameter(name, 'header', attributes)
end
def body(name, attributes={})
parameter(name, 'body', attributes)
end
def parameter(name, location, attributes={})
parameter_metadata = { name: name.to_s, in: location }.merge(attributes)
metadata[:operation][:parameters] << parameter_metadata
end
def response(status, description, &block)
response_metadata = { status: status, description: description }
context(description, response: response_metadata, &block)
end
def run_test!
before do |example|
SwaggerRails::TestVisitor.instance.act!(
self, example.metadata[:path_template], example.metadata[:operation]
)
end
it "returns a #{metadata[:response][:status]} status" do |example|
SwaggerRails::TestVisitor.instance.assert!(
self, example.metadata[:response]
)
end
end
end
end
end

View File

@ -0,0 +1,37 @@
require 'rspec/core/formatters/base_text_formatter'
module SwaggerRails
module RSpec
class Formatter
::RSpec::Core::Formatters.register self,
:example_group_started,
:example_group_finished,
:stop
def initialize(output)
@output = output
@swagger_docs = {}
@group_level = 0
@output.puts 'Generating Swagger Docs ...'
end
def example_group_started(notification)
@group_level += 1
group = notification.group
metadata = group.metadata
@output.puts "group_level: #{@group_level}"
@output.puts metadata.slice(:doc, :path_template, :operation, :response).inspect
end
def example_group_finished(notification)
@group_level -= 1
end
def stop(notification)
end
end
end
end

View File

@ -0,0 +1,86 @@
require 'singleton'
module SwaggerRails
class TestVisitor
include Singleton
def act!(test, path_template, operation)
params_data = params_data_for(test, operation[:parameters])
path = build_path(path_template, params_data)
body_or_params = build_body_or_params(params_data)
headers = build_headers(params_data, operation[:consumes], operation[:produces])
test.send(operation[:method], path, body_or_params, headers)
end
def assert!(test, expected_response)
test.assert_response(expected_response[:status].to_i)
end
private
def params_data_for(test, parameters)
parameters.map do |parameter|
parameter
.slice(:name, :in)
.merge(value: test.send(parameter[:name].to_s.underscore))
end
end
def build_path(path_template, params_data)
path = path_template.dup
params_data.each do |param_data|
path.sub!("\{#{param_data[:name]}\}", param_data[:value].to_s)
end
return path
end
def build_body_or_params(params_data)
body_params_data = params_data.select { |p| p[:in] == 'body' }
return body_params_data.first[:value].to_json if body_params_data.any?
query_params_data = params_data.select { |p| p[:in] == 'query' }
Hash[query_params_data.map { |p| [ p[:name], p[:value] ] }]
end
def build_headers(params_data, consumes, produces)
header_params_data = params_data.select { |p| p[:in] == 'header' }
headers = Hash[header_params_data.map { |p| [ p[:name].underscore.upcase, p[:value] ] }]
headers['ACCEPT'] = consumes.join(';') if consumes.present?
headers['CONTENT_TYPE'] = produces.join(';') if produces.present?
return headers
end
end
end
#require 'swagger_rails/testing/test_case_builder'
#
#module SwaggerRails
#
# class TestVisitor
#
# def initialize(swagger)
# @swagger = swagger
# end
#
# def run_test(path_template, http_method, test, &block)
# builder = TestCaseBuilder.new(path_template, http_method, @swagger)
# builder.instance_exec(&block) if block_given?
# test_data = builder.test_data
#
# test.send(http_method,
# test_data[:path],
# test_data[:params],
# test_data[:headers]
# )
#
# test.assert_response(test_data[:expected_response][:status])
# test.assert_equal(test_data[:expected_response][:body], JSON.parse(test.response.body))
# end
# end
#end
#

View File

@ -1,115 +0,0 @@
module SwaggerRails
class TestCaseBuilder
def initialize(path_template, http_method, swagger)
@path_template = path_template
@http_method = http_method
@swagger = swagger
@param_values = {}
end
def set(param_values)
@param_values.merge!(param_values.stringify_keys)
end
def expect(status)
@expected_status = status.to_s
end
def test_data
operation = find_operation!
parameters = operation['parameters'] || []
responses = operation['responses']
{
path: build_path(parameters),
params: build_params(parameters),
headers: build_headers(parameters),
expected_response: build_expected_response(responses)
}
end
private
def find_operation!
keys = [ 'paths', @path_template, @http_method ]
operation = find_hash_item!(@swagger, keys)
operation || (raise MetadataError.new(keys))
end
def find_hash_item!(hash, keys)
item = hash[keys[0]] || (return nil)
keys.length == 1 ? item : find_hash_item!(item, keys.drop(1))
end
def build_path(parameters)
param_values = param_values_for(parameters, 'path')
@path_template.dup.tap do |template|
template.prepend(@swagger['basePath'].presence || '')
param_values.each { |name, value| template.sub!("\{#{name}\}", value.to_s) }
end
end
def build_params(parameters)
body_param_values = param_values_for(parameters, 'body')
return body_param_values.values.first.to_json if body_param_values.any?
param_values_for(parameters, 'query')
end
def build_headers(parameters)
param_values_for(parameters, 'header')
.merge({
'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json'
})
end
def build_expected_response(responses)
status = @expected_status || responses.keys.find { |k| k.start_with?('2') }
response = responses[status] || (raise MetadataError.new('paths', @path_template, @http_method, 'responses', status))
{
status: status.to_i,
body: response_body_for(response)
}
end
def param_values_for(parameters, location)
applicable_parameters = parameters.select { |p| p['in'] == location }
Hash[applicable_parameters.map { |p| [ p['name'], param_value_for(p) ] }]
end
def param_value_for(parameter)
return @param_values[parameter['name']] if @param_values.has_key?(parameter['name'])
return parameter['default'] unless parameter['in'] == 'body'
schema = schema_for(parameter['schema'])
schema_example_for(schema)
end
def response_body_for(response)
return nil if response['schema'].nil?
schema = schema_for(response['schema'])
schema_example_for(schema)
end
def schema_for(schema_or_ref)
return schema_or_ref if schema_or_ref['$ref'].nil?
@swagger['definitions'][schema_or_ref['$ref'].sub('#/definitions/', '')]
end
def schema_example_for(schema)
return schema['example'] if schema['example'].present?
# If an array, try construct from the item example
if schema['type'] == 'array' && schema['item'].present?
item_schema = schema_for(schema['item'])
return [ schema_example_for(item_schema) ]
end
end
end
class MetadataError < StandardError
def initialize(*path_keys)
path = path_keys.map { |key| "['#{key}']" }.join('')
super("Swagger document is missing expected metadata at #{path}")
end
end
end

View File

@ -1,34 +0,0 @@
require 'swagger_rails/testing/test_visitor'
module SwaggerRails
module TestHelpers
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
attr_reader :test_visitor
def swagger_doc(swagger_doc)
file_path = File.join(Rails.root, 'config/swagger', swagger_doc)
@swagger = JSON.parse(File.read(file_path))
@test_visitor = SwaggerRails::TestVisitor.new(@swagger)
end
def swagger_test_all
@swagger['paths'].each do |path, path_item|
path_item.keys.each do |method|
test "#{path} #{method}" do
swagger_test path, method
end
end
end
end
end
def swagger_test(path, method, &block)
self.class.test_visitor.run_test(path, method, self, &block)
end
end
end

View File

@ -1,27 +0,0 @@
require 'swagger_rails/testing/test_case_builder'
module SwaggerRails
class TestVisitor
def initialize(swagger)
@swagger = swagger
end
def run_test(path_template, http_method, test, &block)
builder = TestCaseBuilder.new(path_template, http_method, @swagger)
builder.instance_exec(&block) if block_given?
test_data = builder.test_data
test.send(http_method,
test_data[:path],
test_data[:params],
test_data[:headers]
)
test.assert_response(test_data[:expected_response][:status])
test.assert_equal(test_data[:expected_response][:body], JSON.parse(test.response.body))
end
end
end

View File

@ -2,3 +2,11 @@
# task :swagger_rails do
# # Task goes here
# end
require 'rspec/core/rake_task'
desc 'Generate Swagger JSON files from integration specs'
RSpec::Core::RakeTask.new('swagger_rails:gen') do |t|
t.pattern = 'spec/integration/**/*_spec.rb'
t.rspec_opts = [ '--format SwaggerRails::RSpec::SwaggerFormatter' ]
end

View File

@ -1,6 +1,8 @@
class Blog < ActiveRecord::Base
attr_accessible :content, :title
validates :content, presence: true
def as_json(options)
{
title: title,

View File

@ -0,0 +1,45 @@
require 'rails_helper'
describe 'Blogs API', doc: 'blogs/v1' do
path '/blogs' do
operation 'post', 'creates a new blog' do
consumes 'application/json'
produces 'application/json'
body :blog
let(:blog) { { title: 'foo', content: 'bar' } }
response '201', 'valid request' do
run_test!
end
response '422', 'invalid request' do
let(:blog) { { title: 'foo' } }
run_test!
end
end
operation 'get', 'searches existing blogs' do
produces 'application/json'
response '200', 'valid request' do
run_test!
end
end
end
path '/blogs/{id}' do
operation 'get', 'retreives a specific blog' do
produces 'application/json'
parameter :id, 'path'
response '200', 'blog found' do
let(:blog) { Blog.create(title: 'foo', content: 'bar') }
let(:id) { blog.id }
run_test!
end
end
end
end

View File

@ -1,19 +0,0 @@
require 'rails_helper'
require 'swagger_rails/testing/test_helpers'
describe 'V1 Contract' do
include SwaggerRails::TestHelpers
swagger_doc 'v1/swagger.json'
# TODO: improve DSL
it 'exposes an API for managing blogs' do
swagger_test '/blogs', 'post'
swagger_test '/blogs', 'get'
swagger_test '/blogs/{id}', 'get' do
set id: Blog.last!.id
end
end
end

View File

@ -89,4 +89,7 @@ RSpec.configure do |config|
# as the one that triggered the failure.
Kernel.srand config.seed
=end
require 'swagger_rails/rspec/adapter'
config.extend SwaggerRails::RSpec::Adapter
end

View File

@ -89,4 +89,7 @@ RSpec.configure do |config|
# as the one that triggered the failure.
Kernel.srand config.seed
=end
require 'swagger_rails/rspec/adapter'
config.extend SwaggerRails::RSpec::Adapter
end