rspec-rails-api 0.4.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06e9bc31ff4e310ca952011e76cc3fb6dcc39b15ea090229edb1385b51ed16f6
4
- data.tar.gz: 8be9cba238ce37496e2e7eb2db221754cde5c673b1e404541ea84e71095e6ccb
3
+ metadata.gz: 232e1ea8b43dd1e3b5258f0029d28a54d886156634e87d4a6854a3543752fb0f
4
+ data.tar.gz: 71828ab75d08ff900cbe9f018af219bc79b98231118712d127e35c274690d7ad
5
5
  SHA512:
6
- metadata.gz: 5a9058a1121e702991d55696687b64cb8d2bb3872596b4d92f79aa33de8ef54a5b9ef33d2275f92087256cd6d7afcd1fc16472157d52529e19a97b5b66118b70
7
- data.tar.gz: 85777059873dd5fba1842ce286c0c0c4c445aa2dad584a2992b2add593de51cb267e1b398dd6038fe71f5ec36bfa38bf8f03fa4ce0877d0438ae62f4f58fb82f
6
+ metadata.gz: dbb37718158f52e0a2056bbe9b8ccde44782cafe915cc146f05333532484b10ae8260e7abbd3f571e657c778b8d8ac37c731adfa452d3e94af253dcf6849aede
7
+ data.tar.gz: 731f623490f7e15b3f0acb8851955835083249b60e62f722ab9870c6658d3e690ed0b6aca8ac4edcdc3f6b1c892517511937d190459b8020fcb2b3c2ef99047a
data/.gitignore CHANGED
@@ -10,9 +10,4 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
 
13
- # Ignore lockfile for Gemfile
14
- Gemfile.lock
15
- # Keep the dummy one
16
- !dummy/Gemfile.lock
17
-
18
13
  .byebug_history
data/.gitlab-ci.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- image: ruby:2.5
2
+ image: ruby:2.7
3
3
 
4
4
  stages:
5
5
  - prepare
data/.rubocop.yml CHANGED
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  require:
3
3
  - rubocop-performance
4
+ - rubocop-rake
5
+ - rubocop-rspec
4
6
 
5
7
  AllCops:
6
- TargetRubyVersion: 2.5
8
+ TargetRubyVersion: 2.7
7
9
  Exclude:
8
10
  - dummy/**/*
9
11
  - vendor/bundle/**/*
@@ -33,3 +35,5 @@ Style/TrailingCommaInArrayLiteral:
33
35
  Style/TrailingCommaInHashLiteral:
34
36
  EnforcedStyleForMultiline: comma
35
37
 
38
+ RSpec/NestedGroups:
39
+ Max: 4
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.0
data/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  ## Not released
5
5
 
6
+ ## 0.5.0 - 2023-01-02
7
+
8
+ - Improved error messages
9
+ - Improved usage of primitive types: use `:string` instead of `:type_string`, and more generally, remove the `type_`
10
+ prefix
11
+ - Fixed an error when an object is defined with an attribute named `type`.
12
+
13
+ ## 0.4.0 - 2021-12-19
14
+
6
15
  - All parameters attributes are considered required unless specified
7
16
  - Fix object `attributes` key in spec and documentation.
8
17
  When defining object attributes, documentation and tests used `properties` key
