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
@@ -15,7 +15,7 @@ module Pacto
15
15
  begin
16
16
  notify_observers request, response
17
17
  rescue StandardError => e
18
- logger.error(e)
18
+ logger.error Pacto::Errors.formatted_trace(e)
19
19
  end
20
20
  end
21
21
  end
@@ -6,6 +6,8 @@ module Pacto
6
6
  # FIXME: Need case insensitive header lookup, but case-sensitive storage
7
7
  attr_accessor :headers, :body, :method, :uri
8
8
 
9
+ include BodyParsing
10
+
9
11
  def initialize(data)
10
12
  mash = Hashie::Mash.new data
11
13
  @headers = mash.headers.nil? ? {} : mash.headers
@@ -27,7 +29,7 @@ module Pacto
27
29
  def to_s
28
30
  string = Pacto::UI.colorize_method(method)
29
31
  string << " #{relative_uri}"
30
- string << " with body (#{body.bytesize} bytes)" if body
32
+ string << " with body (#{raw_body.bytesize} bytes)" if raw_body
31
33
  string
32
34
  end
33
35
 
@@ -37,20 +39,6 @@ module Pacto
37
39
  end
38
40
  end
39
41
 
40
- def parsed_body
41
- if body.is_a?(String) && content_type == 'application/json'
42
- JSON.parse(body)
43
- else
44
- body
45
- end
46
- rescue
47
- body
48
- end
49
-
50
- def content_type
51
- headers['Content-Type']
52
- end
53
-
54
42
  def normalize
55
43
  @method = @method.to_s.downcase.to_sym
56
44
  @uri = @uri.normalize if @uri
@@ -5,6 +5,8 @@ module Pacto
5
5
  attr_accessor :headers, :body, :status, :parsed_body
6
6
  attr_reader :parsed_body
7
7
 
8
+ include BodyParsing
9
+
8
10
  def initialize(data)
9
11
  mash = Hashie::Mash.new data
10
12
  @headers = mash.headers.nil? ? {} : mash.headers
@@ -22,20 +24,8 @@ module Pacto
22
24
 
23
25
  def to_s
24
26
  string = "STATUS: #{status}"
25
- string << " with body (#{body.bytesize} bytes)" if body
27
+ string << " with body (#{raw_body.bytesize} bytes)" if raw_body
26
28
  string
27
29
  end
28
-
29
- def parsed_body
30
- if body.is_a?(String) && content_type == 'application/json'
31
- JSON.parse(body)
32
- else
33
- body
34
- end
35
- end
36
-
37
- def content_type
38
- headers['Content-Type']
39
- end
40
30
  end
41
31
  end
