pacto 0.2.5 → 0.3.0.pre

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 (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