mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-25 15:22:56 +00:00
Renames and fixes specs in api and specs project to prefix OpenApi module. Gem name to open_api-rswag
This commit is contained in:
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|||||||
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
|
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
|
||||||
s.authors = ["Richie Morris", "Jay Danielian"]
|
s.authors = ["Richie Morris", "Jay Danielian"]
|
||||||
s.email = ["domaindrivendev@gmail.com"]
|
s.email = ["domaindrivendev@gmail.com"]
|
||||||
s.homepage = "https://github.com/domaindrivendev/rswag"
|
s.homepage = "https://github.com/jdanielian/rswag"
|
||||||
s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints"
|
s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints"
|
||||||
s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints"
|
s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints"
|
||||||
s.license = "MIT"
|
s.license = "MIT"
|
||||||
|
|||||||
29
rswag-specs/lib/open_api/rswag/specs.rb
Normal file
29
rswag-specs/lib/open_api/rswag/specs.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
require 'rspec/core'
|
||||||
|
require 'open_api/rswag/specs/example_group_helpers'
|
||||||
|
require 'open_api/rswag/specs/example_helpers'
|
||||||
|
require 'open_api/rswag/specs/configuration'
|
||||||
|
require 'open_api/rswag/specs/railtie' if defined?(Rails::Railtie)
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
|
||||||
|
# Extend RSpec with a swagger-based DSL
|
||||||
|
::RSpec.configure do |c|
|
||||||
|
c.add_setting :swagger_root
|
||||||
|
c.add_setting :swagger_docs
|
||||||
|
c.add_setting :swagger_dry_run
|
||||||
|
c.extend ExampleGroupHelpers, type: :request
|
||||||
|
c.include ExampleHelpers, type: :request
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.config
|
||||||
|
@config ||= Configuration.new(RSpec.configuration)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Support Rails 3+ and RSpec 2+ (sigh!)
|
||||||
|
RAILS_VERSION = Rails::VERSION::MAJOR
|
||||||
|
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
48
rswag-specs/lib/open_api/rswag/specs/configuration.rb
Normal file
48
rswag-specs/lib/open_api/rswag/specs/configuration.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class Configuration
|
||||||
|
def initialize(rspec_config)
|
||||||
|
@rspec_config = rspec_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def swagger_root
|
||||||
|
@swagger_root ||= begin
|
||||||
|
if @rspec_config.swagger_root.nil?
|
||||||
|
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
|
||||||
|
end
|
||||||
|
|
||||||
|
@rspec_config.swagger_root
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def swagger_docs
|
||||||
|
@swagger_docs ||= begin
|
||||||
|
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
|
||||||
|
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
|
||||||
|
end
|
||||||
|
|
||||||
|
@rspec_config.swagger_docs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def swagger_dry_run
|
||||||
|
@swagger_dry_run ||= begin
|
||||||
|
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_swagger_doc(name)
|
||||||
|
return swagger_docs.values.first if name.nil?
|
||||||
|
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
|
||||||
|
|
||||||
|
swagger_docs[name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ConfigurationError < StandardError; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
266
rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb
Normal file
266
rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require 'hashie'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
module ExampleGroupHelpers
|
||||||
|
def path(template, metadata = {}, &block)
|
||||||
|
metadata[:path_item] = { template: template }
|
||||||
|
describe(template, metadata, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[get post patch put delete head].each do |verb|
|
||||||
|
define_method(verb) do |summary, &block|
|
||||||
|
api_metadata = { operation: { verb: verb, summary: summary } }
|
||||||
|
describe(verb, api_metadata, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[operationId deprecated security].each do |attr_name|
|
||||||
|
define_method(attr_name) do |value|
|
||||||
|
metadata[:operation][attr_name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE: 'description' requires special treatment because ExampleGroup already
|
||||||
|
# defines a method with that name. Provide an override that supports the existing
|
||||||
|
# functionality while also setting the appropriate metadata if applicable
|
||||||
|
def description(value = nil)
|
||||||
|
return super() if value.nil?
|
||||||
|
|
||||||
|
metadata[:operation][:description] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
# These are array properties - note the splat operator
|
||||||
|
%i[tags consumes produces schemes].each do |attr_name|
|
||||||
|
define_method(attr_name) do |*value|
|
||||||
|
metadata[:operation][attr_name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# NICE TO HAVE
|
||||||
|
# TODO: update generator templates to include 3.0 syntax
|
||||||
|
# TODO: setup travis CI?
|
||||||
|
|
||||||
|
# MUST HAVES
|
||||||
|
# TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response
|
||||||
|
# can we handle all of them?
|
||||||
|
# Then can look at handling different request_body things like $ref, etc
|
||||||
|
# TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0
|
||||||
|
# TODO: look at adding examples in content request_body
|
||||||
|
# https://swagger.io/docs/specification/describing-request-body/
|
||||||
|
# need to make sure we output requestBody in the swagger generator .json
|
||||||
|
# also need to make sure that it can handle content: , required: true/false, schema: ref
|
||||||
|
|
||||||
|
def request_body(attributes)
|
||||||
|
# can make this generic, and accept any incoming hash (like parameter method)
|
||||||
|
attributes.compact!
|
||||||
|
|
||||||
|
if metadata[:operation][:requestBody].blank?
|
||||||
|
metadata[:operation][:requestBody] = attributes
|
||||||
|
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
|
||||||
|
# merge in
|
||||||
|
content_hash = metadata[:operation][:requestBody][:content]
|
||||||
|
incoming_content_hash = attributes[:content]
|
||||||
|
content_hash.merge!(incoming_content_hash) if incoming_content_hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_body_json(schema:, required: true, description: nil, examples: nil)
|
||||||
|
passed_examples = Array(examples)
|
||||||
|
content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
|
||||||
|
request_body(description: description, required: required, content: content_hash)
|
||||||
|
if passed_examples.any?
|
||||||
|
# the request_factory is going to have to resolve the different ways that the example can be given
|
||||||
|
# it can contain a 'value' key which is a direct hash (easiest)
|
||||||
|
# it can contain a 'external_value' key which makes an external call to load the json
|
||||||
|
# it can contain a '$ref' key. Which points to #/components/examples/blog
|
||||||
|
passed_examples.each do |passed_example|
|
||||||
|
if passed_example.is_a?(Symbol)
|
||||||
|
example_key_name = passed_example
|
||||||
|
# TODO: write more tests around this adding to the parameter
|
||||||
|
# if symbol try and use save_request_example
|
||||||
|
param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
|
||||||
|
parameter(param_attributes)
|
||||||
|
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
|
||||||
|
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
|
||||||
|
parameter(param_attributes)
|
||||||
|
elsif passed_example.is_a?(Hash) && passed_example['$ref']
|
||||||
|
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
|
||||||
|
parameter(param_attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_body_text_plain(required: false, description: nil, examples: nil)
|
||||||
|
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
|
||||||
|
request_body(description: description, required: required, content: content_hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
|
||||||
|
def request_body_xml(schema:,required: false, description: nil, examples: nil)
|
||||||
|
passed_examples = Array(examples)
|
||||||
|
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
|
||||||
|
request_body(description: description, required: required, content: content_hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_body_multipart(schema:, description: nil)
|
||||||
|
content_hash = { 'multipart/form-data' => { schema: schema }}
|
||||||
|
request_body(description: description, content: content_hash)
|
||||||
|
|
||||||
|
schema.extend(Hashie::Extensions::DeepLocate)
|
||||||
|
file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary }
|
||||||
|
|
||||||
|
hash_locator = []
|
||||||
|
|
||||||
|
file_properties.each do |match|
|
||||||
|
hash_match = schema.deep_locate -> (_k, v, _obj) { v == match }
|
||||||
|
hash_locator.concat(hash_match) unless hash_match.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
property_hashes = hash_locator.flat_map do |locator|
|
||||||
|
locator.select { |_k,v| file_properties.include?(v) }
|
||||||
|
end
|
||||||
|
|
||||||
|
property_hashes.each do |property_hash|
|
||||||
|
file_name = property_hash.keys.first
|
||||||
|
parameter name: file_name, in: :formData, type: :file, required: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameter(attributes)
|
||||||
|
if attributes[:in] && attributes[:in].to_sym == :path
|
||||||
|
attributes[:required] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if attributes[:type] && attributes[:schema].nil?
|
||||||
|
attributes[:schema] = {type: attributes[:type]}
|
||||||
|
end
|
||||||
|
|
||||||
|
if metadata.key?(:operation)
|
||||||
|
metadata[:operation][:parameters] ||= []
|
||||||
|
metadata[:operation][:parameters] << attributes
|
||||||
|
else
|
||||||
|
metadata[:path_item][:parameters] ||= []
|
||||||
|
metadata[:path_item][:parameters] << attributes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def response(code, description, metadata = {}, &block)
|
||||||
|
metadata[:response] = { code: code, description: description }
|
||||||
|
context(description, metadata, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def schema(value, content_type: 'application/json')
|
||||||
|
content_hash = {content_type => {schema: value}}
|
||||||
|
metadata[:response][:content] = content_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def header(name, attributes)
|
||||||
|
metadata[:response][:headers] ||= {}
|
||||||
|
|
||||||
|
if attributes[:type] && attributes[:schema].nil?
|
||||||
|
attributes[:schema] = {type: attributes[:type]}
|
||||||
|
attributes.delete(:type)
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata[:response][:headers][name] = attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE: Similar to 'description', 'examples' need to handle the case when
|
||||||
|
# being invoked with no params to avoid overriding 'examples' method of
|
||||||
|
# rspec-core ExampleGroup
|
||||||
|
def examples(example = nil)
|
||||||
|
return super() if example.nil?
|
||||||
|
|
||||||
|
metadata[:response][:examples] = example
|
||||||
|
end
|
||||||
|
|
||||||
|
# checks the examples in the parameters should be able to add $ref and externalValue examples.
|
||||||
|
# This syntax would look something like this in the integration _spec.rb file
|
||||||
|
#
|
||||||
|
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
|
||||||
|
# examples: [:blog, {name: :external_blog,
|
||||||
|
# externalValue: 'http://api.sample.org/myjson_example'},
|
||||||
|
# {name: :another_example,
|
||||||
|
# '$ref' => '#/components/examples/flexible_blog_example'}]
|
||||||
|
# The first value :blog, points to a let param of the same name, and is used to make the request in the
|
||||||
|
# integration test (it is used to build the request payload)
|
||||||
|
#
|
||||||
|
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
|
||||||
|
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
|
||||||
|
#
|
||||||
|
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
|
||||||
|
# will not show it yet
|
||||||
|
def merge_other_examples!(example_metadata)
|
||||||
|
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
||||||
|
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
|
||||||
|
return unless content_node
|
||||||
|
|
||||||
|
external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
|
||||||
|
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
|
||||||
|
examples_node = content_node[:examples] ||= {}
|
||||||
|
|
||||||
|
nodes_to_add = []
|
||||||
|
nodes_to_add << external_example unless external_example.empty?
|
||||||
|
nodes_to_add << ref_example unless ref_example.empty?
|
||||||
|
|
||||||
|
nodes_to_add.each do |node|
|
||||||
|
json_request_examples = examples_node ||= {}
|
||||||
|
other_name = node[:name][:name]
|
||||||
|
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
|
||||||
|
if other_name
|
||||||
|
json_request_examples.merge!(other_name => {other_key => node[:param_value]})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_test!(&block)
|
||||||
|
# NOTE: rspec 2.x support
|
||||||
|
if RSPEC_VERSION < 3
|
||||||
|
before do
|
||||||
|
submit_request(example.metadata)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a #{metadata[:response][:code]} response" do
|
||||||
|
assert_response_matches_metadata(metadata)
|
||||||
|
block.call(response) if block_given?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
before do |example|
|
||||||
|
submit_request(example.metadata) #
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a #{metadata[:response][:code]} response" do |example|
|
||||||
|
assert_response_matches_metadata(example.metadata, &block)
|
||||||
|
example.instance_exec(response, &block) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
after do |example|
|
||||||
|
body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] }
|
||||||
|
|
||||||
|
if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json']
|
||||||
|
# save response examples by default
|
||||||
|
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty?
|
||||||
|
|
||||||
|
# save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
|
||||||
|
if response.code.to_s =~ /^2\d{2}$/
|
||||||
|
example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
||||||
|
json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
||||||
|
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
|
||||||
|
|
||||||
|
example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
36
rswag-specs/lib/open_api/rswag/specs/example_helpers.rb
Normal file
36
rswag-specs/lib/open_api/rswag/specs/example_helpers.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'open_api/rswag/specs/request_factory'
|
||||||
|
require 'open_api/rswag/specs/response_validator'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
module ExampleHelpers
|
||||||
|
def submit_request(metadata)
|
||||||
|
request = RequestFactory.new.build_request(metadata, self)
|
||||||
|
|
||||||
|
if RAILS_VERSION < 5
|
||||||
|
send(
|
||||||
|
request[:verb],
|
||||||
|
request[:path],
|
||||||
|
request[:payload],
|
||||||
|
request[:headers]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
send(
|
||||||
|
request[:verb],
|
||||||
|
request[:path],
|
||||||
|
params: request[:payload],
|
||||||
|
headers: request[:headers]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_response_matches_metadata(metadata)
|
||||||
|
ResponseValidator.new.validate!(metadata, response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
28
rswag-specs/lib/open_api/rswag/specs/extended_schema.rb
Normal file
28
rswag-specs/lib/open_api/rswag/specs/extended_schema.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'json-schema'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class ExtendedSchema < JSON::Schema::Draft4
|
||||||
|
def initialize
|
||||||
|
super
|
||||||
|
@attributes['type'] = ExtendedTypeAttribute
|
||||||
|
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
|
||||||
|
@names = ['http://tempuri.org/rswag/specs/extended_schema']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
|
||||||
|
def self.validate(current_schema, data, fragments, processor, validator, options = {})
|
||||||
|
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON::Validator.register_validator(ExtendedSchema.new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
rswag-specs/lib/open_api/rswag/specs/railtie.rb
Normal file
13
rswag-specs/lib/open_api/rswag/specs/railtie.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class Railtie < ::Rails::Railtie
|
||||||
|
rake_tasks do
|
||||||
|
load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __dir__)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
166
rswag-specs/lib/open_api/rswag/specs/request_factory.rb
Normal file
166
rswag-specs/lib/open_api/rswag/specs/request_factory.rb
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_support/core_ext/hash/slice'
|
||||||
|
require 'active_support/core_ext/hash/conversions'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class RequestFactory
|
||||||
|
def initialize(config = ::OpenApi::Rswag::Specs.config)
|
||||||
|
@config = config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_request(metadata, example)
|
||||||
|
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
||||||
|
parameters = expand_parameters(metadata, swagger_doc, example)
|
||||||
|
|
||||||
|
{}.tap do |request|
|
||||||
|
add_verb(request, metadata)
|
||||||
|
add_path(request, metadata, swagger_doc, parameters, example)
|
||||||
|
add_headers(request, metadata, swagger_doc, parameters, example)
|
||||||
|
add_payload(request, parameters, example)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def expand_parameters(metadata, swagger_doc, example)
|
||||||
|
operation_params = metadata[:operation][:parameters] || []
|
||||||
|
path_item_params = metadata[:path_item][:parameters] || []
|
||||||
|
security_params = derive_security_params(metadata, swagger_doc)
|
||||||
|
|
||||||
|
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
|
||||||
|
(operation_params + path_item_params + security_params)
|
||||||
|
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
|
||||||
|
.uniq { |p| p[:name] }
|
||||||
|
.reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def derive_security_params(metadata, swagger_doc)
|
||||||
|
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
|
||||||
|
scheme_names = requirements.flat_map(&:keys)
|
||||||
|
components = swagger_doc[:components] || {}
|
||||||
|
schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values
|
||||||
|
|
||||||
|
schemes.map do |scheme|
|
||||||
|
param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
|
||||||
|
param.merge(type: :string, required: requirements.one?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_parameter(ref, swagger_doc)
|
||||||
|
key = ref.sub('#/parameters/', '').to_sym
|
||||||
|
definitions = swagger_doc[:parameters]
|
||||||
|
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
|
||||||
|
|
||||||
|
definitions[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_verb(request, metadata)
|
||||||
|
request[:verb] = metadata[:operation][:verb]
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_path(request, metadata, swagger_doc, parameters, example)
|
||||||
|
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
|
||||||
|
|
||||||
|
request[:path] = template.tap do |template|
|
||||||
|
parameters.select { |p| p[:in] == :path }.each do |p|
|
||||||
|
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
|
||||||
|
template.concat(i == 0 ? '?' : '&')
|
||||||
|
template.concat(build_query_string_part(p, example.send(p[:name])))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_query_string_part(param, value)
|
||||||
|
name = param[:name]
|
||||||
|
return "#{name}=#{value}" unless param[:type].to_sym == :array
|
||||||
|
|
||||||
|
case param[:collectionFormat]
|
||||||
|
when :ssv
|
||||||
|
"#{name}=#{value.join(' ')}"
|
||||||
|
when :tsv
|
||||||
|
"#{name}=#{value.join('\t')}"
|
||||||
|
when :pipes
|
||||||
|
"#{name}=#{value.join('|')}"
|
||||||
|
when :multi
|
||||||
|
value.map { |v| "#{name}=#{v}" }.join('&')
|
||||||
|
else
|
||||||
|
"#{name}=#{value.join(',')}" # csv is default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_headers(request, metadata, swagger_doc, parameters, example)
|
||||||
|
tuples = parameters
|
||||||
|
.select { |p| p[:in] == :header }
|
||||||
|
.map { |p| [p[:name], example.send(p[:name]).to_s] }
|
||||||
|
|
||||||
|
# Accept header
|
||||||
|
produces = metadata[:operation][:produces] || swagger_doc[:produces]
|
||||||
|
if produces
|
||||||
|
accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
|
||||||
|
tuples << ['Accept', accept]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Content-Type header
|
||||||
|
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
|
||||||
|
if consumes
|
||||||
|
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
|
||||||
|
tuples << ['Content-Type', content_type]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rails test infrastructure requires rackified headers
|
||||||
|
rackified_tuples = tuples.map do |pair|
|
||||||
|
[
|
||||||
|
case pair[0]
|
||||||
|
when 'Accept' then 'HTTP_ACCEPT'
|
||||||
|
when 'Content-Type' then 'CONTENT_TYPE'
|
||||||
|
when 'Authorization' then 'HTTP_AUTHORIZATION'
|
||||||
|
else pair[0]
|
||||||
|
end,
|
||||||
|
pair[1]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
request[:headers] = Hash[rackified_tuples]
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_payload(request, parameters, example)
|
||||||
|
content_type = request[:headers]['CONTENT_TYPE']
|
||||||
|
return if content_type.nil?
|
||||||
|
|
||||||
|
if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
|
||||||
|
request[:payload] = build_form_payload(parameters, example)
|
||||||
|
else
|
||||||
|
request[:payload] = build_json_payload(parameters, example)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_form_payload(parameters, example)
|
||||||
|
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
|
||||||
|
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
|
||||||
|
# Rails test infrastructure allows us to send the values directly as a hash
|
||||||
|
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
|
||||||
|
tuples = parameters
|
||||||
|
.select { |p| p[:in] == :formData }
|
||||||
|
.map { |p| [p[:name], example.send(p[:name])] }
|
||||||
|
Hash[tuples]
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_json_payload(parameters, example)
|
||||||
|
body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
|
||||||
|
return nil unless body_param
|
||||||
|
|
||||||
|
source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
|
||||||
|
source_body_param ||= body_param[:param_value]
|
||||||
|
source_body_param ? source_body_param.to_json : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
70
rswag-specs/lib/open_api/rswag/specs/response_validator.rb
Normal file
70
rswag-specs/lib/open_api/rswag/specs/response_validator.rb
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_support/core_ext/hash/slice'
|
||||||
|
require 'json-schema'
|
||||||
|
require 'json'
|
||||||
|
require 'open_api/rswag/specs/extended_schema'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class ResponseValidator
|
||||||
|
def initialize(config = ::OpenApi::Rswag::Specs.config)
|
||||||
|
@config = config
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate!(metadata, response)
|
||||||
|
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
||||||
|
|
||||||
|
validate_code!(metadata, response)
|
||||||
|
validate_headers!(metadata, response.headers)
|
||||||
|
validate_body!(metadata, swagger_doc, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_code!(metadata, response)
|
||||||
|
expected = metadata[:response][:code].to_s
|
||||||
|
if response.code != expected
|
||||||
|
raise UnexpectedResponse,
|
||||||
|
"Expected response code '#{response.code}' to match '#{expected}'\n" \
|
||||||
|
"Response body: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_headers!(metadata, headers)
|
||||||
|
expected = (metadata[:response][:headers] || {}).keys
|
||||||
|
expected.each do |name|
|
||||||
|
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_body!(metadata, swagger_doc, body)
|
||||||
|
test_schemas = extract_schemas(metadata)
|
||||||
|
return if test_schemas.nil?
|
||||||
|
|
||||||
|
components = swagger_doc[:components] || {}
|
||||||
|
components_schemas = { components: { schemas: components[:schemas] } }
|
||||||
|
|
||||||
|
validation_schema = test_schemas[:schema] # response_schema
|
||||||
|
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
|
||||||
|
.merge(components_schemas)
|
||||||
|
|
||||||
|
errors = JSON::Validator.fully_validate(validation_schema, body)
|
||||||
|
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_schemas(metadata)
|
||||||
|
metadata[:operation] = {produces: []} if metadata[:operation].nil?
|
||||||
|
produces = Array(metadata[:operation][:produces])
|
||||||
|
|
||||||
|
producer_content = produces.first || 'application/json'
|
||||||
|
response_content = metadata[:response][:content] || {producer_content => {}}
|
||||||
|
response_content[producer_content]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class UnexpectedResponse < StandardError; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
102
rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb
Normal file
102
rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_support/core_ext/hash/deep_merge'
|
||||||
|
require 'swagger_helper'
|
||||||
|
|
||||||
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
|
module Specs
|
||||||
|
class SwaggerFormatter
|
||||||
|
# NOTE: rspec 2.x support
|
||||||
|
if RSPEC_VERSION > 2
|
||||||
|
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(output, config = ::OpenApi::Rswag::Specs.config)
|
||||||
|
@output = output
|
||||||
|
@config = config
|
||||||
|
|
||||||
|
@output.puts 'Generating Swagger docs ...'
|
||||||
|
end
|
||||||
|
|
||||||
|
def example_group_finished(notification)
|
||||||
|
# NOTE: rspec 2.x support
|
||||||
|
metadata = if RSPEC_VERSION > 2
|
||||||
|
notification.group.metadata
|
||||||
|
else
|
||||||
|
notification.metadata
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless metadata.key?(:response)
|
||||||
|
|
||||||
|
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
||||||
|
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop(_notification = nil)
|
||||||
|
@config.swagger_docs.each do |url_path, doc|
|
||||||
|
# remove 2.0 parameters
|
||||||
|
doc[:paths]&.each_pair do |_k, v|
|
||||||
|
v.each_pair do |_verb, value|
|
||||||
|
is_hash = value.is_a?(Hash)
|
||||||
|
if is_hash && value.dig(:parameters)
|
||||||
|
schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] }
|
||||||
|
if value && schema_param && value&.dig(:requestBody, :content, 'application/json')
|
||||||
|
value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema])
|
||||||
|
end
|
||||||
|
|
||||||
|
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
|
||||||
|
value[:parameters].each { |p| p.delete(:type) }
|
||||||
|
value[:headers].each { |p| p.delete(:type)} if value[:headers]
|
||||||
|
end
|
||||||
|
|
||||||
|
value.delete(:consumes) if is_hash && value.dig(:consumes)
|
||||||
|
value.delete(:produces) if is_hash && value.dig(:produces)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path = File.join(@config.swagger_root, url_path)
|
||||||
|
dirname = File.dirname(file_path)
|
||||||
|
FileUtils.mkdir_p dirname unless File.exist?(dirname)
|
||||||
|
|
||||||
|
File.open(file_path, 'w') do |file|
|
||||||
|
file.write(JSON.pretty_generate(doc))
|
||||||
|
end
|
||||||
|
|
||||||
|
@output.puts "Swagger doc generated at #{file_path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def metadata_to_swagger(metadata)
|
||||||
|
response_code = metadata[:response][:code]
|
||||||
|
response = metadata[:response].reject { |k, _v| k == :code }
|
||||||
|
|
||||||
|
# need to merge in to response
|
||||||
|
if response[:examples]&.dig('application/json')
|
||||||
|
example = response[:examples].dig('application/json').dup
|
||||||
|
schema = response.dig(:content, 'application/json', :schema)
|
||||||
|
new_hash = {example: example}
|
||||||
|
new_hash[:schema] = schema if schema
|
||||||
|
response.merge!(content: { 'application/json' => new_hash })
|
||||||
|
response.delete(:examples)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
verb = metadata[:operation][:verb]
|
||||||
|
operation = metadata[:operation]
|
||||||
|
.reject { |k, _v| k == :verb }
|
||||||
|
.merge(responses: { response_code => response })
|
||||||
|
|
||||||
|
path_template = metadata[:path_item][:template]
|
||||||
|
path_item = metadata[:path_item]
|
||||||
|
.reject { |k, _v| k == :template }
|
||||||
|
.merge(verb => operation)
|
||||||
|
|
||||||
|
{ paths: { path_template => path_item } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
require 'rspec/core'
|
|
||||||
require 'rswag/specs/example_group_helpers'
|
|
||||||
require 'rswag/specs/example_helpers'
|
|
||||||
require 'rswag/specs/configuration'
|
|
||||||
require 'rswag/specs/railtie' if defined?(Rails::Railtie)
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
|
|
||||||
# Extend RSpec with a swagger-based DSL
|
|
||||||
::RSpec.configure do |c|
|
|
||||||
c.add_setting :swagger_root
|
|
||||||
c.add_setting :swagger_docs
|
|
||||||
c.add_setting :swagger_dry_run
|
|
||||||
c.extend ExampleGroupHelpers, type: :request
|
|
||||||
c.include ExampleHelpers, type: :request
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.config
|
|
||||||
@config ||= Configuration.new(RSpec.configuration)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Support Rails 3+ and RSpec 2+ (sigh!)
|
|
||||||
RAILS_VERSION = Rails::VERSION::MAJOR
|
|
||||||
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class Configuration
|
|
||||||
def initialize(rspec_config)
|
|
||||||
@rspec_config = rspec_config
|
|
||||||
end
|
|
||||||
|
|
||||||
def swagger_root
|
|
||||||
@swagger_root ||= begin
|
|
||||||
if @rspec_config.swagger_root.nil?
|
|
||||||
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
|
|
||||||
end
|
|
||||||
|
|
||||||
@rspec_config.swagger_root
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def swagger_docs
|
|
||||||
@swagger_docs ||= begin
|
|
||||||
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
|
|
||||||
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
|
|
||||||
end
|
|
||||||
|
|
||||||
@rspec_config.swagger_docs
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def swagger_dry_run
|
|
||||||
@swagger_dry_run ||= begin
|
|
||||||
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_swagger_doc(name)
|
|
||||||
return swagger_docs.values.first if name.nil?
|
|
||||||
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
|
|
||||||
|
|
||||||
swagger_docs[name]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ConfigurationError < StandardError; end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
require 'hashie'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
module ExampleGroupHelpers
|
|
||||||
def path(template, metadata = {}, &block)
|
|
||||||
metadata[:path_item] = { template: template }
|
|
||||||
describe(template, metadata, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
%i[get post patch put delete head].each do |verb|
|
|
||||||
define_method(verb) do |summary, &block|
|
|
||||||
api_metadata = { operation: { verb: verb, summary: summary } }
|
|
||||||
describe(verb, api_metadata, &block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
%i[operationId deprecated security].each do |attr_name|
|
|
||||||
define_method(attr_name) do |value|
|
|
||||||
metadata[:operation][attr_name] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# NOTE: 'description' requires special treatment because ExampleGroup already
|
|
||||||
# defines a method with that name. Provide an override that supports the existing
|
|
||||||
# functionality while also setting the appropriate metadata if applicable
|
|
||||||
def description(value = nil)
|
|
||||||
return super() if value.nil?
|
|
||||||
|
|
||||||
metadata[:operation][:description] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
# These are array properties - note the splat operator
|
|
||||||
%i[tags consumes produces schemes].each do |attr_name|
|
|
||||||
define_method(attr_name) do |*value|
|
|
||||||
metadata[:operation][attr_name] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# NICE TO HAVE
|
|
||||||
# TODO: update generator templates to include 3.0 syntax
|
|
||||||
# TODO: setup travis CI?
|
|
||||||
|
|
||||||
# MUST HAVES
|
|
||||||
# TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response
|
|
||||||
# can we handle all of them?
|
|
||||||
# Then can look at handling different request_body things like $ref, etc
|
|
||||||
# TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0
|
|
||||||
# TODO: look at adding examples in content request_body
|
|
||||||
# https://swagger.io/docs/specification/describing-request-body/
|
|
||||||
# need to make sure we output requestBody in the swagger generator .json
|
|
||||||
# also need to make sure that it can handle content: , required: true/false, schema: ref
|
|
||||||
|
|
||||||
def request_body(attributes)
|
|
||||||
# can make this generic, and accept any incoming hash (like parameter method)
|
|
||||||
attributes.compact!
|
|
||||||
|
|
||||||
if metadata[:operation][:requestBody].blank?
|
|
||||||
metadata[:operation][:requestBody] = attributes
|
|
||||||
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
|
|
||||||
# merge in
|
|
||||||
content_hash = metadata[:operation][:requestBody][:content]
|
|
||||||
incoming_content_hash = attributes[:content]
|
|
||||||
content_hash.merge!(incoming_content_hash) if incoming_content_hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_body_json(schema:, required: true, description: nil, examples: nil)
|
|
||||||
passed_examples = Array(examples)
|
|
||||||
content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
|
|
||||||
request_body(description: description, required: required, content: content_hash)
|
|
||||||
if passed_examples.any?
|
|
||||||
# the request_factory is going to have to resolve the different ways that the example can be given
|
|
||||||
# it can contain a 'value' key which is a direct hash (easiest)
|
|
||||||
# it can contain a 'external_value' key which makes an external call to load the json
|
|
||||||
# it can contain a '$ref' key. Which points to #/components/examples/blog
|
|
||||||
passed_examples.each do |passed_example|
|
|
||||||
if passed_example.is_a?(Symbol)
|
|
||||||
example_key_name = passed_example
|
|
||||||
# TODO: write more tests around this adding to the parameter
|
|
||||||
# if symbol try and use save_request_example
|
|
||||||
param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
|
|
||||||
parameter(param_attributes)
|
|
||||||
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
|
|
||||||
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
|
|
||||||
parameter(param_attributes)
|
|
||||||
elsif passed_example.is_a?(Hash) && passed_example['$ref']
|
|
||||||
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
|
|
||||||
parameter(param_attributes)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_body_text_plain(required: false, description: nil, examples: nil)
|
|
||||||
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
|
|
||||||
request_body(description: description, required: required, content: content_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
|
|
||||||
def request_body_xml(schema:,required: false, description: nil, examples: nil)
|
|
||||||
passed_examples = Array(examples)
|
|
||||||
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
|
|
||||||
request_body(description: description, required: required, content: content_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_body_multipart(schema:, description: nil)
|
|
||||||
content_hash = { 'multipart/form-data' => { schema: schema }}
|
|
||||||
request_body(description: description, content: content_hash)
|
|
||||||
|
|
||||||
schema.extend(Hashie::Extensions::DeepLocate)
|
|
||||||
file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary }
|
|
||||||
|
|
||||||
hash_locator = []
|
|
||||||
|
|
||||||
file_properties.each do |match|
|
|
||||||
hash_match = schema.deep_locate -> (_k, v, _obj) { v == match }
|
|
||||||
hash_locator.concat(hash_match) unless hash_match.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
property_hashes = hash_locator.flat_map do |locator|
|
|
||||||
locator.select { |_k,v| file_properties.include?(v) }
|
|
||||||
end
|
|
||||||
|
|
||||||
property_hashes.each do |property_hash|
|
|
||||||
file_name = property_hash.keys.first
|
|
||||||
parameter name: file_name, in: :formData, type: :file, required: true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parameter(attributes)
|
|
||||||
if attributes[:in] && attributes[:in].to_sym == :path
|
|
||||||
attributes[:required] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if attributes[:type] && attributes[:schema].nil?
|
|
||||||
attributes[:schema] = {type: attributes[:type]}
|
|
||||||
end
|
|
||||||
|
|
||||||
if metadata.key?(:operation)
|
|
||||||
metadata[:operation][:parameters] ||= []
|
|
||||||
metadata[:operation][:parameters] << attributes
|
|
||||||
else
|
|
||||||
metadata[:path_item][:parameters] ||= []
|
|
||||||
metadata[:path_item][:parameters] << attributes
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def response(code, description, metadata = {}, &block)
|
|
||||||
metadata[:response] = { code: code, description: description }
|
|
||||||
context(description, metadata, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def schema(value, content_type: 'application/json')
|
|
||||||
content_hash = {content_type => {schema: value}}
|
|
||||||
metadata[:response][:content] = content_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def header(name, attributes)
|
|
||||||
metadata[:response][:headers] ||= {}
|
|
||||||
|
|
||||||
if attributes[:type] && attributes[:schema].nil?
|
|
||||||
attributes[:schema] = {type: attributes[:type]}
|
|
||||||
attributes.delete(:type)
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata[:response][:headers][name] = attributes
|
|
||||||
end
|
|
||||||
|
|
||||||
# NOTE: Similar to 'description', 'examples' need to handle the case when
|
|
||||||
# being invoked with no params to avoid overriding 'examples' method of
|
|
||||||
# rspec-core ExampleGroup
|
|
||||||
def examples(example = nil)
|
|
||||||
return super() if example.nil?
|
|
||||||
|
|
||||||
metadata[:response][:examples] = example
|
|
||||||
end
|
|
||||||
|
|
||||||
# checks the examples in the parameters should be able to add $ref and externalValue examples.
|
|
||||||
# This syntax would look something like this in the integration _spec.rb file
|
|
||||||
#
|
|
||||||
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
|
|
||||||
# examples: [:blog, {name: :external_blog,
|
|
||||||
# externalValue: 'http://api.sample.org/myjson_example'},
|
|
||||||
# {name: :another_example,
|
|
||||||
# '$ref' => '#/components/examples/flexible_blog_example'}]
|
|
||||||
# The first value :blog, points to a let param of the same name, and is used to make the request in the
|
|
||||||
# integration test (it is used to build the request payload)
|
|
||||||
#
|
|
||||||
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
|
|
||||||
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
|
|
||||||
#
|
|
||||||
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
|
|
||||||
# will not show it yet
|
|
||||||
def merge_other_examples!(example_metadata)
|
|
||||||
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
|
||||||
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
|
|
||||||
return unless content_node
|
|
||||||
|
|
||||||
external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
|
|
||||||
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
|
|
||||||
examples_node = content_node[:examples] ||= {}
|
|
||||||
|
|
||||||
nodes_to_add = []
|
|
||||||
nodes_to_add << external_example unless external_example.empty?
|
|
||||||
nodes_to_add << ref_example unless ref_example.empty?
|
|
||||||
|
|
||||||
nodes_to_add.each do |node|
|
|
||||||
json_request_examples = examples_node ||= {}
|
|
||||||
other_name = node[:name][:name]
|
|
||||||
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
|
|
||||||
if other_name
|
|
||||||
json_request_examples.merge!(other_name => {other_key => node[:param_value]})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_test!(&block)
|
|
||||||
# NOTE: rspec 2.x support
|
|
||||||
if RSPEC_VERSION < 3
|
|
||||||
before do
|
|
||||||
submit_request(example.metadata)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a #{metadata[:response][:code]} response" do
|
|
||||||
assert_response_matches_metadata(metadata)
|
|
||||||
block.call(response) if block_given?
|
|
||||||
end
|
|
||||||
else
|
|
||||||
before do |example|
|
|
||||||
submit_request(example.metadata) #
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a #{metadata[:response][:code]} response" do |example|
|
|
||||||
assert_response_matches_metadata(example.metadata, &block)
|
|
||||||
example.instance_exec(response, &block) if block_given?
|
|
||||||
end
|
|
||||||
|
|
||||||
after do |example|
|
|
||||||
body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] }
|
|
||||||
|
|
||||||
if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json']
|
|
||||||
# save response examples by default
|
|
||||||
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty?
|
|
||||||
|
|
||||||
# save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
|
|
||||||
if response.code.to_s =~ /^2\d{2}$/
|
|
||||||
example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
|
||||||
json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
|
||||||
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
|
|
||||||
|
|
||||||
example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rswag/specs/request_factory'
|
|
||||||
require 'rswag/specs/response_validator'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
module ExampleHelpers
|
|
||||||
def submit_request(metadata)
|
|
||||||
request = RequestFactory.new.build_request(metadata, self)
|
|
||||||
|
|
||||||
if RAILS_VERSION < 5
|
|
||||||
send(
|
|
||||||
request[:verb],
|
|
||||||
request[:path],
|
|
||||||
request[:payload],
|
|
||||||
request[:headers]
|
|
||||||
)
|
|
||||||
else
|
|
||||||
send(
|
|
||||||
request[:verb],
|
|
||||||
request[:path],
|
|
||||||
params: request[:payload],
|
|
||||||
headers: request[:headers]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_response_matches_metadata(metadata)
|
|
||||||
ResponseValidator.new.validate!(metadata, response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'json-schema'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class ExtendedSchema < JSON::Schema::Draft4
|
|
||||||
def initialize
|
|
||||||
super
|
|
||||||
@attributes['type'] = ExtendedTypeAttribute
|
|
||||||
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
|
|
||||||
@names = ['http://tempuri.org/rswag/specs/extended_schema']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
|
|
||||||
def self.validate(current_schema, data, fragments, processor, validator, options = {})
|
|
||||||
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
JSON::Validator.register_validator(ExtendedSchema.new)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class Railtie < ::Rails::Railtie
|
|
||||||
rake_tasks do
|
|
||||||
load File.expand_path('../../tasks/rswag-specs_tasks.rake', __dir__)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'active_support/core_ext/hash/slice'
|
|
||||||
require 'active_support/core_ext/hash/conversions'
|
|
||||||
require 'json'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class RequestFactory
|
|
||||||
def initialize(config = ::Rswag::Specs.config)
|
|
||||||
@config = config
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_request(metadata, example)
|
|
||||||
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
|
||||||
parameters = expand_parameters(metadata, swagger_doc, example)
|
|
||||||
|
|
||||||
{}.tap do |request|
|
|
||||||
add_verb(request, metadata)
|
|
||||||
add_path(request, metadata, swagger_doc, parameters, example)
|
|
||||||
add_headers(request, metadata, swagger_doc, parameters, example)
|
|
||||||
add_payload(request, parameters, example)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def expand_parameters(metadata, swagger_doc, example)
|
|
||||||
operation_params = metadata[:operation][:parameters] || []
|
|
||||||
path_item_params = metadata[:path_item][:parameters] || []
|
|
||||||
security_params = derive_security_params(metadata, swagger_doc)
|
|
||||||
|
|
||||||
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
|
|
||||||
(operation_params + path_item_params + security_params)
|
|
||||||
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
|
|
||||||
.uniq { |p| p[:name] }
|
|
||||||
.reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def derive_security_params(metadata, swagger_doc)
|
|
||||||
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
|
|
||||||
scheme_names = requirements.flat_map(&:keys)
|
|
||||||
components = swagger_doc[:components] || {}
|
|
||||||
schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values
|
|
||||||
|
|
||||||
schemes.map do |scheme|
|
|
||||||
param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
|
|
||||||
param.merge(type: :string, required: requirements.one?)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve_parameter(ref, swagger_doc)
|
|
||||||
key = ref.sub('#/parameters/', '').to_sym
|
|
||||||
definitions = swagger_doc[:parameters]
|
|
||||||
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
|
|
||||||
|
|
||||||
definitions[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_verb(request, metadata)
|
|
||||||
request[:verb] = metadata[:operation][:verb]
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_path(request, metadata, swagger_doc, parameters, example)
|
|
||||||
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
|
|
||||||
|
|
||||||
request[:path] = template.tap do |template|
|
|
||||||
parameters.select { |p| p[:in] == :path }.each do |p|
|
|
||||||
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
|
|
||||||
template.concat(i == 0 ? '?' : '&')
|
|
||||||
template.concat(build_query_string_part(p, example.send(p[:name])))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_query_string_part(param, value)
|
|
||||||
name = param[:name]
|
|
||||||
return "#{name}=#{value}" unless param[:type].to_sym == :array
|
|
||||||
|
|
||||||
case param[:collectionFormat]
|
|
||||||
when :ssv
|
|
||||||
"#{name}=#{value.join(' ')}"
|
|
||||||
when :tsv
|
|
||||||
"#{name}=#{value.join('\t')}"
|
|
||||||
when :pipes
|
|
||||||
"#{name}=#{value.join('|')}"
|
|
||||||
when :multi
|
|
||||||
value.map { |v| "#{name}=#{v}" }.join('&')
|
|
||||||
else
|
|
||||||
"#{name}=#{value.join(',')}" # csv is default
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_headers(request, metadata, swagger_doc, parameters, example)
|
|
||||||
tuples = parameters
|
|
||||||
.select { |p| p[:in] == :header }
|
|
||||||
.map { |p| [p[:name], example.send(p[:name]).to_s] }
|
|
||||||
|
|
||||||
# Accept header
|
|
||||||
produces = metadata[:operation][:produces] || swagger_doc[:produces]
|
|
||||||
if produces
|
|
||||||
accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
|
|
||||||
tuples << ['Accept', accept]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Content-Type header
|
|
||||||
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
|
|
||||||
if consumes
|
|
||||||
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
|
|
||||||
tuples << ['Content-Type', content_type]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Rails test infrastructure requires rackified headers
|
|
||||||
rackified_tuples = tuples.map do |pair|
|
|
||||||
[
|
|
||||||
case pair[0]
|
|
||||||
when 'Accept' then 'HTTP_ACCEPT'
|
|
||||||
when 'Content-Type' then 'CONTENT_TYPE'
|
|
||||||
when 'Authorization' then 'HTTP_AUTHORIZATION'
|
|
||||||
else pair[0]
|
|
||||||
end,
|
|
||||||
pair[1]
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
request[:headers] = Hash[rackified_tuples]
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_payload(request, parameters, example)
|
|
||||||
content_type = request[:headers]['CONTENT_TYPE']
|
|
||||||
return if content_type.nil?
|
|
||||||
|
|
||||||
if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
|
|
||||||
request[:payload] = build_form_payload(parameters, example)
|
|
||||||
else
|
|
||||||
request[:payload] = build_json_payload(parameters, example)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_form_payload(parameters, example)
|
|
||||||
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
|
|
||||||
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
|
|
||||||
# Rails test infrastructure allows us to send the values directly as a hash
|
|
||||||
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
|
|
||||||
tuples = parameters
|
|
||||||
.select { |p| p[:in] == :formData }
|
|
||||||
.map { |p| [p[:name], example.send(p[:name])] }
|
|
||||||
Hash[tuples]
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_json_payload(parameters, example)
|
|
||||||
body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
|
|
||||||
return nil unless body_param
|
|
||||||
|
|
||||||
source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
|
|
||||||
source_body_param ||= body_param[:param_value]
|
|
||||||
source_body_param ? source_body_param.to_json : nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'active_support/core_ext/hash/slice'
|
|
||||||
require 'json-schema'
|
|
||||||
require 'json'
|
|
||||||
require 'rswag/specs/extended_schema'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class ResponseValidator
|
|
||||||
def initialize(config = ::Rswag::Specs.config)
|
|
||||||
@config = config
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate!(metadata, response)
|
|
||||||
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
|
||||||
|
|
||||||
validate_code!(metadata, response)
|
|
||||||
validate_headers!(metadata, response.headers)
|
|
||||||
validate_body!(metadata, swagger_doc, response.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validate_code!(metadata, response)
|
|
||||||
expected = metadata[:response][:code].to_s
|
|
||||||
if response.code != expected
|
|
||||||
raise UnexpectedResponse,
|
|
||||||
"Expected response code '#{response.code}' to match '#{expected}'\n" \
|
|
||||||
"Response body: #{response.body}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_headers!(metadata, headers)
|
|
||||||
expected = (metadata[:response][:headers] || {}).keys
|
|
||||||
expected.each do |name|
|
|
||||||
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_body!(metadata, swagger_doc, body)
|
|
||||||
test_schemas = extract_schemas(metadata)
|
|
||||||
return if test_schemas.nil?
|
|
||||||
|
|
||||||
components = swagger_doc[:components] || {}
|
|
||||||
components_schemas = { components: { schemas: components[:schemas] } }
|
|
||||||
|
|
||||||
validation_schema = test_schemas[:schema] # response_schema
|
|
||||||
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
|
|
||||||
.merge(components_schemas)
|
|
||||||
errors = JSON::Validator.fully_validate(validation_schema, body)
|
|
||||||
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def extract_schemas(metadata)
|
|
||||||
produces = Array(metadata[:operation][:produces])
|
|
||||||
response_content = metadata[:response][:content] || {}
|
|
||||||
producer_content = produces.first || 'application/json'
|
|
||||||
response_content[producer_content]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class UnexpectedResponse < StandardError; end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'active_support/core_ext/hash/deep_merge'
|
|
||||||
require 'swagger_helper'
|
|
||||||
|
|
||||||
module Rswag
|
|
||||||
module Specs
|
|
||||||
class SwaggerFormatter
|
|
||||||
# NOTE: rspec 2.x support
|
|
||||||
if RSPEC_VERSION > 2
|
|
||||||
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(output, config = Rswag::Specs.config)
|
|
||||||
@output = output
|
|
||||||
@config = config
|
|
||||||
|
|
||||||
@output.puts 'Generating Swagger docs ...'
|
|
||||||
end
|
|
||||||
|
|
||||||
def example_group_finished(notification)
|
|
||||||
# NOTE: rspec 2.x support
|
|
||||||
metadata = if RSPEC_VERSION > 2
|
|
||||||
notification.group.metadata
|
|
||||||
else
|
|
||||||
notification.metadata
|
|
||||||
end
|
|
||||||
|
|
||||||
return unless metadata.key?(:response)
|
|
||||||
|
|
||||||
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
|
||||||
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop(_notification = nil)
|
|
||||||
@config.swagger_docs.each do |url_path, doc|
|
|
||||||
# remove 2.0 parameters
|
|
||||||
doc[:paths]&.each_pair do |_k, v|
|
|
||||||
v.each_pair do |_verb, value|
|
|
||||||
is_hash = value.is_a?(Hash)
|
|
||||||
if is_hash && value.dig(:parameters)
|
|
||||||
schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] }
|
|
||||||
if value && schema_param && value&.dig(:requestBody, :content, 'application/json')
|
|
||||||
value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema])
|
|
||||||
end
|
|
||||||
|
|
||||||
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
|
|
||||||
value[:parameters].each { |p| p.delete(:type) }
|
|
||||||
value[:headers].each { |p| p.delete(:type)} if value[:headers]
|
|
||||||
end
|
|
||||||
|
|
||||||
value.delete(:consumes) if is_hash && value.dig(:consumes)
|
|
||||||
value.delete(:produces) if is_hash && value.dig(:produces)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
file_path = File.join(@config.swagger_root, url_path)
|
|
||||||
dirname = File.dirname(file_path)
|
|
||||||
FileUtils.mkdir_p dirname unless File.exist?(dirname)
|
|
||||||
|
|
||||||
File.open(file_path, 'w') do |file|
|
|
||||||
file.write(JSON.pretty_generate(doc))
|
|
||||||
end
|
|
||||||
|
|
||||||
@output.puts "Swagger doc generated at #{file_path}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def metadata_to_swagger(metadata)
|
|
||||||
response_code = metadata[:response][:code]
|
|
||||||
response = metadata[:response].reject { |k, _v| k == :code }
|
|
||||||
|
|
||||||
# need to merge in to response
|
|
||||||
if response[:examples]&.dig('application/json')
|
|
||||||
example = response[:examples].dig('application/json').dup
|
|
||||||
schema = response.dig(:content, 'application/json', :schema)
|
|
||||||
new_hash = {example: example}
|
|
||||||
new_hash[:schema] = schema if schema
|
|
||||||
response.merge!(content: { 'application/json' => new_hash })
|
|
||||||
response.delete(:examples)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
verb = metadata[:operation][:verb]
|
|
||||||
operation = metadata[:operation]
|
|
||||||
.reject { |k, _v| k == :verb }
|
|
||||||
.merge(responses: { response_code => response })
|
|
||||||
|
|
||||||
path_template = metadata[:path_item][:template]
|
|
||||||
path_item = metadata[:path_item]
|
|
||||||
.reject { |k, _v| k == :template }
|
|
||||||
.merge(verb => operation)
|
|
||||||
|
|
||||||
{ paths: { path_template => path_item } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -6,9 +6,9 @@ $LOAD_PATH.push File.expand_path('lib', __dir__)
|
|||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.name = 'rswag-specs'
|
s.name = 'rswag-specs'
|
||||||
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
|
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
|
||||||
s.authors = ['Richie Morris']
|
s.authors = ['Richie Morris', 'Jay Danielian']
|
||||||
s.email = ['domaindrivendev@gmail.com']
|
s.email = ['domaindrivendev@gmail.com']
|
||||||
s.homepage = 'https://github.com/domaindrivendev/rswag'
|
s.homepage = 'https://github.com/jdanielian/rswag'
|
||||||
s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files'
|
s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files'
|
||||||
s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs'
|
s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs'
|
||||||
s.license = 'MIT'
|
s.license = 'MIT'
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/configuration'
|
require 'open_api/rswag/specs/configuration'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe Configuration do
|
describe Configuration do
|
||||||
subject { described_class.new(rspec_config) }
|
subject { described_class.new(rspec_config) }
|
||||||
@@ -75,4 +76,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/example_group_helpers'
|
require 'open_api/rswag/specs/example_group_helpers'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe ExampleGroupHelpers do
|
describe ExampleGroupHelpers do
|
||||||
subject { double('example_group') }
|
subject { double('example_group') }
|
||||||
@@ -214,4 +215,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/example_helpers'
|
require 'open_api/rswag/specs/example_helpers'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe ExampleHelpers do
|
describe ExampleHelpers do
|
||||||
subject { double('example') }
|
subject { double('example') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.extend(ExampleHelpers)
|
subject.extend(ExampleHelpers)
|
||||||
allow(Rswag::Specs).to receive(:config).and_return(config)
|
allow(OpenApi::Rswag::Specs).to receive(:config).and_return(config)
|
||||||
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
|
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
|
||||||
stub_const('Rswag::Specs::RAILS_VERSION', 3)
|
stub_const('Rswag::Specs::RAILS_VERSION', 3)
|
||||||
end
|
end
|
||||||
@@ -68,4 +69,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/request_factory'
|
require 'open_api/rswag/specs/request_factory'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe RequestFactory do
|
describe RequestFactory do
|
||||||
subject { RequestFactory.new(config) }
|
subject { RequestFactory.new(config) }
|
||||||
@@ -151,7 +152,7 @@ module Rswag
|
|||||||
|
|
||||||
context 'JSON payload' do
|
context 'JSON payload' do
|
||||||
before do
|
before do
|
||||||
metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }]
|
metadata[:operation][:parameters] = [{ name: :comment, in: :body, schema: { type: 'object' } }]
|
||||||
allow(example).to receive(:comment).and_return(text: 'Some comment')
|
allow(example).to receive(:comment).and_return(text: 'Some comment')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -349,4 +350,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/response_validator'
|
require 'open_api/rswag/specs/response_validator'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe ResponseValidator do
|
describe ResponseValidator do
|
||||||
subject { ResponseValidator.new(config) }
|
subject { ResponseValidator.new(config) }
|
||||||
@@ -18,6 +19,8 @@ module Rswag
|
|||||||
response: {
|
response: {
|
||||||
code: 200,
|
code: 200,
|
||||||
headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
|
headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
|
||||||
|
content:
|
||||||
|
{'application/json' => {
|
||||||
schema: {
|
schema: {
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: { text: { type: :string } },
|
properties: { text: { type: :string } },
|
||||||
@@ -25,6 +28,8 @@ module Rswag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#validate!(metadata, response)' do
|
describe '#validate!(metadata, response)' do
|
||||||
@@ -66,7 +71,7 @@ module Rswag
|
|||||||
required: ['foo']
|
required: ['foo']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata[:response][:schema] = { '$ref' => '#/components/schemas/blog' }
|
metadata[:response][:content]['application/json'][:schema] = { '$ref' => '#/components/schemas/blog' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'uses the referenced schema to validate the response body' do
|
it 'uses the referenced schema to validate the response body' do
|
||||||
@@ -76,4 +81,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rswag/specs/swagger_formatter'
|
require 'open_api/rswag/specs/swagger_formatter'
|
||||||
require 'ostruct'
|
require 'ostruct'
|
||||||
|
|
||||||
module Rswag
|
module OpenApi
|
||||||
|
module Rswag
|
||||||
module Specs
|
module Specs
|
||||||
describe SwaggerFormatter do
|
describe SwaggerFormatter do
|
||||||
subject { described_class.new(output, config) }
|
subject { described_class.new(output, config) }
|
||||||
@@ -70,4 +71,5 @@ module Rswag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ module Rails
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require 'rswag/specs'
|
require 'open_api/rswag/specs'
|
||||||
|
|||||||
Reference in New Issue
Block a user