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.
@@ -9,99 +9,185 @@ module RSpec
9
9
  # All these methods will be available in example groups
10
10
  # (anything but 'it', 'example', 'for_code')
11
11
  module ExampleGroup
12
- # First method to be called in a spec file
13
- # as it will initialize the metadatas.
12
+ ##
13
+ # First method to be called in a spec file # as it will initialize the
14
+ # metadata.
15
+ #
16
+ # @param name [String] Resource name
17
+ # @param description [String] Resource description
14
18
  def resource(name, description = '')
15
- metadata[:rrad] ||= Metadata.new
16
- metadata[:rrad].add_resource name, description
19
+ metadata[:rra] ||= Metadata.new
20
+ metadata[:rra].add_resource name, description
17
21
  end
18
22
 
19
- # Used to describe an entity
23
+ ##
24
+ # Describes an entity
25
+ #
26
+ # @param type [Symbol] Name of the entity for reference
27
+ # @param fields [Hash] Fields declarations
28
+ #
29
+ # @return [void]
20
30
  def entity(type, fields)
21
- metadata[:rrad].add_entity type, fields
31
+ metadata[:rra].add_entity type, fields
22
32
  end
23
33
 
24
- # Used to describe a request or path param which will be available as reference
34
+ ##
35
+ # Describes request or path parameters
36
+ #
37
+ # @param type [Symbol] Name of the parameters set for reference
38
+ # @param fields [Hash] Fields declarations
39
+ #
40
+ # @return [void]
25
41
  def parameters(type, fields)
26
- metadata[:rrad].add_parameter type, fields
42
+ metadata[:rra].add_parameter type, fields
27
43
  end
28
44
 
29
- # Used to describe query parameters
45
+ ##
46
+ # Declares parameters used in URLS (_path_)
47
+ # Use `fields` or `defined` but not both.
48
+ #
49
+ # @param [Hash, nil] fields An attributes declaration
50
+ # @param [Symbol, nil] defined An entity reference
51
+ #
52
+ # @return [void]
30
53
  def path_params(fields: nil, defined: nil)
31
- if defined && !metadata[:rrad].parameters[defined]
54
+ if defined && !metadata[:rra].parameters[defined]
32
55
  raise "Parameter #{defined} was not defined with the 'parameters' method"
33
56
  end
34
57
 
35
- fields ||= metadata[:rrad].parameters[defined]
58
+ fields ||= metadata[:rra].parameters[defined]
36
59
 
37
- metadata[:rrad].add_path_params fields
60
+ metadata[:rra].add_path_params fields
38
61
  end
39
62
 
63
+ ##
64
+ # Declares parameters for a request body
65
+ # Use `attributes` or `defined` but not both.
66
+ #
67
+ # @param [Hash, nil] attributes An attributes declaration
68
+ # @param [Symbol, nil] defined An entity reference.
69
+ #
70
+ # @return [void]
40
71
  def request_params(attributes: nil, defined: nil)
41
- if defined && !metadata[:rrad].parameters[defined]
72
+ if defined && !metadata[:rra].parameters[defined]
42
73
  raise "Parameter #{defined} was not defined with the 'parameters' method"
43
74
  end
44
75
 
45
- attributes ||= metadata[:rrad].parameters[defined]
76
+ attributes ||= metadata[:rra].parameters[defined]
46
77
 
47
- metadata[:rrad].add_request_params attributes
78
+ metadata[:rra].add_request_params attributes
48
79
  end
49
80
 
50
- def on_get(url, description = nil, &block)
51
- on_action(:get, url, description, &block)
81
+ ##
82
+ # Defines a GET action
83
+ #
84
+ # @param [String] url URL to test
85
+ # @param [String] summary What the action does
86
+ # @param [String] description Longer description
87
+ #
88
+ # @return [void]
89
+ def on_get(url, summary = nil, description = nil, &block)
90
+ on_action(:get, url, summary, description, &block)
52
91
  end
53
92
 
54
- def on_post(url, description = nil, &block)
55
- on_action(:post, url, description, &block)
93
+ ##
94
+ # Defines a POST action
95
+ #
96
+ # @param [String] url URL to test
97
+ # @param [String] summary What the action does
98
+ # @param [String] description Longer description
99
+ #
100
+ # @return [void]
101
+ def on_post(url, summary = nil, description = nil, &block)
102
+ on_action(:post, url, summary, description, &block)
56
103
  end
57
104
 
