openapi-rswag-specs 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +27 -0
- data/lib/generators/rspec/USAGE +9 -0
- data/lib/generators/rspec/swagger_generator.rb +22 -0
- data/lib/generators/rspec/templates/spec.rb +30 -0
- data/lib/generators/rswag/specs/install/USAGE +8 -0
- data/lib/generators/rswag/specs/install/install_generator.rb +14 -0
- data/lib/generators/rswag/specs/install/templates/swagger_helper.rb +31 -0
- data/lib/openapi/rswag/route_parser.rb +60 -0
- data/lib/openapi/rswag/specs.rb +30 -0
- data/lib/openapi/rswag/specs/configuration.rb +53 -0
- data/lib/openapi/rswag/specs/example_group_helpers.rb +100 -0
- data/lib/openapi/rswag/specs/example_helpers.rb +37 -0
- data/lib/openapi/rswag/specs/extended_schema.rb +27 -0
- data/lib/openapi/rswag/specs/railtie.rb +16 -0
- data/lib/openapi/rswag/specs/request_factory.rb +159 -0
- data/lib/openapi/rswag/specs/response_validator.rb +56 -0
- data/lib/openapi/rswag/specs/swagger_formatter.rb +87 -0
- data/lib/tasks/rswag-specs_tasks.rake +23 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7fe4364f6d0dabffc857791ff28c23dc133635743b276d7131099e2b981ebcc2
|
4
|
+
data.tar.gz: c2a7f905dfed6852891d1b5a6bdaa4720efb7a3bada3c5b5b735f0924374ea5e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 925d424f35bf7d0d8d58766e6b2b7752be4f0a1b414dbc2757fe7af12e70bcdc624ad61e009a6b82427a570835ab721ec94d454c0481dce87f36510aa9d9a95e
|
7
|
+
data.tar.gz: 4ae03e3e2f9a7e851fa0e3d8115dcc8d536b7f7648ce409bb57ed3d3e4a96917a80500a507c327387d5a59612c270783dc73c4ff059d3fe717e6b3b499ebc984
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 domaindrivendev
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'rswag-specs'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Description:
|
2
|
+
This creates an RSpec request spec to define Swagger documentation for a
|
3
|
+
controller. It will create a test for each of the controller's methods.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
rails generate rspec:swagger V3::AccountsController
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
spec/requests/v3/accounts_spec.rb
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'openapi/rswag/route_parser'
|
2
|
+
require 'rails/generators'
|
3
|
+
|
4
|
+
module Rspec
|
5
|
+
class SwaggerGenerator < ::Rails::Generators::NamedBase
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@routes = Openapi::Rswag::RouteParser.new(controller_path).routes
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_spec_file
|
13
|
+
template 'spec.rb', File.join('spec', 'requests', "#{controller_path}_spec.rb")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def controller_path
|
19
|
+
file_path.chomp('_controller')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'swagger_helper'
|
2
|
+
|
3
|
+
RSpec.describe '<%= controller_path %>', type: :request do
|
4
|
+
<% @routes.each do | template, path_item | %>
|
5
|
+
path '<%= template %>' do
|
6
|
+
<% unless path_item[:params].empty? -%>
|
7
|
+
# You'll want to customize the parameter types...
|
8
|
+
<% path_item[:params].each do |param| -%>
|
9
|
+
parameter name: '<%= param %>', in: :path, type: :string, description: '<%= param %>'
|
10
|
+
<% end -%>
|
11
|
+
<% end -%>
|
12
|
+
<% path_item[:actions].each do | action, details | %>
|
13
|
+
<%= action %>('<%= details[:summary] %>') do
|
14
|
+
response(200, 'successful') do
|
15
|
+
<% unless path_item[:params].empty? -%>
|
16
|
+
<% path_item[:params].each do |param| -%>
|
17
|
+
let(:<%= param %>) { '123' }
|
18
|
+
<% end -%>
|
19
|
+
<% end -%>
|
20
|
+
|
21
|
+
after do |example|
|
22
|
+
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
|
23
|
+
end
|
24
|
+
run_test!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
<% end -%>
|
28
|
+
end
|
29
|
+
<% end -%>
|
30
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Rswag
|
4
|
+
module Specs
|
5
|
+
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
8
|
+
|
9
|
+
def add_swagger_helper
|
10
|
+
template('swagger_helper.rb', 'spec/swagger_helper.rb')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
# Specify a root folder where Swagger JSON files are generated
|
5
|
+
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
6
|
+
# to ensure that it's configured to serve Swagger from the same folder
|
7
|
+
config.swagger_root = Rails.root.join('swagger').to_s
|
8
|
+
|
9
|
+
# Define one or more Swagger documents and provide global metadata for each one
|
10
|
+
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
|
11
|
+
# be generated at the provided relative path under swagger_root
|
12
|
+
# By default, the operations defined in spec files are added to the first
|
13
|
+
# document below. You can override this behavior by adding a swagger_doc tag to the
|
14
|
+
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
|
15
|
+
config.swagger_docs = {
|
16
|
+
'v1/swagger.yaml' => {
|
17
|
+
openapi: '3.0.1',
|
18
|
+
info: {
|
19
|
+
title: 'API V1',
|
20
|
+
version: 'v1'
|
21
|
+
},
|
22
|
+
paths: {}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
|
27
|
+
# The swagger_docs configuration option has the filename including format in
|
28
|
+
# the key, this may want to be changed to avoid putting yaml in json files.
|
29
|
+
# Defaults to json. Accepts ':json' and ':yaml'.
|
30
|
+
config.swagger_format = :yaml
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Openapi
|
2
|
+
module Rswag
|
3
|
+
class RouteParser
|
4
|
+
attr_reader :controller
|
5
|
+
|
6
|
+
def initialize(controller)
|
7
|
+
@controller = controller
|
8
|
+
end
|
9
|
+
|
10
|
+
def routes
|
11
|
+
::Rails.application.routes.routes.select do |route|
|
12
|
+
route.defaults[:controller] == controller
|
13
|
+
end.reduce({}) do |tree, route|
|
14
|
+
path = path_from(route)
|
15
|
+
verb = verb_from(route)
|
16
|
+
tree[path] ||= { params: params_from(route), actions: {} }
|
17
|
+
tree[path][:actions][verb] = { summary: summary_from(route) }
|
18
|
+
tree
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def path_from(route)
|
25
|
+
route.path.spec.to_s
|
26
|
+
.chomp('(.:format)') # Ignore any format suffix
|
27
|
+
.gsub(/:([^\/.?]+)/, '{\1}') # Convert :id to {id}
|
28
|
+
end
|
29
|
+
|
30
|
+
def verb_from(route)
|
31
|
+
verb = route.verb
|
32
|
+
if verb.kind_of? String
|
33
|
+
verb.downcase
|
34
|
+
else
|
35
|
+
verb.source.gsub(/[$^]/, '').downcase
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def summary_from(route)
|
40
|
+
verb = route.requirements[:action]
|
41
|
+
noun = route.requirements[:controller].split('/').last.singularize
|
42
|
+
|
43
|
+
# Apply a few customizations to make things more readable
|
44
|
+
case verb
|
45
|
+
when 'index'
|
46
|
+
verb = 'list'
|
47
|
+
noun = noun.pluralize
|
48
|
+
when 'destroy'
|
49
|
+
verb = 'delete'
|
50
|
+
end
|
51
|
+
|
52
|
+
"#{verb} #{noun}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def params_from(route)
|
56
|
+
route.segments - ['format']
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
require 'openapi/rswag/specs/example_group_helpers'
|
3
|
+
require 'openapi/rswag/specs/example_helpers'
|
4
|
+
require 'openapi/rswag/specs/configuration'
|
5
|
+
require 'openapi/rswag/specs/railtie' if defined?(Rails::Railtie)
|
6
|
+
|
7
|
+
module Openapi
|
8
|
+
module Rswag
|
9
|
+
module Specs
|
10
|
+
|
11
|
+
# Extend RSpec with a swagger-based DSL
|
12
|
+
::RSpec.configure do |c|
|
13
|
+
c.add_setting :swagger_root
|
14
|
+
c.add_setting :swagger_docs
|
15
|
+
c.add_setting :swagger_dry_run
|
16
|
+
c.add_setting :swagger_format
|
17
|
+
c.extend ExampleGroupHelpers, type: :request
|
18
|
+
c.include ExampleHelpers, type: :request
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
@config ||= Configuration.new(RSpec.configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Support Rails 3+ and RSpec 2+ (sigh!)
|
26
|
+
RAILS_VERSION = Rails::VERSION::MAJOR
|
27
|
+
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Openapi
|
2
|
+
module Rswag
|
3
|
+
module Specs
|
4
|
+
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
def initialize(rspec_config)
|
8
|
+
@rspec_config = rspec_config
|
9
|
+
end
|
10
|
+
|
11
|
+
def swagger_root
|
12
|
+
@swagger_root ||= begin
|
13
|
+
if @rspec_config.swagger_root.nil?
|
14
|
+
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
|
15
|
+
end
|
16
|
+
@rspec_config.swagger_root
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def swagger_docs
|
21
|
+
@swagger_docs ||= begin
|
22
|
+
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
|
23
|
+
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
|
24
|
+
end
|
25
|
+
@rspec_config.swagger_docs
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def swagger_dry_run
|
30
|
+
@swagger_dry_run ||= begin
|
31
|
+
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def swagger_format
|
36
|
+
@swagger_format ||= begin
|
37
|
+
@rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty?
|
38
|
+
raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
|
39
|
+
@rspec_config.swagger_format
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_swagger_doc(name)
|
44
|
+
return swagger_docs.values.first if name.nil?
|
45
|
+
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
|
46
|
+
swagger_docs[name]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ConfigurationError < StandardError; end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Openapi
|
2
|
+
module Rswag
|
3
|
+
module Specs
|
4
|
+
module ExampleGroupHelpers
|
5
|
+
|
6
|
+
def path(template, metadata={}, &block)
|
7
|
+
metadata[:path_item] = { template: template }
|
8
|
+
describe(template, metadata, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
[ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb|
|
12
|
+
define_method(verb) do |summary, &block|
|
13
|
+
api_metadata = { operation: { verb: verb, summary: summary } }
|
14
|
+
describe(verb, api_metadata, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
[ :operationId, :deprecated, :security ].each do |attr_name|
|
19
|
+
define_method(attr_name) do |value|
|
20
|
+
metadata[:operation][attr_name] = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# NOTE: 'description' requires special treatment because ExampleGroup already
|
25
|
+
# defines a method with that name. Provide an override that supports the existing
|
26
|
+
# functionality while also setting the appropriate metadata if applicable
|
27
|
+
def description(value=nil)
|
28
|
+
return super() if value.nil?
|
29
|
+
metadata[:operation][:description] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
# These are array properties - note the splat operator
|
33
|
+
[ :tags, :consumes, :produces, :schemes ].each do |attr_name|
|
34
|
+
define_method(attr_name) do |*value|
|
35
|
+
metadata[:operation][attr_name] = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parameter(attributes)
|
40
|
+
if attributes[:in] && attributes[:in].to_sym == :path
|
41
|
+
attributes[:required] = true
|
42
|
+
end
|
43
|
+
|
44
|
+
if metadata.has_key?(:operation)
|
45
|
+
metadata[:operation][:parameters] ||= []
|
46
|
+
metadata[:operation][:parameters] << attributes
|
47
|
+
else
|
48
|
+
metadata[:path_item][:parameters] ||= []
|
49
|
+
metadata[:path_item][:parameters] << attributes
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def response(code, description, metadata={}, &block)
|
54
|
+
metadata[:response] = { code: code, description: description }
|
55
|
+
context(description, metadata, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def schema(value)
|
59
|
+
metadata[:response][:schema] = value
|
60
|
+
end
|
61
|
+
|
62
|
+
def header(name, attributes)
|
63
|
+
metadata[:response][:headers] ||= {}
|
64
|
+
metadata[:response][:headers][name] = attributes
|
65
|
+
end
|
66
|
+
|
67
|
+
# NOTE: Similar to 'description', 'examples' need to handle the case when
|
68
|
+
# being invoked with no params to avoid overriding 'examples' method of
|
69
|
+
# rspec-core ExampleGroup
|
70
|
+
def examples(example = nil)
|
71
|
+
return super() if example.nil?
|
72
|
+
metadata[:response][:examples] = example
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_test!(&block)
|
76
|
+
# NOTE: rspec 2.x support
|
77
|
+
if RSPEC_VERSION < 3
|
78
|
+
before do
|
79
|
+
submit_request(example.metadata)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns a #{metadata[:response][:code]} response" do
|
83
|
+
assert_response_matches_metadata(metadata)
|
84
|
+
block.call(response) if block_given?
|
85
|
+
end
|
86
|
+
else
|
87
|
+
before do |example|
|
88
|
+
submit_request(example.metadata)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns a #{metadata[:response][:code]} response" do |example|
|
92
|
+
assert_response_matches_metadata(example.metadata, &block)
|
93
|
+
example.instance_exec(response, &block) if block_given?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'openapi/rswag/specs/request_factory'
|
2
|
+
require 'openapi/rswag/specs/response_validator'
|
3
|
+
|
4
|
+
module Openapi
|
5
|
+
module Rswag
|
6
|
+
module Specs
|
7
|
+
module ExampleHelpers
|
8
|
+
|
9
|
+
def submit_request(metadata)
|
10
|
+
request = RequestFactory.new.build_request(metadata, self)
|
11
|
+
|
12
|
+
if RAILS_VERSION < 5
|
13
|
+
send(
|
14
|
+
request[:verb],
|
15
|
+
request[:path],
|
16
|
+
request[:payload],
|
17
|
+
request[:headers]
|
18
|
+
)
|
19
|
+
else
|
20
|
+
send(
|
21
|
+
request[:verb],
|
22
|
+
request[:path],
|
23
|
+
{
|
24
|
+
params: request[:payload],
|
25
|
+
headers: request[:headers]
|
26
|
+
}
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def assert_response_matches_metadata(metadata)
|
32
|
+
ResponseValidator.new.validate!(metadata, response)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json-schema'
|
2
|
+
|
3
|
+
module Openapi
|
4
|
+
module Rswag
|
5
|
+
module Specs
|
6
|
+
class ExtendedSchema < JSON::Schema::Draft4
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@attributes['type'] = ExtendedTypeAttribute
|
11
|
+
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
|
12
|
+
@names = ['http://tempuri.org/rswag/specs/extended_schema']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
|
17
|
+
|
18
|
+
def self.validate(current_schema, data, fragments, processor, validator, options={})
|
19
|
+
return if data.nil? && current_schema.schema['x-nullable'] == true
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
JSON::Validator.register_validator(ExtendedSchema.new)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Openapi
|
2
|
+
module Rswag
|
3
|
+
module Specs
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
|
6
|
+
rake_tasks do
|
7
|
+
load File.expand_path('../../../../tasks/rswag-specs_tasks.rake', __FILE__)
|
8
|
+
end
|
9
|
+
|
10
|
+
generators do
|
11
|
+
require 'generators/rspec/swagger_generator.rb'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
require 'active_support/core_ext/hash/conversions'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Openapi
|
6
|
+
module Rswag
|
7
|
+
module Specs
|
8
|
+
class RequestFactory
|
9
|
+
|
10
|
+
def initialize(config = ::Openapi::Rswag::Specs.config)
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_request(metadata, example)
|
15
|
+
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
16
|
+
parameters = expand_parameters(metadata, swagger_doc, example)
|
17
|
+
|
18
|
+
{}.tap do |request|
|
19
|
+
add_verb(request, metadata)
|
20
|
+
add_path(request, metadata, swagger_doc, parameters, example)
|
21
|
+
add_headers(request, metadata, swagger_doc, parameters, example)
|
22
|
+
add_payload(request, parameters, example)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def expand_parameters(metadata, swagger_doc, example)
|
29
|
+
operation_params = metadata[:operation][:parameters] || []
|
30
|
+
path_item_params = metadata[:path_item][:parameters] || []
|
31
|
+
security_params = derive_security_params(metadata, swagger_doc)
|
32
|
+
|
33
|
+
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
|
34
|
+
(operation_params + path_item_params + security_params)
|
35
|
+
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
|
36
|
+
.uniq { |p| p[:name] }
|
37
|
+
.reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def derive_security_params(metadata, swagger_doc)
|
41
|
+
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
|
42
|
+
scheme_names = requirements.flat_map { |r| r.keys }
|
43
|
+
schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
|
44
|
+
|
45
|
+
schemes.map do |scheme|
|
46
|
+
param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
|
47
|
+
param.merge(type: :string, required: requirements.one?)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def resolve_parameter(ref, swagger_doc)
|
52
|
+
key = ref.sub('#/parameters/', '').to_sym
|
53
|
+
definitions = swagger_doc[:parameters]
|
54
|
+
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
|
55
|
+
definitions[key]
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_verb(request, metadata)
|
59
|
+
request[:verb] = metadata[:operation][:verb]
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_path(request, metadata, swagger_doc, parameters, example)
|
63
|
+
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
|
64
|
+
|
65
|
+
request[:path] = template.tap do |template|
|
66
|
+
parameters.select { |p| p[:in] == :path }.each do |p|
|
67
|
+
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
|
68
|
+
end
|
69
|
+
|
70
|
+
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
|
71
|
+
template.concat(i == 0 ? '?' : '&')
|
72
|
+
template.concat(build_query_string_part(p, example.send(p[:name])))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_query_string_part(param, value)
|
78
|
+
name = param[:name]
|
79
|
+
return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
|
80
|
+
|
81
|
+
case param[:collectionFormat]
|
82
|
+
when :ssv
|
83
|
+
"#{name}=#{value.join(' ')}"
|
84
|
+
when :tsv
|
85
|
+
"#{name}=#{value.join('\t')}"
|
86
|
+
when :pipes
|
87
|
+
"#{name}=#{value.join('|')}"
|
88
|
+
when :multi
|
89
|
+
value.map { |v| "#{name}=#{v}" }.join('&')
|
90
|
+
else
|
91
|
+
"#{name}=#{value.join(',')}" # csv is default
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_headers(request, metadata, swagger_doc, parameters, example)
|
96
|
+
tuples = parameters
|
97
|
+
.select { |p| p[:in] == :header }
|
98
|
+
.map { |p| [ p[:name], example.send(p[:name]).to_s ] }
|
99
|
+
|
100
|
+
# Accept header
|
101
|
+
produces = metadata[:operation][:produces] || swagger_doc[:produces]
|
102
|
+
if produces
|
103
|
+
accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first
|
104
|
+
tuples << [ 'Accept', accept ]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Content-Type header
|
108
|
+
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
|
109
|
+
if consumes
|
110
|
+
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
|
111
|
+
tuples << [ 'Content-Type', content_type ]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Rails test infrastructure requires rackified headers
|
115
|
+
rackified_tuples = tuples.map do |pair|
|
116
|
+
[
|
117
|
+
case pair[0]
|
118
|
+
when 'Accept' then 'HTTP_ACCEPT'
|
119
|
+
when 'Content-Type' then 'CONTENT_TYPE'
|
120
|
+
when 'Authorization' then 'HTTP_AUTHORIZATION'
|
121
|
+
else pair[0]
|
122
|
+
end,
|
123
|
+
pair[1]
|
124
|
+
]
|
125
|
+
end
|
126
|
+
|
127
|
+
request[:headers] = Hash[ rackified_tuples ]
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_payload(request, parameters, example)
|
131
|
+
content_type = request[:headers]['CONTENT_TYPE']
|
132
|
+
return if content_type.nil?
|
133
|
+
|
134
|
+
if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type)
|
135
|
+
request[:payload] = build_form_payload(parameters, example)
|
136
|
+
else
|
137
|
+
request[:payload] = build_json_payload(parameters, example)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_form_payload(parameters, example)
|
142
|
+
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
|
143
|
+
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
|
144
|
+
# Rails test infrastructure allows us to send the values directly as a hash
|
145
|
+
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
|
146
|
+
tuples = parameters
|
147
|
+
.select { |p| p[:in] == :formData }
|
148
|
+
.map { |p| [ p[:name], example.send(p[:name]) ] }
|
149
|
+
Hash[ tuples ]
|
150
|
+
end
|
151
|
+
|
152
|
+
def build_json_payload(parameters, example)
|
153
|
+
body_param = parameters.select { |p| p[:in] == :body }.first
|
154
|
+
body_param ? example.send(body_param[:name]).to_json : nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
require 'json-schema'
|
3
|
+
require 'json'
|
4
|
+
require 'openapi/rswag/specs/extended_schema'
|
5
|
+
|
6
|
+
module Openapi
|
7
|
+
module Rswag
|
8
|
+
module Specs
|
9
|
+
class ResponseValidator
|
10
|
+
|
11
|
+
def initialize(config = ::Openapi::Rswag::Specs.config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!(metadata, response)
|
16
|
+
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
17
|
+
|
18
|
+
validate_code!(metadata, response)
|
19
|
+
validate_headers!(metadata, response.headers)
|
20
|
+
validate_body!(metadata, swagger_doc, response.body)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_code!(metadata, response)
|
26
|
+
expected = metadata[:response][:code].to_s
|
27
|
+
if response.code != expected
|
28
|
+
raise UnexpectedResponse,
|
29
|
+
"Expected response code '#{response.code}' to match '#{expected}'\n" \
|
30
|
+
"Response body: #{response.body}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_headers!(metadata, headers)
|
35
|
+
expected = (metadata[:response][:headers] || {}).keys
|
36
|
+
expected.each do |name|
|
37
|
+
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_body!(metadata, swagger_doc, body)
|
42
|
+
response_schema = metadata[:response][:schema]
|
43
|
+
return if response_schema.nil?
|
44
|
+
|
45
|
+
validation_schema = response_schema
|
46
|
+
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
|
47
|
+
.merge(swagger_doc.slice(:definitions))
|
48
|
+
errors = JSON::Validator.fully_validate(validation_schema, body)
|
49
|
+
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class UnexpectedResponse < StandardError; end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'active_support/core_ext/hash/deep_merge'
|
2
|
+
require 'swagger_helper'
|
3
|
+
|
4
|
+
module Openapi
|
5
|
+
module Rswag
|
6
|
+
module Specs
|
7
|
+
class SwaggerFormatter
|
8
|
+
|
9
|
+
# NOTE: rspec 2.x support
|
10
|
+
if RSPEC_VERSION > 2
|
11
|
+
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(output, config = Rswag::Specs.config)
|
15
|
+
@output = output
|
16
|
+
@config = config
|
17
|
+
|
18
|
+
@output.puts 'Generating Swagger docs ...'
|
19
|
+
end
|
20
|
+
|
21
|
+
def example_group_finished(notification)
|
22
|
+
# NOTE: rspec 2.x support
|
23
|
+
if RSPEC_VERSION > 2
|
24
|
+
metadata = notification.group.metadata
|
25
|
+
else
|
26
|
+
metadata = notification.metadata
|
27
|
+
end
|
28
|
+
|
29
|
+
# !metadata[:document] won't work, since nil means we should generate
|
30
|
+
# docs.
|
31
|
+
return if metadata[:document] == false
|
32
|
+
return unless metadata.has_key?(:response)
|
33
|
+
|
34
|
+
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
|
35
|
+
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop(notification=nil)
|
39
|
+
@config.swagger_docs.each do |url_path, doc|
|
40
|
+
file_path = File.join(@config.swagger_root, url_path)
|
41
|
+
dirname = File.dirname(file_path)
|
42
|
+
FileUtils.mkdir_p dirname unless File.exists?(dirname)
|
43
|
+
|
44
|
+
File.open(file_path, 'w') do |file|
|
45
|
+
file.write(pretty_generate(doc))
|
46
|
+
end
|
47
|
+
|
48
|
+
@output.puts "Swagger doc generated at #{file_path}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def pretty_generate(doc)
|
55
|
+
if @config.swagger_format == :yaml
|
56
|
+
clean_doc = yaml_prepare(doc)
|
57
|
+
YAML.dump(clean_doc)
|
58
|
+
else # config errors are thrown in 'def swagger_format', no throw needed here
|
59
|
+
JSON.pretty_generate(doc)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def yaml_prepare(doc)
|
64
|
+
json_doc = JSON.pretty_generate(doc)
|
65
|
+
clean_doc = JSON.parse(json_doc)
|
66
|
+
end
|
67
|
+
|
68
|
+
def metadata_to_swagger(metadata)
|
69
|
+
response_code = metadata[:response][:code]
|
70
|
+
response = metadata[:response].reject { |k,v| k == :code }
|
71
|
+
|
72
|
+
verb = metadata[:operation][:verb]
|
73
|
+
operation = metadata[:operation]
|
74
|
+
.reject { |k,v| k == :verb }
|
75
|
+
.merge(responses: { response_code => response })
|
76
|
+
|
77
|
+
path_template = metadata[:path_item][:template]
|
78
|
+
path_item = metadata[:path_item]
|
79
|
+
.reject { |k,v| k == :template }
|
80
|
+
.merge(verb => operation)
|
81
|
+
|
82
|
+
{ paths: { path_template => path_item } }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
namespace :rswag do
|
4
|
+
namespace :specs do
|
5
|
+
|
6
|
+
desc 'Generate Swagger JSON files from integration specs'
|
7
|
+
RSpec::Core::RakeTask.new('swaggerize') do |t|
|
8
|
+
t.pattern = ENV.fetch(
|
9
|
+
'PATTERN',
|
10
|
+
'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
|
11
|
+
)
|
12
|
+
|
13
|
+
# NOTE: rspec 2.x support
|
14
|
+
if Openapi::Rswag::Specs::RSPEC_VERSION > 2 && Openapi::Rswag::Specs.config.swagger_dry_run
|
15
|
+
t.rspec_opts = [ '--format Openapi::Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
|
16
|
+
else
|
17
|
+
t.rspec_opts = [ '--format Openapi::Rswag::Specs::SwaggerFormatter', '--order defined' ]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :rswag => ['rswag:specs:swaggerize']
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: openapi-rswag-specs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emmanuel Ndangurura
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.1'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: railties
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.1'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '7.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '3.1'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '7.0'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: json-schema
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '2.2'
|
60
|
+
type: :runtime
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.2'
|
67
|
+
description: Simplify API integration testing with a succinct rspec DSL and generate
|
68
|
+
Swagger files directly from your rspecs
|
69
|
+
email:
|
70
|
+
- endangurura@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- MIT-LICENSE
|
76
|
+
- Rakefile
|
77
|
+
- lib/generators/rspec/USAGE
|
78
|
+
- lib/generators/rspec/swagger_generator.rb
|
79
|
+
- lib/generators/rspec/templates/spec.rb
|
80
|
+
- lib/generators/rswag/specs/install/USAGE
|
81
|
+
- lib/generators/rswag/specs/install/install_generator.rb
|
82
|
+
- lib/generators/rswag/specs/install/templates/swagger_helper.rb
|
83
|
+
- lib/openapi/rswag/route_parser.rb
|
84
|
+
- lib/openapi/rswag/specs.rb
|
85
|
+
- lib/openapi/rswag/specs/configuration.rb
|
86
|
+
- lib/openapi/rswag/specs/example_group_helpers.rb
|
87
|
+
- lib/openapi/rswag/specs/example_helpers.rb
|
88
|
+
- lib/openapi/rswag/specs/extended_schema.rb
|
89
|
+
- lib/openapi/rswag/specs/railtie.rb
|
90
|
+
- lib/openapi/rswag/specs/request_factory.rb
|
91
|
+
- lib/openapi/rswag/specs/response_validator.rb
|
92
|
+
- lib/openapi/rswag/specs/swagger_formatter.rb
|
93
|
+
- lib/tasks/rswag-specs_tasks.rake
|
94
|
+
homepage: https://github.com/endangurura/rswag
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubygems_version: 3.1.2
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: A Swagger-based DSL for rspec-rails & accompanying rake task for generating
|
117
|
+
Swagger files
|
118
|
+
test_files: []
|