rspec-rails-api 0.3.4 → 0.4.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.
@@ -27,6 +27,13 @@ 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
38
  @metadata[:resources].deep_merge! context[:resources]
32
39
  @metadata[:entities].deep_merge! context[:entities]
@@ -35,8 +42,14 @@ module RSpec
35
42
  File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'), context.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
40
53
  path ||= ::Rails.root.join('tmp', 'rspec_api_rails')
41
54
 
42
55
  file_types = %i[yaml json]
@@ -44,29 +57,49 @@ module RSpec
44
57
  only.each do |type|
45
58
  next unless file_types.include? type
46
59
 
47
- File.write "#{path}.#{type}", JSON.parse(JSON.pretty_generate(content)).send("to_#{type}")
60
+ File.write "#{path}.#{type}", prepare_metadata.send("to_#{type}")
48
61
  end
49
62
  end
50
63
 
64
+ ##
65
+ # Extracts metadata from context to generate an OpenAPI structure
66
+ #
67
+ # @return [Hash] The OpenAPI structure
51
68
  def prepare_metadata
52
- # Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
53
69
  extract_metadatas
54
- {
70
+ # Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
71
+ hash = {
55
72
  openapi: '3.0.0',
56
73
  info: @api_infos,
57
74
  servers: @api_servers,
58
75
  paths: @api_paths,
59
76
  components: @api_components,
60
77
  tags: @api_tags,
61
- }.deep_stringify_keys
78
+ }
79
+ JSON.parse(JSON.pretty_generate(hash))
62
80
  end
63
81
 
82
+ ##
83
+ # Sets the contact field
84
+ #
85
+ # @param name [String, nil] Contact name
86
+ # @param email [String, nil] Contact Email
87
+ # @param url [String, nil] Contact URL
88
+ #
89
+ # @return [void]
64
90
  def api_contact=(name: nil, email: nil, url: nil)
65
91
  @api_contact[:name] = name if name
66
92
  @api_contact[:email] = email if email
67
93
  @api_contact[:url] = url if url
68
94
  end
69
95
 
96
+ ##
97
+ # Sets the license field
98
+ #
99
+ # @param name [String, nil] License name
100
+ # @param url [String, nil] License URL
101
+ #
102
+ # @return [void]
70
103
  def api_license=(name: nil, url: nil)
71
104
  @api_license[:name] = name if name
72
105
  @api_license[:url] = url if url
@@ -74,14 +107,26 @@ module RSpec
74
107
 
75
108
  private
76
109
 
110
+ ##
111
+ # Extracts metadata for rendering
112
+ #
113
+ # @return [void]
77
114
  def extract_metadatas
78
115
  extract_from_resources
79
116
  api_infos
80
117
  api_servers
81
118
  end
82
119
 
83
- def extract_from_resources
120
+ ##
121
+ # Extracts metadata from resources for rendering
122
+ #
123
+ # @return [void]
124
+ def extract_from_resources # rubocop:disable Metrics/MethodLength
125
+ @api_components[:schemas] ||= {}
84
126
  @metadata[:resources].each do |resource_key, resource|
