rspec-rails-swagger 0.1.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: ee3d27df0e7ec24ced93bd4a298f649b86715b12
4
+ data.tar.gz: 0e96a4167c43c555b94b81d93901939311e0dd98
5
+ SHA512:
6
+ metadata.gz: 217c592d0a3331819d7264a7356885ec3e69d8cc075aa7a6f98e524f5dcf96466adefefd16214772abdbd42a494f5df78d4bc3e6cc1482f86c797d3f12bc0c35
7
+ data.tar.gz: d9b5651ac3f9c4a76c09ca9efce6e0f671f4c5264c79dfaa0356ada5e7f13c18cdf1336352a7e8f0a9a70fb45cd427f1fd591015d755c63442c85d359f86eb66
@@ -0,0 +1,16 @@
1
+ require 'rspec/core'
2
+ require 'rspec/rails/swagger/configuration'
3
+ require 'rspec/rails/swagger/document'
4
+ require 'rspec/rails/swagger/formatter'
5
+ require 'rspec/rails/swagger/helpers'
6
+ require 'rspec/rails/swagger/request_builder'
7
+ require 'rspec/rails/swagger/version'
8
+
9
+ module RSpec
10
+ module Rails
11
+ module Swagger
12
+ initialize_configuration RSpec.configuration
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,16 @@
1
+ module RSpec
2
+ module Rails
3
+ module Swagger
4
+ # Fake class to document RSpec Swagger configuration options.
5
+ class Configuration
6
+ end
7
+
8
+ def self.initialize_configuration(config)
9
+ config.add_setting :swagger_root
10
+ config.add_setting :swagger_docs, default: {}
11
+
12
+ Helpers.add_swagger_type_configurations(config)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ module RSpec
2
+ module Rails
3
+ module Swagger
4
+ class Document
5
+ attr_accessor :data
6
+
7
+ def initialize(data)
8
+ @data = data.deep_symbolize_keys
9
+ end
10
+
11
+ def [](value)
12
+ data[value]
13
+ end
14
+
15
+ def resolve_ref(ref)
16
+ unless %r{#/(?<location>parameters|definitions)/(?<name>.+)} =~ ref
17
+ raise ArgumentError, "Invalid reference: #{ref}"
18
+ end
19
+
20
+ result = data.fetch(location.to_sym, {})[name.to_sym]
21
+ raise ArgumentError, "Reference value does not exist: #{ref}" unless result
22
+
23
+ if location == 'parameters'
24
+ result.merge(name: name)
25
+ end
26
+
27
+ result
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,105 @@
1
+ require 'rspec/core/formatters/base_text_formatter'
2
+
3
+ module RSpec
4
+ module Rails
5
+ module Swagger
6
+ class Formatter < RSpec::Core::Formatters::BaseTextFormatter
7
+ RSpec::Core::Formatters.register self, :example_finished, :close
8
+
9
+ def documents
10
+ # We don't try to load the docs in `initalize` because when running
11
+ # `rspec -f RSpec::Swagger::Formatter` RSpec initalized this class
12
+ # before `swagger_helper` has run.
13
+ @documents ||= ::RSpec.configuration.swagger_docs
14
+ end
15
+
16
+ def example_finished(notification)
17
+ metadata = notification.example.metadata
18
+ return unless metadata[:swagger_object] == :response
19
+
20
+ # metadata.each do |k, v|
21
+ # puts "#{k}\t#{v}" if k.to_s.starts_with?("swagger")
22
+ # end
23
+
24
+ document = document_for(metadata[:swagger_document])
25
+ path_item = path_item_for(document, metadata[:swagger_path_item])
26
+ operation = operation_for(path_item, metadata[:swagger_operation])
27
+ response_for(operation, metadata[:swagger_response])
28
+ end
29
+
30
+ def close(_notification)
31
+ documents.each{|k, v| write_json(k, v)}
32
+ end
33
+
34
+ def write_json(name, document)
35
+ root = ::RSpec.configuration.swagger_root
36
+ # It would be good to at least warn if the name includes some '../' that
37
+ # takes it out of root directory.
38
+ target = Pathname(name).expand_path(root)
39
+ target.dirname.mkpath
40
+ target.write(JSON.pretty_generate(document))
41
+ end
42
+
43
+ def document_for(doc_name = nil)
44
+ if doc_name
45
+ documents.fetch(doc_name)
46
+ else
47
+ documents.values.first
48
+ end
49
+ end
50
+
51
+ def path_item_for(document, swagger_path_item)
52
+ name = swagger_path_item[:path]
53
+
54
+ document[:paths] ||= {}
55
+ document[:paths][name] ||= {}
56
+ if swagger_path_item[:parameters]
57
+ document[:paths][name][:parameters] = prepare_parameters(swagger_path_item[:parameters])
58
+ end
59
+ document[:paths][name]
60
+ end
61
+
62
+ def operation_for(path, swagger_operation)
63
+ method = swagger_operation[:method]
64
+
65
+ path[method] ||= {responses: {}}
66
+ path[method].tap do |operation|
67
+ if swagger_operation[:parameters]
68
+ operation[:parameters] = prepare_parameters(swagger_operation[:parameters])
69
+ end
70
+ operation.merge!(swagger_operation.slice(
71
+ :summary, :description, :externalDocs, :operationId,
72
+ :consumes, :produces, :schemes, :deprecated, :security
73
+ ))
74
+ end
75
+ end
76
+
77
+ def response_for(operation, swagger_response)
78
+ status = swagger_response[:status_code]
79
+
80
+ operation[:responses][status] ||= {}
81
+ operation[:responses][status].tap do |response|
82
+ if swagger_response[:examples]
83
+ response[:examples] = prepare_examples(swagger_response[:examples])
84
+ end
85
+ response.merge!(swagger_response.slice(:description, :schema, :headers))
86
+ end
87
+ end
88
+
89
+ def prepare_parameters(params)
90
+ params.values
91
+ end
92
+
93
+ def prepare_examples(examples)
94
+ if examples["application/json"].present?
95
+ begin
96
+ examples["application/json"] = JSON.parse(examples["application/json"])
97
+ rescue JSON::ParserError
98
+ end
99
+ end
100
+ examples
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,258 @@
1
+ module RSpec
2
+ module Rails
3
+ module Swagger
4
+ module Helpers
5
+ # paths: (Paths)
6
+ # /pets: (Path Item)
7
+ # post: (Operation)
8
+ # tags:
9
+ # - pet
10
+ # summary: Add a new pet to the store
11
+ # description: ""
12
+ # operationId: addPet
13
+ # consumes:
14
+ # - application/json
15
+ # produces:
16
+ # - application/json
17
+ # parameters: (Parameters)
18
+ # - in: body
19
+ # name: body
20
+ # description: Pet object that needs to be added to the store
21
+ # required: false
22
+ # schema:
23
+ # $ref: "#/definitions/Pet"
24
+ # responses: (Responses)
25
+ # "405": (Response)
26
+ # description: Invalid input
27
+
28
+ # The helpers serve as a DSL.
29
+ def self.add_swagger_type_configurations(config)
30
+ # The filters are used to ensure that the methods are nested correctly
31
+ # and following the Swagger schema.
32
+ config.extend Paths, type: :request
33
+ config.extend PathItem, swagger_object: :path_item
34
+ config.extend Parameters, swagger_object: :path_item
35
+ config.extend Operation, swagger_object: :operation
36
+ config.extend Parameters, swagger_object: :operation
37
+ config.extend Response, swagger_object: :response
38
+ end
39
+
40
+ module Paths
41
+ def path template, attributes = {}, &block
42
+ attributes.symbolize_keys!
43
+
44
+ raise ArgumentError, "Path must start with a /" unless template.starts_with?('/')
45
+
46
+ #TODO template might be a $ref
47
+ meta = {
48
+ swagger_object: :path_item,
49
+ swagger_document: attributes[:swagger_document] || RSpec.configuration.swagger_docs.keys.first,
50
+ swagger_path_item: {path: template}
51
+ }
52
+ describe(template, meta, &block)
53
+ end
54
+ end
55
+
56
+ module PathItem
57
+ METHODS = %w(get put post delete options head patch).freeze
58
+
59
+ def operation method, attributes = {}, &block
60
+ attributes.symbolize_keys!
61
+
62
+ method = method.to_s.downcase
63
+ validate_method! method
64
+
65
+ meta = {
66
+ swagger_object: :operation,
67
+ swagger_operation: attributes.merge(method: method.to_sym).reject{ |v| v.nil? }
68
+ }
69
+ describe(method.to_s, meta, &block)
70
+ end
71
+
72
+ METHODS.each do |method|
73
+ define_method(method) do |attributes = {}, &block|
74
+ operation(method, attributes, &block)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def validate_method! method
81
+ unless METHODS.include? method.to_s
82
+ raise ArgumentError, "Operation has an invalid 'method' value. Try: #{METHODS}."
83
+ end
84
+ end
85
+ end
86
+
87
+ module Parameters
88
+ def parameter name, attributes = {}
89
+ attributes.symbolize_keys!
90
+
91
+ # Look for $refs
92
+ if name.respond_to?(:has_key?)
93
+ ref = name.delete(:ref) || name.delete('ref')
94
+ full_param = resolve_document(metadata).resolve_ref(ref)
95
+
96
+ validate_parameter! full_param
97
+
98
+ param = { '$ref' => ref }
99
+ key = parameter_key(full_param)
100
+ else
101
+ validate_parameter! attributes
102
+
103
+ # Path attributes are always required
104
+ attributes[:required] = true if attributes[:in] == :path
105
+
106
+ param = { name: name.to_s }.merge(attributes)
107
+ key = parameter_key(param)
108
+ end
109
+
110
+ parameters_for_object[key] = param
111
+ end
112
+
113
+ def resolve_document metadata
114
+ # TODO: It's really inefficient to keep recreating this. It'd be nice
115
+ # if we could cache them some place.
116
+ name = metadata[:swagger_document]
117
+ Document.new(RSpec.configuration.swagger_docs[name])
118
+ end
119
+
120
+ private
121
+
122
+ # This key ensures uniqueness based on the 'name' and 'in' values.
123
+ def parameter_key parameter
124
+ "#{parameter[:in]}&#{parameter[:name]}"
125
+ end
126
+
127
+ def parameters_for_object
128
+ object_key = "swagger_#{metadata[:swagger_object]}".to_sym
129
+ object_data = metadata[object_key] ||= {}
130
+ object_data[:parameters] ||= {}
131
+ end
132
+
133
+ def validate_parameter! attributes
134
+ validate_location! attributes[:in]
135
+
136
+ if attributes[:in].to_s == 'body'
137
+ unless attributes[:schema].present?
138
+ raise ArgumentError, "Parameter is missing required 'schema' value."
139
+ end
140
+ else
141
+ validate_type! attributes[:type]
142
+ end
143
+ end
144
+
145
+ def validate_location! location
146
+ unless location.present?
147
+ raise ArgumentError, "Parameter is missing required 'in' value."
148
+ end
149
+
150
+ locations = %w(query header path formData body)
151
+ unless locations.include? location.to_s
152
+ raise ArgumentError, "Parameter has an invalid 'in' value. Try: #{locations}."
153
+ end
154
+ end
155
+
156
+ def validate_type! type
157
+ unless type.present?
158
+ raise ArgumentError, "Parameter is missing required 'type' value."
159
+ end
160
+
161
+ types = %w(string number integer boolean array file)
162
+ unless types.include? type.to_s
163
+ raise ArgumentError, "Parameter has an invalid 'type' value. Try: #{types}."
164
+ end
165
+ end
166
+ end
167
+
168
+ module Operation
169
+ def consumes *mime_types
170
+ metadata[:swagger_operation][:consumes] = mime_types
171
+ end
172
+
173
+ def produces *mime_types
174
+ metadata[:swagger_operation][:produces] = mime_types
175
+ end
176
+
177
+ def response status_code, attributes = {}, &block
178
+ attributes.symbolize_keys!
179
+
180
+ validate_status_code! status_code
181
+ validate_description! attributes[:description]
182
+
183
+ meta = {
184
+ swagger_object: :response,
185
+ swagger_response: attributes.merge(status_code: status_code)
186
+ }
187
+ describe(status_code, meta) do
188
+ self.module_exec(&block) if block_given?
189
+
190
+ # To make a request we need:
191
+ # - the details we've collected in the metadata
192
+ # - parameter values defined using let()
193
+ # RSpec tries to limit access to metadata inside of it() / before()
194
+ # / after() blocks but that scope is the only place you can access
195
+ # the let() values. The solution the swagger_rails dev came up with
196
+ # is to use the example.metadata passed into the block with the
197
+ # block's scope which has access to the let() values.
198
+ before do |example|
199
+ builder = RequestBuilder.new(example.metadata, self)
200
+ method = builder.method
201
+ path = [builder.path, builder.query].join
202
+ headers = builder.headers
203
+ body = builder.body
204
+
205
+ # Run the request
206
+ if ::Rails::VERSION::MAJOR >= 5
207
+ self.send(method, path, {params: body, headers: headers})
208
+ else
209
+ self.send(method, path, body, headers)
210
+ end
211
+
212
+ if example.metadata[:capture_examples]
213
+ examples = example.metadata[:swagger_response][:examples] ||= {}
214
+ examples[response.content_type.to_s] = response.body
215
+ end
216
+ end
217
+
218
+ # TODO: see if we can get the caller to show up in the error
219
+ # backtrace for this test.
220
+ it("returns the correct status code") do
221
+ expect(response).to have_http_status(status_code)
222
+ end
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ def validate_status_code! status_code
229
+ unless status_code == :default || (100..599).cover?(status_code)
230
+ raise ArgumentError, "status_code must be an integer 100 to 599, or :default"
231
+ end
232
+ end
233
+
234
+ def validate_description! description
235
+ unless description.present?
236
+ raise ArgumentError, "Response is missing required 'description' value."
237
+ end
238
+ end
239
+ end
240
+
241
+ module Response
242
+ def capture_example
243
+ metadata[:capture_examples] = true
244
+ end
245
+
246
+ def schema definition
247
+ definition.symbolize_keys!
248
+
249
+ ref = definition.delete(:ref)
250
+ schema = ref ? { '$ref' => ref } : definition
251
+
252
+ metadata[:swagger_response][:schema] = schema
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,91 @@
1
+ module RSpec
2
+ module Rails
3
+ module Swagger
4
+ class RequestBuilder
5
+ attr_reader :metadata, :instance
6
+
7
+ def initialize(metadata, instance)
8
+ @metadata, @instance = metadata, instance
9
+ end
10
+
11
+ def document
12
+ @document ||= begin
13
+ name = metadata[:swagger_document]
14
+ Document.new(RSpec.configuration.swagger_docs[name])
15
+ end
16
+ end
17
+
18
+ def method
19
+ metadata[:swagger_operation][:method]
20
+ end
21
+
22
+ def produces
23
+ metadata[:swagger_operation][:produces] || document[:produces]
24
+ end
25
+
26
+ def consumes
27
+ metadata[:swagger_operation][:consumes] || document[:consumes]
28
+ end
29
+
30
+ def parameters location = nil
31
+ path_item = metadata[:swagger_path_item] || {}
32
+ operation = metadata[:swagger_operation] || {}
33
+ params = path_item.fetch(:parameters, {}).merge(operation.fetch(:parameters, {}))
34
+ if location.present?
35
+ params.select{ |k, _| k.starts_with? "#{location}&" }
36
+ else
37
+ params
38
+ end
39
+ end
40
+
41
+ def parameter_values location
42
+ # Don't bother looking at the full parameter bodies since all we need
43
+ # are location and name which are in the key.
44
+ values = parameters(location)
45
+ .keys
46
+ .map{ |k| k.split('&').last }
47
+ .map{ |name| [name, instance.send(name)] }
48
+ Hash[values]
49
+ end
50
+
51
+ def headers
52
+ headers = {}
53
+
54
+ # Match the names that Rails uses internally
55
+ headers['HTTP_ACCEPT'] = produces.join(';') if produces.present?
56
+ headers['CONTENT_TYPE'] = consumes.first if consumes.present?
57
+
58
+ # TODO: do we need to do some capitalization to match the rack
59
+ # conventions?
60
+ parameter_values(:header).each { |k, v| headers[k] = v }
61
+
62
+ headers
63
+ end
64
+
65
+ def path
66
+ base_path = document[:basePath] || ''
67
+ # Find params in the path and replace them with values defined in
68
+ # in the example group.
69
+ base_path + metadata[:swagger_path_item][:path].gsub(/(\{.*?\})/) do |match|
70
+ # QUESTION: Should check that the parameter is actually defined in
71
+ # `parameters` before fetch a value?
72
+ instance.send(match[1...-1])
73
+ end
74
+ end
75
+
76
+ def query
77
+ query_params = parameter_values(:query).to_query
78
+ "?#{query_params}" unless query_params.blank?
79
+ end
80
+
81
+ def body
82
+ # And here all we need is the first half of the key to find the body
83
+ # parameter and its name to fetch a value.
84
+ if key = parameters(:body).keys.first
85
+ instance.send(key.split('&').last).to_json
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,10 @@
1
+ module RSpec
2
+ module Rails
3
+ # Version information for RSpec Swagger.
4
+ module Swagger
5
+ module Version
6
+ STRING = '0.1.0'
7
+ end
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-rails-swagger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - andrew morton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-26 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
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: Inspired by swagger_rails
42
+ email: drewish@katherinehouse.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/rspec/rails/swagger.rb
48
+ - lib/rspec/rails/swagger/configuration.rb
49
+ - lib/rspec/rails/swagger/document.rb
50
+ - lib/rspec/rails/swagger/formatter.rb
51
+ - lib/rspec/rails/swagger/helpers.rb
52
+ - lib/rspec/rails/swagger/request_builder.rb
53
+ - lib/rspec/rails/swagger/version.rb
54
+ homepage: https://github.com/drewish/rspec-rails-swagger
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.5.1
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Generate Swagger docs from RSpec integration tests
78
+ test_files: []