pacto 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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