58
- def on_put(url, description = nil, &block)
59
- on_action(:put, url, description, &block)
105
+ ##
106
+ # Defines a PUT action
107
+ #
108
+ # @param [String] url URL to test
109
+ # @param [String] summary What the action does
110
+ # @param [String] description Longer description
111
+ #
112
+ # @return [void]
113
+ def on_put(url, summary = nil, description = nil, &block)
114
+ on_action(:put, url, summary, description, &block)
60
115
  end
61
116
 
62
- def on_patch(url, description = nil, &block)
63
- on_action(:patch, url, description, &block)
117
+ ##
118
+ # Defines a PATCH action
119
+ #
120
+ # @param [String] url URL to test
121
+ # @param [String] summary What the action does
122
+ # @param [String] description Longer description
123
+ #
124
+ # @return [void]
125
+ def on_patch(url, summary = nil, description = nil, &block)
126
+ on_action(:patch, url, summary, description, &block)
64
127
  end
65
128
 
66
- def on_delete(url, description = nil, &block)
67
- on_action(:delete, url, description, &block)
129
+ ##
130
+ # Defines a DELETE action
131
+ #
132
+ # @param [String] url URL to test
133
+ # @param [String] summary What the action does
134
+ # @param [String] description Longer description
135
+ #
136
+ # @return [void]
137
+ def on_delete(url, summary = nil, description = nil, &block)
138
+ on_action(:delete, url, summary, description, &block)
68
139
  end
69
140
 
70
- # Currently fill metadatas with the action
71
- def on_action(action, url, description, &block)
72
- metadata[:rrad].add_action(action, url, description)
73
-
74
- describe("#{action.upcase} #{url}", &block)
75
- end
76
-
77
- def for_code(status_code, description = nil, doc_only: false, test_only: false, &block)
141
+ ##
142
+ # Adds an HTTP code declaration to metadata, with expected result
143
+ # If no expectation is provided, the response will be expected to be empty
144
+ #
145
+ # @param status_code [Number] Status code to test for
146
+ # @param description [String] Description of the route/status pair
147
+ # @param expect_many [Symbol] Check the response for a list of given entity
148
+ # @param expect_one [Symbol] Check the response for a given entity
149
+ # @param test_only [Boolean] When true, test the response without filling the documentation
150
+ #
151
+ # @return [void]
152
+ #
153
+ def for_code(status_code, description = nil, expect_many: nil, expect_one: false, test_only: false, &block)
78
154
  description ||= Rack::Utils::HTTP_STATUS_CODES[status_code]
79
155
 
80
- metadata[:rrad].add_status_code(status_code, description) unless test_only
156
+ metadata[:rra].add_status_code(status_code, description) unless test_only
157
+ metadata[:rra].add_expectations(expect_one, expect_many)
158
+ metadata[:rra_current_example] = metadata[:rra].current_example
81
159
 
82
160
  describe "->#{test_only ? ' test' : ''} #{status_code} - #{description}" do
83
- execute_for_code_block(status_code, doc_only, block)
161
+ execute_for_code_block(block)
84
162
  end
85
163
  end
86
164
 
87
165
  private
88
166
 
89
- def document_only(status_code)
90
- example 'Create documentation' do |example|
91
- parent_example = example.example_group
92
- request_params = prepare_request_params parent_example.module_parent.description
167
+ ##
168
+ # Currently fill metadata with the action
169
+ #
170
+ # @param [Symbol] action HTTP verb
171
+ # @param [String] url URL to test
172
+ # @param [String, nil] summary What the action does
173
+ # @param [String, nil] description Longer description
174
+ #
175
+ # @return [void]
176
+ def on_action(action, url, summary, description, &block)
177
+ metadata[:rra].add_action(action, url, summary, description)
93
178
 
94
- set_request_example parent_example.metadata[:rrad], request_params, status_code
95
- end
179
+ describe("#{action.upcase} #{url}", &block)
96
180
  end
97
181
 
98
- def execute_for_code_block(status_code, doc_only, callback_block)
99
- if (!ENV['DOC_ONLY'] || ENV['DOC_ONLY'] == 'false' || !doc_only) && callback_block
100
- example 'Test and create documentation', caller: callback_block.send(:caller) do
101
- instance_eval(&callback_block) if callback_block
102
- end
103
- else
104
- document_only status_code
182
+ ##
183
+ # Visit the URL and test response
184
+ #
185
+ # @param callback_block [block] Block to execute for testing the response
186
+ #
187
+ # @return [void]
188
+ def execute_for_code_block(callback_block)
189
+ example 'Test and create documentation', caller: callback_block.send(:caller) do
190
+ instance_eval(&callback_block) if callback_block
105
191
  end
