rspec_api_blueprint_matchers 0.1.4 → 0.1.5
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 +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
|