rspec-rails-swagger 0.1.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: 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: []