rspec-rails-api 0.3.4 → 0.5.0

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