rswag-specs 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf953519fd1b82b220dc55df8ac263dd12f260ad
4
+ data.tar.gz: 61696eb4201f033a4c39daba75d56eb2052b9ff8
5
+ SHA512:
6
+ metadata.gz: ba2ae1c5751ec7ed740fd2572cb37e78b89c584c4fd42095f9a1c8076a3f72c49e2251905fbfd69fea7a3dd73dfa0a072299c09e21febc52574cca1224504929
7
+ data.tar.gz: e886995f37ced2fe10457c1a6d8df03a3106eb9f08408a00e776b007d0048ac082c9362445a34c48685b77c4c1b5f619027b3f816ffb5b254f40bbe3e036ddf8
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,8 @@
1
+ Description:
2
+ Adds swagger_helper to enable Swagger DSL in integration specs
3
+
4
+ Example:
5
+ rails generate rswag:specs:install
6
+
7
+ This will create:
8
+ spec/swagger_helper.rb
@@ -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,25 @@
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 confiugred to server Swagger from the same folder
7
+ config.swagger_root = Rails.root.to_s + '/swagger'
8
+
9
+ # Define one or more Swagger documents and provide global metadata for each one
10
+ # When you run the 'rswag:specs:to_swagger' 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.json' => {
17
+ swagger: '2.0',
18
+ info: {
19
+ title: 'API V1',
20
+ version: 'v1'
21
+ },
22
+ paths: {}
23
+ }
24
+ }
25
+ end
@@ -0,0 +1,80 @@
1
+ module Rswag
2
+ module Specs
3
+ module ExampleGroupHelpers
4
+
5
+ def path(path, &block)
6
+ api_metadata = { path: path}
7
+ describe(path, api_metadata, &block)
8
+ end
9
+
10
+ [ :get, :post, :patch, :put, :delete, :head ].each do |verb|
11
+ define_method(verb) do |summary, &block|
12
+ api_metadata = { operation: { verb: verb, summary: summary } }
13
+ describe(verb, api_metadata, &block)
14
+ end
15
+ end
16
+
17
+ [ :operationId, :deprecated, :security ].each do |attr_name|
18
+ define_method(attr_name) do |value|
19
+ metadata[:operation][attr_name] = value
20
+ end
21
+ end
22
+
23
+ # NOTE: 'description' requires special treatment because ExampleGroup already
24
+ # defines a method with that name. Provide an override that supports the existing
25
+ # functionality while also setting the appropriate metadata if applicable
26
+ def description(value=nil)
27
+ return super() if value.nil?
28
+ metadata[:operation][:description] = value
29
+ end
30
+
31
+ # These are array properties - note the splat operator
32
+ [ :tags, :consumes, :produces, :schemes ].each do |attr_name|
33
+ define_method(attr_name) do |*value|
34
+ metadata[:operation][attr_name] = value
35
+ end
36
+ end
37
+
38
+ def parameter(attributes)
39
+ attributes[:required] = true if attributes[:in].to_sym == :path
40
+ metadata[:operation][:parameters] ||= []
41
+ metadata[:operation][:parameters] << attributes
42
+ end
43
+
44
+ def response(code, description, &block)
45
+ api_metadata = { response: { code: code, description: description } }
46
+ context(description, api_metadata, &block)
47
+ end
48
+
49
+ def schema(value)
50
+ metadata[:response][:schema] = value
51
+ end
52
+
53
+ def header(name, attributes)
54
+ metadata[:response][:headers] ||= {}
55
+ metadata[:response][:headers][name] = attributes
56
+ end
57
+
58
+ def run_test!
59
+ # NOTE: rspec 2.x support
60
+ if RSPEC_VERSION < 3
61
+ before do
62
+ submit_request(example.metadata)
63
+ end
64
+
65
+ it "returns a #{metadata[:response][:code]} response" do
66
+ assert_response_matches_metadata(example.metadata)
67
+ end
68
+ else
69
+ before do |example|
70
+ submit_request(example.metadata)
71
+ end
72
+
73
+ it "returns a #{metadata[:response][:code]} response" do |example|
74
+ assert_response_matches_metadata(example.metadata)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ require 'rswag/specs/request_factory'
2
+ require 'rswag/specs/response_validator'
3
+
4
+ module Rswag
5
+ module Specs
6
+ module ExampleHelpers
7
+
8
+ def submit_request(api_metadata)
9
+ factory = RequestFactory.new(api_metadata, global_metadata(api_metadata[:swagger_doc]))
10
+
11
+ if RAILS_VERSION < 5
12
+ send(
13
+ api_metadata[:operation][:verb],
14
+ factory.build_fullpath(self),
15
+ factory.build_body(self),
16
+ factory.build_headers(self)
17
+ )
18
+ else
19
+ send(
20
+ api_metadata[:operation][:verb],
21
+ factory.build_fullpath(self),
22
+ {
23
+ params: factory.build_body(self),
24
+ headers: factory.build_headers(self)
25
+ }
26
+ )
27
+ end
28
+ end
29
+
30
+ def assert_response_matches_metadata(api_metadata)
31
+ validator = ResponseValidator.new(api_metadata, global_metadata(api_metadata[:swagger_doc]))
32
+ validator.validate!(response)
33
+ end
34
+
35
+ private
36
+
37
+ def global_metadata(swagger_doc)
38
+ swagger_docs = ::RSpec.configuration.swagger_docs
39
+ swagger_doc.nil? ? swagger_docs.values.first : swagger_docs[swagger_doc]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ module Rswag
2
+ module Specs
3
+ class Railtie < ::Rails::Railtie
4
+
5
+ rake_tasks do
6
+ load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'json'
3
+
4
+ module Rswag
5
+ module Specs
6
+ class RequestFactory
7
+
8
+ def initialize(api_metadata, global_metadata)
9
+ @api_metadata = api_metadata
10
+ @global_metadata = global_metadata
11
+ end
12
+
13
+ def build_fullpath(example)
14
+ @api_metadata[:path].dup.tap do |t|
15
+ parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
16
+ t.concat(build_query(example))
17
+ t.prepend(@global_metadata[:basePath] || '')
18
+ end
19
+ end
20
+
21
+ def build_query(example)
22
+ query_string = parameters_in(:query)
23
+ .map { |p| "#{p[:name]}=#{example.send(p[:name])}" }
24
+ .join('&')
25
+
26
+ query_string.empty? ? '' : "?#{query_string}"
27
+ end
28
+
29
+ def build_body(example)
30
+ body_parameter = parameters_in(:body).first
31
+ body_parameter.nil? ? '' : example.send(body_parameter[:name]).to_json
32
+ end
33
+
34
+ def build_headers(example)
35
+ headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ]
36
+ headers.tap do |h|
37
+ produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
38
+ consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
39
+ h['ACCEPT'] = produces.join(';') unless produces.nil?
40
+ h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def parameters_in(location)
47
+ (@api_metadata[:operation][:parameters] || [])
48
+ .map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
49
+ .concat(resolve_api_key_parameters)
50
+ .select { |p| p[:in] == location }
51
+ end
52
+
53
+ def resolve_parameter(ref)
54
+ defined_params = @global_metadata[:parameters]
55
+ key = ref.sub('#/parameters/', '')
56
+ raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key]
57
+ defined_params[key]
58
+ end
59
+
60
+ def resolve_api_key_parameters
61
+ @api_key_params ||= begin
62
+ global_requirements = (@global_metadata[:security] || {})
63
+ requirements = global_requirements.merge(@api_metadata[:operation][:security] || {})
64
+ definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements.keys)
65
+ definitions.values.select { |d| d[:type] == :apiKey }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ require 'json-schema'
2
+
3
+ module Rswag
4
+ module Specs
5
+ class ResponseValidator
6
+
7
+ def initialize(api_metadata, global_metadata)
8
+ @api_metadata = api_metadata
9
+ @global_metadata = global_metadata
10
+ end
11
+
12
+ def validate!(response)
13
+ validate_code!(response.code)
14
+ validate_body!(response.body)
15
+ end
16
+
17
+ private
18
+
19
+ def validate_code!(code)
20
+ if code.to_s != @api_metadata[:response][:code].to_s
21
+ raise UnexpectedResponse, "Expected response code '#{code}' to match '#{@api_metadata[:response][:code]}'"
22
+ end
23
+ end
24
+
25
+ def validate_body!(body)
26
+ schema = @api_metadata[:response][:schema]
27
+ return if schema.nil?
28
+ begin
29
+ JSON::Validator.validate!(schema.merge(@global_metadata), body)
30
+ rescue JSON::Schema::ValidationError => ex
31
+ raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
32
+ end
33
+ end
34
+ end
35
+
36
+ class UnexpectedResponse < StandardError; end
37
+ end
38
+ end
@@ -0,0 +1,79 @@
1
+ require 'active_support/core_ext/hash/deep_merge'
2
+ require 'rspec/core/formatters/base_text_formatter'
3
+ require 'swagger_helper'
4
+
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)
15
+ @output = output
16
+ @swagger_root = ::RSpec.configuration.swagger_root
17
+ raise ConfigurationError, 'Missing swagger_root. See swagger_helper.rb' if @swagger_root.nil?
18
+ @swagger_docs = ::RSpec.configuration.swagger_docs || []
19
+ raise ConfigurationError, 'Missing swagger_docs. See swagger_helper.rb' if @swagger_docs.empty?
20
+
21
+ @output.puts 'Generating Swagger docs ...'
22
+ end
23
+
24
+ def example_group_finished(notification)
25
+ # NOTE: rspec 2.x support
26
+ if RSPEC_VERSION > 2
27
+ metadata = notification.group.metadata
28
+ else
29
+ metadata = notification.metadata
30
+ end
31
+
32
+ return unless metadata.has_key?(:response)
33
+ swagger_doc = get_swagger_doc(metadata[:swagger_doc])
34
+ swagger_doc.deep_merge!(metadata_to_swagger(metadata))
35
+ end
36
+
37
+ def stop(notification=nil)
38
+ @swagger_docs.each do |url_path, doc|
39
+ file_path = File.join(@swagger_root, url_path)
40
+ dirname = File.dirname(file_path)
41
+ FileUtils.mkdir_p dirname unless File.exists?(dirname)
42
+
43
+ File.open(file_path, 'w') do |file|
44
+ file.write(JSON.pretty_generate(doc))
45
+ end
46
+
47
+ @output.puts "Swagger doc generated at #{file_path}"
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def get_swagger_doc(tag)
54
+ return @swagger_docs.values.first if tag.nil?
55
+ raise ConfigurationError, "Unknown swagger_doc '#{tag}'" unless @swagger_docs.has_key?(tag)
56
+ @swagger_docs[tag]
57
+ end
58
+
59
+ def metadata_to_swagger(metadata)
60
+ response_code = metadata[:response][:code]
61
+ response = metadata[:response].reject { |k,v| k == :code }
62
+ verb = metadata[:operation][:verb]
63
+ operation = metadata[:operation]
64
+ .reject { |k,v| k == :verb }
65
+ .merge(responses: { response_code => response })
66
+
67
+ {
68
+ paths: {
69
+ metadata[:path] => {
70
+ verb => operation
71
+ }
72
+ }
73
+ }
74
+ end
75
+ end
76
+
77
+ class ConfigurationError < StandardError; end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ module Rswag
2
+ module Specs
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ require 'rspec/core'
2
+ require 'rswag/specs/version'
3
+ require 'rswag/specs/example_group_helpers'
4
+ require 'rswag/specs/example_helpers'
5
+ require 'rswag/specs/railtie' if defined?(Rails::Railtie)
6
+
7
+ module Rswag
8
+ module Specs
9
+
10
+ # Extend RSpec with a swagger-based DSL
11
+ ::RSpec.configure do |c|
12
+ c.add_setting :swagger_root
13
+ c.add_setting :swagger_docs
14
+ c.extend ExampleGroupHelpers, type: :request
15
+ c.include ExampleHelpers, type: :request
16
+ end
17
+
18
+ # Support Rails 3+ and RSpec 2+ (sigh!)
19
+ RAILS_VERSION = Rails::VERSION::MAJOR
20
+ RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
21
+ end
22
+ end
@@ -0,0 +1,18 @@
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 = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
9
+
10
+ # NOTE: rspec 2.x support
11
+ if Rswag::Specs::RSPEC_VERSION > 2
12
+ t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
13
+ else
14
+ t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ]
15
+ end
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rswag-specs
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Richie Morris
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
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: '5.1'
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: '5.1'
33
+ - !ruby/object:Gem::Dependency
34
+ name: json-schema
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '2.14'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '2.14'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4'
67
+ description: Simplify API integration testing with a succinct rspec DSL and generate
68
+ Swagger files directly from your rspecs
69
+ email:
70
+ - domaindrivendev@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - MIT-LICENSE
76
+ - Rakefile
77
+ - lib/generators/rswag/specs/install/USAGE
78
+ - lib/generators/rswag/specs/install/install_generator.rb
79
+ - lib/generators/rswag/specs/install/templates/swagger_helper.rb
80
+ - lib/rswag/specs.rb
81
+ - lib/rswag/specs/example_group_helpers.rb
82
+ - lib/rswag/specs/example_helpers.rb
83
+ - lib/rswag/specs/railtie.rb
84
+ - lib/rswag/specs/request_factory.rb
85
+ - lib/rswag/specs/response_validator.rb
86
+ - lib/rswag/specs/swagger_formatter.rb
87
+ - lib/rswag/specs/version.rb
88
+ - lib/tasks/rswag-specs_tasks.rake
89
+ homepage: https://github.com/domaindrivendev/rswag
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.4.5
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: A Swagger-based DSL for rspec-rails & accompanying rake task for generating
113
+ Swagger files
114
+ test_files: []