@@ -0,0 +1,68 @@
1
+ module Pacto
2
+ class InvalidContract < ArgumentError
3
+ attr_reader :errors
4
+
5
+ def initialize(errors)
6
+ @errors = errors
7
+ end
8
+
9
+ def message
10
+ @errors.join "\n"
11
+ end
12
+ end
13
+
14
+ module Errors
15
+ # Creates an array of strings, representing a formatted exception,
16
+ # containing backtrace and nested exception info as necessary, that can
17
+ # be viewed by a human.
18
+ #
19
+ # For example:
20
+ #
21
+ # ------Exception-------
22
+ # Class: Crosstest::StandardError
23
+ # Message: Failure starting the party
24
+ # ---Nested Exception---
25
+ # Class: IOError
26
+ # Message: not enough directories for a party
27
+ # ------Backtrace-------
28
+ # nil
29
+ # ----------------------
30
+ #
31
+ # @param exception [::StandardError] an exception
32
+ # @return [Array<String>] a formatted message
33
+ def self.formatted_trace(exception)
34
+ arr = formatted_exception(exception).dup
35
+ last = arr.pop
36
+ if exception.respond_to?(:original) && exception.original
37
+ arr += formatted_exception(exception.original, 'Nested Exception')
38
+ last = arr.pop
39
+ end
40
+ arr += ['Backtrace'.center(22, '-'), exception.backtrace, last].flatten
41
+ arr
42
+ end
43
+
44
+ # Creates an array of strings, representing a formatted exception that
45
+ # can be viewed by a human. Thanks to MiniTest for the inspiration
46
+ # upon which this output has been designed.
47
+ #
48
+ # For example:
49
+ #
50
+ # ------Exception-------
51
+ # Class: Crosstest::StandardError
52
+ # Message: I have failed you
53
+ # ----------------------
54
+ #
55
+ # @param exception [::StandardError] an exception
56
+ # @param title [String] a custom title for the message
57
+ # (default: `"Exception"`)
58
+ # @return [Array<String>] a formatted message
59
+ def self.formatted_exception(exception, title = 'Exception')
60
+ [
61
+ title.center(22, '-'),
62
+ "Class: #{exception.class}",
63
+ "Message: #{exception.message}",
64
+ ''.center(22, '-')
65
+ ]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,49 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'pacto/formats/legacy/request_clause'
3
+ require 'pacto/formats/legacy/response_clause'
4
+
5
+ module Pacto
6
+ module Formats
7
+ module Legacy
8
+ class Contract < Pacto::Dash
9
+ include Pacto::Contract
10
+
11
+ property :id
12
+ property :file
13
+ property :request, required: true
14
+ # Although I'd like response to be required, it complicates
15
+ # the partial contracts used the rake generation task...
16
+ # yet another reason I'd like to deprecate that feature
17
+ property :response # , required: true
18
+ property :values, default: {}
19
+ # Gotta figure out how to use test doubles w/ coercion
20
+ coerce_key :request, RequestClause
21
+ coerce_key :response, ResponseClause
22
+ property :examples
23
+ property :name, required: true
24
+ property :adapter, default: proc { Pacto.configuration.adapter }
25
+ property :consumer, default: proc { Pacto.configuration.default_consumer }
26
+ property :provider, default: proc { Pacto.configuration.default_provider }
27
+
28
+ def initialize(opts)
29
+ skip_freeze = opts.delete(:skip_freeze)
30
+
31
+ if opts[:file]
32
+ opts[:file] = Addressable::URI.convert_path(File.expand_path(opts[:file])).to_s
33
+ opts[:name] ||= opts[:file]
34
+ end
35
+ opts[:id] ||= (opts[:summary] || opts[:file])
36
+ super
37
+ freeze unless skip_freeze
38
+ end
39
+
40
+ def freeze
41
+ (keys.map(&:to_sym) - [:values, :adapter, :consumer, :provider]).each do | key |
42
+ send(key).freeze
43
+ end
44
+ self
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,129 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Formats
4
+ module Legacy
5
+ class ContractBuilder < Hashie::Dash # rubocop:disable Metrics/ClassLength
6
+ extend Forwardable
7
+ attr_accessor :source
8
+
9
+ def initialize(options = {})
10
+ @schema_generator = options[:schema_generator] ||= JSON::SchemaGenerator
11
+ @filters = options[:filters] ||= Generator::Filters.new
12
+ @data = { request: {}, response: {}, examples: {} }
13
+ @source = 'Pacto' # Currently used by JSONSchemaGeneator, but not really useful
14
+ end
15
+
16
+ def name=(name)
17
+ @data[:name] = name
18
+ end
19
+
20
+ def add_example(name, pacto_request, pacto_response)
21
+ @data[:examples][name] ||= {}
22
+ @data[:examples][name][:request] = clean(pacto_request.to_hash)
23
+ @data[:examples][name][:response] = clean(pacto_response.to_hash)
24
+ self
25
+ end
26
+
27
+ def infer_all
28
+ # infer_file # The target file is being chosen inferred by the Generator
29
+ infer_name
30
+ infer_schemas
31
+ end
32
+
33
+ def infer_name
34
+ if @data[:examples].empty?
35
+ @data[:name] = @data[:request][:path] if @data[:request]
36
+ return self
37
+ end
38
+
39
+ example, hint = example_and_hint
40
+ @data[:name] = hint.nil? ? PactoRequest.new(example[:request]).uri.path : hint.service_name
41
+ self
42
+ end
43
+
44
+ def infer_schemas
45
+ return self if @data[:examples].empty?
46
+
47
+ # TODO: It'd be awesome if we could infer across all examples
48
+ example, _hint = example_and_hint
49
+ sample_request_body = example[:request][:body]
50
+ sample_response_body = example[:response][:body]
51
+ @data[:request][:schema] = generate_schema(sample_request_body) if sample_request_body && !sample_request_body.empty?
52
+ @data[:response][:schema] = generate_schema(sample_response_body) if sample_response_body && !sample_response_body.empty?
53
+ self
54
+ end
55
+
56
+ def without_examples
57
+ @export_examples = false
58
+ self
59
+ end
60
+
61
+ def generate_contract(request, response)
62
+ generate_request(request, response)
63
+ generate_response(request, response)
64
+ infer_all
65
+ self
66
+ end
67
+
68
+ def generate_request(request, response)
69
+ hint = hint_for(request)
70
+ request = clean(
71
+ headers: @filters.filter_request_headers(request, response),
72
+ http_method: request.method,
73
+ params: request.uri.query_values,
74
+ path: hint.nil? ? request.uri.path : hint.path
75
+ )
76
+ @data[:request] = request
77
+ self
78
+ end
79
+
80
+ def generate_response(request, response)
81
+ response = clean(
82
+ headers: @filters.filter_response_headers(request, response),
83
+ status: response.status
84
+ )
85
+ @data[:response] = response
86
+ self
87
+ end
88
+
89
+ def build_hash
90
+ instance_eval(&block) if block_given?
91
+ @final_data = @data.dup
92
+ @final_data.delete(:examples) if exclude_examples?
93
+ clean(@final_data)
94
+ end
95
+
96
+ def build(&block)
97
+ Contract.new build_hash(&block)
98
+ end
99
+
100
+ protected
101
+
102
+ def example_and_hint
103
+ example = @data[:examples].values.first
104
+ example_request = PactoRequest.new example[:request]
105
+ [example, Pacto::Generator.hint_for(example_request)]
106
+ end
107
+
108
+ def exclude_examples?
109
+ @export_examples == false
110
+ end
111
+
112
+ def generate_schema(body, generator_options = Pacto.configuration.generator_options)
113
+ return if body.nil? || body.empty?
114
+
115
+ body_schema = @schema_generator.generate @source, body, generator_options
116
+ MultiJson.load(body_schema)
117
+ end
118
+
119
+ def clean(data)
120
+ data.delete_if { |_k, v| v.nil? }
121
+ end
122
+
123
+ def hint_for(pacto_request)
124
+ Pacto::Generator.hint_for(pacto_request)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,63 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'pacto/formats/legacy/contract'
3
+
4
+ module Pacto
5
+ module Formats
6
+ module Legacy
7
+ # Builds {Pacto::Formats::Legacy::Contract} instances from Pacto's legacy Contract format.
8
+ class ContractFactory
9
+ attr_reader :schema
10
+
11
+ def initialize(options = {})
12
+ @schema = options[:schema] || MetaSchema.new
13
+ end
14
+
15
+ def build_from_file(contract_path, host)
16
+ contract_definition = File.read(contract_path)
17
+ definition = JSON.parse(contract_definition)
18
+ schema.validate definition
19
+ definition['request'].merge!('host' => host)
20
+ body_to_schema(definition, 'request', contract_path)
21
+ body_to_schema(definition, 'response', contract_path)
22
+ method_to_http_method(definition, contract_path)
23
+ request = RequestClause.new(definition['request'])
24
+ response = ResponseClause.new(definition['response'])
25
+ Contract.new(request: request, response: response, file: contract_path, name: definition['name'], examples: definition['examples'])
26
+ end
27
+
28
+ def files_for(contracts_dir)
29
+ full_path = Pathname.new(contracts_dir).realpath
30
+
31
+ if full_path.directory?
32
+ all_json_files = "#{full_path}/**/*.json"
33
+ Dir.glob(all_json_files).map do |f|
34
+ Pathname.new(f)
35
+ end
36
+ else
37
+ [full_path]
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def body_to_schema(definition, section, file)
44
+ schema = definition[section].delete 'body'
45
+ return nil unless schema
46
+
47
+ Pacto::UI.deprecation "Contract format deprecation: #{section}:body will be moved to #{section}:schema (#{file})"
48
+ definition[section]['schema'] = schema
49
+ end
50
+
51
+ def method_to_http_method(definition, file)
52
+ method = definition['request'].delete 'method'
53
+ return nil unless method
54
+
55
+ Pacto::UI.deprecation "Contract format deprecation: request:method will be moved to request:http_method (#{file})"
56
+ definition['request']['http_method'] = method
57
+ end
58
+
59
+ Pacto::ContractFactory.add_factory(:legacy, ContractFactory.new)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,77 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'json/schema_generator'
3
+ require 'pacto/formats/legacy/contract_builder'
4
+ require 'pacto/formats/legacy/generator/filters'
5
+
6
+ module Pacto
7
+ module Formats
8
+ module Legacy
9
+ class ContractGenerator
10
+ include Logger
11
+
12
+ def initialize(_schema_version = 'draft3',
13
+ schema_generator = JSON::SchemaGenerator,
14
+ validator = Pacto::MetaSchema.new,
15
+ filters = Generator::Filters.new,
16
+ consumer = Pacto::Consumer.new)
17
+ @contract_builder = ContractBuilder.new(schema_generator: schema_generator, filters: filters)
18
+ @consumer = consumer
19
+ @validator = validator
20
+ end
21
+
22
+ def generate(pacto_request, pacto_response)
23
+ return unless Pacto.generating?
24
+ logger.debug("Generating Contract for #{pacto_request}, #{pacto_response}")
25
+ begin
26
+ contract_file = load_contract_file(pacto_request)
27
+
28
+ unless File.exist? contract_file
29
+ uri = URI(pacto_request.uri)
30
+ FileUtils.mkdir_p(File.dirname contract_file)
31
+ raw_contract = save(uri, pacto_request, pacto_response)
32
+ File.write(contract_file, raw_contract)
33
+ logger.debug("Generating #{contract_file}")
34
+
35
+ Pacto.load_contract contract_file, uri.host
36
+ end
37
+ rescue => e
38
+ raise StandardError, "Error while generating Contract #{contract_file}: #{e.message}", e.backtrace
39
+ end
40
+ end
41
+
42
+ def generate_from_partial_contract(request_file, host)
43
+ contract = Pacto.load_contract request_file, host
44
+ request, response = @consumer.request(contract)
45
+ save(request_file, request, response)
46
+ end
47
+
48
+ def save(source, request, response)
49
+ @contract_builder.source = source
50
+ # TODO: Get rid of the generate_contract call, just use add_example/infer_all
51
+ @contract_builder.add_example('default', request, response).generate_contract(request, response) # .infer_all
52
+ @contract_builder.without_examples if Pacto.configuration.generator_options[:no_examples]
53
+ contract = @contract_builder.build_hash
54
+ pretty_contract = MultiJson.encode(contract, pretty: true)
55
+ # This is because of a discrepency w/ jruby vs MRI pretty json
56
+ pretty_contract.gsub!(/^$\n/, '')
57
+ @validator.validate pretty_contract
58
+ pretty_contract
59
+ end
60
+
61
+ private
62
+
63
+ def load_contract_file(pacto_request)
64
+ hint = Pacto::Generator.hint_for(pacto_request)
65
+ if hint.nil?
66
+ uri = URI(pacto_request.uri)
67
+ path = uri.path
68
+ basename = File.basename(path, '.json') + '.json'
69
+ File.join(Pacto.configuration.contracts_path, uri.host, File.dirname(path), basename)
70
+ else
71
+ File.expand_path(hint.target_file, Pacto.configuration.contracts_path)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end