rspec-rails-api 0.3.4 → 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.
@@ -7,80 +7,38 @@ module RSpec
7
7
  module Api
8
8
  # Helper methods
9
9
  class Utils
10
- def self.deep_get(hash, path)
11
- path.split('.').inject(hash) do |sub_hash, key|
12
- return nil unless sub_hash.is_a?(Hash) && sub_hash.key?(key.to_sym)
13
-
14
- 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
15
29
  end
16
- end
17
-
18
- def self.deep_set(hash, path, value)
19
- path = path.split('.') unless path.is_a? Array
20
-
21
- return value if path.count.zero?
22
-
23
- current_key = path.shift.to_sym
24
- hash[current_key] = {} unless hash[current_key].is_a?(Hash)
25
- hash[current_key] = deep_set(hash[current_key], path, value)
26
-
27
- hash
28
- end
29
-
30
- def self.check_value_type(type, value)
31
- return true if type == :boolean && (value.is_a?(TrueClass) || value.is_a?(FalseClass))
32
- return true if type == :array && value.is_a?(Array)
33
-
34
- raise "Unknown type #{type}" unless PARAM_TYPES.key? type
35
-
36
- value.is_a? PARAM_TYPES[type][:class]
37
- end
38
-
39
- def self.validate_object_structure(actual, expected)
40
- # Check keys
41
- return false unless same_keys? actual, expected
42
30
 
43
- expected.each_key do |key|
44
- next unless expected[key][:required]
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
45
39
 
46
- expected_type = expected[key][:type]
47
- expected_attributes = expected[key][:attributes]
48
-
49
- # Type
50
- return false unless check_value_type expected_type, actual[key.to_s]
51
-
52
- # Deep object ?
53
- return false unless validate_deep_object expected_type, expected_attributes, actual[key.to_s]
54
- end
55
-
56
- true
57
- end
58
-
59
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
60
- def self.validate_deep_object(expected_type, expected_attributes, actual)
61
- if %i[object array].include?(expected_type) && expected_attributes.is_a?(Hash)
62
- case expected_type
63
- when :object
64
- return false unless validate_object_structure actual, expected_attributes
65
- when :array
66
- actual.each do |array_entry|
67
- return false unless validate_object_structure array_entry, expected_attributes
68
- end
69
- end
40
+ value
70
41
  end
71
-
72
- true
73
- end
74
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
75
-
76
- def self.same_keys?(actual, expected)
77
- optional = expected.reject { |_key, value| value[:required] }.keys
78
- actual.symbolize_keys.keys.sort - optional == expected.keys.sort - optional
79
- end
80
-
81
- def self.check_attribute_type(type, except: [])
82
- keys = PARAM_TYPES.keys.reject { |key| except.include? key }
83
- keys.include?(type)
84
42
  end
85
43
  end
86
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.3.4'
6
+ VERSION = '0.5.0'
7
7
  end
8
8
  end
9
9
  end
@@ -15,6 +15,8 @@ module RSpec
15
15
  class Error < StandardError
16
16
  end
17
17
 
18
+ ##
19
+ # OpenAPI types, format and Ruby class correspondence
18
20
  PARAM_TYPES = {
19
21
  int32: { type: 'integer', format: 'int32', class: Integer },
20
22
  int64: { type: 'integer', format: 'int64', class: Integer },
@@ -32,6 +34,8 @@ module RSpec
32
34
  array: { type: 'array', format: nil, class: Array },
33
35
  object: { type: 'object', format: nil, class: Hash },
34
36
  }.freeze
37
+
38
+ PRIMITIVES = PARAM_TYPES.keys
35
39
  end
36
40
  end
37
41
  end
@@ -18,9 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.homepage = 'https://gitlab.com/experimentslabs/rspec-rails-api'
19
19
  spec.license = 'MIT'
