mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-22 22:06:43 +00:00
wip:swagger-based dsl for rspec
This commit is contained in:
parent
fc877b4047
commit
d579dab7d8
62
lib/swagger_rails/rspec/adapter.rb
Normal file
62
lib/swagger_rails/rspec/adapter.rb
Normal 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
|
||||
|
||||
37
lib/swagger_rails/rspec/formatter.rb
Normal file
37
lib/swagger_rails/rspec/formatter.rb
Normal 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
|
||||
86
lib/swagger_rails/test_visitor.rb
Normal file
86
lib/swagger_rails/test_visitor.rb
Normal 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
|
||||
#
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
class Blog < ActiveRecord::Base
|
||||
attr_accessible :content, :title
|
||||
|
||||
validates :content, presence: true
|
||||
|
||||
def as_json(options)
|
||||
{
|
||||
title: title,
|
||||
|
||||
45
spec/dummy/spec/integration/blogs_spec.rb
Normal file
45
spec/dummy/spec/integration/blogs_spec.rb
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user