pacto 0.2.5 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +0 -2
  3. data/.rubocop-todo.yml +51 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +4 -2
  6. data/Guardfile +28 -14
  7. data/README.md +81 -51
  8. data/Rakefile +24 -11
  9. data/features/generation/generation.feature +25 -0
  10. data/features/journeys/validation.feature +74 -0
  11. data/features/support/env.rb +16 -0
  12. data/lib/pacto.rb +63 -34
  13. data/lib/pacto/contract.rb +25 -11
  14. data/lib/pacto/contract_factory.rb +13 -44
  15. data/lib/pacto/core/callback.rb +11 -0
  16. data/lib/pacto/core/configuration.rb +34 -0
  17. data/lib/pacto/core/contract_repository.rb +44 -0
  18. data/lib/pacto/erb_processor.rb +18 -0
  19. data/lib/pacto/exceptions/invalid_contract.rb +10 -1
  20. data/lib/pacto/extensions.rb +2 -2
  21. data/lib/pacto/generator.rb +75 -0
  22. data/lib/pacto/hash_merge_processor.rb +14 -0
  23. data/lib/pacto/hooks/erb_hook.rb +17 -0
  24. data/lib/pacto/logger.rb +42 -0
  25. data/lib/pacto/meta_schema.rb +17 -0
  26. data/lib/pacto/rake_task.rb +75 -12
  27. data/lib/pacto/request.rb +3 -4
  28. data/lib/pacto/response.rb +27 -19
  29. data/lib/pacto/server.rb +2 -0
  30. data/lib/pacto/server/dummy.rb +45 -0
  31. data/lib/pacto/server/playback_servlet.rb +21 -0
  32. data/lib/pacto/stubs/built_in.rb +57 -0
  33. data/lib/pacto/version.rb +1 -1
  34. data/pacto.gemspec +8 -2
  35. data/resources/contract_schema.json +216 -0
  36. data/spec/coveralls_helper.rb +10 -0
  37. data/spec/integration/data/strict_contract.json +33 -0
  38. data/spec/integration/data/templating_contract.json +25 -0
  39. data/spec/integration/e2e_spec.rb +40 -7
  40. data/spec/integration/templating_spec.rb +55 -0
  41. data/spec/spec_helper.rb +17 -0
  42. data/spec/unit/data/simple_contract.json +22 -0
  43. data/spec/unit/hooks/erb_hook_spec.rb +51 -0
  44. data/spec/unit/pacto/configuration_spec.rb +51 -0
  45. data/spec/unit/pacto/contract_factory_spec.rb +4 -35
  46. data/spec/unit/pacto/contract_spec.rb +59 -31
  47. data/spec/unit/pacto/core/configuration_spec.rb +28 -0
  48. data/spec/unit/pacto/core/contract_repository_spec.rb +133 -0
  49. data/spec/unit/pacto/erb_processor_spec.rb +23 -0
  50. data/spec/unit/pacto/extensions_spec.rb +11 -11
  51. data/spec/unit/pacto/generator_spec.rb +142 -0
  52. data/spec/unit/pacto/hash_merge_processor_spec.rb +20 -0
  53. data/spec/unit/pacto/logger_spec.rb +44 -0
  54. data/spec/unit/pacto/meta_schema_spec.rb +70 -0
  55. data/spec/unit/pacto/pacto_spec.rb +32 -58
  56. data/spec/unit/pacto/request_spec.rb +83 -34
  57. data/spec/unit/pacto/response_adapter_spec.rb +9 -11
  58. data/spec/unit/pacto/response_spec.rb +68 -68
  59. data/spec/unit/pacto/server/playback_servlet_spec.rb +24 -0
  60. data/spec/unit/pacto/stubs/built_in_spec.rb +168 -0
  61. metadata +291 -147
  62. data/.rspec_integration +0 -4
  63. data/.rspec_unit +0 -4
  64. data/lib/pacto/file_pre_processor.rb +0 -12
  65. data/lib/pacto/instantiated_contract.rb +0 -62
  66. data/spec/integration/spec_helper.rb +0 -1
  67. data/spec/integration/utils/dummy_server.rb +0 -34
  68. data/spec/unit/pacto/file_pre_processor_spec.rb +0 -13
  69. data/spec/unit/pacto/instantiated_contract_spec.rb +0 -224
  70. data/spec/unit/spec_helper.rb +0 -5