20
20
  spec.metadata = {
21
- 'source_code_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api',
22
- 'bug_tracker_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/issues',
23
- 'changelog_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md',
21
+ 'source_code_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api',
22
+ 'bug_tracker_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/issues',
23
+ 'changelog_uri' => 'https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md',
24
+ 'rubygems_mfa_required' => 'true',
24
25
  }
25
26
 
26
27
  # Specify which files should be added to the gem when it is released.
@@ -32,14 +33,18 @@ Gem::Specification.new do |spec|
32
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
34
  spec.require_paths = ['lib']
34
35
 
35
- spec.required_ruby_version = '>= 2.5.0'
36
+ spec.required_ruby_version = '>= 2.7.0'
36
37
 
37
38
  spec.add_development_dependency 'activesupport', '~> 6.0'
38
- spec.add_development_dependency 'bundler', '~> 1.17'
39
+ spec.add_development_dependency 'bundler'
39
40
  spec.add_development_dependency 'byebug'
41
+ spec.add_development_dependency 'rack'
40
42
  spec.add_development_dependency 'rake', '~> 10.0'
41
43
  spec.add_development_dependency 'rspec', '~> 3.0'
42
44
  spec.add_development_dependency 'rubocop'
43
45
  spec.add_development_dependency 'rubocop-performance'
46
+ spec.add_development_dependency 'rubocop-rake'
47
+ spec.add_development_dependency 'rubocop-rspec'
44
48
  spec.add_development_dependency 'simplecov'
49
+ spec.add_development_dependency 'yard'
45
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.3.4
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-10-20 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
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.17'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.17'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: byebug
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -122,6 +164,20 @@ dependencies:
122
164
  - - ">="
123
165
  - !ruby/object:Gem::Version
124
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: yard
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
125
181
  description: |
126
182
  Create acceptance tests to check the Rails API responses and generate
127
183
  documentation from it.
@@ -135,10 +191,12 @@ files:
135
191
  - ".gitlab-ci.yml"
136
192
  - ".rspec"
137
193
  - ".rubocop.yml"
194
+ - ".ruby-version"
138
195
  - ".travis.yml"
139
196
  - CHANGELOG.md
140
197
  - CODE_OF_CONDUCT.md
141
198
  - Gemfile
199
+ - Gemfile.lock
142
200
  - LICENSE.txt
143
201
  - README.md
144
202
  - Rakefile
@@ -152,6 +210,7 @@ files:
152
210
  - lib/rspec/rails/api/metadata.rb
153
211
  - lib/rspec/rails/api/open_api_renderer.rb
154
212
  - lib/rspec/rails/api/utils.rb
213
+ - lib/rspec/rails/api/validator.rb
155
214
  - lib/rspec/rails/api/version.rb
156
215
  - lib/rspec_rails_api.rb
157
216
  - rspec-rails-api.gemspec
@@ -162,6 +221,7 @@ metadata:
162
221
  source_code_uri: https://gitlab.com/experimentslabs/rspec-rails-api
163
222
  bug_tracker_uri: https://gitlab.com/experimentslabs/rspec-rails-api/issues
164
223
  changelog_uri: https://gitlab.com/experimentslabs/rspec-rails-api/blob/master/CHANGELOG.md
224
+ rubygems_mfa_required: 'true'
165
225
  post_install_message:
166
226
  rdoc_options: []
167
227
  require_paths:
@@ -170,15 +230,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
170
230
  requirements:
171
231
  - - ">="
172
232
  - !ruby/object:Gem::Version
173
- version: 2.5.0
233
+ version: 2.7.0
174
234
  required_rubygems_version: !ruby/object:Gem::Requirement
175
235
  requirements:
176
236
  - - ">="
177
237
  - !ruby/object:Gem::Version
178
238
  version: '0'
179
239
  requirements: []
180
- rubyforge_project:
181
- rubygems_version: 2.7.6
240
+ rubygems_version: 3.4.1
182
241
  signing_key:
183
242
  specification_version: 4
184
243
  summary: Tests standard Rails API responses and generate doc