rspec-rails-api 0.3.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -5
- data/.gitlab-ci.yml +1 -1
- data/.rubocop.yml +5 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +51 -0
- data/Gemfile.lock +98 -0
- data/README.md +39 -36
- data/Rakefile +3 -0
- data/lib/rspec/rails/api/dsl/example.rb +94 -18
- data/lib/rspec/rails/api/dsl/example_group.rb +134 -48
- data/lib/rspec/rails/api/entity_config.rb +28 -4
- data/lib/rspec/rails/api/field_config.rb +10 -2
- data/lib/rspec/rails/api/matchers.rb +26 -18
- data/lib/rspec/rails/api/metadata.rb +153 -21
- data/lib/rspec/rails/api/open_api_renderer.rb +189 -20
- data/lib/rspec/rails/api/utils.rb +28 -70
- data/lib/rspec/rails/api/validator.rb +211 -0
- data/lib/rspec/rails/api/version.rb +1 -1
- data/lib/rspec_rails_api.rb +4 -0
- data/rspec-rails-api.gemspec +10 -5
- metadata +68 -9
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rspec/rails/api/utils'
|
4
|
+
require 'rspec/rails/api/validator'
|
4
5
|
require 'rspec/rails/api/open_api_renderer'
|
5
6
|
require 'rspec/rails/api/entity_config'
|
6
7
|
|
7
8
|
module RSpec
|
8
9
|
module Rails
|
9
10
|
module Api
|
10
|
-
# Handles contexts and examples
|
11
|
+
# Handles contexts and examples metadata.
|
11
12
|
class Metadata # rubocop:disable Metrics/ClassLength
|
12
|
-
attr_reader :entities, :resources, :current_resource, :
|
13
|
+
attr_reader :entities, :resources, :parameters, :current_resource, :current_url, :current_method, :current_code
|
13
14
|
|
14
15
|
def initialize
|
15
16
|
@resources = {}
|
@@ -22,37 +23,65 @@ module RSpec
|
|
22
23
|
@current_code = nil
|
23
24
|
end
|
24
25
|
|
26
|
+
##
|
27
|
+
# Adds a resource to metadata
|
28
|
+
#
|
29
|
+
# @param name [String] Resource name
|
30
|
+
# @param description [String] Resource description
|
31
|
+
#
|
32
|
+
# @return [void]
|
25
33
|
def add_resource(name, description)
|
26
|
-
@resources[name.to_sym] = { description: description, paths: {} }
|
34
|
+
@resources[name.to_sym] = { description: description, paths: {}, entities: {} }
|
27
35
|
@current_resource = name.to_sym
|
28
36
|
end
|
29
37
|
|
30
|
-
|
38
|
+
##
|
39
|
+
# Adds an entity definition
|
40
|
+
#
|
41
|
+
# @param name [Symbol] Entity name
|
42
|
+
# @param fields [Hash] Fields definitions
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# @return [void]
|
46
|
+
def add_entity(name, fields)
|
31
47
|
Utils.deep_set(@resources,
|
32
|
-
|
48
|
+
[@current_resource, 'entities', name],
|
33
49
|
EntityConfig.new(fields))
|
34
50
|
end
|
35
51
|
|
36
|
-
|
37
|
-
|
52
|
+
##
|
53
|
+
# Adds a parameter definition
|
54
|
+
#
|
55
|
+
# @param name [Symbol] Parameter definition name
|
56
|
+
# @param fields [Hash] Fields definitions
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def add_parameter(name, fields)
|
60
|
+
raise "Parameter #{name} is already defined" if @parameters[name]
|
38
61
|
|
39
62
|
fields.each_value do |field|
|
40
63
|
field[:required] = true unless field[:required] == false
|
41
64
|
end
|
42
|
-
@parameters[
|
65
|
+
@parameters[name] = fields
|
43
66
|
end
|
44
67
|
|
68
|
+
##
|
69
|
+
# Adds path parameters definition
|
70
|
+
#
|
71
|
+
# @param fields [Hash] Parameters definitions
|
72
|
+
#
|
73
|
+
# @return [void]
|
45
74
|
def add_path_params(fields) # rubocop:disable Metrics/MethodLength
|
46
75
|
check_current_context :resource, :url
|
47
76
|
|
48
77
|
chunks = @current_url.split('?')
|
49
78
|
|
50
79
|
fields.each do |name, field|
|
51
|
-
valid_attribute =
|
80
|
+
valid_attribute = Validator.valid_type?(field[:type], except: %i[array object])
|
52
81
|
raise "Field type not allowed: #{field[:type]}" unless valid_attribute
|
53
82
|
|
54
83
|
scope = path_param_scope(chunks, name)
|
55
|
-
Utils.deep_set(@resources,
|
84
|
+
Utils.deep_set(@resources, [@current_resource, 'paths', @current_url, 'path_params', name],
|
56
85
|
description: field[:description] || nil,
|
57
86
|
type: field[:type] || nil,
|
58
87
|
required: field[:required] || true,
|
@@ -60,6 +89,9 @@ module RSpec
|
|
60
89
|
end
|
61
90
|
end
|
62
91
|
|
92
|
+
##
|
93
|
+
# Add request parameters (_body_)
|
94
|
+
#
|
63
95
|
# Fields should be something like:
|
64
96
|
# id: {type: :number, description: 'Something'},
|
65
97
|
# name: {type: string, description: 'Something'}
|
@@ -69,20 +101,34 @@ module RSpec
|
|
69
101
|
# property: {type: :string, description: 'Something'},
|
70
102
|
# ...
|
71
103
|
# }}
|
104
|
+
#
|
105
|
+
# @param fields [Hash] Parameters definitions
|
106
|
+
#
|
107
|
+
# @return [void]
|
72
108
|
def add_request_params(fields)
|
73
109
|
check_current_context :resource, :url, :method
|
74
110
|
|
75
111
|
params = organize_params fields
|
76
112
|
Utils.deep_set(@resources,
|
77
|
-
|
113
|
+
[@current_resource, 'paths', @current_url, 'actions', @current_method, 'params'],
|
78
114
|
params)
|
79
115
|
end
|
80
116
|
|
81
|
-
|
117
|
+
##
|
118
|
+
# Adds an action and sets `@current_url` and `@current_method`
|
119
|
+
#
|
120
|
+
# @param method [:get, :post, :put, :patch, delete] Method name
|
121
|
+
# @param url [String] Associated URL
|
122
|
+
# @param summary [String] What the route does for given method
|
123
|
+
# @param description [String] Longer description of this action
|
124
|
+
#
|
125
|
+
# @return [void]
|
126
|
+
def add_action(method, url, summary, description = '')
|
82
127
|
check_current_context :resource
|
83
128
|
|
84
|
-
Utils.deep_set(@resources,
|
85
|
-
description: description,
|
129
|
+
Utils.deep_set(@resources, [@current_resource, 'paths', url, 'actions', method],
|
130
|
+
description: description || '',
|
131
|
+
summary: summary,
|
86
132
|
statuses: {},
|
87
133
|
params: {})
|
88
134
|
|
@@ -90,35 +136,91 @@ module RSpec
|
|
90
136
|
@current_method = method
|
91
137
|
end
|
92
138
|
|
139
|
+
##
|
140
|
+
# Adds a status code to metadata and sets `@current_code`
|
141
|
+
#
|
142
|
+
# @param status_code [Integer] The status code
|
143
|
+
# @param description [String] Code description
|
144
|
+
#
|
145
|
+
# @return [void]
|
146
|
+
#
|
93
147
|
# rubocop:disable Layout/LineLength
|
94
148
|
def add_status_code(status_code, description)
|
95
149
|
check_current_context :resource, :url, :method
|
96
150
|
|
97
151
|
Utils.deep_set(@resources,
|
98
|
-
|
152
|
+
[@current_resource, 'paths', @current_url, 'actions', @current_method, 'statuses', status_code],
|
99
153
|
description: description,
|
100
154
|
example: { response: nil })
|
101
155
|
@current_code = status_code
|
102
156
|
end
|
103
157
|
# rubocop:enable Layout/LineLength
|
104
158
|
|
159
|
+
##
|
160
|
+
# Gets the current example
|
161
|
+
#
|
162
|
+
# @return [Hash] Current example metadata
|
163
|
+
def current_example
|
164
|
+
@resources.dig @current_resource,
|
165
|
+
:paths,
|
166
|
+
@current_url.to_sym,
|
167
|
+
:actions,
|
168
|
+
@current_method.to_sym,
|
169
|
+
:statuses,
|
170
|
+
@current_code.to_s.to_sym
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Adds expectations for current example
|
175
|
+
#
|
176
|
+
# @param one [Hash, nil] Entity definition
|
177
|
+
# @param many [Hash, nil] Entity definition
|
178
|
+
#
|
179
|
+
# @return [void]
|
180
|
+
def add_expectations(one, many)
|
181
|
+
check_current_context :resource, :url, :method, :code
|
182
|
+
none = !many && !one
|
183
|
+
|
184
|
+
# rubocop:disable Layout/LineLength
|
185
|
+
Utils.deep_set(@resources,
|
186
|
+
[@current_resource, 'paths', @current_url, 'actions', @current_method, 'statuses', @current_code, 'expectations'],
|
187
|
+
{
|
188
|
+
one: one,
|
189
|
+
many: many,
|
190
|
+
none: none,
|
191
|
+
})
|
192
|
+
# rubocop:enable Layout/LineLength
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Adds a request example
|
197
|
+
#
|
198
|
+
# @param url [String, nil] Visited URL
|
199
|
+
# @param action [String, nil] HTTP verb
|
200
|
+
# @param status_code [Integer, nil] Status code
|
201
|
+
# @param response [String, nil] Response body
|
202
|
+
# @param path_params [Hash, nil] Used path parameters
|
203
|
+
# @param params [Hash, nil] Used body parameters
|
204
|
+
#
|
105
205
|
# rubocop:disable Metrics/ParameterLists
|
106
206
|
def add_request_example(url: nil, action: nil, status_code: nil, response: nil, path_params: nil, params: nil)
|
107
207
|
resource = nil
|
108
208
|
@resources.each do |key, res|
|
109
|
-
resource = key if
|
209
|
+
resource = key if res.dig :paths, url.to_sym, :actions, action.to_sym, :statuses, status_code.to_s.to_sym
|
110
210
|
end
|
111
211
|
|
112
212
|
raise "Resource not found for #{action.upcase} #{url}" unless resource
|
113
213
|
|
114
214
|
Utils.deep_set(@resources,
|
115
|
-
|
215
|
+
[resource, 'paths', url, 'actions', action, 'statuses', status_code, 'example'],
|
116
216
|
path_params: path_params,
|
117
217
|
params: params,
|
118
218
|
response: response)
|
119
219
|
end
|
120
220
|
# rubocop:enable Metrics/ParameterLists
|
121
221
|
|
222
|
+
##
|
223
|
+
# @return [Hash] Hash representation of the metadata
|
122
224
|
def to_h
|
123
225
|
{
|
124
226
|
resources: @resources,
|
@@ -128,6 +230,13 @@ module RSpec
|
|
128
230
|
|
129
231
|
private
|
130
232
|
|
233
|
+
##
|
234
|
+
# Checks for the definition of given scopes.
|
235
|
+
# This is useful to verify if all metadata is set for the current example
|
236
|
+
#
|
237
|
+
# @param scope [Symbol[]] List of scope to check for
|
238
|
+
#
|
239
|
+
# @return [Boolean]
|
131
240
|
def check_current_context(*scope) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
132
241
|
scope ||= []
|
133
242
|
raise 'No resource declared' if scope.include?(:resource) && !@current_resource
|
@@ -136,6 +245,13 @@ module RSpec
|
|
136
245
|
raise 'No status code declared' if scope.include?(:code) && !@current_code
|
137
246
|
end
|
138
247
|
|
248
|
+
##
|
249
|
+
# Checks if a given parameter is used in the URL (_path_) or querystring (query)
|
250
|
+
#
|
251
|
+
# @param url_chunks [String[]] Chunks of an url splitted on the query separator (`?`)
|
252
|
+
# @param name [Symbol] Name of the parameter
|
253
|
+
#
|
254
|
+
# @return [:path, :query]
|
139
255
|
def path_param_scope(url_chunks, name)
|
140
256
|
if /:#{name}/.match?(url_chunks[0])
|
141
257
|
:path
|
@@ -146,15 +262,25 @@ module RSpec
|
|
146
262
|
end
|
147
263
|
end
|
148
264
|
|
149
|
-
|
265
|
+
##
|
266
|
+
# Checks and complete a field definition
|
267
|
+
#
|
268
|
+
# @param fields [Hash,Symbol] Fields definitions
|
269
|
+
#
|
270
|
+
# @return [Hash,Symbol] Completed field definition
|
271
|
+
def organize_params(fields) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
272
|
+
return fields if fields.is_a?(Symbol) && PRIMITIVES.include?(fields)
|
273
|
+
raise "Unsupported type \"#{fields}\"" unless fields.is_a? Hash
|
274
|
+
|
150
275
|
out = { properties: {} }
|
151
276
|
required = []
|
277
|
+
|
152
278
|
allowed_types = %i[array object]
|
153
279
|
fields.each do |name, field|
|
154
280
|
allowed_type = allowed_types.include?(field[:type]) || PARAM_TYPES.key?(field[:type])
|
155
281
|
raise "Field type not allowed: #{field[:type]}" unless allowed_type
|
156
282
|
|
157
|
-
required.push name.to_s if field[:required]
|
283
|
+
required.push name.to_s if field[:required] != false
|
158
284
|
|
159
285
|
out[:properties][name] = fill_request_param field
|
160
286
|
end
|
@@ -162,9 +288,15 @@ module RSpec
|
|
162
288
|
out
|
163
289
|
end
|
164
290
|
|
291
|
+
##
|
292
|
+
# Checks and completes a request parameter definition
|
293
|
+
#
|
294
|
+
# @param field [Hash] Parameter definition
|
295
|
+
#
|
296
|
+
# @return [Hash] Completed parameter
|
165
297
|
def fill_request_param(field)
|
166
|
-
if field[:type] == :object && field[:
|
167
|
-
organize_params field[:
|
298
|
+
if field[:type] == :object && field[:attributes]
|
299
|
+
organize_params field[:attributes]
|
168
300
|
else
|
169
301
|
properties = {
|
170
302
|
type: PARAM_TYPES[field[:type]][:type],
|
@@ -6,7 +6,7 @@ require 'active_support'
|
|
6
6
|
module RSpec
|
7
7
|
module Rails
|
8
8
|
module Api
|
9
|
-
# Class to render
|
9
|
+
# Class to render metadata.
|
10
10
|
# Example:
|
11
11
|
# ```rb
|
12
12
|
# renderer = RSpec::Rails::Api::OpenApiRenderer.new
|
@@ -27,46 +27,80 @@ module RSpec
|
|
27
27
|
@api_license = {}
|
28
28
|
end
|
29
29
|
|
30
|
+
##
|
31
|
+
# Merges example context definition with the renderer data
|
32
|
+
#
|
33
|
+
# @param context [Hash] Metadata hash
|
34
|
+
# @param dump_metadata [Boolean] Saves the raw metadata in `tmp/rra_metadata.yaml` for debugging
|
35
|
+
#
|
36
|
+
# @return [void
|
30
37
|
def merge_context(context, dump_metadata: false)
|
31
|
-
@metadata[:resources].deep_merge! context[:resources]
|
32
|
-
@metadata[:entities].deep_merge! context[:entities]
|
38
|
+
@metadata[:resources].deep_merge! context.respond_to?(:resources) ? context.resources : context[:resources]
|
39
|
+
@metadata[:entities].deep_merge! context.respond_to?(:entities) ? context.entities : context[:entities]
|
33
40
|
|
34
41
|
# Save context for debug and fixtures
|
35
|
-
File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'),
|
42
|
+
File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'), @metadata.to_yaml if dump_metadata
|
36
43
|
end
|
37
44
|
|
45
|
+
##
|
46
|
+
# Write OpenAPI files
|
47
|
+
#
|
48
|
+
# @param path [String, nil] Where to save the files. Defaults to `/tmp/rspec_api_rails.*` when unset
|
49
|
+
# @param only [[Symbol]] Formats to save the file to. Allowed values are `:yaml` and `:json`
|
50
|
+
#
|
51
|
+
# @return [void]
|
38
52
|
def write_files(path = nil, only: %i[yaml json])
|
39
|
-
|
53
|
+
return unless write_file? RSpec.world.filtered_examples
|
54
|
+
|
40
55
|
path ||= ::Rails.root.join('tmp', 'rspec_api_rails')
|
41
56
|
|
42
57
|
file_types = %i[yaml json]
|
43
|
-
|
44
58
|
only.each do |type|
|
45
59
|
next unless file_types.include? type
|
46
60
|
|
47
|
-
File.write "#{path}.#{type}",
|
61
|
+
File.write "#{path}.#{type}", prepare_metadata.send("to_#{type}")
|
48
62
|
end
|
49
63
|
end
|
50
64
|
|
65
|
+
##
|
66
|
+
# Extracts metadata from context to generate an OpenAPI structure
|
67
|
+
#
|
68
|
+
# @return [Hash] The OpenAPI structure
|
51
69
|
def prepare_metadata
|
70
|
+
extract_metadata
|
52
71
|
# Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
|
53
|
-
|
54
|
-
{
|
72
|
+
hash = {
|
55
73
|
openapi: '3.0.0',
|
56
74
|
info: @api_infos,
|
57
75
|
servers: @api_servers,
|
58
76
|
paths: @api_paths,
|
59
77
|
components: @api_components,
|
60
78
|
tags: @api_tags,
|
61
|
-
}
|
79
|
+
}
|
80
|
+
JSON.parse(JSON.pretty_generate(hash))
|
62
81
|
end
|
63
82
|
|
83
|
+
##
|
84
|
+
# Sets the contact field
|
85
|
+
#
|
86
|
+
# @param name [String, nil] Contact name
|
87
|
+
# @param email [String, nil] Contact Email
|
88
|
+
# @param url [String, nil] Contact URL
|
89
|
+
#
|
90
|
+
# @return [void]
|
64
91
|
def api_contact=(name: nil, email: nil, url: nil)
|
65
92
|
@api_contact[:name] = name if name
|
66
93
|
@api_contact[:email] = email if email
|
67
94
|
@api_contact[:url] = url if url
|
68
95
|
end
|
69
96
|
|
97
|
+
##
|
98
|
+
# Sets the license field
|
99
|
+
#
|
100
|
+
# @param name [String, nil] License name
|
101
|
+
# @param url [String, nil] License URL
|
102
|
+
#
|
103
|
+
# @return [void]
|
70
104
|
def api_license=(name: nil, url: nil)
|
71
105
|
@api_license[:name] = name if name
|
72
106
|
@api_license[:url] = url if url
|
@@ -74,14 +108,38 @@ module RSpec
|
|
74
108
|
|
75
109
|
private
|
76
110
|
|
77
|
-
def
|
111
|
+
def write_file?(examples)
|
112
|
+
acceptance_examples = examples.values.flatten.filter do |e|
|
113
|
+
e.metadata[:type] == :acceptance
|
114
|
+
end
|
115
|
+
unless acceptance_examples.none?(&:exception)
|
116
|
+
puts "\n\e[00;31mSome acceptance tests failed. OpenApi specification file was not updated.\n\e[00m"
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Extracts metadata for rendering
|
125
|
+
#
|
126
|
+
# @return [void]
|
127
|
+
def extract_metadata
|
78
128
|
extract_from_resources
|
79
129
|
api_infos
|
80
130
|
api_servers
|
81
131
|
end
|
82
132
|
|
83
|
-
|
133
|
+
##
|
134
|
+
# Extracts metadata from resources for rendering
|
135
|
+
#
|
136
|
+
# @return [void]
|
137
|
+
def extract_from_resources # rubocop:disable Metrics/MethodLength
|
138
|
+
@api_components[:schemas] ||= {}
|
84
139
|
@metadata[:resources].each do |resource_key, resource|
|
140
|
+
resource[:entities].each do |name, entity|
|
141
|
+
@api_components[:schemas][name] = process_entity(entity)
|
142
|
+
end
|
85
143
|
@api_tags.push(
|
86
144
|
name: resource_key.to_s,
|
87
145
|
description: resource[:description]
|
@@ -90,6 +148,14 @@ module RSpec
|
|
90
148
|
end
|
91
149
|
end
|
92
150
|
|
151
|
+
##
|
152
|
+
# Processes a resource from metadata
|
153
|
+
#
|
154
|
+
# @param resource [Symbol, nil] Resource name
|
155
|
+
# @param resource_config [Hash, nil] Resource declaration
|
156
|
+
#
|
157
|
+
#
|
158
|
+
# @return [void]
|
93
159
|
def process_resource(resource: nil, resource_config: nil) # rubocop:disable Metrics/MethodLength
|
94
160
|
http_verbs = %i[get post put patch delete]
|
95
161
|
resource_config[:paths].each do |path_key, path|
|
@@ -111,6 +177,10 @@ module RSpec
|
|
111
177
|
end
|
112
178
|
end
|
113
179
|
|
180
|
+
##
|
181
|
+
# Processes path parameters for rendering
|
182
|
+
#
|
183
|
+
# @param params [Hash] Path parameters
|
114
184
|
def process_path_params(params)
|
115
185
|
parameters = []
|
116
186
|
params.each do |name, param|
|
@@ -120,6 +190,37 @@ module RSpec
|
|
120
190
|
parameters
|
121
191
|
end
|
122
192
|
|
193
|
+
def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
194
|
+
schema = {
|
195
|
+
properties: {},
|
196
|
+
}
|
197
|
+
required = []
|
198
|
+
entity.fields.each do |name, field|
|
199
|
+
property = {
|
200
|
+
description: field.description,
|
201
|
+
type: PARAM_TYPES[field.type][:type],
|
202
|
+
}
|
203
|
+
property[:format] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
|
204
|
+
schema[:properties][name] = property
|
205
|
+
# Primitives support
|
206
|
+
property[:items] = { type: field.attributes } if PRIMITIVES.include? field.attributes
|
207
|
+
|
208
|
+
required.push name unless field.required == false
|
209
|
+
end
|
210
|
+
|
211
|
+
schema[:required] = required unless required.size.zero?
|
212
|
+
|
213
|
+
schema
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Processes a path parameter from metadata
|
218
|
+
#
|
219
|
+
# @param name [Symbol, nil] Parameter name
|
220
|
+
# @param param [Hash, nil] Parameter declaration
|
221
|
+
#
|
222
|
+
#
|
223
|
+
# @return [void]
|
123
224
|
def process_path_param(name, param) # rubocop:disable Metrics/MethodLength
|
124
225
|
parameter = {
|
125
226
|
name: name.to_s,
|
@@ -136,6 +237,19 @@ module RSpec
|
|
136
237
|
parameter
|
137
238
|
end
|
138
239
|
|
240
|
+
##
|
241
|
+
# Processes an action from metadata
|
242
|
+
#
|
243
|
+
# @param resource [Symbol, nil] Target resource
|
244
|
+
# @param path [Symbol, nil] Target path
|
245
|
+
# @param path_config [Hash, nil] Path configuraton
|
246
|
+
# @param action_config [Symbol, nil] Target action
|
247
|
+
# @param parameters [Array, nil] Path parameters
|
248
|
+
#
|
249
|
+
# @return [void]
|
250
|
+
#
|
251
|
+
# FIXME: Rename "action_config" to "action"
|
252
|
+
# FIXME: Rename "parameters" to "path_parameters"
|
139
253
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
140
254
|
def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil)
|
141
255
|
responses = {}
|
@@ -154,10 +268,11 @@ module RSpec
|
|
154
268
|
responses[status_key] = process_response status: status_key, status_config: status, content: content
|
155
269
|
end
|
156
270
|
|
157
|
-
|
158
|
-
action
|
159
|
-
|
160
|
-
|
271
|
+
summary = path_config[:actions][action_config][:summary]
|
272
|
+
action = {
|
273
|
+
summary: summary,
|
274
|
+
description: path_config[:actions][action_config][:description],
|
275
|
+
operationId: "#{resource} #{summary}".downcase.gsub(/[^\w]/, '_'),
|
161
276
|
parameters: parameters,
|
162
277
|
responses: responses,
|
163
278
|
tags: [resource.to_s],
|
@@ -169,8 +284,16 @@ module RSpec
|
|
169
284
|
end
|
170
285
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
171
286
|
|
287
|
+
##
|
288
|
+
# Processes a request body from metadata
|
289
|
+
#
|
290
|
+
# @param schema [Hash] Schema
|
291
|
+
# @param ref [String] Reference
|
292
|
+
# @param examples [Hash] Example
|
293
|
+
#
|
294
|
+
# @return [void]
|
172
295
|
def process_request_body(schema: nil, ref: nil, examples: {})
|
173
|
-
Utils.deep_set @api_components,
|
296
|
+
Utils.deep_set @api_components, ['schemas', ref], schema
|
174
297
|
{
|
175
298
|
# description: '',
|
176
299
|
required: true,
|
@@ -183,15 +306,22 @@ module RSpec
|
|
183
306
|
}
|
184
307
|
end
|
185
308
|
|
309
|
+
##
|
310
|
+
# Process a response from metadata
|
311
|
+
#
|
312
|
+
# @param status [Symbol] Status code
|
313
|
+
# @param status_config [Hash] Configuration for status code
|
314
|
+
# @param content [String] Response content
|
315
|
+
#
|
316
|
+
# @return [void]
|
186
317
|
def process_response(status: nil, status_config: nil, content: nil)
|
187
|
-
response = {
|
188
|
-
description: status_config[:description],
|
189
|
-
}
|
318
|
+
response = { description: status_config[:description] }
|
190
319
|
|
191
320
|
return response if status.to_s == '204' && content # No content
|
192
321
|
|
193
322
|
response[:content] = {
|
194
323
|
'application/json': {
|
324
|
+
schema: response_schema(status_config[:expectations]),
|
195
325
|
examples: { default: { value: JSON.parse(content) } },
|
196
326
|
},
|
197
327
|
}
|
@@ -199,6 +329,25 @@ module RSpec
|
|
199
329
|
response
|
200
330
|
end
|
201
331
|
|
332
|
+
def response_schema(expectations)
|
333
|
+
if expectations[:many]
|
334
|
+
items = if PRIMITIVES.include?(expectations[:many])
|
335
|
+
{ type: expectations[:many] }
|
336
|
+
else
|
337
|
+
{ '$ref' => "#/components/schemas/#{expectations[:many]}" }
|
338
|
+
end
|
339
|
+
{ type: 'array', items: items }
|
340
|
+
elsif expectations[:one]
|
341
|
+
{ '$ref' => "#/components/schemas/#{expectations[:one]}" }
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
##
|
346
|
+
# Processes examples from statuses
|
347
|
+
#
|
348
|
+
# @param statuses [Hash]
|
349
|
+
#
|
350
|
+
# @return [Hash] Request examples
|
202
351
|
def process_examples(statuses)
|
203
352
|
request_examples = {}
|
204
353
|
|
@@ -212,16 +361,32 @@ module RSpec
|
|
212
361
|
request_examples
|
213
362
|
end
|
214
363
|
|
364
|
+
##
|
365
|
+
# Converts path with params like ":id" to their OpenAPI representation
|
366
|
+
#
|
367
|
+
# @param string [String] The original path
|
368
|
+
#
|
369
|
+
# @return [String] OpenAPI path representation
|
215
370
|
def path_with_params(string)
|
216
371
|
string.gsub(/(?::(\w*))/) do |e|
|
217
372
|
"{#{e.sub(':', '')}}"
|
218
373
|
end
|
219
374
|
end
|
220
375
|
|
376
|
+
##
|
377
|
+
# Converts a string to a snake_cased string to use as operationId
|
378
|
+
#
|
379
|
+
# @param string [String] Original string
|
380
|
+
#
|
381
|
+
# @return [String] Snake_cased string
|
221
382
|
def escape_operation_id(string)
|
222
383
|
string.downcase.gsub(/[^\w]+/, '_')
|
223
384
|
end
|
224
385
|
|
386
|
+
##
|
387
|
+
# Fills the API general information sections
|
388
|
+
#
|
389
|
+
# @return [void]
|
225
390
|
def api_infos
|
226
391
|
@api_infos = {
|
227
392
|
title: @api_title || 'Some sample app',
|
@@ -235,6 +400,10 @@ module RSpec
|
|
235
400
|
@api_infos
|
236
401
|
end
|
237
402
|
|
403
|
+
##
|
404
|
+
# Fills the API servers section
|
405
|
+
#
|
406
|
+
# @return [void]
|
238
407
|
def api_servers
|
239
408
|
@api_servers || [
|
240
409
|
{ url: 'http://api.example.com' },
|