pacto 0.4.0.rc1 → 0.4.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile +2 -7
  4. data/Rakefile +0 -5
  5. data/appveyor.yml +12 -0
  6. data/features/configuration/strict_matchers.feature +4 -4
  7. data/features/generate/generation.feature +8 -10
  8. data/features/support/env.rb +3 -7
  9. data/features/validate/validation.feature +3 -3
  10. data/lib/pacto.rb +5 -4
  11. data/lib/pacto/actors/json_generator.rb +1 -1
  12. data/lib/pacto/body_parsing.rb +42 -0
  13. data/lib/pacto/consumer/faraday_driver.rb +5 -2
  14. data/lib/pacto/contract.rb +23 -24
  15. data/lib/pacto/contract_factory.rb +4 -4
  16. data/lib/pacto/core/configuration.rb +18 -10
  17. data/lib/pacto/core/http_middleware.rb +1 -1
  18. data/lib/pacto/core/pacto_request.rb +3 -15
  19. data/lib/pacto/core/pacto_response.rb +3 -13
  20. data/lib/pacto/errors.rb +68 -0
  21. data/lib/pacto/formats/legacy/contract.rb +49 -0
  22. data/lib/pacto/formats/legacy/contract_builder.rb +129 -0
  23. data/lib/pacto/formats/legacy/contract_factory.rb +63 -0
  24. data/lib/pacto/formats/legacy/contract_generator.rb +77 -0
  25. data/lib/pacto/formats/legacy/generator/filters.rb +46 -0
  26. data/lib/pacto/formats/legacy/generator_hint.rb +36 -0
  27. data/lib/pacto/formats/legacy/request_clause.rb +39 -0
  28. data/lib/pacto/formats/legacy/response_clause.rb +31 -0
  29. data/lib/pacto/formats/swagger/contract.rb +86 -0
  30. data/lib/pacto/formats/swagger/contract_factory.rb +45 -0
  31. data/lib/pacto/formats/swagger/request_clause.rb +53 -0
  32. data/lib/pacto/formats/swagger/response_clause.rb +31 -0
  33. data/lib/pacto/generator.rb +4 -4
  34. data/lib/pacto/handlers/json_handler.rb +19 -0
  35. data/lib/pacto/handlers/text_handler.rb +17 -0
  36. data/lib/pacto/request_clause.rb +9 -19
  37. data/lib/pacto/response_clause.rb +4 -4
  38. data/lib/pacto/server.rb +41 -2
  39. data/lib/pacto/stubs/uri_pattern.rb +5 -5
  40. data/lib/pacto/stubs/webmock_adapter.rb +4 -1
  41. data/lib/pacto/test_helper.rb +16 -13
  42. data/lib/pacto/version.rb +1 -1
  43. data/pacto-server.gemspec +1 -3
  44. data/pacto.gemspec +1 -1
  45. data/sample_apis/user_api.rb +16 -0
  46. data/samples/contracts/user.json +51 -0
  47. data/samples/cops.rb +3 -0
  48. data/samples/server_cli.sh +3 -3
  49. data/spec/fabricators/contract_fabricator.rb +17 -8
  50. data/spec/fixtures/{deprecated_contracts → contracts/deprecated}/deprecated_contract.json +2 -2
  51. data/spec/fixtures/contracts/{contract.json → legacy/contract.json} +0 -0
  52. data/spec/fixtures/contracts/{contract_with_examples.json → legacy/contract_with_examples.json} +0 -0
  53. data/spec/fixtures/contracts/{simple_contract.json → legacy/simple_contract.json} +1 -1
  54. data/spec/fixtures/contracts/{strict_contract.json → legacy/strict_contract.json} +0 -0
  55. data/spec/fixtures/contracts/{templating_contract.json → legacy/templating_contract.json} +0 -0
  56. data/spec/fixtures/{swagger → contracts/swagger}/petstore.yaml +0 -0
  57. data/spec/integration/e2e_spec.rb +6 -12
  58. data/spec/integration/forensics/integration_matcher_spec.rb +5 -11
  59. data/spec/integration/rspec_spec.rb +12 -12
  60. data/spec/integration/templating_spec.rb +1 -1
  61. data/spec/spec_helper.rb +14 -2
  62. data/spec/unit/pacto/contract_factory_spec.rb +1 -2
  63. data/spec/unit/pacto/contract_spec.rb +44 -70
  64. data/spec/unit/pacto/core/investigation_spec.rb +4 -3
  65. data/spec/unit/pacto/formats/legacy/contract_builder_spec.rb +93 -0
  66. data/spec/unit/pacto/formats/legacy/contract_factory_spec.rb +29 -0
  67. data/spec/unit/pacto/formats/legacy/contract_generator_spec.rb +173 -0
  68. data/spec/unit/pacto/formats/legacy/contract_spec.rb +41 -0
  69. data/spec/unit/pacto/formats/legacy/generator/filters_spec.rb +104 -0
  70. data/spec/unit/pacto/formats/legacy/request_clause_spec.rb +79 -0
  71. data/spec/unit/pacto/formats/legacy/response_clause_spec.rb +45 -0
  72. data/spec/unit/pacto/formats/swagger/contract_factory_spec.rb +58 -0
  73. data/spec/unit/pacto/formats/swagger/contract_spec.rb +47 -0
  74. data/spec/unit/pacto/investigation_registry_spec.rb +1 -2
  75. data/spec/unit/pacto/pacto_spec.rb +6 -4
  76. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +7 -8
  77. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +2 -4
  78. data/tasks/release.rake +1 -1
  79. metadata +53 -53
  80. data/lib/pacto/contract_builder.rb +0 -125
  81. data/lib/pacto/exceptions/invalid_contract.rb +0 -12
  82. data/lib/pacto/generator/filters.rb +0 -42
  83. data/lib/pacto/generator/hint.rb +0 -26
  84. data/lib/pacto/generator/native_contract_generator.rb +0 -74
  85. data/lib/pacto/native_contract_factory.rb +0 -60
  86. data/lib/pacto/swagger_contract_factory.rb +0 -90
  87. data/spec/pacto/dummy_server.rb +0 -4
  88. data/spec/pacto/dummy_server/dummy.rb +0 -51
  89. data/spec/pacto/dummy_server/jruby_workaround_helper.rb +0 -23
  90. data/spec/pacto/dummy_server/playback_servlet.rb +0 -22
  91. data/spec/unit/pacto/contract_builder_spec.rb +0 -89
  92. data/spec/unit/pacto/generator/filters_spec.rb +0 -100
  93. data/spec/unit/pacto/generator/native_contract_generator_spec.rb +0 -171
  94. data/spec/unit/pacto/native_contract_factory_spec.rb +0 -26
  95. data/spec/unit/pacto/request_clause_spec.rb +0 -75
  96. data/spec/unit/pacto/response_clause_spec.rb +0 -41
  97. data/spec/unit/pacto/server/playback_servlet_spec.rb +0 -27
  98. data/spec/unit/pacto/swagger_contract_factory_spec.rb +0 -56
