rswag-specs 1.0.0

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 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: []