data/lib/pacto.rb CHANGED
@@ -1,46 +1,75 @@
1
- require "pacto/version"
2
-
3
- require "httparty"
4
- require "hash_deep_merge"
5
- require "json"
6
- require "json-schema"
7
- require "json-generator"
8
- require "webmock"
9
- require "ostruct"
10
- require "erb"
11
-
12
- require "pacto/exceptions/invalid_contract.rb"
13
- require "pacto/extensions"
14
- require "pacto/request"
15
- require "pacto/response_adapter"
16
- require "pacto/response"
17
- require "pacto/instantiated_contract"
18
- require "pacto/contract"
19
- require "pacto/contract_factory"
20
- require "pacto/file_pre_processor"
1
+ require 'pacto/version'
2
+
3
+ require 'httparty'
4
+ require 'hash_deep_merge'
5
+ require 'multi_json'
6
+ require 'json-schema'
7
+ require 'json-generator'
8
+ require 'webmock'
9
+ require 'ostruct'
10
+ require 'erb'
11
+ require 'logger'
12
+
13
+ require 'pacto/core/contract_repository'
14
+ require 'pacto/core/configuration'
15
+ require 'pacto/core/callback'
16
+ require 'pacto/logger'
17
+ require 'pacto/exceptions/invalid_contract.rb'
18
+ require 'pacto/extensions'
19
+ require 'pacto/request'
20
+ require 'pacto/response_adapter'
21
+ require 'pacto/response'
22
+ require 'pacto/stubs/built_in'
23
+ require 'pacto/contract'
24
+ require 'pacto/contract_factory'
25
+ require 'pacto/erb_processor'
26
+ require 'pacto/hash_merge_processor'
27
+ require 'pacto/stubs/built_in'
28
+ require 'pacto/meta_schema'
29
+ require 'pacto/hooks/erb_hook'
30
+ require 'pacto/generator'
21
31
 
22
32
  module Pacto
23
- def self.build_from_file(contract_path, host, file_pre_processor=FilePreProcessor.new)
24
- ContractFactory.build_from_file(contract_path, host, file_pre_processor)
33
+ class << self
34
+
35
+ def configuration
36
+ @configuration ||= Configuration.new
37
+ end
38
+
39
+ def clear!
40
+ Pacto.configuration.provider.reset!
41
+ @configuration = nil
42
+ unregister_all!
43
+ end
44
+
45
+ def configure
46
+ yield(configuration)
47
+ end
25
48
  end
26
49
 
27
- def self.register(name, contract)
28
- raise ArgumentError, "contract \" #{name}\" has already been registered" if registered.has_key?(name)
29
- registered[name] = contract
50
+ def self.validate_contract contract
51
+ Pacto::MetaSchema.new.validate contract
52
+ puts 'All contracts successfully meta-validated'
53
+ true
54
+ rescue InvalidContract => exception
55
+ puts 'Validation errors detected'
56
+ exception.errors.each do |error|
57
+ puts " Error: #{error}"
58
+ end
59
+ false
30
60
  end
31
61
 
32
- def self.use(contract_name, values = nil)
33
- raise ArgumentError, "contract \"#{contract_name}\" not found" unless registered.has_key?(contract_name)
34
- instantiated_contract = registered[contract_name].instantiate(values)
35
- instantiated_contract.stub!
36
- instantiated_contract
62
+ def self.build_from_file(contract_path, host, file_pre_processor = Pacto.configuration.preprocessor)
63
+ ContractFactory.build_from_file(contract_path, host, file_pre_processor)
37
64
  end
38
65
 
39
- def self.registered
40
- @registered ||= {}
66
+ def self.load(contract_name)
67
+ build_from_file(path_for(contract_name), nil)
41
68
  end
42
69
 
43
- def self.unregister_all!
44
- @registered = {}
70
+ private
71
+
72
+ def self.path_for(contract)
73
+ File.join(configuration.contracts_path, "#{contract}.json")
45
74
  end
46
75
  end
@@ -1,22 +1,36 @@
1
1
  module Pacto
2
2
  class Contract
3
- def initialize(request, response)
3
+ attr_reader :values
4
+ attr_reader :request, :response
5
+
6
+ def initialize(request, response, file = nil)
4
7
  @request = request
5
8
  @response = response
9
+ @file = file
10
+ end
11
+
12
+ def stub_contract! values = {}
13
+ @values = values
14
+ @stub = Pacto.configuration.provider.stub_request!(@request, stub_response) unless @request.nil?
6
15
  end
7
16
 
8
- def instantiate(values = nil)
9
- instantiated_contract = InstantiatedContract.new(@request, @response.instantiate)
10
- instantiated_contract.replace!(values) unless values.nil?
11
- instantiated_contract
17
+ def validate(response_gotten = provider_response, opt = {})
18
+ @response.validate(response_gotten, opt)
12
19
  end
13
20
 