@@ -0,0 +1,46 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Legacy
5
+ module Generator
6
+ class Filters
7
+ CONNECTION_CONTROL_HEADERS = %w(
8
+ Via
9
+ Server
10
+ Connection
11
+ Transfer-Encoding
12
+ Content-Length
13
+ )
14
+
15
+ FRESHNESS_HEADERS =
16
+ %w(
17
+ Date
18
+ Last-Modified
19
+ ETag
20
+ )
21
+
22
+ HEADERS_TO_FILTER = CONNECTION_CONTROL_HEADERS + FRESHNESS_HEADERS
23
+
24
+ def filter_request_headers(request, response)
25
+ # FIXME: Do we need to handle all these cases in real situations, or just because of stubbing?
26
+ vary_headers = response.headers['vary'] || response.headers['Vary'] || []
27
+ vary_headers = [vary_headers] if vary_headers.is_a? String
28
+ vary_headers = vary_headers.map do |h|
29
+ h.split(',').map(&:strip)
30
+ end.flatten
31
+
32
+ request.headers.select do |header|
33
+ vary_headers.map(&:downcase).include? header.downcase
34
+ end
35
+ end
36
+
37
+ def filter_response_headers(_request, response)
38
+ Pacto::Extensions.normalize_header_keys(response.headers).reject do |header|
39
+ (HEADERS_TO_FILTER.include? header) || (header.start_with?('X-'))
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Legacy
5
+ class GeneratorHint < Pacto::Dash
6
+ extend Forwardable
7
+
8
+ property :request_clause
9
+ coerce_key :request_clause, RequestClause
10
+ property :service_name, required: true
11
+ property :target_file
12
+
13
+ def_delegators :request_clause, *RequestClause::Data.properties.map(&:to_sym)
14
+
15
+ def initialize(data)
16
+ data[:request_clause] = RequestClause::Data.properties.each_with_object({}) do | prop, hash |
17
+ hash[prop] = data.delete prop
18
+ end
19
+ super
20
+ self.target_file ||= "#{slugify(service_name)}.json"
21
+ end
22
+
23
+ def matches?(pacto_request)
24
+ return false if pacto_request.nil?
25
+ Pacto::RequestPattern.for(request_clause).matches?(pacto_request)
26
+ end
27
+
28
+ private
29
+
30
+ def slugify(path)
31
+ path.downcase.gsub(' ', '_')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Legacy
5
+ class RequestClause < Pacto::Dash
6
+ include Pacto::RequestClause
7
+ extend Forwardable
8
+ attr_reader :data
9
+ def_delegators :data, :to_hash
10
+ def_delegators :data, :host, :http_method, :schema, :path, :headers, :params
11
+ def_delegators :data, :host=, :http_method=, :schema=, :path=, :headers=, :params=
12
+
13
+ class Data < Pacto::Dash
14
+ property :host # required?
15
+ property :http_method, required: true
16
+ property :schema, default: {}
17
+ property :path, default: '/'
18
+ property :headers, default: {}
19
+ property :params, default: {}
20
+ end
21
+
22
+ def initialize(data)
23
+ skip_freeze = data.delete(:skip_freeze)
24
+ mash = Hashie::Mash.new data
25
+ mash['http_method'] = normalize(mash['http_method'])
26
+ @data = Data.new(mash)
27
+ freeze unless skip_freeze
28
+ super({})
29
+ @pattern = Pacto::RequestPattern.for(self)
30
+ end
31
+
32
+ def freeze
33
+ @data.freeze
34
+ self
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ module Pacto
2
+ module Formats
3
+ module Legacy
4
+ class ResponseClause
5
+ include Pacto::ResponseClause
6
+ extend Forwardable
7
+ attr_reader :data
8
+ def_delegators :data, :to_hash
9
+ def_delegators :data, :status, :headers, :schema
10
+ def_delegators :data, :status=, :headers=, :schema=
11
+
12
+ class Data < Pacto::Dash
13
+ property :status
14
+ property :headers, default: {}
15
+ property :schema, default: {}
16
+ end
17
+
18
+ def initialize(data)
19
+ skip_freeze = data.delete(:skip_freeze)
20
+ @data = Data.new(data)
21
+ freeze unless skip_freeze
22
+ end
23
+
24
+ def freeze
25
+ @data.freeze
26
+ self
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,86 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'pacto/formats/swagger/request_clause'
4
+ require 'pacto/formats/swagger/response_clause'
5
+
6
+ module Pacto
7
+ module Formats
8
+ module Swagger
9
+ class Contract < Pacto::Dash
10
+ include Pacto::Contract
11
+
12
+ attr_reader :swagger_api_operation
13
+
14
+ property :id
15
+ property :file
16
+ property :request, required: true
17
+ # Although I'd like response to be required, it complicates
18
+ # the partial contracts used the rake generation task...
19
+ # yet another reason I'd like to deprecate that feature
20
+ property :response # , required: true
21
+ property :values, default: {}
22
+ # Gotta figure out how to use test doubles w/ coercion
23
+ coerce_key :request, RequestClause
24
+ coerce_key :response, ResponseClause
25
+ property :examples
26
+ property :name, required: true
27
+ property :adapter, default: proc { Pacto.configuration.adapter }
28
+ property :consumer, default: proc { Pacto.configuration.default_consumer }
29
+ property :provider, default: proc { Pacto.configuration.default_provider }
30
+
31
+ def initialize(swagger_api_operation, base_data = {}) # rubocop:disable Metrics/MethodLength
32
+ if base_data[:file]
33
+ base_data[:file] = Addressable::URI.convert_path(File.expand_path(base_data[:file])).to_s
34
+ base_data[:name] ||= base_data[:file]
35
+ end
36
+ base_data[:id] ||= (base_data[:summary] || base_data[:file])
37
+
38
+ @swagger_api_operation = swagger_api_operation
39
+ host = base_data.delete(:host) || swagger_api_operation.host
40
+ default_response = swagger_api_operation.default_response
41
+ request_clause = Pacto::Formats::Swagger::RequestClause.new(swagger_api_operation, host: host)
42
+
43
+ if default_response.nil?
44
+ logger.warn("No response defined for #{swagger_api_operation.full_name}")
45
+ response_clause = ResponseClause.new(status: 200)
46
+ else
47
+ response_clause = ResponseClause.new(default_response)
48
+ end
49
+
50
+ examples = build_examples(default_response)
51
+ super base_data.merge(
52
+ id: swagger_api_operation.operationId,
53
+ name: swagger_api_operation.full_name,
54
+ request: request_clause, response: response_clause,
55
+ examples: examples
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def build_examples(response)
62
+ return nil if response.nil? || response.examples.nil? || response.examples.empty?
63
+
64
+ if response.examples.empty?
65
+ response_body = nil
66
+ else
67
+ response_body = response.examples.values.first
68
+ end
69
+
70
+ {
71
+ default: {
72
+ request: {}, # Swagger doesn't have a clear way to capture request examples
73
+ response: {
74
+ body: response_body
75
+ }
76
+ }
77
+ }
78
+ rescue => e # FIXME: Only parsing errors?
79
+ logger.warn("Error while trying to parse response example for #{swagger_api_operation.full_name}")
80
+ logger.debug(" Error details: #{e.inspect}")
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,45 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'swagger'
3
+ require 'pacto/formats/swagger/contract'
4
+
5
+ module Pacto
6
+ module Formats
7
+ module Swagger
8
+ # Builds {Pacto::Formats::Swagger::Contract} instances from Swagger documents
9
+ class ContractFactory
10
+ include Logger
11
+
12
+ def load_hints(_contract_path, _host = nil)
13
+ fail NotImplementedError, 'Contract generation from hints is not currently supported for Swagger'
14
+ end
15
+
16
+ def build_from_file(contract_path, host = nil)
17
+ app = ::Swagger.load(contract_path)
18
+ app.operations.map do |op|
19
+ Contract.new(op,
20
+ file: contract_path,
21
+ host: host
22
+ )
23
+ end
24
+ rescue ArgumentError => e
25
+ logger.error(e)
26
+ raise "Could not load #{contract_path}: #{e.message}"
27
+ end
28
+
29
+ def files_for(contracts_dir)
30
+ full_path = Pathname.new(contracts_dir).realpath
31
+
32
+ if full_path.directory?
33
+ all_json_files = "#{full_path}/**/*.{json,yaml,yml}"
34
+ Dir.glob(all_json_files).map do |f|
35
+ Pathname.new(f)
36
+ end
37
+ else
38
+ [full_path]
39
+ end
40
+ end
41
+ end
42
+ Pacto::ContractFactory.add_factory(:swagger, ContractFactory.new)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,53 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Swagger
5
+ class RequestClause
6
+ include Pacto::RequestClause
7
+
8
+ extend Forwardable
9
+ attr_writer :host
10
+ attr_reader :swagger_api_operation
11
+ def_delegator :swagger_api_operation, :verb, :http_method
12
+ def_delegators :swagger_api_operation, :path
13
+
14
+ def initialize(swagger_api_operation, base_data = {})
15
+ @swagger_api_operation = swagger_api_operation
16
+ @host = base_data[:host] || swagger_api_operation.host
17
+ @pattern = Pacto::RequestPattern.for(self)
18
+ end
19
+
20
+ def schema
21
+ return nil if body_parameter.nil?
22
+ return nil if body_parameter.schema.nil?
23
+ body_parameter.schema.parse
24
+ end
25
+
26
+ def params
27
+ return {} if swagger_api_operation.parameters.nil?
28
+
29
+ swagger_api_operation.parameters.select { |p| p.in == 'query' }
30
+ end
31
+
32
+ def headers
33
+ return {} if swagger_api_operation.parameters.nil?
34
+
35
+ swagger_api_operation.parameters.select { |p| p.in == 'header' }
36
+ end
37
+
38
+ def to_hash
39
+ [:http_method, :schema, :path, :headers, :params].each_with_object({}) do | key, hash |
40
+ hash[key.to_s] = send key
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def body_parameter
47
+ return nil if swagger_api_operation.parameters.nil?
48
+ swagger_api_operation.parameters.find { |p| p.in == 'body' }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Swagger
5
+ class ResponseClause
6
+ extend Forwardable
7
+ include Pacto::ResponseClause
8
+ attr_reader :swagger_response
9
+
10
+ def_delegators :swagger_response, :schema
11
+
12
+ def initialize(swagger_response, _base_data = {})
13
+ @swagger_response = swagger_response
14
+ end
15
+
16
+ def status
17
+ swagger_response.status_code || 200
18
+ end
19
+
20
+ def headers
21
+ swagger_response.headers || {}
22
+ end
23
+
24
+ def schema
25
+ return nil unless swagger_response.schema
26
+ swagger_response.schema.parse
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require 'pacto/generator/native_contract_generator'
3
- require 'pacto/generator/hint'
2
+ require 'pacto/formats/legacy/contract_generator'
3
+ require 'pacto/formats/legacy/generator_hint'
4
4
 
5
5
  module Pacto
6
6
  module Generator
@@ -9,7 +9,7 @@ module Pacto
9
9
  class << self
10
10
  # Factory method to return the active contract generator implementation
11
11
  def contract_generator
12
- NativeContractGenerator.new
12
+ Pacto::Formats::Legacy::ContractGenerator.new
13
13
  end
14
14
 
15
15
  # Factory method to return the active contract generator implementation
@@ -38,7 +38,7 @@ module Pacto
38
38
  end
39
39
 
40
40
  def hint(name, hint_data)
41
- @hints << Pacto::Generator::Hint.new(hint_data.merge(service_name: name))
41
+ @hints << Formats::Legacy::GeneratorHint.new(hint_data.merge(service_name: name))
42
42
  end
43
43
  end
44
44
  end
@@ -0,0 +1,19 @@
1
+ require 'json'
2
+
3
+ module Pacto
4
+ module Handlers
5
+ module JSONHandler
6
+ class << self
7
+ def raw(body)
8
+ JSON.dump(body)
9
+ end
10
+
11
+ def parse(body)
12
+ JSON.parse(body)
13
+ end
14
+
15
+ # TODO: Something like validate(contract, body)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Pacto
2
+ module Handlers
3
+ module TextHandler
4
+ class << self
5
+ def raw(body)
6
+ body.to_s
7
+ end
8
+
9
+ def parse(body)
10
+ body.to_s
11
+ end
12
+
13
+ # TODO: Something like validate(contract, body)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,33 +1,23 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Pacto
3
- class RequestClause < Pacto::Dash
3
+ module RequestClause
4
4
  include Logger
5
- property :host # required?
6
- property :http_method, required: true
7
- property :schema, default: {}
8
- property :path, default: '/'
9
- property :headers
10
- property :params, default: {}
11
- property :request_pattern_provider, default: Pacto::RequestPattern
12
-
13
- def initialize(definition)
14
- mash = Hashie::Mash.new definition
15
- mash['http_method'] = normalize(mash['http_method'])
16
- super mash
17
- end
5
+ attr_reader :host
6
+ attr_reader :http_method
7
+ attr_reader :schema
8
+ attr_reader :path
9
+ attr_reader :headers
10
+ attr_reader :params
11
+ attr_reader :pattern
18
12
 
19
13
  def http_method=(method)
20
14
  normalize(method)
21
15
  end
22
16
 
23
- def pattern
24
- @pattern ||= request_pattern_provider.for(self)
25
- end
26
-
27
17
  def uri(values = {})
28
18
  values ||= {}
29
19
  uri_template = pattern.uri_template
30
- missing_keys = uri_template.keys - values.keys
20
+ missing_keys = uri_template.keys.map(&:to_sym) - values.keys.map(&:to_sym)
31
21
  values[:scheme] = 'http' if missing_keys.delete(:scheme)
32
22
  values[:server] = 'localhost' if missing_keys.delete(:server)
33
23
  logger.warn "Missing keys for building a complete URL: #{missing_keys.inspect}" unless missing_keys.empty?