106
192
  end
107
193
  end
@@ -18,6 +18,8 @@ module RSpec
18
18
  end
19
19
  end
20
20
 
21
+ ##
22
+ # @return [Hash] Entity configuration
21
23
  def to_h
22
24
  out = {}
23
25
  @fields.each_key do |key|
@@ -26,16 +28,41 @@ module RSpec
26
28
  out
27
29
  end
28
30
 
31
+ ##
32
+ # Replaces the arrays 'of' and objects 'attributes' with the corresponding
33
+ # entities, recursively
34
+ #
35
+ # @param entities [Hash] List of entities
36
+ #
37
+ # @return [Hash]
29
38
  def expand_with(entities)
30
39
  hash = to_h
31
40
  hash.each_pair do |field, config|
32
41
  next unless %i[array object].include? config[:type]
33
42
 
34
- attributes = config[:attributes]
35
- next unless attributes.is_a? Symbol
36
- raise "Entity #{attributes} not found for entity completion." unless entities[attributes]
43
+ attribute = config[:attributes]
44
+ next unless attribute.is_a? Symbol
37
45
 
38
- hash[field][:attributes] = entities[attributes].expand_with(entities)
46
+ hash[field][:attributes] = expand_attribute attribute, entities
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ ##
53
+ # Expands an attribute for "for" and "attributes" keys
54
+ #
55
+ # @param attribute [Symbol] Attribute name
56
+ # @param entities [Hash] List of entities
57
+ def expand_attribute(attribute, entities)
58
+ if PRIMITIVES.include? attribute
59
+ # Primitives support
60
+ { type: attribute.to_s.split('_').last.to_sym }
61
+ else
62
+ # Defined attribute
63
+ raise "Entity #{attribute} not found for entity completion." unless entities[attribute]
64
+
65
+ entities[attribute].expand_with(entities)
39
66
  end
40
67
  end
41
68
  end
@@ -22,6 +22,8 @@ module RSpec
22
22
  @type = type
23
23
  end
24
24
 
25
+ ##
26
+ # @return [Hash] Field configuration
25
27
  def to_h
26
28
  out = { required: @required, type: @type }
27
29
  out[:description] = @description unless @description.nil?
@@ -39,6 +41,12 @@ module RSpec
39
41
 
40
42
  private
41
43
 
44
+ ##
45
+ # Sets @attributes of the field when it's an Array or Hash
46
+ #
47
+ # @param attributes [Hash, Symbol] The attributes definition or reference
48
+ #
49
+ # @return [void]
42
50
  def define_attributes(attributes)
43
51
  @attributes = case attributes
44
52
  when Hash
@@ -4,6 +4,9 @@ require 'active_support/hash_with_indifferent_access'
4
4
 
5
5
  require 'rspec/rails/api/utils'
6
6
 
7
+ ##
8
+ # RSpec matcher to check something against an array of `expected`
9
+ #
7
10
  # FIXME: Split the matcher in something else; it's too messy.
8
11
  RSpec::Matchers.define :have_many do |expected|
9
12
  match do |actual|
@@ -13,18 +16,28 @@ RSpec::Matchers.define :have_many do |expected|
13
16
  raise "Response is not an array: #{@actual.class}" unless @actual.is_a? Array
14
17
  raise 'Response has no item to compare with' unless @actual.count.positive?
15
18
 
19
+ # Primitive type
20
+ if expected[:type].is_a?(Symbol)
21
+ @actual.each do |item|
22
+ return false unless RSpec::Rails::Api::Utils.check_value_type(expected[:type], item)
23
+ end
24
+
25
+ return true
26
+ end
27
+
16
28
  # Check every entry
17
- ok = true
18
29
  @actual.each do |item|
19
- ok = false unless RSpec::Rails::Api::Utils.validate_object_structure item, expected
30
+ return false unless RSpec::Rails::Api::Utils.validate_object_structure item, expected
20
31
  end
21
32
 
22
- ok
33
+ true
23
34
  end
24
35
 
25
36
  diffable
26
37
  end
27
38
 
39
+ ##
40
+ # RSpec matcher to check something against the `expected` definition
28
41
  # FIXME: Split the matcher in something else; it's too messy.
29
42
  RSpec::Matchers.define :have_one do |expected|
30
43
  match do |actual|
@@ -9,7 +9,7 @@ module RSpec
9
9
  module Api
10
10
  # Handles contexts and examples metadatas.
11
11
  class Metadata # rubocop:disable Metrics/ClassLength
