pacto 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ *.swo
20
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in contracts.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ guard 'rspec', :cli => '--color --require spec_helper', :version => 2 do
2
+ watch(%r{^spec/contracts/.+_spec\.rb$})
3
+ watch(%r{^spec/json/.+_spec\.rb$})
4
+ watch(%r{^lib/contracts\.rb$}) { |m| "spec" }
5
+ watch(%r{^lib/json-generator\.rb$}) { |m| "spec" }
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 ThoughtWorks Brasil & Abril Midia
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Pacto
2
+
3
+ Pacto is a Ruby implementation of the [Consumer-Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html)
4
+ pattern for evolving services. It's main features are:
5
+
6
+ - A simple language for specifying a contract.
7
+ - An automated way to validate that a producer meets it's consumers requirements.
8
+ - An auto-generated stub to be used in the consumer's acceptance tests.
9
+
10
+ It was developed in a micro-services environment, specifically a RESTful one, so expect it to be opinionated. Although
11
+ there is enough functionality implemented to motivate us to open-source this, it is still a work in progress and under active
12
+ development. Check the Constraints session for further information on what works and what doesn't.
13
+
14
+ ## Specifying Contracts
15
+
16
+ A contract specifies a single message exchange between a consumer and a provider. In a RESTful world, this means
17
+ an HTTP interaction, which is composed of two main parts: a request and a response.
18
+
19
+ A request has the following attributes:
20
+
21
+ - Method: the method of the HTTP request (e.g. GET, POST, PUT, DELETE).
22
+ - Path: the relative path (without host) of the provider's endpoint.
23
+ - Headers: headers sent in the HTTP request.
24
+ - Params: any data or parameters of the HTTP request (e.g. query string for GET, body for POST).
25
+
26
+ A response has the following attributes:
27
+
28
+ - Status: the HTTP response status code (e.g. 200, 404, 500).
29
+ - Headers: the HTTP response headers.
30
+ - Body: a JSON Schema defining the expected structure of the HTTP response body.
31
+
32
+ Pacto relies on a simple, JSON based language for defining contracts. Below is an example contract for a GET request
33
+ to the /hello_world endpoint of a provider:
34
+
35
+ {
36
+ "request": {
37
+ "method": "GET",
38
+ "path": "/hello_world",
39
+ "headers": {
40
+ "Accept": "application/json"
41
+ },
42
+ "params": {}
43
+ },
44
+
45
+ "response": {
46
+ "status": 200,
47
+ "headers": {
48
+ "Content-Type": "application/json"
49
+ },
50
+ "body": {
51
+ "description": "A simple response",
52
+ "type": "object",
53
+ "properties": {
54
+ "message": {
55
+ "type": "string"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ The host address is intentionally left out of the request specification so that we can validate a contract against any provider.
63
+ It also reinforces the fact that a contract defines the expectation of a consumer, and not the implementation of any specific provider.
64
+
65
+ ## Validating Contracts
66
+
67
+ There are two ways to validate a contract against a provider: through a Rake task or programatically.
68
+
69
+ ### Rake Task
70
+
71
+ Pacto includes a default Rake task. To use it, include it in your Rakefile:
72
+
73
+ require 'contracts/rake_task'
74
+
75
+ Validating a contract against a provider is as simple as running:
76
+
77
+ $ rake contracts:validate[host,dir] # Validates all contracts in a given directory against a given host
78
+
79
+ It is recommended that you also include [colorize](https://github.com/fazibear/colorize) to get prettier, colorful output.
80
+
81
+ ### Programatically
82
+
83
+ The easiest way to load a contract from a file and validate it against a host is by using the builder interface:
84
+
85
+ require 'contracts'
86
+
87
+ WebMock.allow_net_connect!
88
+ contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
89
+ contract.validate
90
+
91
+ ## Auto-Generated Stubs
92
+
93
+ Pacto provides an API to be used in the consumer's acceptance tests. It uses a custom JSON Schema parser and generator
94
+ to generate a valid JSON document as the response body, and relies on [WebMock](https://github.com/bblimke/webmock)
95
+ to stub any HTTP requests made by your application. Important: the JSON generator is in very early stages and does not work
96
+ with the entire JSON Schema specification.
97
+
98
+ First, register the contracts that are going to be used in the acceptance tests suite:
99
+
100
+ require 'contracts'
101
+
102
+ contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
103
+ Pacto.register('my_contract', contract)
104
+
105
+ Then, in the setup phase of the test, specify which contracts will be used for that test:
106
+
107
+ Pacto.use('my_contract')
108
+
109
+ If default values are not specified in the contract's response body, a default value will be automatically generated. It is possible
110
+ to overwrite those values, however, by passing a second argument:
111
+
112
+ Pacto.use('my_contract', :value => 'new value')
113
+
114
+ The values are merged using [hash-deep-merge](https://github.com/Offirmo/hash-deep-merge).
115
+
116
+ ## Code status
117
+
118
+ [![Build Status](https://travis-ci.org/thoughtworks/contracts.png)](https://travis-ci.org/thoughtworks/contracts)
119
+ [![Code Climate](https://codeclimate.com/github/thoughtworks/contracts.png)](https://codeclimate.com/github/thoughtworks/contracts)
120
+ [![Dependency Status](https://gemnasium.com/thoughtworks/contracts.png)](https://gemnasium.com/thoughtworks/contracts)
121
+ [![Coverage Status](https://coveralls.io/repos/thoughtworks/contracts/badge.png)](https://coveralls.io/r/thoughtworks/contracts)
122
+
123
+ ## Contributing
124
+
125
+ 1. Fork it
126
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
127
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
128
+ 4. Push to the branch (`git push origin my-new-feature`)
129
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ if defined?(RSpec)
5
+ RSpec::Core::RakeTask.new('spec')
6
+ task :default => :spec
7
+ end
data/TODO.md ADDED
@@ -0,0 +1,19 @@
1
+ # TODO
2
+
3
+ ## Nice to have
4
+
5
+ - Cucumber Tests as docs (see https://relishapp.com/cucumber/cucumber/docs/);
6
+ - Fake Server (sinatra app generating fake responses based on the contracts);
7
+ - Optional "require" format for JSON Schema: # 'required': ['id', 'categorias', 'titulo', ...];
8
+ - Contract variables for easy writing. Such as: 'path': '/member/{id}';
9
+ - Add JSHint rake task to validate contracts syntax;
10
+ - Pretty output for hash difference (using something like [hashdiff](https://github.com/liufengyun/hashdiff)).
11
+ - A default header in the response marking the response as "mocked"
12
+ - Parameter matcher should use an idea of "subset" instead of matching all the parameters
13
+ - 'default' value to be used when it is present with an array of types
14
+ - Support 'null' attribute type
15
+ - Validate contract structure in a rake task. Then assume all contracts are valid.
16
+
17
+ ## Assumptions
18
+
19
+ - JSON Schema references are stored in the 'definitions' attribute, in the schema's root element.
data/lib/pacto.rb ADDED
@@ -0,0 +1,48 @@
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/extensions"
13
+ require "pacto/request"
14
+ require "pacto/response_adapter"
15
+ require "pacto/response"
16
+ require "pacto/instantiated_contract"
17
+ require "pacto/contract"
18
+ require "pacto/file_pre_processor"
19
+
20
+ module Pacto
21
+ def self.build_from_file(contract_path, host, file_pre_processor=FilePreProcessor.new)
22
+ contract_definition_expanded = file_pre_processor.process(File.read(contract_path))
23
+ definition = JSON.parse(contract_definition_expanded)
24
+ request = Request.new(host, definition["request"])
25
+ response = Response.new(definition["response"])
26
+ Contract.new(request, response)
27
+ end
28
+
29
+ def self.register(name, contract)
30
+ raise ArgumentError, "contract \" #{name}\" has already been registered" if registered.has_key?(name)
31
+ registered[name] = contract
32
+ end
33
+
34
+ def self.use(contract_name, values = nil)
35
+ raise ArgumentError, "contract \"#{contract_name}\" not found" unless registered.has_key?(contract_name)
36
+ instantiated_contract = registered[contract_name].instantiate(values)
37
+ instantiated_contract.stub!
38
+ instantiated_contract
39
+ end
40
+
41
+ def self.registered
42
+ @registered ||= {}
43
+ end
44
+
45
+ def self.unregister_all!
46
+ @registered = {}
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ module Pacto
2
+ class Contract
3
+ def initialize(request, response)
4
+ @request = request
5
+ @response = response
6
+ end
7
+
8
+ def instantiate(values = nil)
9
+ instantiated_contract = InstantiatedContract.new(@request, @response.instantiate)
10
+ instantiated_contract.replace!(values) unless values.nil?
11
+ instantiated_contract
12
+ end
13
+
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)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ module Pacto
2
+ module Extensions
3
+ module HashSubsetOf
4
+ def subset_of?(other)
5
+ (self.to_a - other.to_a).empty?
6
+ end
7
+
8
+ def normalize_keys
9
+ self.inject({}) do |normalized, (key, value)|
10
+ normalized[key.to_s.downcase] = value
11
+ normalized
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Hash.send(:include, Pacto::Extensions::HashSubsetOf)
@@ -0,0 +1,12 @@
1
+ module Pacto
2
+ class FilePreProcessor
3
+ def process(file_string)
4
+ erb = ERB.new file_string
5
+ erb_result = erb.result binding
6
+ if ENV["DEBUG_CONTRACTS"]
7
+ puts "[DEBUG] Processed contract: #{erb_result.inspect}"
8
+ end
9
+ erb_result
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,62 @@
1
+ module Pacto
2
+ class InstantiatedContract
3
+ attr_reader :response_body
4
+
5
+ def initialize(request, response)
6
+ @request = request
7
+ @response = response
8
+ @response_body = response.body
9
+ end
10
+
11
+ def request_path
12
+ @request.absolute_uri
13
+ end
14
+
15
+ def request_uri
16
+ @request.full_uri
17
+ end
18
+
19
+ def replace!(values)
20
+ if @response_body.respond_to?(:normalize_keys)
21
+ @response_body = @response_body.normalize_keys.deep_merge(values.normalize_keys)
22
+ else
23
+ @response_body = values
24
+ end
25
+ end
26
+
27
+ def stub!
28
+ WebMock.stub_request(@request.method, "#{@request.host}#{@request.path}").
29
+ with(request_details).
30
+ to_return({
31
+ :status => @response.status,
32
+ :headers => @response.headers,
33
+ :body => format_body(@response_body)
34
+ })
35
+ end
36
+
37
+ private
38
+
39
+ def format_body(body)
40
+ if body.is_a?(Hash) or body.is_a?(Array)
41
+ body.to_json
42
+ else
43
+ body
44
+ end
45
+ end
46
+
47
+ def request_details
48
+ details = {}
49
+ unless @request.params.empty?
50
+ details[webmock_params_key] = @request.params
51
+ end
52
+ unless @request.headers.empty?
53
+ details[:headers] = @request.headers
54
+ end
55
+ details
56
+ end
57
+
58
+ def webmock_params_key
59
+ @request.method == :get ? :query : :body
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ require 'pacto'
2
+
3
+ unless String.respond_to?(:colors)
4
+ class String
5
+ def colorize(*args)
6
+ self
7
+ end
8
+ end
9
+ end
10
+
11
+ module Pacto
12
+ class RakeTask
13
+ include Rake::DSL
14
+
15
+ def initialize
16
+ @exit_with_error = false
17
+ end
18
+
19
+ def install
20
+ desc "Tasks for contracts gem"
21
+ namespace :contracts do
22
+ validate_task
23
+ end
24
+ end
25
+
26
+ def validate_task
27
+ desc "Validates all contracts in a given directory against a given host"
28
+ task :validate, :host, :dir do |t, args|
29
+ if args.to_a.size < 2
30
+ fail "USAGE: rake contracts:validate[<host>, <contract_dir>]".colorize(:yellow)
31
+ end
32
+
33
+ validate_contracts(args[:host], args[:dir])
34
+ end
35
+ end
36
+
37
+ def validate_contracts(host, dir)
38
+ WebMock.allow_net_connect!
39
+
40
+ contracts = Dir[File.join(dir, '*{.json.erb,.json}')]
41
+ if contracts.empty?
42
+ fail "No contracts found in directory #{dir}".colorize(:yellow)
43
+ end
44
+
45
+ puts "Validating contracts in directory #{dir} against host #{host}\n\n"
46
+
47
+ total_failed = 0
48
+ contracts.each do |contract_file|
49
+ print "#{contract_file.split('/').last}:"
50
+ contract = Pacto.build_from_file(contract_file, host)
51
+ errors = contract.validate
52
+
53
+ if errors.empty?
54
+ puts " OK!".colorize(:green)
55
+ else
56
+ @exit_with_error = true
57
+ total_failed += 1
58
+ puts " FAILED!".colorize(:red)
59
+ errors.each do |error|
60
+ puts "\t* #{error}".colorize(:light_red)
61
+ end
62
+ puts ""
63
+ end
64
+ end
65
+
66
+ if @exit_with_error
67
+ fail "#{total_failed} of #{contracts.size} failed. Check output for detailed error messages.".colorize(:red)
68
+ else
69
+ puts "#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}".colorize(:green)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ Pacto::RakeTask.new.install