diff --git a/lib/swagger_rails/rspec/adapter.rb b/lib/swagger_rails/rspec/adapter.rb new file mode 100644 index 0000000..7d315a0 --- /dev/null +++ b/lib/swagger_rails/rspec/adapter.rb @@ -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 + diff --git a/lib/swagger_rails/rspec/formatter.rb b/lib/swagger_rails/rspec/formatter.rb new file mode 100644 index 0000000..ce58cd0 --- /dev/null +++ b/lib/swagger_rails/rspec/formatter.rb @@ -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 diff --git a/lib/swagger_rails/test_visitor.rb b/lib/swagger_rails/test_visitor.rb new file mode 100644 index 0000000..2f546a2 --- /dev/null +++ b/lib/swagger_rails/test_visitor.rb @@ -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 +# diff --git a/lib/swagger_rails/testing/test_case_builder.rb b/lib/swagger_rails/testing/test_case_builder.rb deleted file mode 100644 index 775fd5a..0000000 --- a/lib/swagger_rails/testing/test_case_builder.rb +++ /dev/null @@ -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 diff --git a/lib/swagger_rails/testing/test_helpers.rb b/lib/swagger_rails/testing/test_helpers.rb deleted file mode 100644 index 086c852..0000000 --- a/lib/swagger_rails/testing/test_helpers.rb +++ /dev/null @@ -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 diff --git a/lib/swagger_rails/testing/test_visitor.rb b/lib/swagger_rails/testing/test_visitor.rb deleted file mode 100644 index 9603e1c..0000000 --- a/lib/swagger_rails/testing/test_visitor.rb +++ /dev/null @@ -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 - diff --git a/lib/tasks/swagger_rails_tasks.rake b/lib/tasks/swagger_rails_tasks.rake index 6c738ac..80888c9 100644 --- a/lib/tasks/swagger_rails_tasks.rake +++ b/lib/tasks/swagger_rails_tasks.rake @@ -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 diff --git a/spec/dummy/app/models/blog.rb b/spec/dummy/app/models/blog.rb index 2f4b8f8..95e5541 100644 --- a/spec/dummy/app/models/blog.rb +++ b/spec/dummy/app/models/blog.rb @@ -1,6 +1,8 @@ class Blog < ActiveRecord::Base attr_accessible :content, :title + validates :content, presence: true + def as_json(options) { title: title, diff --git a/spec/dummy/spec/integration/blogs_spec.rb b/spec/dummy/spec/integration/blogs_spec.rb new file mode 100644 index 0000000..5370af2 --- /dev/null +++ b/spec/dummy/spec/integration/blogs_spec.rb @@ -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 diff --git a/spec/dummy/spec/integration/v1_contract_spec.rb b/spec/dummy/spec/integration/v1_contract_spec.rb deleted file mode 100644 index 2d05f30..0000000 --- a/spec/dummy/spec/integration/v1_contract_spec.rb +++ /dev/null @@ -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 diff --git a/spec/dummy/spec/spec_helper.rb b/spec/dummy/spec/spec_helper.rb index 61e2738..90d5cac 100644 --- a/spec/dummy/spec/spec_helper.rb +++ b/spec/dummy/spec/spec_helper.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 913e28a..1af1101 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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