12
- attr_reader :entities, :resources, :current_resource, :parameters
12
+ attr_reader :entities, :resources, :parameters, :current_resource, :current_url, :current_method, :current_code
13
13
 
14
14
  def initialize
15
15
  @resources = {}
@@ -22,26 +22,54 @@ module RSpec
22
22
  @current_code = nil
23
23
  end
24
24
 
25
+ ##
26
+ # Adds a resource to metadata
27
+ #
28
+ # @param name [String] Resource name
29
+ # @param description [String] Resource description
30
+ #
31
+ # @return [void]
25
32
  def add_resource(name, description)
26
33
  @resources[name.to_sym] = { description: description, paths: {} }
27
34
  @current_resource = name.to_sym
28
35
  end
29
36
 
30
- def add_entity(type, fields)
37
+ ##
38
+ # Adds an entity definition
39
+ #
40
+ # @param name [Symbol] Entity name
41
+ # @param fields [Hash] Fields definitions
42
+ #
43
+ #
44
+ # @return [void]
45
+ def add_entity(name, fields)
31
46
  Utils.deep_set(@resources,
32
- "#{@current_resource}.entities.#{type}",
47
+ "#{@current_resource}.entities.#{name}",
33
48
  EntityConfig.new(fields))
34
49
  end
35
50
 
36
- def add_parameter(type, fields)
37
- raise "Parameter #{type} is already defined" if @parameters[type]
51
+ ##
52
+ # Adds a parameter definition
53
+ #
54
+ # @param name [Symbol] Parameter definition name
55
+ # @param fields [Hash] Fields definitions
56
+ #
57
+ # @return [void]
58
+ def add_parameter(name, fields)
59
+ raise "Parameter #{name} is already defined" if @parameters[name]
38
60
 
39
61
  fields.each_value do |field|
40
62
  field[:required] = true unless field[:required] == false
41
63
  end
42
- @parameters[type] = fields
64
+ @parameters[name] = fields
43
65
  end
44
66
 
67
+ ##
68
+ # Adds path parameters definition
69
+ #
70
+ # @param fields [Hash] Parameters definitions
71
+ #
72
+ # @return [void]
45
73
  def add_path_params(fields) # rubocop:disable Metrics/MethodLength
46
74
  check_current_context :resource, :url
47
75
 
@@ -60,6 +88,9 @@ module RSpec
60
88
  end
61
89
  end
62
90
 
91
+ ##
92
+ # Add request parameters (_body_)
93
+ #
63
94
  # Fields should be something like:
64
95
  # id: {type: :number, description: 'Something'},
65
96
  # name: {type: string, description: 'Something'}
@@ -69,6 +100,10 @@ module RSpec
69
100
  # property: {type: :string, description: 'Something'},
70
101
  # ...
71
102
  # }}
103
+ #
104
+ # @param fields [Hash] Parameters definitions
105
+ #
106
+ # @return [void]
72
107
  def add_request_params(fields)
73
108
  check_current_context :resource, :url, :method
74
109
 
@@ -78,11 +113,21 @@ module RSpec
78
113
  params)
79
114
  end
80
115
 
81
- def add_action(method, url, description)
116
+ ##
117
+ # Adds an action and sets `@current_url` and `@current_method`
118
+ #
119
+ # @param method [:get, :post, :put, :patch, delete] Method name
120
+ # @param url [String] Associated URL
121
+ # @param summary [String] What the route does for given method
122
+ # @param description [String] Longer description of this action
123
+ #
124
+ # @return [void]
125
+ def add_action(method, url, summary, description = '')
82
126
  check_current_context :resource
83
127
 
84
128
  Utils.deep_set(@resources, "#{@current_resource}.paths.#{url}.actions.#{method}",
85
- description: description,
129
+ description: description || '',
130
+ summary: summary,
86
131
  statuses: {},
87
132
  params: {})
88
133
 
@@ -90,6 +135,14 @@ module RSpec
90
135
  @current_method = method
91
136
  end
92
137
 
138
+ ##
139
+ # Adds a status code to metadata and sets `@current_code`
140
+ #
141
+ # @param status_code [Integer] The status code
142
+ # @param description [String] Code description
143
+ #
144
+ # @return [void]
145
+ #
93
146
  # rubocop:disable Layout/LineLength
94
147
  def add_status_code(status_code, description)
95
148
  check_current_context :resource, :url, :method
@@ -102,6 +155,49 @@ module RSpec
102
155
  end
103
156
  # rubocop:enable Layout/LineLength
104
157
 
