rspec_api_blueprint_matchers 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/config/rubocop/.metrics_rubocop.yml +2 -3
- data/lib/rspec_apib.rb +0 -1
- data/lib/rspec_apib/elements/array.rb +2 -0
- data/lib/rspec_apib/elements/base.rb +5 -2
- data/lib/rspec_apib/elements/http_message_payload.rb +6 -2
- data/lib/rspec_apib/elements/http_response.rb +1 -0
- data/lib/rspec_apib/elements/resource.rb +1 -5
- data/lib/rspec_apib/elements/string.rb +2 -0
- data/lib/rspec_apib/elements/templated_href.rb +8 -5
- data/lib/rspec_apib/elements/transition.rb +1 -2
- data/lib/rspec_apib/parser.rb +1 -2
- data/lib/rspec_apib/response.rb +2 -2
- data/lib/rspec_apib/rspec.rb +2 -1
- data/lib/rspec_apib/transaction_coverage_report.rb +22 -1
- data/lib/rspec_apib/transaction_coverage_validator.rb +8 -23
- data/lib/rspec_apib/transaction_validator.rb +14 -7
- data/lib/rspec_apib/version.rb +1 -1
- metadata +2 -5
- data/lib/rspec_apib/extractors.rb +0 -3
- data/lib/rspec_apib/extractors/http_transaction.rb +0 -24
- data/lib/rspec_apib/extractors/resource.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8bd0abe26c064df373492e78343f325abbd43c9
|
4
|
+
data.tar.gz: 7403ee224404cac77bc7706b11ed82e48285cb6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f82510ff4c0051f2a614ba4df5dfbb77b2a28c97cc63b2d4e3574618d50aca81872f5de728289db9e31fcf0e304e62dba3227b6ad4073064d5242cde174484ed
|
7
|
+
data.tar.gz: 36f71401e0e26aae242359d060753a34c12f3afb46d9ca2eea935c5ce953db655c17339abbc0f0c9b770e8159c7757c659cfc54db9bcfe2dca28a6179d2ee2d6
|
data/.rubocop.yml
CHANGED
@@ -48,7 +48,7 @@ Metrics/CyclomaticComplexity:
|
|
48
48
|
# Checks the length of lines in the source code.
|
49
49
|
|
50
50
|
Metrics/LineLength:
|
51
|
-
Max:
|
51
|
+
Max: 180
|
52
52
|
AllowHereDoc: true
|
53
53
|
AllowURI: true
|
54
54
|
URISchemes:
|
@@ -80,7 +80,7 @@ Metrics/ModuleLength:
|
|
80
80
|
# Checks for methods with too many parameters.
|
81
81
|
|
82
82
|
Metrics/ParameterLists:
|
83
|
-
Max:
|
83
|
+
Max: 6
|
84
84
|
CountKeywordArgs: true
|
85
85
|
|
86
86
|
|
@@ -91,4 +91,3 @@ Metrics/ParameterLists:
|
|
91
91
|
|
92
92
|
Metrics/PerceivedComplexity:
|
93
93
|
Max: 7
|
94
|
-
|
data/lib/rspec_apib.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module RSpecApib
|
3
3
|
module Element
|
4
|
+
# Represents an array in api-elements (http://api-elements.readthedocs.io/en/latest/)
|
4
5
|
class Array < ::Array
|
6
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
5
7
|
def self.from_hash(hash, index:, parent:)
|
6
8
|
new(hash["content"])
|
7
9
|
end
|
@@ -19,10 +19,13 @@ module RSpecApib
|
|
19
19
|
return node_or_nodes.map { |node| parse(node, index: index, parent: parent) } if node_or_nodes.is_a?(::Array)
|
20
20
|
return transformed_basic_hash(node_or_nodes, index: index, parent: parent) if basic_hash?(node_or_nodes)
|
21
21
|
return node_or_nodes unless !klass.nil? || base_element?(node_or_nodes)
|
22
|
-
|
22
|
+
parse_node(node_or_nodes, index: index, parent: parent, klass: klass)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_node(hash, index:, parent:, klass: nil)
|
23
26
|
klass_name = klass
|
24
27
|
klass_name ||= hash["element"].slice(0, 1).capitalize + hash["element"].slice(1..-1).delete(" ")
|
25
|
-
return
|
28
|
+
return hash unless RSpecApib::Element.const_defined?(klass_name)
|
26
29
|
klass = RSpecApib::Element.const_get(klass_name)
|
27
30
|
index[klass] ||= []
|
28
31
|
element = klass.from_hash(hash, index: index, parent: parent)
|
@@ -23,6 +23,12 @@ module RSpecApib
|
|
23
23
|
}
|
24
24
|
return [failure_reason]
|
25
25
|
end
|
26
|
+
validate_json_schema(schema, request_or_response)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_json_schema(schema, request_or_response)
|
26
32
|
schema = JSON.parse schema.content
|
27
33
|
errors = JSON::Validator.fully_validate(schema, request_or_response.body)
|
28
34
|
return [] if errors.length.zero?
|
@@ -35,8 +41,6 @@ module RSpecApib
|
|
35
41
|
[failure_reason]
|
36
42
|
end
|
37
43
|
|
38
|
-
private
|
39
|
-
|
40
44
|
def body_schema_asset
|
41
45
|
content.find { |node| node.is_a?(Asset) && node.meta && node.meta["classes"] && node.meta["classes"].include?("messageBodySchema")}
|
42
46
|
end
|
@@ -7,6 +7,7 @@ module RSpecApib
|
|
7
7
|
# Indicates if the incoming request matches the method, path and path vars
|
8
8
|
# @param [::RSpecApib::Request] The incoming request - normalized
|
9
9
|
# @return [Boolean] true if matches else false
|
10
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
10
11
|
def matches?(response, options: {})
|
11
12
|
matches_status?(response) &&
|
12
13
|
matches_content_type?(response)
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module RSpecApib
|
3
3
|
module Element
|
4
|
-
# (
|
5
|
-
# {"meta"=>[#<struct RSpecApib::Element::Member element="member", meta={"classes"=>["user"]}, attributes={}, content={"FORMAT"=>"1A"}, parent=#<struct RSpecApib::Element::ParseResult element="parseResult", meta=nil, attributes=nil, content=nil, parent=nil>>, #<struct RSpecApib::Element::Member element="member", meta={"classes"=>["user"]}, attributes={}, content={"HOST"=>"http://api.shiftcommerce.com/inventory/v1"}, parent=#<struct RSpecApib::Element::ParseResult element="parseResult", meta=nil, attributes=nil, content=nil, parent=nil>>]}
|
6
|
-
|
4
|
+
# Represents a resource in api-elements (http://api-elements.readthedocs.io/en/latest/)
|
7
5
|
class Resource < Base
|
8
6
|
def transitions
|
9
7
|
content.select { |item| item.is_a?(Transition) }
|
@@ -13,8 +11,6 @@ module RSpecApib
|
|
13
11
|
content.select { |item| item.is_a?(Category) }
|
14
12
|
end
|
15
13
|
|
16
|
-
private
|
17
|
-
|
18
14
|
def self.attributes_schema
|
19
15
|
{
|
20
16
|
href: "TemplatedHref"
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module RSpecApib
|
3
3
|
module Element
|
4
|
+
# Represents a string in api-elements (http://api-elements.readthedocs.io/en/latest/)
|
4
5
|
class String < ::String
|
6
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
5
7
|
def self.from_hash(hash, index:, parent:)
|
6
8
|
new(hash["content"] || "")
|
7
9
|
end
|
@@ -2,8 +2,11 @@
|
|
2
2
|
require "addressable"
|
3
3
|
module RSpecApib
|
4
4
|
module Element
|
5
|
+
# Represents a templated href in api-elements (http://api-elements.readthedocs.io/en/latest/)
|
5
6
|
class TemplatedHref < Base
|
6
|
-
# Note this is not really a hash
|
7
|
+
# Note this is not really a hash but the interface named it
|
8
|
+
# that early on as everything was a hash !!
|
9
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
7
10
|
def self.from_hash(hash, index:, parent:)
|
8
11
|
new("templatedHref", nil, nil, hash, parent)
|
9
12
|
end
|
@@ -23,10 +26,10 @@ module RSpecApib
|
|
23
26
|
end
|
24
27
|
|
25
28
|
def path
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
url_1 = Addressable::URI.parse(url)
|
30
|
+
url_1.path, url_1.query, url_1.fragment = nil
|
31
|
+
url_2 = Addressable::URI.parse(url)
|
32
|
+
url_2.to_s.gsub(url_1.to_s, "")
|
30
33
|
end
|
31
34
|
|
32
35
|
def self.host_from_parent(node)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module RSpecApib
|
3
3
|
module Element
|
4
|
+
# Represents a transition in api-elements (http://api-elements.readthedocs.io/en/latest/)
|
4
5
|
class Transition < Base
|
5
6
|
def http_transactions
|
6
7
|
content.select { |item| item.is_a?(HttpTransaction) }
|
@@ -11,8 +12,6 @@ module RSpecApib
|
|
11
12
|
[:href, :hrefVariables]
|
12
13
|
end
|
13
14
|
|
14
|
-
private
|
15
|
-
|
16
15
|
def self.attributes_schema
|
17
16
|
{
|
18
17
|
href: "TemplatedHref"
|
data/lib/rspec_apib/parser.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "rspec_apib/transcluder"
|
3
|
-
require "rspec_apib/extractors"
|
4
3
|
require "rspec_apib/elements"
|
5
4
|
require "open3"
|
6
5
|
require "json"
|
@@ -73,7 +72,7 @@ module RSpecApib
|
|
73
72
|
Open3.popen3("#{bin_path} -f json") do |stdin, stdout, _stderr, wait_thr|
|
74
73
|
send_document(file: file, buffer: stdin)
|
75
74
|
op = stdout.read
|
76
|
-
|
75
|
+
wait_thr.value
|
77
76
|
end
|
78
77
|
op
|
79
78
|
end
|
data/lib/rspec_apib/response.rb
CHANGED
@@ -17,7 +17,7 @@ module RSpecApib
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def validate_body_with_json_schema?
|
20
|
-
|
20
|
+
json?
|
21
21
|
end
|
22
22
|
|
23
23
|
def content_type
|
@@ -32,7 +32,7 @@ module RSpecApib
|
|
32
32
|
|
33
33
|
attr_accessor :raw_response
|
34
34
|
|
35
|
-
def
|
35
|
+
def json?
|
36
36
|
content_type =~ /json/
|
37
37
|
end
|
38
38
|
end
|
data/lib/rspec_apib/rspec.rb
CHANGED
@@ -12,7 +12,8 @@ RSpec::Matchers.define :match_api_docs_for do |path:, request_method:, content_t
|
|
12
12
|
error_messages << "Expected the transaction to match api docs for #{method}:#{path} but the provided parser was invalid"
|
13
13
|
false
|
14
14
|
else
|
15
|
-
::RSpecApib::TransactionValidator.new(path: path, request_method: request_method, content_type: content_type, parser: parser)
|
15
|
+
validator = ::RSpecApib::TransactionValidator.new(path: path, request_method: request_method, content_type: content_type, parser: parser)
|
16
|
+
validator.validate(request: ::RSpecApib.normalize_request(actual.request), response: ::RSpecApib.normalize_response(actual.response), error_messages: error_messages)
|
16
17
|
end
|
17
18
|
|
18
19
|
end
|
@@ -12,7 +12,7 @@ module RSpecApib
|
|
12
12
|
acc
|
13
13
|
end
|
14
14
|
transactions.each do |requested_tx|
|
15
|
-
|
15
|
+
documented_transaction_tracker.keys.each do |dtx|
|
16
16
|
documented_transaction_tracker[dtx] = true if dtx.matches?(requested_tx.request, requested_tx.response)
|
17
17
|
end
|
18
18
|
end
|
@@ -30,8 +30,29 @@ module RSpecApib
|
|
30
30
|
results
|
31
31
|
end
|
32
32
|
|
33
|
+
def report_uncovered(errors:)
|
34
|
+
uncovered_transactions.each do |tx|
|
35
|
+
errors << self.class.uncovered_tx_message(tx)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def report_undocumented(errors:)
|
40
|
+
undocumented.each do |tx|
|
41
|
+
errors << self.class.undocumented_tx_message(tx)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
33
45
|
private
|
34
46
|
|
35
47
|
attr_accessor :transactions, :parser
|
48
|
+
|
49
|
+
def self.uncovered_tx_message(tx)
|
50
|
+
"#{tx.request.request_method.to_s.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status}- Not covered"
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.undocumented_tx_message(tx)
|
54
|
+
"#{tx.request.request_method.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status} - Not documented"
|
55
|
+
end
|
56
|
+
|
36
57
|
end
|
37
58
|
end
|
@@ -9,18 +9,14 @@ module RSpecApib
|
|
9
9
|
def validate(transactions:, error_messages:)
|
10
10
|
errors = []
|
11
11
|
reporter = TransactionCoverageReport.new(transactions: transactions, parser: parser)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
undocumented.each do |tx|
|
21
|
-
errors << self.class.undocumented_tx_message(tx)
|
22
|
-
end
|
23
|
-
end
|
12
|
+
reporter.report_uncovered(errors: errors)
|
13
|
+
reporter.report_undocumented(errors: errors)
|
14
|
+
report_summary(errors: errors, error_messages: error_messages)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def report_summary(errors:, error_messages:)
|
24
20
|
if !errors.empty?
|
25
21
|
error_messages.concat errors
|
26
22
|
error_messages << "Coverage Summary: #{uncovered.length} uncovered and #{undocumented.length} undocumented transactions"
|
@@ -30,21 +26,10 @@ module RSpecApib
|
|
30
26
|
end
|
31
27
|
end
|
32
28
|
|
33
|
-
private
|
34
|
-
|
35
29
|
attr_accessor :parser
|
36
30
|
|
37
31
|
def matched_transaction(request:, response:)
|
38
32
|
parser.http_transactions.find { |t| t.matches?(request, response, options: {validate_request_schema: :never, validate_response_schema: :never}) }
|
39
33
|
end
|
40
|
-
|
41
|
-
def self.uncovered_tx_message(tx)
|
42
|
-
"#{tx.request.request_method.to_s.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status}- Not covered"
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.undocumented_tx_message(tx)
|
46
|
-
"#{tx.request.request_method.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status} - Not documented"
|
47
|
-
end
|
48
|
-
|
49
34
|
end
|
50
35
|
end
|
@@ -12,25 +12,32 @@ module RSpecApib
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def validate(request:, response:, error_messages:, options: {})
|
15
|
-
|
16
|
-
results = candidates.map do |candidate|
|
17
|
-
candidate.validate_schema(request, response, validate_request_schema: validate_request_schema, validate_response_schema: validate_response_schema)
|
18
|
-
end
|
19
|
-
|
15
|
+
results = matched_transactions(request, response, options)
|
20
16
|
if results.empty?
|
21
17
|
error_messages << "No candidates for #{request.inspect} with response #{response.inspect}"
|
22
18
|
return false
|
23
19
|
end
|
24
20
|
|
25
21
|
return true unless results.flatten.find {|r| !r[:request_errors].empty? || !r[:response_errors].empty?}
|
22
|
+
report_error_messages(results, error_messages)
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def report_error_messages(results, error_messages)
|
26
29
|
results.each do |result|
|
27
30
|
error_messages << "The request validation failed - reasons #{result[:request_errors].join("\n")}" unless result[:request_errors].empty?
|
28
31
|
error_messages << "The response validation failed - reasons #{result[:response_errors].join("\n")}" unless result[:response_errors].empty?
|
29
32
|
end
|
30
|
-
false
|
31
33
|
end
|
32
34
|
|
33
|
-
|
35
|
+
def matched_transactions(request, response, options)
|
36
|
+
candidates = transaction_candidates(request: request, response: response, options: options)
|
37
|
+
candidates.map do |candidate|
|
38
|
+
candidate.validate_schema(request, response, validate_request_schema: validate_request_schema, validate_response_schema: validate_response_schema)
|
39
|
+
end
|
40
|
+
end
|
34
41
|
|
35
42
|
def transaction_candidates(request:, response:, options: {})
|
36
43
|
transactions = parser.http_transactions.select do |t|
|
data/lib/rspec_apib/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec_api_blueprint_matchers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shift Commerce Ltd
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-05-
|
11
|
+
date: 2017-05-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -163,9 +163,6 @@ files:
|
|
163
163
|
- lib/rspec_apib/elements/string.rb
|
164
164
|
- lib/rspec_apib/elements/templated_href.rb
|
165
165
|
- lib/rspec_apib/elements/transition.rb
|
166
|
-
- lib/rspec_apib/extractors.rb
|
167
|
-
- lib/rspec_apib/extractors/http_transaction.rb
|
168
|
-
- lib/rspec_apib/extractors/resource.rb
|
169
166
|
- lib/rspec_apib/parser.rb
|
170
167
|
- lib/rspec_apib/request.rb
|
171
168
|
- lib/rspec_apib/response.rb
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module RSpecApib
|
3
|
-
module Extractor
|
4
|
-
class HttpTransaction
|
5
|
-
def self.call(document)
|
6
|
-
collector = []
|
7
|
-
find_nodes_within(document, collector: collector)
|
8
|
-
collector
|
9
|
-
end
|
10
|
-
|
11
|
-
private
|
12
|
-
|
13
|
-
def self.find_nodes_within(node, collector:)
|
14
|
-
return node.each { |node| find_nodes_within(node, collector: collector)} if node.is_a?(Array)
|
15
|
-
return unless node.is_a?(Hash)
|
16
|
-
if node["element"] == "httpTransaction"
|
17
|
-
collector << node
|
18
|
-
elsif node.key?("content")
|
19
|
-
find_nodes_within(node["content"], collector: collector)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module RSpecApib
|
3
|
-
module Extractor
|
4
|
-
class Resource
|
5
|
-
def self.call(document)
|
6
|
-
collector = []
|
7
|
-
find_nodes_within(document, collector: collector)
|
8
|
-
collector
|
9
|
-
end
|
10
|
-
|
11
|
-
private
|
12
|
-
|
13
|
-
def self.find_nodes_within(node, collector:)
|
14
|
-
return node.each { |node| find_nodes_within(node, collector: collector)} if node.is_a?(Array)
|
15
|
-
return unless node.is_a?(Hash)
|
16
|
-
if node["element"] == "resource"
|
17
|
-
collector << node
|
18
|
-
elsif node.key?("content")
|
19
|
-
find_nodes_within(node["content"], collector: collector)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|