data/Gemfile.lock ADDED
@@ -0,0 +1,98 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rspec-rails-api (0.5.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activesupport (6.1.7)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 1.6, < 2)
12
+ minitest (>= 5.1)
13
+ tzinfo (~> 2.0)
14
+ zeitwerk (~> 2.3)
15
+ ast (2.4.2)
16
+ byebug (11.1.3)
17
+ concurrent-ruby (1.1.10)
18
+ diff-lcs (1.5.0)
19
+ docile (1.4.0)
20
+ i18n (1.12.0)
21
+ concurrent-ruby (~> 1.0)
22
+ json (2.6.3)
23
+ minitest (5.16.3)
24
+ parallel (1.22.1)
25
+ parser (3.1.3.0)
26
+ ast (~> 2.4.1)
27
+ rack (3.0.3)
28
+ rainbow (3.1.1)
29
+ rake (10.5.0)
30
+ regexp_parser (2.6.1)
31
+ rexml (3.2.5)
32
+ rspec (3.12.0)
33
+ rspec-core (~> 3.12.0)
34
+ rspec-expectations (~> 3.12.0)
35
+ rspec-mocks (~> 3.12.0)
36
+ rspec-core (3.12.0)
37
+ rspec-support (~> 3.12.0)
38
+ rspec-expectations (3.12.1)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.12.0)
41
+ rspec-mocks (3.12.1)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.12.0)
44
+ rspec-support (3.12.0)
45
+ rubocop (1.41.1)
46
+ json (~> 2.3)
47
+ parallel (~> 1.10)
48
+ parser (>= 3.1.2.1)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 1.8, < 3.0)
51
+ rexml (>= 3.2.5, < 4.0)
52
+ rubocop-ast (>= 1.23.0, < 2.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 1.4.0, < 3.0)
55
+ rubocop-ast (1.24.1)
56
+ parser (>= 3.1.1.0)
57
+ rubocop-performance (1.15.2)
58
+ rubocop (>= 1.7.0, < 2.0)
59
+ rubocop-ast (>= 0.4.0)
60
+ rubocop-rake (0.6.0)
61
+ rubocop (~> 1.0)
62
+ rubocop-rspec (2.16.0)
63
+ rubocop (~> 1.33)
64
+ ruby-progressbar (1.11.0)
65
+ simplecov (0.22.0)
66
+ docile (~> 1.1)
67
+ simplecov-html (~> 0.11)
68
+ simplecov_json_formatter (~> 0.1)
69
+ simplecov-html (0.12.3)
70
+ simplecov_json_formatter (0.1.4)
71
+ tzinfo (2.0.5)
72
+ concurrent-ruby (~> 1.0)
73
+ unicode-display_width (2.3.0)
74
+ webrick (1.7.0)
75
+ yard (0.9.28)
76
+ webrick (~> 1.7.0)
77
+ zeitwerk (2.6.6)
78
+
79
+ PLATFORMS
80
+ x86_64-linux
81
+
82
+ DEPENDENCIES
83
+ activesupport (~> 6.0)
84
+ bundler
85
+ byebug
86
+ rack
87
+ rake (~> 10.0)
88
+ rspec (~> 3.0)
89
+ rspec-rails-api!
90
+ rubocop
91
+ rubocop-performance
92
+ rubocop-rake
93
+ rubocop-rspec
94
+ simplecov
95
+ yard
96
+
97
+ BUNDLED WITH
98
+ 2.4.1
data/README.md CHANGED
@@ -263,12 +263,12 @@ inline.
263
263
  Both `:of` and `attributes` may be a hash of fields or a symbol. If they
264
264
  are omitted, they will be documented, but responses won't be validated.
265
265
 
266
- Arrays of primitives are supported; the type should be prefixed by `type_` to
267
- avoid collisions with defined entities of the same name:
266
+ Arrays of primitives are supported; refer to the [documentation](https://swagger.io/specification/#data-types) for the
267
+ list. Usage:
268
268
 
269
269
  ```rb
270
270
  entity :user,
271
- surnames: { type: :array, of: :type_int32 }
271
+ favorite_numbers: { type: :array, of: :int32 }
272
272
  ```
273
273
 
274
274
  Check `lib/rspec_rails_api.rb` for the full list.
@@ -44,17 +44,15 @@ module RSpec
44
44
  #
45
45
  # @return [RSpec::Rails::Api::EntityConfig, Hash] Defined entity
46
46
  def defined(entity)
47
- return { type: entity.to_s.split('_').last.to_sym } if PRIMITIVES.include? entity
47
+ return { type: entity } if PRIMITIVES.include? entity
48
48
 
49
49
  current_resource = rra_metadata.current_resource
50
50
  raise '@current_resource is unset' unless current_resource
51
51
 
52
52
  entities = rra_metadata.resources[current_resource][:entities]
53
+ raise "Unknown entity '#{entity}' in resource '#{current_resource}'" unless entities.key? entity.to_sym
53
54
 
54
- out = entities[entity]
55
- raise "Unknown entity '#{entity}' in resource '#{current_resource}'" unless out
56
-
57
- out.expand_with(entities)
55
+ entities[entity.to_sym].expand_with(entities)
58
56
  end
59
57
 
60
58
  ##
@@ -186,7 +186,7 @@ module RSpec
186
186
  #
187
187
  # @return [void]
188
188
  def execute_for_code_block(callback_block)
189
- example 'Test and create documentation', caller: callback_block.send(:caller) do
189
+ example 'Test response and create documentation', caller: callback_block.send(:caller) do
190
190
  instance_eval(&callback_block) if callback_block
191
191
  end
192
192
  end
@@ -55,15 +55,12 @@ module RSpec
55
55
  # @param attribute [Symbol] Attribute name
56
56
  # @param entities [Hash] List of entities
57
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]
58
+ # Primitives support
59
+ return { type: attribute } if PRIMITIVES.include? attribute
64
60
 
