rspec-rails-api 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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