mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-24 23:06:41 +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 :swagger_rails do
|
||||||
# # Task goes here
|
# # Task goes here
|
||||||
# end
|
# 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
|
class Blog < ActiveRecord::Base
|
||||||
attr_accessible :content, :title
|
attr_accessible :content, :title
|
||||||
|
|
||||||
|
validates :content, presence: true
|
||||||
|
|
||||||
def as_json(options)
|
def as_json(options)
|
||||||
{
|
{
|
||||||
title: title,
|
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.
|
# as the one that triggered the failure.
|
||||||
Kernel.srand config.seed
|
Kernel.srand config.seed
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
require 'swagger_rails/rspec/adapter'
|
||||||
|
config.extend SwaggerRails::RSpec::Adapter
|
||||||
end
|
end
|
||||||
|
|||||||
@ -89,4 +89,7 @@ RSpec.configure do |config|
|
|||||||
# as the one that triggered the failure.
|
# as the one that triggered the failure.
|
||||||
Kernel.srand config.seed
|
Kernel.srand config.seed
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
require 'swagger_rails/rspec/adapter'
|
||||||
|
config.extend SwaggerRails::RSpec::Adapter
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user