127
+ resource[:entities].each do |name, entity|
128
+ @api_components[:schemas][name] = process_entity(entity)
129
+ end
85
130
  @api_tags.push(
86
131
  name: resource_key.to_s,
87
132
  description: resource[:description]
@@ -90,6 +135,14 @@ module RSpec
90
135
  end
91
136
  end
92
137
 
138
+ ##
139
+ # Processes a resource from metadata
140
+ #
141
+ # @param resource [Symbol, nil] Resource name
142
+ # @param resource_config [Hash, nil] Resource declaration
143
+ #
144
+ #
145
+ # @return [void]
93
146
  def process_resource(resource: nil, resource_config: nil) # rubocop:disable Metrics/MethodLength
94
147
  http_verbs = %i[get post put patch delete]
95
148
  resource_config[:paths].each do |path_key, path|
@@ -111,6 +164,10 @@ module RSpec
111
164
  end
112
165
  end
113
166
 
167
+ ##
168
+ # Processes path parameters for rendering
169
+ #
170
+ # @param params [Hash] Path parameters
114
171
  def process_path_params(params)
115
172
  parameters = []
116
173
  params.each do |name, param|
@@ -120,6 +177,39 @@ module RSpec
120
177
  parameters
121
178
  end
122
179
 
180
+ def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
181
+ schema = {
182
+ properties: {},
183
+ }
184
+ required = []
185
+ entity.fields.each do |name, field|
186
+ property = {
187
+ description: field.description,
188
+ type: PARAM_TYPES[field.type][:type],
189
+ }
190
+ property[:format] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
191
+ schema[:properties][name] = property
192
+ # Primitives support
193
+ if PRIMITIVES.include? field.attributes
194
+ property[:items] =
195
+ { type: field.attributes.to_s.split('_').last.to_sym }
196
+ end
197
+ required.push name unless field.required == false
198
+ end
199
+
200
+ schema[:required] = required unless required.size.zero?
201
+
202
+ schema
203
+ end
204
+
205
+ ##
206
+ # Processes a path parameter from metadata
207
+ #
208
+ # @param name [Symbol, nil] Parameter name
209
+ # @param param [Hash, nil] Parameter declaration
210
+ #
211
+ #
212
+ # @return [void]
123
213
  def process_path_param(name, param) # rubocop:disable Metrics/MethodLength
124
214
  parameter = {
125
215
  name: name.to_s,
@@ -136,6 +226,19 @@ module RSpec
136
226
  parameter
137
227
  end
138
228
 
229
+ ##
230
+ # Processes an action from metadata
231
+ #
232
+ # @param resource [Symbol, nil] Target resource
233
+ # @param path [Symbol, nil] Target path
234
+ # @param path_config [Hash, nil] Path configuraton
235
+ # @param action_config [Symbol, nil] Target action
236
+ # @param parameters [Array, nil] Path parameters
237
+ #
238
+ # @return [void]
239
+ #
240
+ # FIXME: Rename "action_config" to "action"
241
+ # FIXME: Rename "parameters" to "path_parameters"
139
242
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
140
243
  def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil)
141
244
  responses = {}
@@ -154,10 +257,11 @@ module RSpec
154
257
  responses[status_key] = process_response status: status_key, status_config: status, content: content
155
258
  end
156
259
 
157
- description = path_config[:actions][action_config][:description]
158
- action = {
159
- description: description,
160
- operationId: "#{resource} #{description}".downcase.gsub(/[^\w]/, '_'),
260
+ summary = path_config[:actions][action_config][:summary]
261
+ action = {
262
+ summary: summary,
263
+ description: path_config[:actions][action_config][:description],
264
+ operationId: "#{resource} #{summary}".downcase.gsub(/[^\w]/, '_'),
161
265
  parameters: parameters,
162
266
  responses: responses,
163
267
  tags: [resource.to_s],
@@ -169,6 +273,14 @@ module RSpec
169
273
  end
170
274
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
171
275
 
276
+ ##
277
+ # Processes a request body from metadata
278
+ #
279
+ # @param schema [Hash] Schema
280
+ # @param ref [String] Reference
281
+ # @param examples [Hash] Example
282
+ #
283
+ # @return [void]
172
284
  def process_request_body(schema: nil, ref: nil, examples: {})
173
285
  Utils.deep_set @api_components, "schemas.#{ref}", schema
174
286
  {
@@ -183,15 +295,22 @@ module RSpec
183
295
  }
184
296
  end
185
297
 
298
+ ##
299
+ # Process a response from metadata
300
+ #
301
+ # @param status [Symbol] Status code
302
+ # @param status_config [Hash] Configuration for status code
303
+ # @param content [String] Response content
304
+ #
305
+ # @return [void]
186
306
  def process_response(status: nil, status_config: nil, content: nil)
187
- response = {
188
- description: status_config[:description],
189
- }
307
+ response = { description: status_config[:description] }
190
308
 
191
309
  return response if status.to_s == '204' && content # No content
192
310
 
193
311
  response[:content] = {
194
312
  'application/json': {
313
+ schema: response_schema(status_config[:expectations]),
195
314
  examples: { default: { value: JSON.parse(content) } },
196
315
  },
197
316
  }
@@ -199,6 +318,25 @@ module RSpec
199
318
  response
200
319
  end
201
320
 
321
+ def response_schema(expectations)
322
+ if expectations[:many]
323
+ items = if PRIMITIVES.include?(expectations[:many])
324
+ { type: expectations[:many].to_s.split('_').last }
325
+ else
326
+ { '$ref' => "#/components/schemas/#{expectations[:many]}" }
327
+ end
328
+ { type: 'array', items: items }
329
+ elsif expectations[:one]
330
+ { '$ref' => "#/components/schemas/#{expectations[:one]}" }
331
+ end
332
+ end
333
+
334
+ ##
335
+ # Processes examples from statuses
336
+ #
337
+ # @param statuses [Hash]
338
+ #
339
+ # @return [Hash] Request examples
202
340
  def process_examples(statuses)
203
341
  request_examples = {}
204
342
 
@@ -212,16 +350,32 @@ module RSpec
212
350
  request_examples
213
351
  end
214
352
 
353
+ ##
354
+ # Converts path with params like ":id" to their OpenAPI representation
355
+ #
356
+ # @param string [String] The original path
357
+ #
358
+ # @return [String] OpenAPI path representation
215
359
  def path_with_params(string)
216
360
  string.gsub(/(?::(\w*))/) do |e|
217
361
  "{#{e.sub(':', '')}}"
218
362
  end
219
363
  end
220
364
 
365
+ ##
366
+ # Converts a string to a snake_cased string to use as operationId
367
+ #
368
+ # @param string [String] Original string
369
+ #
370
+ # @return [String] Snake_cased string
221
371
  def escape_operation_id(string)
222
372
  string.downcase.gsub(/[^\w]+/, '_')
223
373
  end
224
374
 
375
+ ##
376
+ # Fills the API general information sections
377
+ #
378
+ # @return [void]
225
379
  def api_infos
226
380
  @api_infos = {
227
381
  title: @api_title || 'Some sample app',
@@ -235,6 +389,10 @@ module RSpec
235
389
  @api_infos
236
390
  end
237
391
 
392
+ ##
393
+ # Fills the API servers section
394
+ #
395
+ # @return [void]
238
396
  def api_servers
239
397
  @api_servers || [
240
398
  { url: 'http://api.example.com' },
@@ -7,6 +7,13 @@ module RSpec
7
7
  module Api
8
8
  # Helper methods
9
9
  class Utils
10
+ ##
11
+ # Gets a value in a hash by specifying a dotted path
12
+ #
13
+ # @param hash [Hash] The hash to search in
14
+ # @param path [String] The dotted path
15
+ #
16
+ # @return [*] The value or nil
10
17
  def self.deep_get(hash, path)
11
18
  path.split('.').inject(hash) do |sub_hash, key|
12
19
  return nil unless sub_hash.is_a?(Hash) && sub_hash.key?(key.to_sym)
@@ -15,6 +22,14 @@ module RSpec
15
22
  end
16
23
  end
17
24
 
25
+ ##
26
+ # Sets a value at given dotted path in a hash
27
+ #
28
+ # @param hash [Hash] The target hash
29
+ # @param path [String] Dotted path
30
+ # @param value [*] Value to set
31
+ #
32
+ # @return [Hash] The modified hash
18
33
  def self.deep_set(hash, path, value)
19
34
  path = path.split('.') unless path.is_a? Array
20
35
 
@@ -27,6 +42,13 @@ module RSpec
27
42
  hash
28
43
  end
29
44
 
45
+ ##
46
+ # Checks if a value is of the given parameter type
47
+ #
48
+ # @param type [Symbol] Type to compare to
49
+ # @param value [*] Value to test
50
+ #
51
+ # @return [Boolean] True when the value corresponds to the given type
30
52
  def self.check_value_type(type, value)
31
53
  return true if type == :boolean && (value.is_a?(TrueClass) || value.is_a?(FalseClass))
32
54
  return true if type == :array && value.is_a?(Array)
@@ -36,6 +58,13 @@ module RSpec
36
58
  value.is_a? PARAM_TYPES[type][:class]
37
59
  end
38
60
 
61
+ ##
62
+ # Validates an object keys and values types
63
+ #
64
+ # @param actual [Hash] Hash to compare
65
+ # @param expected [Hash] Structure to compare
66
+ #
67
+ # @return [Boolean] True when the object matches the structure
39
68
  def self.validate_object_structure(actual, expected)
40
69
  # Check keys
41
70
  return false unless same_keys? actual, expected
@@ -56,23 +85,53 @@ module RSpec
56
85
  true
57
86
  end
58
87
 
59
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
88
+ ##
89
+ # Validates an array or hash against a definition
90
+ #
91
+ # @param expected_type [:object, :array] The expected type
92
+ # @param expected_attributes [Hash] Attributes configuration
93
+ # @param actual [Array, Hash] Value to check
94
+ #
95
+ # @return [Boolean] True when `actual` is of the expected definition
60
96
  def self.validate_deep_object(expected_type, expected_attributes, actual)
61
97
  if %i[object array].include?(expected_type) && expected_attributes.is_a?(Hash)
62
98
  case expected_type
63
99
  when :object
64
100
  return false unless validate_object_structure actual, expected_attributes
65
101
  when :array
66
- actual.each do |array_entry|
67
- return false unless validate_object_structure array_entry, expected_attributes
68
- end
102
+ return false unless validate_deep_object_array actual, expected_attributes
69
103
  end
70
104
  end
71
105
 
72
106
  true
73
107
  end
74
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
75
108
 
109
+ ##
110
+ # Validates each entry of an array
111
+ #
112
+ # @param array [Array] The array to check
113
+ # @param expected_attributes [Hash] Attributes configuration
114
+ #
115
+ # @return [Boolean] True when all values matches the attribute configuration
116
+ def self.validate_deep_object_array(array, expected_attributes)
117
+ array.each do |array_entry|
118
+ if expected_attributes[:type]
119
+ return false unless check_value_type expected_attributes[:type], array_entry
120
+ else
121
+ return false unless validate_object_structure array_entry, expected_attributes
122
+ end
123
+ end
124
+
125
+ true
126
+ end
127
+
128
+ ##
129
+ # Checks if a hash have the required keys in it
130
+ #
131
+ # @param actual [Hash] The hash
132
+ # @param expected [Hash] Attributes definitions
133
+ #
134
+ # @return [Boolean] True when the object is valid
76
135
  def self.same_keys?(actual, expected)
77
136
  optional = expected.reject { |_key, value| value[:required] }.keys
78
137
  actual.symbolize_keys.keys.sort - optional == expected.keys.sort - optional
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module Rails
5
5
  module Api
6
- VERSION = '0.3.4'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
9
9
  end
@@ -15,6 +15,8 @@ module RSpec
15
15
  class Error < StandardError
16
16
  end
17
17
 
18
+ ##
19
+ # OpenAPI types, format and Ruby class correspondence
18
20
  PARAM_TYPES = {
19
21
  int32: { type: 'integer', format: 'int32', class: Integer },
20
22
  int64: { type: 'integer', format: 'int64', class: Integer },
@@ -32,6 +34,12 @@ module RSpec
32
34
  array: { type: 'array', format: nil, class: Array },
33
35
  object: { type: 'object', format: nil, class: Hash },
34
36
  }.freeze
37
+
38
+ EXCLUDED_PRIMITIVES = %i[array object].freeze
39
+
40
+ PRIMITIVES = PARAM_TYPES.keys
41
+ .reject { |key| EXCLUDED_PRIMITIVES.include? key }
42
+ .map { |key| "type_#{key}".to_sym }
35
43
  end
36
44
  end
37
45
  end
@@ -18,9 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.homepage = 'https://gitlab.com/experimentslabs/rspec-rails-api'
19
19
  spec.license = 'MIT'
20
20
  spec.metadata = {
21
- 'source_code_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api',
22
- 'bug_tracker_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/issues',
23
- 'changelog_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md',
21
+ 'source_code_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api',
22
+ 'bug_tracker_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/issues',
23
+ 'changelog_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md',
24
+ 'rubygems_mfa_required' => 'true',
24
25
  }
25
26
 
26
27
  # Specify which files should be added to the gem when it is released.
@@ -35,11 +36,12 @@ Gem::Specification.new do |spec|
35
36
  spec.required_ruby_version = '>= 2.5.0'
36
37
 
37
38
  spec.add_development_dependency 'activesupport', '~> 6.0'
38
- spec.add_development_dependency 'bundler', '~> 1.17'
39
+ spec.add_development_dependency 'bundler'
39
40
  spec.add_development_dependency 'byebug'
40
41
  spec.add_development_dependency 'rake', '~> 10.0'
41
42
  spec.add_development_dependency 'rspec', '~> 3.0'
42
43
  spec.add_development_dependency 'rubocop'
43
44
  spec.add_development_dependency 'rubocop-performance'
44
45
  spec.add_development_dependency 'simplecov'
46
+ spec.add_development_dependency 'yard'
45
47
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-rails-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Tancoigne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-20 00:00:00.000000000 Z
11
+ date: 2021-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.17'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.17'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: byebug
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  description: |
126
140
  Create acceptance tests to check the Rails API responses and generate
127
141
  documentation from it.
@@ -162,6 +176,7 @@ metadata:
162
176
  source_code_uri: https://gitlab.com/experimentslabs/rspec-rails-api
163
177
  bug_tracker_uri: https://gitlab.com/experimentslabs/rspec-rails-api/issues
164
178
  changelog_uri: https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md
179
+ rubygems_mfa_required: 'true'
165
180
  post_install_message:
166
181
  rdoc_options: []
167
182
  require_paths: