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.
@@ -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 metadatas.
11
+ # Handles contexts and examples metadata.
11
12
  class Metadata # rubocop:disable Metrics/ClassLength
12
- attr_reader :entities, :resources, :current_resource, :parameters
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
- def add_entity(type, fields)
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
- "#{@current_resource}.entities.#{type}",
48
+ [@current_resource, 'entities', name],
33
49
  EntityConfig.new(fields))
34
50
  end
35
51
 
36
- def add_parameter(type, fields)
37
- raise "Parameter #{type} is already defined" if @parameters[type]
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[type] = fields
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 = Utils.check_attribute_type(field[:type], except: %i[array object])
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, "#{@current_resource}.paths.#{@current_url}.path_params.#{name}",
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
- "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.params",
113
+ [@current_resource, 'paths', @current_url, 'actions', @current_method, 'params'],
78
114
  params)
79
115
  end
80
116
 
81
- def add_action(method, url, description)
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, "#{@current_resource}.paths.#{url}.actions.#{method}",
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
- "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{status_code}",
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 Utils.deep_get(res, "paths.#{url}.actions.#{action}.statuses.#{status_code}")
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
- "#{resource}.paths.#{url}.actions.#{action}.statuses.#{status_code}.example",
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
- def organize_params(fields) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
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[:properties]
167
- organize_params field[:properties]
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 metadatas.
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'), context.to_yaml if dump_metadata
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
- content = prepare_metadata
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}", JSON.parse(JSON.pretty_generate(content)).send("to_#{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
- extract_metadatas
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
- }.deep_stringify_keys
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 extract_metadatas
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
- def extract_from_resources
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
- description = path_config[:actions][action_config][:description]
158
- action = {
159
- description: description,
160
- operationId: "#{resource} #{description}".downcase.gsub(/[^\w]/, '_'),
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, "schemas.#{ref}", schema
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' },