14
- def validate
15
- response_gotten = @request.execute
16
- if ENV["DEBUG_CONTRACTS"]
17
- puts "[DEBUG] Response: #{response_gotten.inspect}"
18
- end
19
- @response.validate(response_gotten)
21
+ def matches? request_signature
22
+ @stub.matches? request_signature unless @stub.nil?
20
23
  end
24
+
25
+ private
26
+
27
+ def provider_response
28
+ @request.execute
29
+ end
30
+
31
+ def stub_response
32
+ @response.instantiate
33
+ end
34
+
21
35
  end
22
36
  end
@@ -1,50 +1,19 @@
1
1
  module Pacto
2
2
  class ContractFactory
3
- def self.build_from_file(contract_path, host, file_pre_processor)
4
- contract_definition_expanded = file_pre_processor.process(File.read(contract_path))
5
- definition = JSON.parse(contract_definition_expanded)
6
- validate_contract definition, contract_path
7
- request = Request.new(host, definition["request"])
8
- response = Response.new(definition["response"])
9
- Contract.new(request, response)
10
- end
11
-
12
- def self.validate_contract definition, contract_path
13
- contract_format = {
14
- type: "object",
15
- required: true,
16
- properties: {
17
- request: {
18
- type: "object",
19
- required: true,
20
- properties: {
21
- method: {type: "string", required: true, pattern: "(GET)|(POST)|(PUT)|(DELETE)"},
22
- path: {type: "string", required: true},
23
- params: {type: "object", required: true},
24
- headers: {type: "object", required: true}
25
- }
26
- },
27
- response: {
28
- type: "object",
29
- required: true,
30
- properties: {
31
- status: {type: "integer", required: true},
32
- headers: {type: "object", required: true},
33
- body: {
34
- type: "object",
35
- required: false,
36
- properties: {
37
- type: { type: "string", required: true, pattern: "(string)|(object)|(array)"}
38
- }
39
- }
40
- }
41
- }
42
- }
43
- }.to_json
44
- errors = JSON::Validator.fully_validate(contract_format, definition)
45
- unless errors.empty?
46
- raise InvalidContract, errors.join("\n")
3
+ def self.build_from_file(contract_path, host, preprocessor)
4
+ contract_definition = File.read(contract_path)
5
+ if preprocessor
6
+ contract_definition = preprocessor.process(contract_definition)
47
7
  end
8
+ definition = JSON.parse(contract_definition)
9
+ schema.validate definition
10
+ request = Request.new(host, definition['request'])
11
+ response = Response.new(definition['response'])
12
+ Contract.new(request, response, contract_path)
13
+ end
14
+
15
+ def self.schema
16
+ @schema ||= MetaSchema.new
48
17
  end
49
18
  end
50
19
  end
@@ -0,0 +1,11 @@
1
+ module Pacto
2
+ class Callback
3
+ def initialize(&block)
4
+ @callback = block
5
+ end
6
+
7
+ def process(contracts, request_signature, response)
8
+ @callback.call contracts, request_signature, response
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ module Pacto
2
+ class Configuration
3
+ attr_accessor :preprocessor, :postprocessor, :provider, :strict_matchers, :contracts_path, :logger
4
+ attr_reader :callback
5
+
6
+ def initialize
7
+ @preprocessor = ERBProcessor.new
8
+ @postprocessor = HashMergeProcessor.new
9
+ @provider = Pacto::Stubs::BuiltIn.new
10
+ @strict_matchers = true
11
+ @contracts_path = nil
12
+ @logger = Logger.instance
13
+ if ENV['PACTO_DEBUG']
14
+ @logger.level = :debug
15
+ else
16
+ @logger.level = :default
17
+ end
18
+ @callback = Pacto::Hooks::ERBHook.new
19
+ end
20
+
21
+ def register_contract(contract = nil, *tags)
22
+ Pacto.register_contract(contract, *tags)
23
+ end
24
+
25
+ def register_callback(callback = nil, &block)
26
+ if block_given?
27
+ @callback = Pacto::Callback.new(&block)
28
+ else
29
+ raise 'Expected a Pacto::Callback' unless callback.is_a? Pacto::Callback
30
+ @callback = callback
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ module Pacto
2
+ class << self
3
+
4
+ def register_contract(contract = nil, *tags)
5
+ tags << :default if tags.empty?
6
+ start_count = registered.count
7
+ tags.uniq.each do |tag|
8
+ registered[tag] << contract
9
+ end
10
+ registered.count - start_count
11
+ end
12
+
13
+ def use(tag, values = {})
14
+ merged_contracts = registered[:default] + registered[tag]
15
+
16
+ raise ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty?
17
+
18
+ merged_contracts.each do |contract|
19
+ contract.stub_contract! values
20
+ end
21
+ merged_contracts.count
22
+ end
23
+
24
+ def registered
25
+ @registered ||= Hash.new { |hash, key| hash[key] = Set.new }
26
+ end
27
+
28
+ def unregister_all!
29
+ registered.clear
30
+ end
31
+
32
+ def contract_for(request_signature)
33
+ matches = Set.new
34
+ registered.values.each do |contract_set|
35
+ contract_set.each do |contract|
36
+ if contract.matches? request_signature
37
+ matches.add contract
38
+ end
39
+ end
40
+ end
41
+ matches
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ module Pacto
2
+ class ERBProcessor
3
+ def process(contract, values = {})
4
+ values ||= {}
5
+ erb = ERB.new(contract)
6
+ erb_result = erb.result hash_binding(values)
7
+ Logger.instance.debug "Processed contract: #{erb_result.inspect}"
8
+ erb_result
9
+ end
10
+
11
+ private
12
+
13
+ def hash_binding(values)
14
+ namespace = OpenStruct.new(values)
15
+ namespace.instance_eval { binding }
16
+ end
17
+ end
18
+ end
@@ -1,2 +1,11 @@
1
1
  class InvalidContract < ArgumentError
