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