rspec-rails-api 0.3.4 → 0.5.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 +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' },
|