diff --git a/README.md b/README.md index baafacb..d802d45 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RSpec Swagger -[![Build Status](https://travis-ci.org/drewish/rspec-swagger.svg?branch=master)](https://travis-ci.org/drewish/rspec-swagger) -[![Code Climate](https://codeclimate.com/github/drewish/rspec-swagger/badges/gpa.svg)](https://codeclimate.com/github/drewish/rspec-swagger) +[![Build Status](https://travis-ci.org/drewish/rspec-rails-swagger.svg?branch=master)](https://travis-ci.org/drewish/rspec-rails-swagger) +[![Code Climate](https://codeclimate.com/github/drewish/rspec-rails-swagger/badges/gpa.svg)](https://codeclimate.com/github/drewish/rspec-rails-swagger) This gem helps you generate Swagger docs by using RSpec to document the paths. You execute a command to run the tests and generate the `.json` output. Running @@ -15,7 +15,7 @@ The design of this was heavily influenced by the awesome [swagger_rails gem](htt - Add the gem to your Rails app's `Gemfile`: ```rb group :development, :test do - gem 'rspec-swagger' + gem 'rspec-rails-swagger' end ``` - If you don't already have a `spec/rails_helper.rb` file run: @@ -23,7 +23,7 @@ end rails generate rspec:install ``` - Create `spec/swagger_helper.rb` file (eventually [this will become a -generator](https://github.com/drewish/rspec-swagger/issues/3)): +generator](https://github.com/drewish/rspec-rails-swagger/issues/3)): ```rb require 'rspec/swagger' require 'rails_helper' @@ -55,7 +55,7 @@ end ## Generate the docs -Eventually [this will become a rake task](https://github.com/drewish/rspec-swagger/issues/2): +Eventually [this will become a rake task](https://github.com/drewish/rspec-rails-swagger/issues/2): ``` bundle exec rspec -f RSpec::Swagger::Formatter --order defined -t swagger_object ``` diff --git a/lib/rspec/rails/swagger.rb b/lib/rspec/rails/swagger.rb new file mode 100644 index 0000000..579b5c9 --- /dev/null +++ b/lib/rspec/rails/swagger.rb @@ -0,0 +1,16 @@ +require 'rspec/core' +require 'rspec/rails/swagger/configuration' +require 'rspec/rails/swagger/document' +require 'rspec/rails/swagger/formatter' +require 'rspec/rails/swagger/helpers' +require 'rspec/rails/swagger/request_builder' +require 'rspec/rails/swagger/version' + +module RSpec + module Rails + module Swagger + initialize_configuration RSpec.configuration + end + end +end + diff --git a/lib/rspec/rails/swagger/configuration.rb b/lib/rspec/rails/swagger/configuration.rb new file mode 100644 index 0000000..833a683 --- /dev/null +++ b/lib/rspec/rails/swagger/configuration.rb @@ -0,0 +1,16 @@ +module RSpec + module Rails + module Swagger + # Fake class to document RSpec Swagger configuration options. + class Configuration + end + + def self.initialize_configuration(config) + config.add_setting :swagger_root + config.add_setting :swagger_docs, default: {} + + Helpers.add_swagger_type_configurations(config) + end + end + end +end diff --git a/lib/rspec/rails/swagger/document.rb b/lib/rspec/rails/swagger/document.rb new file mode 100644 index 0000000..bd3bf0b --- /dev/null +++ b/lib/rspec/rails/swagger/document.rb @@ -0,0 +1,32 @@ +module RSpec + module Rails + module Swagger + class Document + attr_accessor :data + + def initialize(data) + @data = data.deep_symbolize_keys + end + + def [](value) + data[value] + end + + def resolve_ref(ref) + unless %r{#/(?parameters|definitions)/(?.+)} =~ ref + raise ArgumentError, "Invalid reference: #{ref}" + end + + result = data.fetch(location.to_sym, {})[name.to_sym] + raise ArgumentError, "Reference value does not exist: #{ref}" unless result + + if location == 'parameters' + result.merge(name: name) + end + + result + end + end + end + end +end diff --git a/lib/rspec/rails/swagger/formatter.rb b/lib/rspec/rails/swagger/formatter.rb new file mode 100644 index 0000000..375fe2c --- /dev/null +++ b/lib/rspec/rails/swagger/formatter.rb @@ -0,0 +1,105 @@ +require 'rspec/core/formatters/base_text_formatter' + +module RSpec + module Rails + module Swagger + class Formatter < RSpec::Core::Formatters::BaseTextFormatter + RSpec::Core::Formatters.register self, :example_finished, :close + + def documents + # We don't try to load the docs in `initalize` because when running + # `rspec -f RSpec::Swagger::Formatter` RSpec initalized this class + # before `swagger_helper` has run. + @documents ||= ::RSpec.configuration.swagger_docs + end + + def example_finished(notification) + metadata = notification.example.metadata + return unless metadata[:swagger_object] == :response + + # metadata.each do |k, v| + # puts "#{k}\t#{v}" if k.to_s.starts_with?("swagger") + # end + + document = document_for(metadata[:swagger_document]) + path_item = path_item_for(document, metadata[:swagger_path_item]) + operation = operation_for(path_item, metadata[:swagger_operation]) + response_for(operation, metadata[:swagger_response]) + end + + def close(_notification) + documents.each{|k, v| write_json(k, v)} + end + + def write_json(name, document) + root = ::RSpec.configuration.swagger_root + # It would be good to at least warn if the name includes some '../' that + # takes it out of root directory. + target = Pathname(name).expand_path(root) + target.dirname.mkpath + target.write(JSON.pretty_generate(document)) + end + + def document_for(doc_name = nil) + if doc_name + documents.fetch(doc_name) + else + documents.values.first + end + end + + def path_item_for(document, swagger_path_item) + name = swagger_path_item[:path] + + document[:paths] ||= {} + document[:paths][name] ||= {} + if swagger_path_item[:parameters] + document[:paths][name][:parameters] = prepare_parameters(swagger_path_item[:parameters]) + end + document[:paths][name] + end + + def operation_for(path, swagger_operation) + method = swagger_operation[:method] + + path[method] ||= {responses: {}} + path[method].tap do |operation| + if swagger_operation[:parameters] + operation[:parameters] = prepare_parameters(swagger_operation[:parameters]) + end + operation.merge!(swagger_operation.slice( + :summary, :description, :externalDocs, :operationId, + :consumes, :produces, :schemes, :deprecated, :security + )) + end + end + + def response_for(operation, swagger_response) + status = swagger_response[:status_code] + + operation[:responses][status] ||= {} + operation[:responses][status].tap do |response| + if swagger_response[:examples] + response[:examples] = prepare_examples(swagger_response[:examples]) + end + response.merge!(swagger_response.slice(:description, :schema, :headers)) + end + end + + def prepare_parameters(params) + params.values + end + + def prepare_examples(examples) + if examples["application/json"].present? + begin + examples["application/json"] = JSON.parse(examples["application/json"]) + rescue JSON::ParserError + end + end + examples + end + end + end + end +end diff --git a/lib/rspec/rails/swagger/helpers.rb b/lib/rspec/rails/swagger/helpers.rb new file mode 100644 index 0000000..cf714b2 --- /dev/null +++ b/lib/rspec/rails/swagger/helpers.rb @@ -0,0 +1,258 @@ +module RSpec + module Rails + module Swagger + module Helpers + # paths: (Paths) + # /pets: (Path Item) + # post: (Operation) + # tags: + # - pet + # summary: Add a new pet to the store + # description: "" + # operationId: addPet + # consumes: + # - application/json + # produces: + # - application/json + # parameters: (Parameters) + # - in: body + # name: body + # description: Pet object that needs to be added to the store + # required: false + # schema: + # $ref: "#/definitions/Pet" + # responses: (Responses) + # "405": (Response) + # description: Invalid input + + # The helpers serve as a DSL. + def self.add_swagger_type_configurations(config) + # The filters are used to ensure that the methods are nested correctly + # and following the Swagger schema. + config.extend Paths, type: :request + config.extend PathItem, swagger_object: :path_item + config.extend Parameters, swagger_object: :path_item + config.extend Operation, swagger_object: :operation + config.extend Parameters, swagger_object: :operation + config.extend Response, swagger_object: :response + end + + module Paths + def path template, attributes = {}, &block + attributes.symbolize_keys! + + raise ArgumentError, "Path must start with a /" unless template.starts_with?('/') + + #TODO template might be a $ref + meta = { + swagger_object: :path_item, + swagger_document: attributes[:swagger_document] || RSpec.configuration.swagger_docs.keys.first, + swagger_path_item: {path: template} + } + describe(template, meta, &block) + end + end + + module PathItem + METHODS = %w(get put post delete options head patch).freeze + + def operation method, attributes = {}, &block + attributes.symbolize_keys! + + method = method.to_s.downcase + validate_method! method + + meta = { + swagger_object: :operation, + swagger_operation: attributes.merge(method: method.to_sym).reject{ |v| v.nil? } + } + describe(method.to_s, meta, &block) + end + + METHODS.each do |method| + define_method(method) do |attributes = {}, &block| + operation(method, attributes, &block) + end + end + + private + + def validate_method! method + unless METHODS.include? method.to_s + raise ArgumentError, "Operation has an invalid 'method' value. Try: #{METHODS}." + end + end + end + + module Parameters + def parameter name, attributes = {} + attributes.symbolize_keys! + + # Look for $refs + if name.respond_to?(:has_key?) + ref = name.delete(:ref) || name.delete('ref') + full_param = resolve_document(metadata).resolve_ref(ref) + + validate_parameter! full_param + + param = { '$ref' => ref } + key = parameter_key(full_param) + else + validate_parameter! attributes + + # Path attributes are always required + attributes[:required] = true if attributes[:in] == :path + + param = { name: name.to_s }.merge(attributes) + key = parameter_key(param) + end + + parameters_for_object[key] = param + end + + def resolve_document metadata + # TODO: It's really inefficient to keep recreating this. It'd be nice + # if we could cache them some place. + name = metadata[:swagger_document] + Document.new(RSpec.configuration.swagger_docs[name]) + end + + private + + # This key ensures uniqueness based on the 'name' and 'in' values. + def parameter_key parameter + "#{parameter[:in]}&#{parameter[:name]}" + end + + def parameters_for_object + object_key = "swagger_#{metadata[:swagger_object]}".to_sym + object_data = metadata[object_key] ||= {} + object_data[:parameters] ||= {} + end + + def validate_parameter! attributes + validate_location! attributes[:in] + + if attributes[:in].to_s == 'body' + unless attributes[:schema].present? + raise ArgumentError, "Parameter is missing required 'schema' value." + end + else + validate_type! attributes[:type] + end + end + + def validate_location! location + unless location.present? + raise ArgumentError, "Parameter is missing required 'in' value." + end + + locations = %w(query header path formData body) + unless locations.include? location.to_s + raise ArgumentError, "Parameter has an invalid 'in' value. Try: #{locations}." + end + end + + def validate_type! type + unless type.present? + raise ArgumentError, "Parameter is missing required 'type' value." + end + + types = %w(string number integer boolean array file) + unless types.include? type.to_s + raise ArgumentError, "Parameter has an invalid 'type' value. Try: #{types}." + end + end + end + + module Operation + def consumes *mime_types + metadata[:swagger_operation][:consumes] = mime_types + end + + def produces *mime_types + metadata[:swagger_operation][:produces] = mime_types + end + + def response status_code, attributes = {}, &block + attributes.symbolize_keys! + + validate_status_code! status_code + validate_description! attributes[:description] + + meta = { + swagger_object: :response, + swagger_response: attributes.merge(status_code: status_code) + } + describe(status_code, meta) do + self.module_exec(&block) if block_given? + + # To make a request we need: + # - the details we've collected in the metadata + # - parameter values defined using let() + # RSpec tries to limit access to metadata inside of it() / before() + # / after() blocks but that scope is the only place you can access + # the let() values. The solution the swagger_rails dev came up with + # is to use the example.metadata passed into the block with the + # block's scope which has access to the let() values. + before do |example| + builder = RequestBuilder.new(example.metadata, self) + method = builder.method + path = [builder.path, builder.query].join + headers = builder.headers + body = builder.body + + # Run the request + if ::Rails::VERSION::MAJOR >= 5 + self.send(method, path, {params: body, headers: headers}) + else + self.send(method, path, body, headers) + end + + if example.metadata[:capture_examples] + examples = example.metadata[:swagger_response][:examples] ||= {} + examples[response.content_type.to_s] = response.body + end + end + + # TODO: see if we can get the caller to show up in the error + # backtrace for this test. + it("returns the correct status code") do + expect(response).to have_http_status(status_code) + end + end + end + + private + + def validate_status_code! status_code + unless status_code == :default || (100..599).cover?(status_code) + raise ArgumentError, "status_code must be an integer 100 to 599, or :default" + end + end + + def validate_description! description + unless description.present? + raise ArgumentError, "Response is missing required 'description' value." + end + end + end + + module Response + def capture_example + metadata[:capture_examples] = true + end + + def schema definition + definition.symbolize_keys! + + ref = definition.delete(:ref) + schema = ref ? { '$ref' => ref } : definition + + metadata[:swagger_response][:schema] = schema + end + end + end + end + end +end diff --git a/lib/rspec/rails/swagger/request_builder.rb b/lib/rspec/rails/swagger/request_builder.rb new file mode 100644 index 0000000..a6885f2 --- /dev/null +++ b/lib/rspec/rails/swagger/request_builder.rb @@ -0,0 +1,91 @@ +module RSpec + module Rails + module Swagger + class RequestBuilder + attr_reader :metadata, :instance + + def initialize(metadata, instance) + @metadata, @instance = metadata, instance + end + + def document + @document ||= begin + name = metadata[:swagger_document] + Document.new(RSpec.configuration.swagger_docs[name]) + end + end + + def method + metadata[:swagger_operation][:method] + end + + def produces + metadata[:swagger_operation][:produces] || document[:produces] + end + + def consumes + metadata[:swagger_operation][:consumes] || document[:consumes] + end + + def parameters location = nil + path_item = metadata[:swagger_path_item] || {} + operation = metadata[:swagger_operation] || {} + params = path_item.fetch(:parameters, {}).merge(operation.fetch(:parameters, {})) + if location.present? + params.select{ |k, _| k.starts_with? "#{location}&" } + else + params + end + end + + def parameter_values location + # Don't bother looking at the full parameter bodies since all we need + # are location and name which are in the key. + values = parameters(location) + .keys + .map{ |k| k.split('&').last } + .map{ |name| [name, instance.send(name)] } + Hash[values] + end + + def headers + headers = {} + + # Match the names that Rails uses internally + headers['HTTP_ACCEPT'] = produces.join(';') if produces.present? + headers['CONTENT_TYPE'] = consumes.first if consumes.present? + + # TODO: do we need to do some capitalization to match the rack + # conventions? + parameter_values(:header).each { |k, v| headers[k] = v } + + headers + end + + def path + base_path = document[:basePath] || '' + # Find params in the path and replace them with values defined in + # in the example group. + base_path + metadata[:swagger_path_item][:path].gsub(/(\{.*?\})/) do |match| + # QUESTION: Should check that the parameter is actually defined in + # `parameters` before fetch a value? + instance.send(match[1...-1]) + end + end + + def query + query_params = parameter_values(:query).to_query + "?#{query_params}" unless query_params.blank? + end + + def body + # And here all we need is the first half of the key to find the body + # parameter and its name to fetch a value. + if key = parameters(:body).keys.first + instance.send(key.split('&').last).to_json + end + end + end + end + end +end diff --git a/lib/rspec/rails/swagger/version.rb b/lib/rspec/rails/swagger/version.rb new file mode 100644 index 0000000..ead69d1 --- /dev/null +++ b/lib/rspec/rails/swagger/version.rb @@ -0,0 +1,10 @@ +module RSpec + module Rails + # Version information for RSpec Swagger. + module Swagger + module Version + STRING = '0.1.0' + end + end + end +end diff --git a/lib/rspec/swagger.rb b/lib/rspec/swagger.rb deleted file mode 100644 index d03cfc0..0000000 --- a/lib/rspec/swagger.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rspec/core' -require 'rspec/swagger/configuration' -require 'rspec/swagger/document' -require 'rspec/swagger/formatter' -require 'rspec/swagger/helpers' -require 'rspec/swagger/request_builder' -require 'rspec/swagger/version' - -module RSpec - module Swagger - initialize_configuration RSpec.configuration - end -end - diff --git a/lib/rspec/swagger/configuration.rb b/lib/rspec/swagger/configuration.rb deleted file mode 100644 index 4ffb5b4..0000000 --- a/lib/rspec/swagger/configuration.rb +++ /dev/null @@ -1,14 +0,0 @@ -module RSpec - module Swagger - # Fake class to document RSpec Swagger configuration options. - class Configuration - end - - def self.initialize_configuration(config) - config.add_setting :swagger_root - config.add_setting :swagger_docs, default: {} - - Helpers.add_swagger_type_configurations(config) - end - end -end diff --git a/lib/rspec/swagger/document.rb b/lib/rspec/swagger/document.rb deleted file mode 100644 index 9d5a985..0000000 --- a/lib/rspec/swagger/document.rb +++ /dev/null @@ -1,30 +0,0 @@ -module RSpec - module Swagger - class Document - attr_accessor :data - - def initialize(data) - @data = data.deep_symbolize_keys - end - - def [](value) - data[value] - end - - def resolve_ref(ref) - unless %r{#/(?parameters|definitions)/(?.+)} =~ ref - raise ArgumentError, "Invalid reference: #{ref}" - end - - result = data.fetch(location.to_sym, {})[name.to_sym] - raise ArgumentError, "Reference value does not exist: #{ref}" unless result - - if location == 'parameters' - result.merge(name: name) - end - - result - end - end - end -end diff --git a/lib/rspec/swagger/formatter.rb b/lib/rspec/swagger/formatter.rb deleted file mode 100644 index abb2d09..0000000 --- a/lib/rspec/swagger/formatter.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'rspec/core/formatters/base_text_formatter' - -module RSpec - module Swagger - class Formatter < RSpec::Core::Formatters::BaseTextFormatter - RSpec::Core::Formatters.register self, :example_finished, :close - - def documents - # We don't try to load the docs in `initalize` because when running - # `rspec -f RSpec::Swagger::Formatter` RSpec initalized this class - # before `swagger_helper` has run. - @documents ||= ::RSpec.configuration.swagger_docs - end - - def example_finished(notification) - metadata = notification.example.metadata - return unless metadata[:swagger_object] == :response - - # metadata.each do |k, v| - # puts "#{k}\t#{v}" if k.to_s.starts_with?("swagger") - # end - - document = document_for(metadata[:swagger_document]) - path_item = path_item_for(document, metadata[:swagger_path_item]) - operation = operation_for(path_item, metadata[:swagger_operation]) - response_for(operation, metadata[:swagger_response]) - end - - def close(_notification) - documents.each{|k, v| write_json(k, v)} - end - - def write_json(name, document) - root = ::RSpec.configuration.swagger_root - # It would be good to at least warn if the name includes some '../' that - # takes it out of root directory. - target = Pathname(name).expand_path(root) - target.dirname.mkpath - target.write(JSON.pretty_generate(document)) - end - - def document_for(doc_name = nil) - if doc_name - documents.fetch(doc_name) - else - documents.values.first - end - end - - def path_item_for(document, swagger_path_item) - name = swagger_path_item[:path] - - document[:paths] ||= {} - document[:paths][name] ||= {} - if swagger_path_item[:parameters] - document[:paths][name][:parameters] = prepare_parameters(swagger_path_item[:parameters]) - end - document[:paths][name] - end - - def operation_for(path, swagger_operation) - method = swagger_operation[:method] - - path[method] ||= {responses: {}} - path[method].tap do |operation| - if swagger_operation[:parameters] - operation[:parameters] = prepare_parameters(swagger_operation[:parameters]) - end - operation.merge!(swagger_operation.slice( - :summary, :description, :externalDocs, :operationId, - :consumes, :produces, :schemes, :deprecated, :security - )) - end - end - - def response_for(operation, swagger_response) - status = swagger_response[:status_code] - - operation[:responses][status] ||= {} - operation[:responses][status].tap do |response| - if swagger_response[:examples] - response[:examples] = prepare_examples(swagger_response[:examples]) - end - response.merge!(swagger_response.slice(:description, :schema, :headers)) - end - end - - def prepare_parameters(params) - params.values - end - - def prepare_examples(examples) - if examples["application/json"].present? - begin - examples["application/json"] = JSON.parse(examples["application/json"]) - rescue JSON::ParserError - end - end - examples - end - end - end -end diff --git a/lib/rspec/swagger/helpers.rb b/lib/rspec/swagger/helpers.rb deleted file mode 100644 index 33f23c6..0000000 --- a/lib/rspec/swagger/helpers.rb +++ /dev/null @@ -1,256 +0,0 @@ -module RSpec - module Swagger - module Helpers - # paths: (Paths) - # /pets: (Path Item) - # post: (Operation) - # tags: - # - pet - # summary: Add a new pet to the store - # description: "" - # operationId: addPet - # consumes: - # - application/json - # produces: - # - application/json - # parameters: (Parameters) - # - in: body - # name: body - # description: Pet object that needs to be added to the store - # required: false - # schema: - # $ref: "#/definitions/Pet" - # responses: (Responses) - # "405": (Response) - # description: Invalid input - - # The helpers serve as a DSL. - def self.add_swagger_type_configurations(config) - # The filters are used to ensure that the methods are nested correctly - # and following the Swagger schema. - config.extend Paths, type: :request - config.extend PathItem, swagger_object: :path_item - config.extend Parameters, swagger_object: :path_item - config.extend Operation, swagger_object: :operation - config.extend Parameters, swagger_object: :operation - config.extend Response, swagger_object: :response - end - - module Paths - def path template, attributes = {}, &block - attributes.symbolize_keys! - - raise ArgumentError, "Path must start with a /" unless template.starts_with?('/') - - #TODO template might be a $ref - meta = { - swagger_object: :path_item, - swagger_document: attributes[:swagger_document] || RSpec.configuration.swagger_docs.keys.first, - swagger_path_item: {path: template} - } - describe(template, meta, &block) - end - end - - module PathItem - METHODS = %w(get put post delete options head patch).freeze - - def operation method, attributes = {}, &block - attributes.symbolize_keys! - - method = method.to_s.downcase - validate_method! method - - meta = { - swagger_object: :operation, - swagger_operation: attributes.merge(method: method.to_sym).reject{ |v| v.nil? } - } - describe(method.to_s, meta, &block) - end - - METHODS.each do |method| - define_method(method) do |attributes = {}, &block| - operation(method, attributes, &block) - end - end - - private - - def validate_method! method - unless METHODS.include? method.to_s - raise ArgumentError, "Operation has an invalid 'method' value. Try: #{METHODS}." - end - end - end - - module Parameters - def parameter name, attributes = {} - attributes.symbolize_keys! - - # Look for $refs - if name.respond_to?(:has_key?) - ref = name.delete(:ref) || name.delete('ref') - full_param = resolve_document(metadata).resolve_ref(ref) - - validate_parameter! full_param - - param = { '$ref' => ref } - key = parameter_key(full_param) - else - validate_parameter! attributes - - # Path attributes are always required - attributes[:required] = true if attributes[:in] == :path - - param = { name: name.to_s }.merge(attributes) - key = parameter_key(param) - end - - parameters_for_object[key] = param - end - - def resolve_document metadata - # TODO: It's really inefficient to keep recreating this. It'd be nice - # if we could cache them some place. - name = metadata[:swagger_document] - Document.new(RSpec.configuration.swagger_docs[name]) - end - - private - - # This key ensures uniqueness based on the 'name' and 'in' values. - def parameter_key parameter - "#{parameter[:in]}&#{parameter[:name]}" - end - - def parameters_for_object - object_key = "swagger_#{metadata[:swagger_object]}".to_sym - object_data = metadata[object_key] ||= {} - object_data[:parameters] ||= {} - end - - def validate_parameter! attributes - validate_location! attributes[:in] - - if attributes[:in].to_s == 'body' - unless attributes[:schema].present? - raise ArgumentError, "Parameter is missing required 'schema' value." - end - else - validate_type! attributes[:type] - end - end - - def validate_location! location - unless location.present? - raise ArgumentError, "Parameter is missing required 'in' value." - end - - locations = %w(query header path formData body) - unless locations.include? location.to_s - raise ArgumentError, "Parameter has an invalid 'in' value. Try: #{locations}." - end - end - - def validate_type! type - unless type.present? - raise ArgumentError, "Parameter is missing required 'type' value." - end - - types = %w(string number integer boolean array file) - unless types.include? type.to_s - raise ArgumentError, "Parameter has an invalid 'type' value. Try: #{types}." - end - end - end - - module Operation - def consumes *mime_types - metadata[:swagger_operation][:consumes] = mime_types - end - - def produces *mime_types - metadata[:swagger_operation][:produces] = mime_types - end - - def response status_code, attributes = {}, &block - attributes.symbolize_keys! - - validate_status_code! status_code - validate_description! attributes[:description] - - meta = { - swagger_object: :response, - swagger_response: attributes.merge(status_code: status_code) - } - describe(status_code, meta) do - self.module_exec(&block) if block_given? - - # To make a request we need: - # - the details we've collected in the metadata - # - parameter values defined using let() - # RSpec tries to limit access to metadata inside of it() / before() - # / after() blocks but that scope is the only place you can access - # the let() values. The solution the swagger_rails dev came up with - # is to use the example.metadata passed into the block with the - # block's scope which has access to the let() values. - before do |example| - builder = RequestBuilder.new(example.metadata, self) - method = builder.method - path = [builder.path, builder.query].join - headers = builder.headers - body = builder.body - - # Run the request - if ::Rails::VERSION::MAJOR >= 5 - self.send(method, path, {params: body, headers: headers}) - else - self.send(method, path, body, headers) - end - - if example.metadata[:capture_examples] - examples = example.metadata[:swagger_response][:examples] ||= {} - examples[response.content_type.to_s] = response.body - end - end - - # TODO: see if we can get the caller to show up in the error - # backtrace for this test. - it("returns the correct status code") do - expect(response).to have_http_status(status_code) - end - end - end - - private - - def validate_status_code! status_code - unless status_code == :default || (100..599).cover?(status_code) - raise ArgumentError, "status_code must be an integer 100 to 599, or :default" - end - end - - def validate_description! description - unless description.present? - raise ArgumentError, "Response is missing required 'description' value." - end - end - end - - module Response - def capture_example - metadata[:capture_examples] = true - end - - def schema definition - definition.symbolize_keys! - - ref = definition.delete(:ref) - schema = ref ? { '$ref' => ref } : definition - - metadata[:swagger_response][:schema] = schema - end - end - end - end -end diff --git a/lib/rspec/swagger/request_builder.rb b/lib/rspec/swagger/request_builder.rb deleted file mode 100644 index d802fec..0000000 --- a/lib/rspec/swagger/request_builder.rb +++ /dev/null @@ -1,89 +0,0 @@ -module RSpec - module Swagger - class RequestBuilder - attr_reader :metadata, :instance - - def initialize(metadata, instance) - @metadata, @instance = metadata, instance - end - - def document - @document ||= begin - name = metadata[:swagger_document] - Document.new(RSpec.configuration.swagger_docs[name]) - end - end - - def method - metadata[:swagger_operation][:method] - end - - def produces - metadata[:swagger_operation][:produces] || document[:produces] - end - - def consumes - metadata[:swagger_operation][:consumes] || document[:consumes] - end - - def parameters location = nil - path_item = metadata[:swagger_path_item] || {} - operation = metadata[:swagger_operation] || {} - params = path_item.fetch(:parameters, {}).merge(operation.fetch(:parameters, {})) - if location.present? - params.select{ |k, _| k.starts_with? "#{location}&" } - else - params - end - end - - def parameter_values location - # Don't bother looking at the full parameter bodies since all we need - # are location and name which are in the key. - values = parameters(location) - .keys - .map{ |k| k.split('&').last } - .map{ |name| [name, instance.send(name)] } - Hash[values] - end - - def headers - headers = {} - - # Match the names that Rails uses internally - headers['HTTP_ACCEPT'] = produces.join(';') if produces.present? - headers['CONTENT_TYPE'] = consumes.first if consumes.present? - - # TODO: do we need to do some capitalization to match the rack - # conventions? - parameter_values(:header).each { |k, v| headers[k] = v } - - headers - end - - def path - base_path = document[:basePath] || '' - # Find params in the path and replace them with values defined in - # in the example group. - base_path + metadata[:swagger_path_item][:path].gsub(/(\{.*?\})/) do |match| - # QUESTION: Should check that the parameter is actually defined in - # `parameters` before fetch a value? - instance.send(match[1...-1]) - end - end - - def query - query_params = parameter_values(:query).to_query - "?#{query_params}" unless query_params.blank? - end - - def body - # And here all we need is the first half of the key to find the body - # parameter and its name to fetch a value. - if key = parameters(:body).keys.first - instance.send(key.split('&').last).to_json - end - end - end - end -end diff --git a/lib/rspec/swagger/version.rb b/lib/rspec/swagger/version.rb deleted file mode 100644 index de57ed9..0000000 --- a/lib/rspec/swagger/version.rb +++ /dev/null @@ -1,8 +0,0 @@ -module RSpec - # Version information for RSpec Swagger. - module Swagger - module Version - STRING = '0.1.0' - end - end -end diff --git a/rspec-rails-swagger.gemspec b/rspec-rails-swagger.gemspec new file mode 100644 index 0000000..8a70b0e --- /dev/null +++ b/rspec-rails-swagger.gemspec @@ -0,0 +1,26 @@ +$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) +require "rspec/rails/swagger/version" + +Gem::Specification.new do |s| + s.name = 'rspec-rails-swagger' + s.version = RSpec::Rails::Swagger::Version::STRING + s.licenses = ['MIT'] + s.summary = "Generate Swagger docs from RSpec integration tests" + s.description = "Inspired by swagger_rails" + s.author = "andrew morton" + s.email = 'drewish@katherinehouse.com' + s.files = [ + 'lib/rspec/rails/swagger.rb', + 'lib/rspec/rails/swagger/configuration.rb', + 'lib/rspec/rails/swagger/document.rb', + 'lib/rspec/rails/swagger/formatter.rb', + 'lib/rspec/rails/swagger/helpers.rb', + 'lib/rspec/rails/swagger/request_builder.rb', + 'lib/rspec/rails/swagger/version.rb', + ] + s.homepage = 'https://github.com/drewish/rspec-rails-swagger' + + s.required_ruby_version = '~> 2.0' + s.add_runtime_dependency 'rails', '>= 3.1' + s.add_runtime_dependency 'rspec-rails', '~> 3.0' +end diff --git a/rspec-swagger.gemspec b/rspec-swagger.gemspec deleted file mode 100644 index 7d6c1d2..0000000 --- a/rspec-swagger.gemspec +++ /dev/null @@ -1,26 +0,0 @@ -$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) -require "rspec/swagger/version" - -Gem::Specification.new do |s| - s.name = 'rspec-swagger' - s.version = RSpec::Swagger::Version::STRING - s.licenses = ['MIT'] - s.summary = "Generate Swagger docs from RSpec integration tests" - s.description = "Inspired by swagger_rails" - s.author = "andrew morton" - s.email = 'drewish@katherinehouse.com' - s.files = [ - 'lib/rspec/swagger.rb', - 'lib/rspec/swagger/configuration.rb', - 'lib/rspec/swagger/document.rb', - 'lib/rspec/swagger/formatter.rb', - 'lib/rspec/swagger/helpers.rb', - 'lib/rspec/swagger/request_builder.rb', - 'lib/rspec/swagger/version.rb', - ] - s.homepage = 'https://github.com/drewish/rspec-swagger' - - s.required_ruby_version = '~> 2.0' - s.add_runtime_dependency 'rails', '>= 3.1' - s.add_runtime_dependency 'rspec-rails', '~> 3.0' -end diff --git a/spec/rspec/swagger/document_spec.rb b/spec/rspec/rails/swagger/document_spec.rb similarity index 86% rename from spec/rspec/swagger/document_spec.rb rename to spec/rspec/rails/swagger/document_spec.rb index 395fa85..e563758 100644 --- a/spec/rspec/swagger/document_spec.rb +++ b/spec/rspec/rails/swagger/document_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -RSpec.describe RSpec::Swagger::Document do +RSpec.describe RSpec::Rails::Swagger::Document do subject { described_class.new(data) } let(:data) { minimial_example } @@ -53,10 +53,10 @@ end def minimial_example - YAML.load_file(File.expand_path('../../../fixtures/files/minimal.yml', __FILE__)) + YAML.load_file(File.expand_path('../../../../fixtures/files/minimal.yml', __FILE__)) end def instagram_example - YAML.load_file(File.expand_path('../../../fixtures/files/instagram.yml', __FILE__)) + YAML.load_file(File.expand_path('../../../../fixtures/files/instagram.yml', __FILE__)) end end diff --git a/spec/rspec/swagger/formatter_spec.rb b/spec/rspec/rails/swagger/formatter_spec.rb similarity index 98% rename from spec/rspec/swagger/formatter_spec.rb rename to spec/rspec/rails/swagger/formatter_spec.rb index c59c3d3..267492b 100644 --- a/spec/rspec/swagger/formatter_spec.rb +++ b/spec/rspec/rails/swagger/formatter_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -RSpec.describe RSpec::Swagger::Formatter do +RSpec.describe RSpec::Rails::Swagger::Formatter do let(:output) { StringIO.new } let(:formatter) { described_class.new(output) } let(:documents) { {'minimal.json' => minimal} } diff --git a/spec/rspec/swagger/helpers_spec.rb b/spec/rspec/rails/swagger/helpers_spec.rb similarity index 92% rename from spec/rspec/swagger/helpers_spec.rb rename to spec/rspec/rails/swagger/helpers_spec.rb index 67196b1..d8a5040 100644 --- a/spec/rspec/swagger/helpers_spec.rb +++ b/spec/rspec/rails/swagger/helpers_spec.rb @@ -1,9 +1,9 @@ require 'swagger_helper' -RSpec.describe RSpec::Swagger::Helpers::Paths do +RSpec.describe RSpec::Rails::Swagger::Helpers::Paths do let(:klass) do Class.new do - include RSpec::Swagger::Helpers::Paths + include RSpec::Rails::Swagger::Helpers::Paths attr_accessor :metadata def describe *args ; end end @@ -36,10 +36,10 @@ def describe *args ; end end end -RSpec.describe RSpec::Swagger::Helpers::PathItem do +RSpec.describe RSpec::Rails::Swagger::Helpers::PathItem do let(:klass) do Class.new do - include RSpec::Swagger::Helpers::PathItem + include RSpec::Rails::Swagger::Helpers::PathItem attr_accessor :metadata def describe *args ; end end @@ -97,10 +97,10 @@ def describe *args ; end end end -RSpec.describe RSpec::Swagger::Helpers::Parameters do +RSpec.describe RSpec::Rails::Swagger::Helpers::Parameters do let(:klass) do Class.new do - include RSpec::Swagger::Helpers::Parameters + include RSpec::Rails::Swagger::Helpers::Parameters attr_accessor :metadata def describe *args ; end def resolve_document *args ; end @@ -176,10 +176,10 @@ def resolve_document *args ; end end -RSpec.describe RSpec::Swagger::Helpers::Operation do +RSpec.describe RSpec::Rails::Swagger::Helpers::Operation do let(:klass) do Class.new do - include RSpec::Swagger::Helpers::Operation + include RSpec::Rails::Swagger::Helpers::Operation attr_accessor :metadata def describe *args ; end end @@ -207,10 +207,10 @@ def describe *args ; end end end -RSpec.describe RSpec::Swagger::Helpers::Response do +RSpec.describe RSpec::Rails::Swagger::Helpers::Response do let(:klass) do Class.new do - include RSpec::Swagger::Helpers::Response + include RSpec::Rails::Swagger::Helpers::Response attr_accessor :metadata def describe *args ; end end diff --git a/spec/rspec/swagger/request_builder_spec.rb b/spec/rspec/rails/swagger/request_builder_spec.rb similarity index 99% rename from spec/rspec/swagger/request_builder_spec.rb rename to spec/rspec/rails/swagger/request_builder_spec.rb index fb2e273..215c30d 100644 --- a/spec/rspec/swagger/request_builder_spec.rb +++ b/spec/rspec/rails/swagger/request_builder_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -RSpec.describe RSpec::Swagger::RequestBuilder do +RSpec.describe RSpec::Rails::Swagger::RequestBuilder do describe '#initialize' do it 'stores metadata and instance' do metadata = { foo: :bar } diff --git a/spec/rspec/rails/swagger/version_spec.rb b/spec/rspec/rails/swagger/version_spec.rb new file mode 100644 index 0000000..72ab879 --- /dev/null +++ b/spec/rspec/rails/swagger/version_spec.rb @@ -0,0 +1,7 @@ +require 'swagger_helper' + +RSpec.describe RSpec::Rails::Swagger do + it "loads" do + expect(RSpec::Rails::Swagger::Version::STRING).to eq '0.1.0' + end +end diff --git a/spec/rspec/swagger/version_spec.rb b/spec/rspec/swagger/version_spec.rb deleted file mode 100644 index 58f81f0..0000000 --- a/spec/rspec/swagger/version_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'swagger_helper' - -RSpec.describe RSpec::Swagger do - it "loads" do - expect(RSpec::Swagger::Version::STRING).to eq '0.1.0' - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ccf1b6e..b7ed67d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ -require 'rspec/swagger' +require 'rspec/rails/swagger' # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index fcb7955..dfa0258 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -1,4 +1,4 @@ -require 'rspec/swagger' +require 'rspec/rails/swagger' require 'rails_helper' RSpec.configure do |config|