65
- entities[attribute].expand_with(entities)
66
- end
61
+ raise "Entity #{attribute} not found for entity completion." unless entities[attribute]
62
+
63
+ entities[attribute].expand_with(entities)
67
64
  end
68
65
  end
69
66
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rspec/rails/api/entity_config'
4
- require 'rspec/rails/api/utils'
4
+ require 'rspec/rails/api/validator'
5
5
 
6
6
  module RSpec
7
7
  module Rails
@@ -14,7 +14,7 @@ module RSpec
14
14
  def initialize(type:, description:, required: true, attributes: nil, of: nil)
15
15
  @required = required
16
16
  @description = description
17
- raise "Field type not allowed: '#{type}'" unless Utils.check_attribute_type(type)
17
+ raise "Field type not allowed: '#{type}'" unless Validator.valid_type?(type)
18
18
 
19
19
  define_attributes attributes if type == :object
20
20
  define_attributes of if type == :array
@@ -1,53 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/hash_with_indifferent_access'
4
+ require 'yaml'
4
5
 
6
+ require 'rspec/rails/api/validator'
5
7
  require 'rspec/rails/api/utils'
6
8
 
7
9
  ##
8
10
  # RSpec matcher to check something against an array of `expected`
9
- #
10
- # FIXME: Split the matcher in something else; it's too messy.
11
11
  RSpec::Matchers.define :have_many do |expected|
12
12
  match do |actual|
13
- @actual = actual
14
- @actual = JSON.parse(actual.body) if actual.respond_to? :body
13
+ actual = RSpec::Rails::Api::Utils.hash_from_response actual
15
14
 
16
- raise "Response is not an array: #{@actual.class}" unless @actual.is_a? Array
17
- raise 'Response has no item to compare with' unless @actual.count.positive?
15
+ raise "Response is not an array: #{actual.class}" unless actual.is_a? Array
16
+ raise 'Response has no item to compare with' unless actual.count.positive?
18
17
 
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
18
+ @errors = RSpec::Rails::Api::Validator.validate_array actual, expected
24
19
 
25
- return true
26
- end
27
-
28
- # Check every entry
29
- @actual.each do |item|
30
- return false unless RSpec::Rails::Api::Utils.validate_object_structure item, expected
31
- end
32
-
33
- true
20
+ @errors.blank?
34
21
  end
35
22
 
36
- diffable
23
+ failure_message do |actual|
24
+ object = RSpec::Rails::Api::Utils.hash_from_response(actual).to_json.chomp
25
+ RSpec::Rails::Api::Validator.format_failure_message @errors, object
26
+ end
37
27
  end
38
28
 
39
29
  ##
40
30
  # RSpec matcher to check something against the `expected` definition
41
- # FIXME: Split the matcher in something else; it's too messy.
42
31
  RSpec::Matchers.define :have_one do |expected|
43
32
  match do |actual|
44
- @actual = actual
45
- @actual = JSON.parse(actual.body) if actual.respond_to? :body
33
+ actual = RSpec::Rails::Api::Utils.hash_from_response actual
46
34
 
47
- raise "Response is not a hash: #{@actual.class}" unless @actual.is_a? Hash
35
+ @errors = if expected.keys.count == 1 && expected.key?(:type)
36
+ RSpec::Rails::Api::Validator.validate_type actual, expected[:type]
37
+ else
38
+ RSpec::Rails::Api::Validator.validate_object actual, expected
39
+ end
48
40
 
49
- RSpec::Rails::Api::Utils.validate_object_structure @actual, expected
41
+ @errors.blank?
50
42
  end
51
43
 
52
- diffable
44
+ failure_message do |actual|
45
+ object = RSpec::Rails::Api::Utils.hash_from_response(actual).to_json.chomp
46
+ RSpec::Rails::Api::Validator.format_failure_message @errors, object
47
+ end
53
48
  end
@@ -1,13 +1,14 @@
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
13
  attr_reader :entities, :resources, :parameters, :current_resource, :current_url, :current_method, :current_code
13
14
 
