rspec_api_blueprint_matchers 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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,79 @@
1
+ require "rspec_apib/transcluder"
2
+ require "rspec_apib/extractors"
3
+ require "rspec_apib/elements"
4
+ require "open3"
5
+ require "json"
6
+ module RSpecApib
7
+ class Parser
8
+
9
+ def initialize(transcluder: Transcluder, base_element: Element::Base)
10
+ self.transcluder = transcluder
11
+ self.base_element = base_element
12
+ end
13
+
14
+ def parse_file(file)
15
+ self.parsed_file = call_parser(file)
16
+ document, index = parse_document
17
+ self.document = document
18
+ self.index = index
19
+ document
20
+ end
21
+
22
+ def resources
23
+ index[Element::Resource]
24
+ end
25
+
26
+ def categories
27
+ index[Element::Category]
28
+ end
29
+
30
+ def copies
31
+ index[Element::Copy]
32
+ end
33
+
34
+ def transitions
35
+ index[Element::Transition]
36
+ end
37
+
38
+ def http_transactions
39
+ index[Element::HttpTransaction]
40
+ end
41
+
42
+ def http_requests
43
+ index[Element::HttpRequest]
44
+ end
45
+
46
+ def http_responses
47
+ index[Element::HttpResponse]
48
+ end
49
+
50
+ def asset
51
+ index[Element::Asset]
52
+ end
53
+
54
+ private
55
+
56
+ attr_accessor :transcluder, :parsed_file, :document, :base_element, :index
57
+
58
+ def parse_document
59
+ base_element.parse_root(parsed_file)
60
+ end
61
+
62
+ def bin_path
63
+ "drafter"
64
+ end
65
+
66
+ def call_parser(file)
67
+ op = nil
68
+ Open3.popen3("#{bin_path} -f json") do |stdin, stdout, stderr, wait_thr|
69
+ transcluder.each_line(file) do |line|
70
+ stdin.write line
71
+ end
72
+ stdin.close
73
+ op = stdout.read
74
+ exit_status = wait_thr.value
75
+ end
76
+ JSON.parse op
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,44 @@
1
+ module RSpecApib
2
+ class Request
3
+ def initialize(request)
4
+ self.raw_request = request
5
+ end
6
+
7
+ def request_method
8
+ raw_request.method
9
+ end
10
+
11
+ def url
12
+ raw_request.url
13
+ end
14
+
15
+ def validate_body_with_json_schema?
16
+ request_method != :get && is_json?
17
+ end
18
+
19
+ # The request body
20
+ # @return [String] The request body - always as a string
21
+ def body
22
+ raw_request.body
23
+ end
24
+
25
+ def content_type
26
+ headers["Content-Type"]
27
+ end
28
+
29
+ def headers
30
+ raw_request.request_headers
31
+ end
32
+
33
+
34
+ private
35
+
36
+ attr_accessor :raw_request
37
+
38
+ def is_json?
39
+ content_type =~ /json/
40
+ end
41
+
42
+
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ require "json"
2
+ module RSpecApib
3
+ class Response
4
+ def initialize(response)
5
+ self.raw_response = response
6
+ end
7
+
8
+ def status
9
+ raw_response.status.to_s
10
+ end
11
+
12
+ # The response body
13
+ # @return [String] The response body - always as a string
14
+ def body
15
+ JSON.generate raw_response.body
16
+ end
17
+
18
+ def validate_body_with_json_schema?
19
+ is_json?
20
+ end
21
+
22
+ def content_type
23
+ headers["Content-Type"]
24
+ end
25
+
26
+ def headers
27
+ raw_response.response_headers
28
+ end
29
+
30
+
31
+ private
32
+
33
+ attr_accessor :raw_response
34
+
35
+ def is_json?
36
+ content_type=~/json/
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ require "rspec/matchers"
2
+ require "rspec_apib/transaction_validator"
3
+ require "rspec_apib/transaction_coverage_validator"
4
+ RSpec::Matchers.define :match_api_docs_for do |path:, request_method:, content_type:, parser: RSpecApib.config.default_parser|
5
+ error_messages = []
6
+ match do |actual|
7
+ if actual.nil?
8
+ error_messages << "Expected the transaction to match api docs for #{method}:#{path} but it was nil"
9
+ false
10
+ elsif !parser.is_a?(RSpecApib::Parser)
11
+ error_messages << "Expected the transaction to match api docs for #{method}:#{path} but the provided parser was invalid"
12
+ false
13
+ else
14
+ ::RSpecApib::TransactionValidator.new(path: path, request_method: request_method, content_type: content_type, parser: parser).validate(request: ::RSpecApib.normalize_request(actual.request), response: ::RSpecApib.normalize_response(actual.response), error_messages: error_messages)
15
+ end
16
+
17
+ end
18
+
19
+ failure_message do |actual|
20
+ error_messages.join("\n")
21
+ end
22
+ end
23
+
24
+ RSpec::Matchers.define :have_covered_all_api_documentation do | parser: RSpecApib.config.default_parser |
25
+ error_messages = []
26
+ Transaction = Struct.new(:request, :response)
27
+ match do |actual|
28
+ error_messages << "No API calls were made" if actual.empty?
29
+
30
+ normalized_transactions = actual.map do |tx|
31
+ Transaction.new(RSpecApib.normalize_request(tx.request), RSpecApib.normalize_response(tx.response))
32
+ end
33
+ ::RSpecApib::TransactionCoverageValidator.new(parser: parser).validate(transactions: normalized_transactions, error_messages: error_messages)
34
+
35
+ end
36
+
37
+ failure_message do |actual|
38
+ error_messages.join("\n")
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ module RSpecApib
2
+ class TransactionCoverageReport
3
+ def initialize(transactions:, parser: RSpecApib.config.default_parser)
4
+ self.transactions = transactions
5
+ self.parser = parser
6
+ end
7
+
8
+ def uncovered_transactions
9
+ documented_transaction_tracker = parser.http_transactions.inject({}) do |acc, t|
10
+ acc[t]=false
11
+ acc
12
+ end
13
+ transactions.each do |requested_tx|
14
+ matching_transactions = documented_transaction_tracker.keys.each do |dtx|
15
+ documented_transaction_tracker[dtx] = true if dtx.matches?(requested_tx.request, requested_tx.response)
16
+ end
17
+ end
18
+ documented_transaction_tracker.reject {|k,v| v}.keys
19
+ end
20
+
21
+ def undocumented_transactions
22
+ results = []
23
+ transactions.each do |requested_tx|
24
+ match = parser.http_transactions.find do |doc_txn|
25
+ doc_txn.matches?(requested_tx.request, requested_tx.response)
26
+ end
27
+ results << requested_tx if match.nil?
28
+ end
29
+ results
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :transactions, :parser
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ require "rspec_apib/transaction_coverage_report"
2
+ module RSpecApib
3
+ class TransactionCoverageValidator
4
+ def initialize(parser: RSpecApib.config.default_parser)
5
+ self.parser = parser
6
+ end
7
+
8
+ def validate(transactions:, error_messages:)
9
+ errors = []
10
+ reporter = TransactionCoverageReport.new(transactions: transactions, parser: parser)
11
+ uncovered = reporter.uncovered_transactions
12
+ if uncovered.length > 0
13
+ uncovered.each do |tx|
14
+ errors << self.class.uncovered_tx_message(tx)
15
+ end
16
+ end
17
+ undocumented = reporter.undocumented_transactions
18
+ if undocumented.length > 0
19
+ undocumented.each do |tx|
20
+ errors << self.class.undocumented_tx_message(tx)
21
+ end
22
+ end
23
+ if errors.length > 0
24
+ error_messages.concat errors
25
+ error_messages << "Coverage Summary: #{uncovered.length} uncovered and #{undocumented.length} undocumented transactions"
26
+ false
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :parser
35
+
36
+ def matched_transaction(request:, response:)
37
+ parser.http_transactions.find { |t| t.matches?(request, response, options: {validate_request_schema: :never, validate_response_schema: :never}) }
38
+ end
39
+
40
+ def self.uncovered_tx_message(tx)
41
+ "#{tx.request.request_method.to_s.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status}- Not covered"
42
+ end
43
+
44
+ def self.undocumented_tx_message(tx)
45
+ "#{tx.request.request_method.upcase} #{tx.request.url} with response (#{tx.response.content_type}) status #{tx.response.status} - Not documented"
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,44 @@
1
+ module RSpecApib
2
+ class TransactionValidator
3
+ # validate_request_schema can be :always, :never, :when_defined
4
+ def initialize(path:, request_method:, content_type:, validate_request_schema: :always, validate_response_schema: :always, parser: RSpecApib.config.default_parser)
5
+ self.path = path
6
+ self.request_method = request_method
7
+ self.content_type = content_type
8
+ self.parser = parser
9
+ self.validate_request_schema = validate_request_schema
10
+ self.validate_response_schema = validate_response_schema
11
+ end
12
+
13
+ def validate(request:, response:, error_messages:, options: {})
14
+ candidates = transaction_candidates(request: request, response: response, options: options)
15
+ results = candidates.map do |candidate|
16
+ candidate.validate_schema(request, response, validate_request_schema: validate_request_schema, validate_response_schema: validate_response_schema)
17
+ end
18
+
19
+ if results.empty?
20
+ error_messages << "No candidates for #{request.inspect} with response #{response.inspect}"
21
+ return false
22
+ end
23
+
24
+ return true unless results.flatten.find {|r| !r[:request_errors].empty? || !r[:response_errors].empty?}
25
+ results.each do |result|
26
+ error_messages << "The request validation failed - reasons #{result[:request_errors].join("\n")}" unless result[:request_errors].empty?
27
+ error_messages << "The response validation failed - reasons #{result[:response_errors].join("\n")}" unless result[:response_errors].empty?
28
+ end
29
+ false
30
+ end
31
+
32
+ private
33
+
34
+ def transaction_candidates(request:, response:, options: {})
35
+ transactions = parser.http_transactions.select do |t|
36
+ t.potential_match?(path: path, request_method: request_method, content_type: content_type)
37
+ end
38
+ transactions.select { |t| t.matches?(request, response, options: options) }
39
+ end
40
+
41
+ attr_accessor :path, :request_method, :content_type, :parser, :validate_request_schema, :validate_response_schema
42
+
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ module RSpecApib::Transcluder
2
+ REGEX = /:\[[^\]]*\]\(([^\)]*)\)/
3
+ def self.each_line(file, &block)
4
+ File.readlines(file, encoding: "UTF-8").each do |line|
5
+ if needs_transclude?(line)
6
+ transclude(file, line, &block)
7
+ else
8
+ yield line
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.needs_transclude?(line)
14
+ line =~ REGEX
15
+ end
16
+
17
+ def self.transclude(file, line, &block)
18
+ line.gsub!(REGEX) do |match|
19
+ transclude_file = $1
20
+ unless transclude_file =~ /^\//
21
+ transclude_file = File.expand_path(transclude_file, File.dirname(file))
22
+ end
23
+ each_line(transclude_file, &block)
24
+ ""
25
+ end
26
+ yield line
27
+ end
28
+
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ module RSpecApib
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,3 @@
1
+ module RSpecApib::Transcluder
2
+
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rspec_apib/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rspec_api_blueprint_matchers"
8
+ spec.version = RSpecApib::VERSION
9
+ spec.authors = ["Shift Commerce Ltd"]
10
+ spec.email = ["team@shiftcommerce.com"]
11
+
12
+ spec.summary = %q{API Blueprint Tools For RSpec}
13
+ spec.description = %q{API Blueprint Tools For RSpec - Matching http transactions against an API Blueprint document to ensure the API that you are implementing matches the API that is documented which your clients will expect to be the case.}
14
+ spec.homepage = "https://github.com/shiftcommerce/rspec_api_blueprint_matchers"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+ spec.add_dependency "addressable", ">= 2.5.0"
24
+ spec.add_dependency "json-schema", ">= 2.8.0"
25
+ spec.add_dependency "rspec", ">= 3.0"
26
+ spec.add_development_dependency "bundler", "~> 1.14"
27
+ spec.add_development_dependency "byebug"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ end
metadata ADDED
@@ -0,0 +1,182 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec_api_blueprint_matchers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Shift Commerce Ltd
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-05-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: json-schema
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.8.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.8.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.14'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.14'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ description: API Blueprint Tools For RSpec - Matching http transactions against an
98
+ API Blueprint document to ensure the API that you are implementing matches the API
99
+ that is documented which your clients will expect to be the case.
100
+ email:
101
+ - team@shiftcommerce.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - ".travis.yml"
109
+ - Dockerfile
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/setup
116
+ - codeship-services.yml
117
+ - codeship-steps.yml
118
+ - lib/rspec_api_blueprint_matchers.rb
119
+ - lib/rspec_apib.rb
120
+ - lib/rspec_apib/config.rb
121
+ - lib/rspec_apib/elements.rb
122
+ - lib/rspec_apib/elements/annotation.rb
123
+ - lib/rspec_apib/elements/array.rb
124
+ - lib/rspec_apib/elements/asset.rb
125
+ - lib/rspec_apib/elements/base.rb
126
+ - lib/rspec_apib/elements/category.rb
127
+ - lib/rspec_apib/elements/copy.rb
128
+ - lib/rspec_apib/elements/data_structure.rb
129
+ - lib/rspec_apib/elements/href_variables.rb
130
+ - lib/rspec_apib/elements/http_headers.rb
131
+ - lib/rspec_apib/elements/http_message_payload.rb
132
+ - lib/rspec_apib/elements/http_request.rb
133
+ - lib/rspec_apib/elements/http_response.rb
134
+ - lib/rspec_apib/elements/http_transaction.rb
135
+ - lib/rspec_apib/elements/member.rb
136
+ - lib/rspec_apib/elements/object.rb
137
+ - lib/rspec_apib/elements/parse_result.rb
138
+ - lib/rspec_apib/elements/resource.rb
139
+ - lib/rspec_apib/elements/source_map.rb
140
+ - lib/rspec_apib/elements/string.rb
141
+ - lib/rspec_apib/elements/templated_href.rb
142
+ - lib/rspec_apib/elements/transition.rb
143
+ - lib/rspec_apib/engine.rb
144
+ - lib/rspec_apib/extractors.rb
145
+ - lib/rspec_apib/extractors/http_transaction.rb
146
+ - lib/rspec_apib/extractors/resource.rb
147
+ - lib/rspec_apib/parser.rb
148
+ - lib/rspec_apib/request.rb
149
+ - lib/rspec_apib/response.rb
150
+ - lib/rspec_apib/rspec.rb
151
+ - lib/rspec_apib/transaction_coverage_report.rb
152
+ - lib/rspec_apib/transaction_coverage_validator.rb
153
+ - lib/rspec_apib/transaction_validator.rb
154
+ - lib/rspec_apib/transcluder.rb
155
+ - lib/rspec_apib/version.rb
156
+ - lib/transcluder.rb
157
+ - rspec_api_blueprint_matchers.gemspec
158
+ homepage: https://github.com/shiftcommerce/rspec_api_blueprint_matchers
159
+ licenses:
160
+ - MIT
161
+ metadata: {}
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubyforge_project:
178
+ rubygems_version: 2.6.10
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: API Blueprint Tools For RSpec
182
+ test_files: []