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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Dockerfile +24 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +77 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/codeship-services.yml +7 -0
  13. data/codeship-steps.yml +4 -0
  14. data/lib/rspec_api_blueprint_matchers.rb +1 -0
  15. data/lib/rspec_apib.rb +41 -0
  16. data/lib/rspec_apib/config.rb +17 -0
  17. data/lib/rspec_apib/elements.rb +20 -0
  18. data/lib/rspec_apib/elements/annotation.rb +7 -0
  19. data/lib/rspec_apib/elements/array.rb +9 -0
  20. data/lib/rspec_apib/elements/asset.rb +7 -0
  21. data/lib/rspec_apib/elements/base.rb +97 -0
  22. data/lib/rspec_apib/elements/category.rb +7 -0
  23. data/lib/rspec_apib/elements/copy.rb +7 -0
  24. data/lib/rspec_apib/elements/data_structure.rb +7 -0
  25. data/lib/rspec_apib/elements/href_variables.rb +13 -0
  26. data/lib/rspec_apib/elements/http_headers.rb +27 -0
  27. data/lib/rspec_apib/elements/http_message_payload.rb +45 -0
  28. data/lib/rspec_apib/elements/http_request.rb +59 -0
  29. data/lib/rspec_apib/elements/http_response.rb +35 -0
  30. data/lib/rspec_apib/elements/http_transaction.rb +58 -0
  31. data/lib/rspec_apib/elements/member.rb +21 -0
  32. data/lib/rspec_apib/elements/object.rb +7 -0
  33. data/lib/rspec_apib/elements/parse_result.rb +7 -0
  34. data/lib/rspec_apib/elements/resource.rb +24 -0
  35. data/lib/rspec_apib/elements/source_map.rb +7 -0
  36. data/lib/rspec_apib/elements/string.rb +9 -0
  37. data/lib/rspec_apib/elements/templated_href.rb +43 -0
  38. data/lib/rspec_apib/elements/transition.rb +22 -0
  39. data/lib/rspec_apib/engine.rb +0 -0
  40. data/lib/rspec_apib/extractors.rb +2 -0
  41. data/lib/rspec_apib/extractors/http_transaction.rb +23 -0
  42. data/lib/rspec_apib/extractors/resource.rb +23 -0
  43. data/lib/rspec_apib/parser.rb +79 -0
  44. data/lib/rspec_apib/request.rb +44 -0
  45. data/lib/rspec_apib/response.rb +39 -0
  46. data/lib/rspec_apib/rspec.rb +40 -0
  47. data/lib/rspec_apib/transaction_coverage_report.rb +36 -0
  48. data/lib/rspec_apib/transaction_coverage_validator.rb +49 -0
  49. data/lib/rspec_apib/transaction_validator.rb +44 -0
  50. data/lib/rspec_apib/transcluder.rb +30 -0
  51. data/lib/rspec_apib/version.rb +3 -0
  52. data/lib/transcluder.rb +3 -0
  53. data/rspec_api_blueprint_matchers.gemspec +29 -0
  54. metadata +182 -0
@@ -0,0 +1,7 @@
1
+ module RSpecApib
2
+ module Element
3
+ class DataStructure < Base
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require "addressable"
2
+ module RSpecApib
3
+ module Element
4
+ class HrefVariables < Base
5
+ def [](key)
6
+ member = content.find {|h| h.is_a?(Member) && h.content.key?(key) }
7
+ return nil if member.nil?
8
+ member.content[key]
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -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,7 @@
1
+ module RSpecApib
2
+ module Element
3
+ class Object < Base
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module RSpecApib
2
+ module Element
3
+ class ParseResult < Base
4
+
5
+ end
6
+ end
7
+ 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,7 @@
1
+ module RSpecApib
2
+ module Element
3
+ class SourceMap < Base
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module RSpecApib
2
+ module Element
3
+ class String < ::String
4
+ def self.from_hash(hash, index:, parent:)
5
+ new(hash["content"] || "")
6
+ end
7
+ end
8
+ end
9
+ 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,2 @@
1
+ require "rspec_apib/extractors/resource"
2
+ require "rspec_apib/extractors/http_transaction"
@@ -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