@@ -30,7 +31,7 @@ module RSpec
30
31
  #
31
32
  # @return [void]
32
33
  def add_resource(name, description)
33
- @resources[name.to_sym] = { description: description, paths: {} }
34
+ @resources[name.to_sym] = { description: description, paths: {}, entities: {} }
34
35
  @current_resource = name.to_sym
35
36
  end
36
37
 
@@ -44,7 +45,7 @@ module RSpec
44
45
  # @return [void]
45
46
  def add_entity(name, fields)
46
47
  Utils.deep_set(@resources,
47
- "#{@current_resource}.entities.#{name}",
48
+ [@current_resource, 'entities', name],
48
49
  EntityConfig.new(fields))
49
50
  end
50
51
 
@@ -76,11 +77,11 @@ module RSpec
76
77
  chunks = @current_url.split('?')
77
78
 
78
79
  fields.each do |name, field|
79
- valid_attribute = Utils.check_attribute_type(field[:type], except: %i[array object])
80
+ valid_attribute = Validator.valid_type?(field[:type], except: %i[array object])
80
81
  raise "Field type not allowed: #{field[:type]}" unless valid_attribute
81
82
 
82
83
  scope = path_param_scope(chunks, name)
83
- 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],
84
85
  description: field[:description] || nil,
85
86
  type: field[:type] || nil,
86
87
  required: field[:required] || true,
@@ -109,7 +110,7 @@ module RSpec
109
110
 
110
111
  params = organize_params fields
111
112
  Utils.deep_set(@resources,
112
- "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.params",
113
+ [@current_resource, 'paths', @current_url, 'actions', @current_method, 'params'],
113
114
  params)
114
115
  end
115
116
 
@@ -125,7 +126,7 @@ module RSpec
125
126
  def add_action(method, url, summary, description = '')
126
127
  check_current_context :resource
127
128
 
