rspec_api_blueprint_matchers 0.1.2
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Dockerfile +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/codeship-services.yml +7 -0
- data/codeship-steps.yml +4 -0
- data/lib/rspec_api_blueprint_matchers.rb +1 -0
- data/lib/rspec_apib.rb +41 -0
- data/lib/rspec_apib/config.rb +17 -0
- data/lib/rspec_apib/elements.rb +20 -0
- data/lib/rspec_apib/elements/annotation.rb +7 -0
- data/lib/rspec_apib/elements/array.rb +9 -0
- data/lib/rspec_apib/elements/asset.rb +7 -0
- data/lib/rspec_apib/elements/base.rb +97 -0
- data/lib/rspec_apib/elements/category.rb +7 -0
- data/lib/rspec_apib/elements/copy.rb +7 -0
- data/lib/rspec_apib/elements/data_structure.rb +7 -0
- data/lib/rspec_apib/elements/href_variables.rb +13 -0
- data/lib/rspec_apib/elements/http_headers.rb +27 -0
- data/lib/rspec_apib/elements/http_message_payload.rb +45 -0
- data/lib/rspec_apib/elements/http_request.rb +59 -0
- data/lib/rspec_apib/elements/http_response.rb +35 -0
- data/lib/rspec_apib/elements/http_transaction.rb +58 -0
- data/lib/rspec_apib/elements/member.rb +21 -0
- data/lib/rspec_apib/elements/object.rb +7 -0
- data/lib/rspec_apib/elements/parse_result.rb +7 -0
- data/lib/rspec_apib/elements/resource.rb +24 -0
- data/lib/rspec_apib/elements/source_map.rb +7 -0
- data/lib/rspec_apib/elements/string.rb +9 -0
- data/lib/rspec_apib/elements/templated_href.rb +43 -0
- data/lib/rspec_apib/elements/transition.rb +22 -0
- data/lib/rspec_apib/engine.rb +0 -0
- data/lib/rspec_apib/extractors.rb +2 -0
- data/lib/rspec_apib/extractors/http_transaction.rb +23 -0
- data/lib/rspec_apib/extractors/resource.rb +23 -0
- data/lib/rspec_apib/parser.rb +79 -0
- data/lib/rspec_apib/request.rb +44 -0
- data/lib/rspec_apib/response.rb +39 -0
- data/lib/rspec_apib/rspec.rb +40 -0
- data/lib/rspec_apib/transaction_coverage_report.rb +36 -0
- data/lib/rspec_apib/transaction_coverage_validator.rb +49 -0
- data/lib/rspec_apib/transaction_validator.rb +44 -0
- data/lib/rspec_apib/transcluder.rb +30 -0
- data/lib/rspec_apib/version.rb +3 -0
- data/lib/transcluder.rb +3 -0
- data/rspec_api_blueprint_matchers.gemspec +29 -0
- metadata +182 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Element
|
3
|
+
class HttpHeaders < Base
|
4
|
+
def [](key)
|
5
|
+
member = content.find {|h| h.is_a?(Member) && h.content.key?(key) }
|
6
|
+
return nil if member.nil?
|
7
|
+
member.content[key]
|
8
|
+
end
|
9
|
+
|
10
|
+
def each_pair
|
11
|
+
content.select {|h| h.is_a?(Member)}.each do |header|
|
12
|
+
yield header.key, header.value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def keep_if
|
17
|
+
results = dup
|
18
|
+
results.content = []
|
19
|
+
content.select {|h| h.is_a?(Member)}.each do |header|
|
20
|
+
results.content << header if yield header.key, header.value
|
21
|
+
end
|
22
|
+
results
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
require "rspec_apib/elements/http_message_payload"
|
3
|
+
module RSpecApib
|
4
|
+
module Element
|
5
|
+
class HttpMessagePayload < Base
|
6
|
+
|
7
|
+
# The content type if defined else nil
|
8
|
+
# @return [String | NilClass] The content type header or nil
|
9
|
+
def content_type
|
10
|
+
attributes["headers"] && attributes["headers"]["Content-Type"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate_schema(request_or_response, allow_no_schema: false)
|
14
|
+
return [] unless request_or_response.validate_body_with_json_schema?
|
15
|
+
schema = body_schema_asset
|
16
|
+
return [] if schema.nil? && allow_no_schema
|
17
|
+
if schema.nil?
|
18
|
+
failure_reason = {
|
19
|
+
success: false,
|
20
|
+
reason: "Missing a body schema",
|
21
|
+
details: []
|
22
|
+
}
|
23
|
+
return [failure_reason]
|
24
|
+
end
|
25
|
+
schema = JSON.parse schema.content
|
26
|
+
errors = JSON::Validator.fully_validate(schema, request_or_response.body)
|
27
|
+
return [] if errors.length.zero?
|
28
|
+
|
29
|
+
failure_reason = {
|
30
|
+
success: false,
|
31
|
+
reason: "Schema validation failure",
|
32
|
+
details: errors
|
33
|
+
}
|
34
|
+
return [failure_reason]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def body_schema_asset
|
40
|
+
content.find { |n| n.is_a?(Asset) && n.meta && n.meta["classes"] && n.meta["classes"].include?("messageBodySchema")}
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
require "rspec_apib/elements/http_message_payload"
|
3
|
+
module RSpecApib
|
4
|
+
module Element
|
5
|
+
class HttpRequest < HttpMessagePayload
|
6
|
+
|
7
|
+
# Indicates if the incoming request matches the method, path and path vars
|
8
|
+
# @param [::RSpecApib::Request] The incoming request - normalized
|
9
|
+
# @return [Boolean] true if matches else false
|
10
|
+
def matches?(request, options: {})
|
11
|
+
matches_method?(request) &&
|
12
|
+
matches_path?(request) &&
|
13
|
+
matches_headers?(request, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Inherit href and hrefVariables from any ancestor
|
17
|
+
def self.attrs_to_inherit
|
18
|
+
[:href, :hrefVariables, :method]
|
19
|
+
end
|
20
|
+
|
21
|
+
def request_method
|
22
|
+
attributes && attributes["method"] && attributes["method"].downcase.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def path
|
26
|
+
attributes && attributes["href"] && attributes["href"].path
|
27
|
+
end
|
28
|
+
|
29
|
+
def url
|
30
|
+
attributes && attributes["href"].to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def matches_headers?(request_or_response, options)
|
36
|
+
headers = attributes && attributes["headers"] && attributes["headers"].keep_if {|k, v| k == "Content-Type" || k == "Accept"}
|
37
|
+
return true if headers.nil?
|
38
|
+
headers.each_pair do |header_key, header_value|
|
39
|
+
return false unless request_or_response.headers.key?(header_key) &&
|
40
|
+
request_or_response.headers[header_key] == header_value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def matches_method?(request)
|
45
|
+
attributes && attributes["method"] && attributes["method"].downcase.to_sym == request.request_method
|
46
|
+
end
|
47
|
+
|
48
|
+
def matches_path?(request)
|
49
|
+
attributes && attributes["href"] && attributes["href"].matches_path?(request)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.attributes_schema
|
53
|
+
{
|
54
|
+
href: "TemplatedHref"
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rspec_apib/elements/http_message_payload"
|
2
|
+
module RSpecApib
|
3
|
+
module Element
|
4
|
+
class HttpResponse < HttpMessagePayload
|
5
|
+
# Indicates if the incoming request matches the method, path and path vars
|
6
|
+
# @param [::RSpecApib::Request] The incoming request - normalized
|
7
|
+
# @return [Boolean] true if matches else false
|
8
|
+
def matches?(response, options: {})
|
9
|
+
matches_status?(response) &&
|
10
|
+
matches_content_type?(response)
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
attributes["statusCode"]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def matches_status?(response)
|
20
|
+
response.status == attributes["statusCode"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def matches_content_type?(response)
|
24
|
+
expected_content_type = content_type
|
25
|
+
expected_content_type.nil? || (expected_content_type == response.content_type)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.attributes_schema
|
29
|
+
{
|
30
|
+
href: "TemplatedHref"
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Element
|
3
|
+
class HttpTransaction < Base
|
4
|
+
|
5
|
+
def matches?(request_in, response_in, options: {})
|
6
|
+
request.matches?(request_in, options: options) && response.matches?(response_in, options: options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def potential_match?(path:, request_method:, content_type:)
|
10
|
+
potential_match_content_type?(content_type) &&
|
11
|
+
(request_method == :any || request_method == request.request_method) &&
|
12
|
+
(path == :any || request.path == path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_schema(request_in, response_in, validate_request_schema: :always, validate_response_schema: :always)
|
16
|
+
request_errors = validate_request_schema(request_in, validate_request_schema)
|
17
|
+
response_errors = validate_response_schema(response_in, validate_response_schema)
|
18
|
+
{ request_errors: request_errors, response_errors: response_errors }
|
19
|
+
end
|
20
|
+
|
21
|
+
def request
|
22
|
+
@request ||= content.find {|r| r.is_a?(HttpRequest)}
|
23
|
+
end
|
24
|
+
|
25
|
+
def response
|
26
|
+
@response ||= content.find {|r| r.is_a?(HttpResponse)}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def potential_match_content_type?(content_type)
|
32
|
+
content_type == :any || content_type == (response.content_type || request.content_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_request_schema(request_in, validate_request_schema)
|
36
|
+
return [] if validate_request_schema == :never
|
37
|
+
request.validate_schema(request_in, allow_no_schema: (validate_request_schema == :when_defined))
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_response_schema(response_in, validate_response_schema)
|
41
|
+
return [] if validate_response_schema == :never
|
42
|
+
response.validate_schema(response_in, allow_no_schema: (validate_response_schema == :when_defined))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Inherit href and hrefVariables from any ancestor
|
46
|
+
def self.attrs_to_inherit
|
47
|
+
[:href, :hrefVariables]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.attributes_schema
|
51
|
+
{
|
52
|
+
href: "TemplatedHref"
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Element
|
3
|
+
class Member < Base
|
4
|
+
|
5
|
+
def self.from_hash(hash, index:, parent:)
|
6
|
+
child = super
|
7
|
+
content = child.content
|
8
|
+
child.content = {content["key"] => content["value"]}
|
9
|
+
child
|
10
|
+
end
|
11
|
+
|
12
|
+
def key
|
13
|
+
content.keys.first
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
content.values.first
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Element
|
3
|
+
# (byebug) parent.parent["attributes"]
|
4
|
+
# {"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>>]}
|
5
|
+
|
6
|
+
class Resource < Base
|
7
|
+
def transitions
|
8
|
+
content.select { |item| item.is_a?(Transition) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def categories
|
12
|
+
content.select { |item| item.is_a?(Category) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def self.attributes_schema
|
18
|
+
{
|
19
|
+
href: "TemplatedHref"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "addressable"
|
2
|
+
module RSpecApib
|
3
|
+
module Element
|
4
|
+
class TemplatedHref < Base
|
5
|
+
# Note this is not really a hash !!
|
6
|
+
def self.from_hash(hash, index:, parent:)
|
7
|
+
new("templatedHref", nil, nil, hash, parent)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
content
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches_path?(request)
|
15
|
+
tpl = Addressable::Template.new(url)
|
16
|
+
result = tpl.extract(request.url)
|
17
|
+
!result.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def url
|
21
|
+
@url ||= File.join(self.class.host_from_parent(parent), content)
|
22
|
+
end
|
23
|
+
|
24
|
+
def path
|
25
|
+
a1 = Addressable::URI.parse(url)
|
26
|
+
a1.path, a1.query, a1.fragment = nil
|
27
|
+
a2 = Addressable::URI.parse(url)
|
28
|
+
a2.to_s.gsub(a1.to_s, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.host_from_parent(node)
|
32
|
+
return "" if node.nil?
|
33
|
+
attrs = node.attributes
|
34
|
+
return host_from_parent(node.parent) unless attrs && attrs["meta"]
|
35
|
+
host_member = attrs["meta"].find do |member|
|
36
|
+
member.is_a?(Member) && member.content.key?("HOST")
|
37
|
+
end
|
38
|
+
return host_from_parent(node.parent) unless host_member
|
39
|
+
host_member.content["HOST"]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Element
|
3
|
+
class Transition < Base
|
4
|
+
def http_transactions
|
5
|
+
content.select { |item| item.is_a?(HttpTransaction) }
|
6
|
+
end
|
7
|
+
|
8
|
+
# Inherit href and hrefVariables from any ancestor (normally resource)
|
9
|
+
def self.attrs_to_inherit
|
10
|
+
[:href, :hrefVariables]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def self.attributes_schema
|
16
|
+
{
|
17
|
+
href: "TemplatedHref"
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Extractor
|
3
|
+
class HttpTransaction
|
4
|
+
def self.call(document)
|
5
|
+
collector = []
|
6
|
+
find_nodes_within(document, collector: collector)
|
7
|
+
collector
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.find_nodes_within(node, collector:)
|
13
|
+
return node.each { |node| find_nodes_within(node, collector: collector)} if node.is_a?(Array)
|
14
|
+
return unless node.is_a?(Hash)
|
15
|
+
if node["element"] == "httpTransaction"
|
16
|
+
collector << node
|
17
|
+
elsif node.key?("content")
|
18
|
+
find_nodes_within(node["content"], collector: collector)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RSpecApib
|
2
|
+
module Extractor
|
3
|
+
class Resource
|
4
|
+
def self.call(document)
|
5
|
+
collector = []
|
6
|
+
find_nodes_within(document, collector: collector)
|
7
|
+
collector
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.find_nodes_within(node, collector:)
|
13
|
+
return node.each { |node| find_nodes_within(node, collector: collector)} if node.is_a?(Array)
|
14
|
+
return unless node.is_a?(Hash)
|
15
|
+
if node["element"] == "resource"
|
16
|
+
collector << node
|
17
|
+
elsif node.key?("content")
|
18
|
+
find_nodes_within(node["content"], collector: collector)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|