158
+ ##
159
+ # Gets the current example
160
+ #
161
+ # @return [Hash] Current example metadata
162
+ def current_example
163
+ # rubocop:disable Layout/LineLength
164
+ Utils.deep_get @resources,
165
+ "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{@current_code}"
166
+ # rubocop:enable Layout/LineLength
167
+ end
168
+
169
+ ##
170
+ # Adds expectations for current example
171
+ #
172
+ # @param one [Hash, nil] Entity definition
173
+ # @param many [Hash, nil] Entity definition
174
+ #
175
+ # @return [void]
176
+ def add_expectations(one, many)
177
+ check_current_context :resource, :url, :method, :code
178
+ none = !many && !one
179
+
180
+ # rubocop:disable Layout/LineLength
181
+ Utils.deep_set(@resources,
182
+ "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{@current_code}.expectations",
183
+ {
184
+ one: one,
185
+ many: many,
186
+ none: none,
187
+ })
188
+ # rubocop:enable Layout/LineLength
189
+ end
190
+
191
+ ##
192
+ # Adds a request example
193
+ #
194
+ # @param url [String, nil] Visited URL
195
+ # @param action [String, nil] HTTP verb
196
+ # @param status_code [Integer, nil] Status code
197
+ # @param response [String, nil] Response body
198
+ # @param path_params [Hash, nil] Used path parameterss
199
+ # @param params [Hash, nil] Used body parameters
200
+ #
105
201
  # rubocop:disable Metrics/ParameterLists
106
202
  def add_request_example(url: nil, action: nil, status_code: nil, response: nil, path_params: nil, params: nil)
107
203
  resource = nil
@@ -119,6 +215,8 @@ module RSpec
119
215
  end
120
216
  # rubocop:enable Metrics/ParameterLists
121
217
 
218
+ ##
219
+ # @return [Hash] Hash representation of the metadata
122
220
  def to_h
123
221
  {
124
222
  resources: @resources,
@@ -128,6 +226,13 @@ module RSpec
128
226
 
129
227
  private
130
228
 
229
+ ##
230
+ # Checks for the definition of given scopes.
231
+ # This is useful to verify if all metadata is set for the current example
232
+ #
233
+ # @param scope [Symbol[]] List of scope to check for
234
+ #
235
+ # @return [Boolean]
131
236
  def check_current_context(*scope) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
132
237
  scope ||= []
133
238
  raise 'No resource declared' if scope.include?(:resource) && !@current_resource
@@ -136,6 +241,13 @@ module RSpec
136
241
  raise 'No status code declared' if scope.include?(:code) && !@current_code
137
242
  end
138
243
 
244
+ ##
245
+ # Checks if a given parameter is used in the URL (_path_) or querystring (query)
246
+ #
247
+ # @param url_chunks [String[]] Chunks of an url splitted on the query separator (`?`)
248
+ # @param name [Symbol] Name of the parameter
249
+ #
250
+ # @return [:path, :query]
139
251
  def path_param_scope(url_chunks, name)
140
252
  if /:#{name}/.match?(url_chunks[0])
141
253
  :path
@@ -146,6 +258,12 @@ module RSpec
146
258
  end
147
259
  end
148
260
 
261
+ ##
262
+ # Checks and complete a field definition
263
+ #
264
+ # @param fields [Hash] Fields definitions
265
+ #
266
+ # @return [Hash] Completed field definition
149
267
  def organize_params(fields) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
150
268
  out = { properties: {} }
151
269
  required = []
@@ -154,7 +272,7 @@ module RSpec
154
272
  allowed_type = allowed_types.include?(field[:type]) || PARAM_TYPES.key?(field[:type])
155
273
  raise "Field type not allowed: #{field[:type]}" unless allowed_type
156
274
 
157
- required.push name.to_s if field[:required]
275
+ required.push name.to_s if field[:required] != false
158
276
 
159
277
  out[:properties][name] = fill_request_param field
160
278
  end
@@ -162,9 +280,15 @@ module RSpec
162
280
  out
163
281
  end
164
282
 
283
+ ##
284
+ # Checks and completes a request parameter definition
285
+ #
286
+ # @param field [Hash] Parameter definition
287
+ #
288
+ # @return [Hash] Completed parameter
165
289
  def fill_request_param(field)
166
- if field[:type] == :object && field[:properties]
167
- organize_params field[:properties]
290
+ if field[:type] == :object && field[:attributes]
291
+ organize_params field[:attributes]
168
292
  else
169
293
  properties = {
170
294
  type: PARAM_TYPES[field[:type]][:type],