128
- Utils.deep_set(@resources, "#{@current_resource}.paths.#{url}.actions.#{method}",
129
+ Utils.deep_set(@resources, [@current_resource, 'paths', url, 'actions', method],
129
130
  description: description || '',
130
131
  summary: summary,
131
132
  statuses: {},
@@ -148,7 +149,7 @@ module RSpec
148
149
  check_current_context :resource, :url, :method
149
150
 
150
151
  Utils.deep_set(@resources,
151
- "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{status_code}",
152
+ [@current_resource, 'paths', @current_url, 'actions', @current_method, 'statuses', status_code],
152
153
  description: description,
153
154
  example: { response: nil })
154
155
  @current_code = status_code
@@ -160,10 +161,13 @@ module RSpec
160
161
  #
161
162
  # @return [Hash] Current example metadata
162
163
  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
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
167
171
  end
168
172
 
169
173
  ##
@@ -179,7 +183,7 @@ module RSpec
179
183
 
180
184
  # rubocop:disable Layout/LineLength
181
185
  Utils.deep_set(@resources,
182
- "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{@current_code}.expectations",
186
+ [@current_resource, 'paths', @current_url, 'actions', @current_method, 'statuses', @current_code, 'expectations'],
183
187
  {
184
188
  one: one,
185
189
  many: many,
@@ -195,20 +199,20 @@ module RSpec
195
199
  # @param action [String, nil] HTTP verb
196
200
  # @param status_code [Integer, nil] Status code
197
201
  # @param response [String, nil] Response body
198
- # @param path_params [Hash, nil] Used path parameterss
202
+ # @param path_params [Hash, nil] Used path parameters
199
203
  # @param params [Hash, nil] Used body parameters
200
204
  #
201
205
  # rubocop:disable Metrics/ParameterLists
202
206
  def add_request_example(url: nil, action: nil, status_code: nil, response: nil, path_params: nil, params: nil)
203
207
  resource = nil
204
208
  @resources.each do |key, res|
205
- 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
206
210
  end
207
211
 
208
212
  raise "Resource not found for #{action.upcase} #{url}" unless resource
209
213
 
210
214
  Utils.deep_set(@resources,
211
- "#{resource}.paths.#{url}.actions.#{action}.statuses.#{status_code}.example",
215
+ [resource, 'paths', url, 'actions', action, 'statuses', status_code, 'example'],
212
216
  path_params: path_params,
213
217
  params: params,
214
218
  response: response)
@@ -261,12 +265,16 @@ module RSpec
261
265
  ##
262
266
  # Checks and complete a field definition
263
267
  #
264
- # @param fields [Hash] Fields definitions
268
+ # @param fields [Hash,Symbol] Fields definitions
265
269
  #
266
- # @return [Hash] Completed field definition
267
- def organize_params(fields) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
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
+
268
275
  out = { properties: {} }
269
276
  required = []
277
+
270
278
  allowed_types = %i[array object]
271
279
  fields.each do |name, field|
272
280
  allowed_type = allowed_types.include?(field[:type]) || PARAM_TYPES.key?(field[: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
@@ -35,11 +35,11 @@ module RSpec
35
35
  #
36
36
  # @return [void
37
37
  def merge_context(context, dump_metadata: false)
38
- @metadata[:resources].deep_merge! context[:resources]
39
- @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]
40
40
 
41
41
  # Save context for debug and fixtures
42
- 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
43
43
  end
44
44
 
45
45
  ##
@@ -50,10 +50,11 @@ module RSpec
50
50
  #
51
51
  # @return [void]
52
52
  def write_files(path = nil, only: %i[yaml json])
53
+ return unless write_file? RSpec.world.filtered_examples
54
+
53
55
  path ||= ::Rails.root.join('tmp', 'rspec_api_rails')
54
56
 
55
57
  file_types = %i[yaml json]
56
-
57
58
  only.each do |type|
58
59
  next unless file_types.include? type
59
60
 
@@ -66,7 +67,7 @@ module RSpec
66
67
  #
67
68
  # @return [Hash] The OpenAPI structure
68
69
  def prepare_metadata
69
- extract_metadatas
70
+ extract_metadata
70
71
  # Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
71
72
  hash = {
72
73
  openapi: '3.0.0',
@@ -107,11 +108,23 @@ module RSpec
107
108
 
108
109
  private
109
110
 
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
+
110
123
  ##
111
124
  # Extracts metadata for rendering
112
125
  #
113
126
  # @return [void]
114
- def extract_metadatas
127
+ def extract_metadata
115
128
  extract_from_resources
116
129
  api_infos
117
130
  api_servers
@@ -190,10 +203,8 @@ module RSpec
190
203
  property[:format] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
191
204
  schema[:properties][name] = property
192
205
  # Primitives support
193
- if PRIMITIVES.include? field.attributes
194
- property[:items] =
195
- { type: field.attributes.to_s.split('_').last.to_sym }
196
- end
206
+ property[:items] = { type: field.attributes } if PRIMITIVES.include? field.attributes
207
+
197
208
  required.push name unless field.required == false
198
209
  end
199
210
 
@@ -282,7 +293,7 @@ module RSpec
282
293
  #
283
294
  # @return [void]
284
295
  def process_request_body(schema: nil, ref: nil, examples: {})
285
- Utils.deep_set @api_components, "schemas.#{ref}", schema
296
+ Utils.deep_set @api_components, ['schemas', ref], schema
286
297
  {
287
298
  # description: '',
288
299
  required: true,
@@ -321,7 +332,7 @@ module RSpec
321
332
  def response_schema(expectations)
322
333
  if expectations[:many]
323
334
  items = if PRIMITIVES.include?(expectations[:many])
324
- { type: expectations[:many].to_s.split('_').last }
335
+ { type: expectations[:many] }
325
336
  else
326
337
  { '$ref' => "#/components/schemas/#{expectations[:many]}" }
327
338
  end
@@ -7,139 +7,38 @@ 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
17
- def self.deep_get(hash, path)
18
- path.split('.').inject(hash) do |sub_hash, key|
19
- return nil unless sub_hash.is_a?(Hash) && sub_hash.key?(key.to_sym)
20
-
21
- sub_hash[key.to_sym] # rubocop:disable Lint/UnmodifiedReduceAccumulator
10
+ class << self
11
+ ##
12
+ # Sets a value at given dotted path in a hash
13
+ #
14
+ # @param hash [Hash] The target hash
15
+ # @param path [Array] List of keys to access value
16
+ # @param value [*] Value to set
17
+ #
18
+ # @return [Hash] The modified hash
19
+ def deep_set(hash, path, value)
20
+ raise 'path should be an array' unless path.is_a? Array
21
+
22
+ return value if path.count.zero?
23
+
24
+ current_key = path.shift.to_s.to_sym
25
+ hash[current_key] = {} unless hash[current_key].is_a?(Hash)
26
+ hash[current_key] = deep_set(hash[current_key], path, value)
27
+
28
+ hash
22
29
  end
23
- end
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
33
- def self.deep_set(hash, path, value)
34
- path = path.split('.') unless path.is_a? Array
35
-
36
- return value if path.count.zero?
37
-
38
- current_key = path.shift.to_sym
39
- hash[current_key] = {} unless hash[current_key].is_a?(Hash)
40
- hash[current_key] = deep_set(hash[current_key], path, value)
41
-
42
- hash
43
- end
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
52
- def self.check_value_type(type, value)
53
- return true if type == :boolean && (value.is_a?(TrueClass) || value.is_a?(FalseClass))
54
- return true if type == :array && value.is_a?(Array)
55
-
56
- raise "Unknown type #{type}" unless PARAM_TYPES.key? type
57
-
58
- value.is_a? PARAM_TYPES[type][:class]
59
- end
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
68
- def self.validate_object_structure(actual, expected)
69
- # Check keys
70
- return false unless same_keys? actual, expected
71
-
72
- expected.each_key do |key|
73
- next unless expected[key][:required]
74
-
75
- expected_type = expected[key][:type]
76
- expected_attributes = expected[key][:attributes]
77
30
 
78
- # Type
79
- return false unless check_value_type expected_type, actual[key.to_s]
31
+ ##
32
+ # Returns a hash from an object
33
+ #
34
+ # @param value [Hash,Class] A hash or something with a "body" (as responses object in tests)
35
+ #
36
+ # @return [Hash]
37
+ def hash_from_response(value)
38
+ return JSON.parse(value.body) if value.respond_to? :body
80
39
 
81
- # Deep object ?
82
- return false unless validate_deep_object expected_type, expected_attributes, actual[key.to_s]
40
+ value
83
41
  end
84
-
85
- true
86
- end
87
-
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
96
- def self.validate_deep_object(expected_type, expected_attributes, actual)
97
- if %i[object array].include?(expected_type) && expected_attributes.is_a?(Hash)
98
- case expected_type
99
- when :object
100
- return false unless validate_object_structure actual, expected_attributes
101
- when :array
102
- return false unless validate_deep_object_array actual, expected_attributes
103
- end
104
- end
105
-
106
- true
107
- end
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
135
- def self.same_keys?(actual, expected)
136
- optional = expected.reject { |_key, value| value[:required] }.keys
137
- actual.symbolize_keys.keys.sort - optional == expected.keys.sort - optional
138
- end
139
-
140
- def self.check_attribute_type(type, except: [])
141
- keys = PARAM_TYPES.keys.reject { |key| except.include? key }
142
- keys.include?(type)
143
42
  end
144
43
  end
145
44
  end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/hash_with_indifferent_access'
4
+ require 'rspec_rails_api'
5
+
6
+ module RSpec
7
+ module Rails
8
+ module Api
9
+ # Set of method to validate data and data structures
10
+ class Validator
11
+ class << self
12
+ ##
13
+ # Validates an object keys and values types
14
+ #
15
+ # @param actual [*] Value to compare
16
+ # @param expected [Hash, NilClass] Definition
17
+ #
18
+ # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors
19
+ # otherwise
20
+ def validate_object(actual, expected)
21
+ return 'is not a hash' unless actual.is_a? Hash
22
+ # Don't validate without a definition
23
+ return unless expected
24
+
25
+ keys_errors = validate_object_keys actual, expected
26
+ return keys_errors unless keys_errors.nil?
27
+
28
+ attributes_errors = validate_object_attributes(actual, expected)
29
+
30
+ attributes_errors unless attributes_errors.keys.empty?
31
+ end
32
+
33
+ ##
34
+ # Validates each entry of an array
35
+ #
36
+ # @param array [*] The array to check
37
+ # @param expected [Symbol, Hash, NilClass] Attributes configuration
38
+ #
39
+ # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors
40
+ # otherwise
41
+ def validate_array(array, expected)
42
+ return 'is not an array' unless array.is_a? Array
43
+ # Arrays without an expected entry type
44
+ return unless expected
45
+
46
+ errors = {}
47
+ array.each_with_index do |array_entry, index|
48
+ value_error = validate_array_entry array_entry, expected
49
+ errors["##{index}"] = value_error if value_error
50
+ end
51
+
52
+ errors unless errors.keys.empty?
53
+ end
54
+
55
+ ##
56
+ # Returns a human-readable string from matcher errors
57
+ #
58
+ # @param errors [String,Hash] Validation errors
59
+ # @param values [String] JSON string representing the value
60
+ #
61
+ # @return [String]
62
+ def format_failure_message(errors, values)
63
+ if errors.is_a? Hash
64
+ errors = errors.deep_stringify_keys.to_yaml.split("\n")
65
+ errors.shift
66
+ errors.map! do |line|
67
+ " #{line.sub(/^(\s+)"(#\d+)":(.*)$/, '\1\2:\3')}"
68
+ end
69
+ errors = errors.join("\n")
70
+ end
71
+
72
+ <<~TXT
73
+ expected object structure not to have these errors:
74
+ #{errors}
75
+
76
+ As a notice, here is the JSON object:
77
+ #{values}
78
+ TXT
79
+ end
80
+
81
+ ##
82
+ # Checks if a given type is in the supported types list
83
+ #
84
+ # @param type [Symbol] Type to check
85
+ # @param except [[Symbol]] List of types to ignore
86
+ #
87
+ # @return [Boolean]
88
+ def valid_type?(type, except: [])
89
+ keys = PARAM_TYPES.keys.reject { |key| except.include? key }
90
+ keys.include?(type)
91
+ end
92
+
93
+ ##
94
+ # Checks if a value is of the given type
95
+ #
96
+ # @param value [*] Value to test
97
+ # @param type [Symbol] Type to compare to
98
+ #
99
+ # @return [String,NilClass] True when the value corresponds to the given type
100
+ def validate_type(value, type)
101
+ if type == :boolean
102
+ return nil if value.is_a?(TrueClass) || value.is_a?(FalseClass)
103
+
104
+ return 'is not a "boolean"'
105
+ end
106
+
107
+ raise "Unknown type #{type}" unless PARAM_TYPES.key? type
108
+
109
+ return nil if value.is_a? PARAM_TYPES[type][:class]
110
+
111
+ "is not a \"#{type}\""
112
+ end
113
+
114
+ private
115
+
116
+ # Checks if a key should be skipped, whether it's missing and optional or nil
117
+ #
118
+ # @param key [Symbol] Key to check
119
+ # @param value [*] Associated value
120
+ # @param definition [Hash] Entity definitions
121
+ #
122
+ # @return [Boolean]
123
+ def skip_key_check?(key, value, definition)
124
+ # Ignore missing optional keys
125
+ return true unless value.key?(key.to_s) || definition[key][:required]
126
+ # Ignore null optional keys
127
+ return true if !definition[key][:required] && value[key.to_s].nil?
128
+
129
+ false
130
+ end
131
+
132
+ # Validates the keys of a hash
133
+ #
134
+ # @param actual [Hash] The hash to check
135
+ # @param definition [Hash] The object definition
136
+ #
137
+ # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors
138
+ # otherwise
139
+ def validate_object_keys(actual, definition)
140
+ # Hashes without an expected attributes type
141
+ return unless definition
142
+
143
+ errors = {}
144
+ actual.each_key do |key|
145
+ errors[key] = 'is not defined' unless definition.key?(key.to_sym)
146
+ end
147
+
148
+ errors unless errors.keys.empty?
149
+ end
150
+
151
+ ##
152
+ # Validates the attributes of a Hash
153
+ #
154
+ # @param actual [Hash] Value to compare
155
+ # @param expected [Hash] Definition
156
+ #
157
+ # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors
158
+ # otherwise
159
+ def validate_object_attributes(actual, expected)
160
+ errors = {}
161
+ expected.each_key do |key|
162
+ next if skip_key_check? key, actual, expected
163
+
164
+ value_error = validate_object_attribute key, actual, expected[key][:type], expected[key][:attributes]
165
+ errors[key] = value_error unless value_error.nil?
166
+ end
167
+
168
+ errors unless errors.keys.nil?
169
+ end
170
+
171
+ # Checks the value of an entry in a Hash
172
+ #
173
+ # @param key [Symbol] Key to check
174
+ # @param actual [Hash] Hash to check
175
+ # @param expected_type [Symbol] Expected type
176
+ # @param definition [Symbol,Hash] Attribute definition
177
+ #
178
+ # @return [String,Hash,NilClass] Nil when no error is met, string when not a primitive and dictionary of
179
+ # errors otherwise
180
+ def validate_object_attribute(key, actual, expected_type, definition)
181
+ return 'is missing' unless actual.key? key.to_s
182
+
183
+ case expected_type
184
+ when :object
185
+ validate_object actual[key.to_s], definition
186
+ when :array
187
+ validate_array actual[key.to_s], definition
188
+ else
189
+ validate_type actual[key.to_s], expected_type
190
+ end
191
+ end
192
+
193
+ # Checks the validity of an array entry against a definition
194
+ #
195
+ # @param entry [*] Entry to check
196
+ # @param definition [Hash] Fields definition
197
+ #
198
+ # @return [String,Hash,NilClass] Nil when no error is met, string when not a primitive and dictionary of
199
+ # errors otherwise
200
+ def validate_array_entry(entry, definition)
201
+ if definition[:type].is_a? Symbol # Array of "simple" values
202
+ validate_type entry, definition[:type]
203
+ else # Objects
204
+ validate_object entry, definition
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module Rails
5
5
  module Api
6
- VERSION = '0.4.0'
6
+ VERSION = '0.5.0'
7
7
  end
8
8
  end
9
9
  end
@@ -35,11 +35,7 @@ module RSpec
35
35
  object: { type: 'object', format: nil, class: Hash },
36
36
  }.freeze
37
37
 
38
- EXCLUDED_PRIMITIVES = %i[array object].freeze
39
-
40
38
  PRIMITIVES = PARAM_TYPES.keys
41
- .reject { |key| EXCLUDED_PRIMITIVES.include? key }
42
- .map { |key| "type_#{key}".to_sym }
43
39
  end
44
40
  end
45
41
  end
@@ -33,15 +33,18 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
34
  spec.require_paths = ['lib']
35
35
 
36
- spec.required_ruby_version = '>= 2.5.0'
36
+ spec.required_ruby_version = '>= 2.7.0'
37
37
 
38
38
  spec.add_development_dependency 'activesupport', '~> 6.0'
39
39
  spec.add_development_dependency 'bundler'
40
40
  spec.add_development_dependency 'byebug'
41
+ spec.add_development_dependency 'rack'
41
42
  spec.add_development_dependency 'rake', '~> 10.0'
42
43
  spec.add_development_dependency 'rspec', '~> 3.0'
43
44
  spec.add_development_dependency 'rubocop'
44
45
  spec.add_development_dependency 'rubocop-performance'
46
+ spec.add_development_dependency 'rubocop-rake'
47
+ spec.add_development_dependency 'rubocop-rspec'
45
48
  spec.add_development_dependency 'simplecov'
46
49
  spec.add_development_dependency 'yard'
47
50
  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.4.0
4
+ version: 0.5.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-12-19 00:00:00.000000000 Z
11
+ date: 2023-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,34 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rake
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'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
111
153
  - !ruby/object:Gem::Dependency
112
154
  name: simplecov
113
155
  requirement: !ruby/object:Gem::Requirement
@@ -149,10 +191,12 @@ files:
149
191
  - ".gitlab-ci.yml"
150
192
  - ".rspec"
151
193
  - ".rubocop.yml"
194
+ - ".ruby-version"
152
195
  - ".travis.yml"
153
196
  - CHANGELOG.md
154
197
  - CODE_OF_CONDUCT.md
155
198
  - Gemfile
199
+ - Gemfile.lock
156
200
  - LICENSE.txt
157
201
  - README.md
158
202
  - Rakefile
@@ -166,6 +210,7 @@ files:
166
210
  - lib/rspec/rails/api/metadata.rb
167
211
  - lib/rspec/rails/api/open_api_renderer.rb
168
212
  - lib/rspec/rails/api/utils.rb
213
+ - lib/rspec/rails/api/validator.rb
169
214
  - lib/rspec/rails/api/version.rb
170
215
  - lib/rspec_rails_api.rb
171
216
  - rspec-rails-api.gemspec
@@ -185,15 +230,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
185
230
  requirements:
186
231
  - - ">="
187
232
  - !ruby/object:Gem::Version
188
- version: 2.5.0
233
+ version: 2.7.0
189
234
  required_rubygems_version: !ruby/object:Gem::Requirement
190
235
  requirements:
191
236
  - - ">="
192
237
  - !ruby/object:Gem::Version
193
238
  version: '0'
194
239
  requirements: []
195
- rubyforge_project:
196
- rubygems_version: 2.7.6
240
+ rubygems_version: 3.4.1
197
241
  signing_key:
198
242
  specification_version: 4
199
243
  summary: Tests standard Rails API responses and generate doc