2
- end
2
+ attr_reader :errors
3
+
4
+ def initialize(errors)
5
+ @errors = errors
6
+ end
7
+
8
+ def message
9
+ @errors.join "\n"
10
+ end
11
+ end
@@ -2,11 +2,11 @@ module Pacto
2
2
  module Extensions
3
3
  module HashSubsetOf
4
4
  def subset_of?(other)
5
- (self.to_a - other.to_a).empty?
5
+ (to_a - other.to_a).empty?
6
6
  end
7
7
 
8
8
  def normalize_keys
9
- self.inject({}) do |normalized, (key, value)|
9
+ inject({}) do |normalized, (key, value)|
10
10
  normalized[key.to_s.downcase] = value
11
11
  normalized
12
12
  end
@@ -0,0 +1,75 @@
1
+ require 'json/schema_generator'
2
+
3
+ module Pacto
4
+ class Generator
5
+ attr_accessor :request_headers_to_filter
6
+ attr_accessor :response_headers_to_filter
7
+
8
+ INFORMATIONAL_REQUEST_HEADERS =
9
+ %w{
10
+ content-length
11
+ via
12
+ }
13
+
14
+ INFORMATIONAL_RESPONSE_HEADERS =
15
+ %w{
16
+ server
17
+ date
18
+ content-length
19
+ connection
20
+ }
21
+
22
+ def initialize(schema_version = 'draft3',
23
+ schema_generator = JSON::SchemaGenerator,
24
+ validator = Pacto::MetaSchema.new)
25
+ @schema_version = schema_version
26
+ @validator = validator
27
+ @schema_generator = schema_generator
28
+ @response_headers_to_filter = INFORMATIONAL_RESPONSE_HEADERS
29
+ @request_headers_to_filter = INFORMATIONAL_REQUEST_HEADERS
30
+ end
31
+
32
+ def generate(request_file, host)
33
+ contract = Pacto.build_from_file request_file, host
34
+ request = contract.request
35
+ response = request.execute
36
+ save(request_file, request, response)
37
+ end
38
+
39
+ def save(source, request, response)
40
+ body_schema = JSON::SchemaGenerator.generate source, response.body, @schema_version
41
+ contract = {
42
+ :request => {
43
+ :headers => filter_request_headers(request.headers),
44
+ :method => request.method,
45
+ :params => request.params,
46
+ :path => request.path
47
+ },
48
+ :response => {
49
+ :headers => filter_response_headers(response.headers),
50
+ :status => response.status,
51
+ :body => MultiJson.load(body_schema)
52
+ }
53
+ }
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 filter_request_headers headers
64
+ headers.reject do |header|
65
+ @request_headers_to_filter.include? header.downcase
66
+ end
67
+ end
68
+
69
+ def filter_response_headers headers
70
+ headers.reject do |header|
71
+ @response_headers_to_filter.include? header.downcase
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,14 @@
1
+ module Pacto
2
+ class HashMergeProcessor
3
+ def process(response_body, values = {})
4
+ unless values.nil? || values.empty?
5
+ if response_body.respond_to?(:normalize_keys)
6
+ response_body = response_body.normalize_keys.deep_merge(values.normalize_keys)
7
+ else
8
+ response_body = values
9
+ end
10
+ end
11
+ response_body.to_s
12
+ end
13
+ end
14
+ end