Showing preview only (327K chars total). Download the full file or copy to clipboard to get everything.
Repository: thoughtworks/pacto
Branch: master
Commit: a1bd9668deca
Files: 200
Total size: 283.1 KB
Directory structure:
gitextract__ymcm0w_/
├── .gitignore
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── CONTRIBUTING.md
├── Gemfile
├── Guardfile
├── LICENSE.txt
├── Procfile
├── README.md
├── Rakefile
├── TODO.md
├── appveyor.yml
├── bin/
│ ├── pacto
│ └── pacto-server
├── changelog.md
├── docs/
│ ├── configuration.md
│ ├── consumer.md
│ ├── cops.md
│ ├── forensics.md
│ ├── generation.md
│ ├── rake_tasks.md
│ ├── rspec.md
│ ├── samples.md
│ ├── server.md
│ ├── server_cli.md
│ └── stenographer.md
├── features/
│ ├── configuration/
│ │ └── strict_matchers.feature
│ ├── evolve/
│ │ ├── README.md
│ │ └── existing_services.feature
│ ├── generate/
│ │ ├── README.md
│ │ └── generation.feature
│ ├── steps/
│ │ └── pacto_steps.rb
│ ├── stub/
│ │ ├── README.md
│ │ └── templates.feature
│ ├── support/
│ │ └── env.rb
│ └── validate/
│ ├── README.md
│ ├── meta_validation.feature
│ └── validation.feature
├── lib/
│ ├── pacto/
│ │ ├── actor.rb
│ │ ├── actors/
│ │ │ ├── from_examples.rb
│ │ │ └── json_generator.rb
│ │ ├── body_parsing.rb
│ │ ├── cli/
│ │ │ └── helpers.rb
│ │ ├── cli.rb
│ │ ├── consumer/
│ │ │ └── faraday_driver.rb
│ │ ├── consumer.rb
│ │ ├── contract.rb
│ │ ├── contract_factory.rb
│ │ ├── contract_files.rb
│ │ ├── contract_set.rb
│ │ ├── cops/
│ │ │ ├── body_cop.rb
│ │ │ ├── request_body_cop.rb
│ │ │ ├── response_body_cop.rb
│ │ │ ├── response_header_cop.rb
│ │ │ └── response_status_cop.rb
│ │ ├── cops.rb
│ │ ├── core/
│ │ │ ├── configuration.rb
│ │ │ ├── contract_registry.rb
│ │ │ ├── hook.rb
│ │ │ ├── http_middleware.rb
│ │ │ ├── investigation_registry.rb
│ │ │ ├── modes.rb
│ │ │ ├── pacto_request.rb
│ │ │ └── pacto_response.rb
│ │ ├── dash.rb
│ │ ├── erb_processor.rb
│ │ ├── errors.rb
│ │ ├── extensions.rb
│ │ ├── forensics/
│ │ │ ├── investigation_filter.rb
│ │ │ └── investigation_matcher.rb
│ │ ├── formats/
│ │ │ ├── legacy/
│ │ │ │ ├── contract.rb
│ │ │ │ ├── contract_builder.rb
│ │ │ │ ├── contract_factory.rb
│ │ │ │ ├── contract_generator.rb
│ │ │ │ ├── generator/
│ │ │ │ │ └── filters.rb
│ │ │ │ ├── generator_hint.rb
│ │ │ │ ├── request_clause.rb
│ │ │ │ └── response_clause.rb
│ │ │ └── swagger/
│ │ │ ├── contract.rb
│ │ │ ├── contract_factory.rb
│ │ │ ├── request_clause.rb
│ │ │ └── response_clause.rb
│ │ ├── generator.rb
│ │ ├── handlers/
│ │ │ ├── json_handler.rb
│ │ │ └── text_handler.rb
│ │ ├── hooks/
│ │ │ └── erb_hook.rb
│ │ ├── investigation.rb
│ │ ├── logger.rb
│ │ ├── meta_schema.rb
│ │ ├── observers/
│ │ │ └── stenographer.rb
│ │ ├── provider.rb
│ │ ├── rake_task.rb
│ │ ├── request_clause.rb
│ │ ├── request_pattern.rb
│ │ ├── resettable.rb
│ │ ├── response_clause.rb
│ │ ├── rspec.rb
│ │ ├── server/
│ │ │ ├── cli.rb
│ │ │ ├── config.rb
│ │ │ ├── proxy.rb
│ │ │ └── settings.rb
│ │ ├── server.rb
│ │ ├── stubs/
│ │ │ ├── uri_pattern.rb
│ │ │ └── webmock_adapter.rb
│ │ ├── test_helper.rb
│ │ ├── ui.rb
│ │ ├── uri.rb
│ │ └── version.rb
│ └── pacto.rb
├── pacto-server.gemspec
├── pacto.gemspec
├── resources/
│ ├── contract_schema.json
│ ├── draft-03.json
│ └── draft-04.json
├── sample_apis/
│ ├── album/
│ │ └── cover_api.rb
│ ├── config.ru
│ ├── echo_api.rb
│ ├── files_api.rb
│ ├── hello_api.rb
│ ├── ping_api.rb
│ ├── reverse_api.rb
│ └── user_api.rb
├── samples/
│ ├── README.md
│ ├── Rakefile
│ ├── configuration.rb
│ ├── consumer.rb
│ ├── contracts/
│ │ ├── README.md
│ │ ├── contract.js
│ │ ├── get_album_cover.json
│ │ ├── localhost/
│ │ │ └── api/
│ │ │ ├── echo.json
│ │ │ └── ping.json
│ │ └── user.json
│ ├── cops.rb
│ ├── forensics.rb
│ ├── generation.rb
│ ├── rake_tasks.sh
│ ├── rspec.rb
│ ├── samples.rb
│ ├── scripts/
│ │ ├── bootstrap
│ │ └── wrapper
│ ├── server.rb
│ ├── server_cli.sh
│ └── stenographer.rb
├── spec/
│ ├── coveralls_helper.rb
│ ├── fabricators/
│ │ ├── contract_fabricator.rb
│ │ ├── http_fabricator.rb
│ │ └── webmock_fabricator.rb
│ ├── fixtures/
│ │ └── contracts/
│ │ ├── deprecated/
│ │ │ └── deprecated_contract.json
│ │ ├── legacy/
│ │ │ ├── contract.json
│ │ │ ├── contract_with_examples.json
│ │ │ ├── simple_contract.json
│ │ │ ├── strict_contract.json
│ │ │ └── templating_contract.json
│ │ └── swagger/
│ │ └── petstore.yaml
│ ├── integration/
│ │ ├── e2e_spec.rb
│ │ ├── forensics/
│ │ │ └── integration_matcher_spec.rb
│ │ ├── rspec_spec.rb
│ │ └── templating_spec.rb
│ ├── spec_helper.rb
│ └── unit/
│ ├── actors/
│ │ ├── from_examples_spec.rb
│ │ └── json_generator_spec.rb
│ └── pacto/
│ ├── actor_spec.rb
│ ├── configuration_spec.rb
│ ├── consumer/
│ │ └── faraday_driver_spec.rb
│ ├── contract_factory_spec.rb
│ ├── contract_files_spec.rb
│ ├── contract_set_spec.rb
│ ├── contract_spec.rb
│ ├── cops/
│ │ ├── body_cop_spec.rb
│ │ ├── response_header_cop_spec.rb
│ │ └── response_status_cop_spec.rb
│ ├── cops_spec.rb
│ ├── core/
│ │ ├── configuration_spec.rb
│ │ ├── contract_registry_spec.rb
│ │ ├── http_middleware_spec.rb
│ │ ├── investigation_spec.rb
│ │ └── modes_spec.rb
│ ├── erb_processor_spec.rb
│ ├── extensions_spec.rb
│ ├── formats/
│ │ ├── legacy/
│ │ │ ├── contract_builder_spec.rb
│ │ │ ├── contract_factory_spec.rb
│ │ │ ├── contract_generator_spec.rb
│ │ │ ├── contract_spec.rb
│ │ │ ├── generator/
│ │ │ │ └── filters_spec.rb
│ │ │ ├── request_clause_spec.rb
│ │ │ └── response_clause_spec.rb
│ │ └── swagger/
│ │ ├── contract_factory_spec.rb
│ │ └── contract_spec.rb
│ ├── hooks/
│ │ └── erb_hook_spec.rb
│ ├── investigation_registry_spec.rb
│ ├── logger_spec.rb
│ ├── meta_schema_spec.rb
│ ├── pacto_spec.rb
│ ├── request_pattern_spec.rb
│ ├── stubs/
│ │ ├── observers/
│ │ │ └── stenographer_spec.rb
│ │ ├── uri_pattern_spec.rb
│ │ └── webmock_adapter_spec.rb
│ └── uri_spec.rb
└── tasks/
└── release.rake
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.gem
*.log
*.pid
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
*.swp
*.swo
.idea/
.floo*
.sublime*
tags
pacto.log
.polytrix/
================================================
FILE: .rspec
================================================
--colour
--require spec_helper
================================================
FILE: .rubocop.yml
================================================
require: rubocop-rspec
Documentation:
Enabled: false
DotPosition:
Enabled: false
LineLength:
Enabled: false
MethodLength:
Max: 20
Style/Encoding:
EnforcedStyle: when_needed
AllCops:
Include:
- Guardfile
- '**/Rakefile'
- pacto*.gemspec
Exclude:
- bin/**/*
- tmp/**/*
RSpec/DescribeClass:
Exclude:
- samples/**/*
- spec/integration/**/*
RSpec/MultipleDescribes:
Exclude:
- samples/**/*
================================================
FILE: .travis.yml
================================================
language: ruby
rvm:
- 2.1.0
- 2.0.0
- 1.9.3
# There is a bug in jruby-1.7.15
# that is blocking testing
- jruby
before_script:
- gem install foreman
- foreman start &
matrix:
allow_failures:
- rvm: jruby
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
You are welcome to contribute to Pacto and this guide will help you to:
- [Setup](#setup) all the needed dependencies in order to start hacking.
- Follow [conventions](#code-conventions) agreed among the project
contributors.
- Follow Pacto's suggested [workflow](#workflow).
- [Submit](#submit-code) new code to the project.
- Run the automated suite of [tests](#run-tests) that is bundled with Pacto.
- Find easily code annotations for [technical debt](#technical-debt) (TODOs,
FIXMEs, etc)
- Be aware of some [troubleshooting tips](#troubleshooting) when running issues
with Pacto.
## <a name="workflow"></a>Development (suggested) workflow
Pacto comes with [`guard`](https://github.com/guard/guard) enabled, this means
that guard will trigger the tests after any change is made on the source code.
We try to keep the feedback loop as fast as we can, so you can be able to run
all the tests everytime you make a change on the project. If you want to follow
this workflow just run:
`bundle exec guard`
Guard will run first the static analysis and then will run the unit test related
with the file that was changed, later the integration test and last the user
journey tests.
## <a name="submit-code"></a>Submit code
Any contribution has to come from a Pull Request via GitHub, to do it just
follow these steps:
1. Fork it (`git clone git@github.com:thoughtworks/pacto.git`).
2. Create your feature branch (`git checkout -b my-new-feature`).
3. Commit your changes (`git commit -am 'Add some feature'`).
4. Verify that the tests are passing (`bundle exec rake`).
5. Push to the branch (`git push origin my-new-feature`).
6. Create new Pull Request.
## <a name="setup"></a>Setting up
You will need to have installed:
- Ruby 1.9.3 or greater installed.
- Bundler gem installed (`gem install bundler`).
- Install all the dependencies (`bundle install`).
## <a name="code-conventions"></a>Coding conventions
### Style guide
Contributing in a project among several authors could lead to different styles
of writting code. In order to create some basic baseline on the source code
Pacto comes with an static code analyzer that will enforce the code to follow
the [Ruby Style Guide](https://github.com/bbatsov/ruby-style-guide). To execute
the analyzer just run:
`bundle exec rubocop`
### Writing tests
Pacto unit tests and integration test are written in RSpec and the user journey
tests are written in Cucumber. For the RSpec tests we suggest to follow the
[Better Specs](http://betterspecs.org/) guideline.
## <a name="run-tests"></a>Running tests
Pacto comes with a set of automated tests. All the tests are runnable via rake
tasks:
- Unit tests (`bundle exec rake unit`).
- Integration tests (`bundle exec rake integration`).
- User journey tests (`bundle exec rake journeys`).
It is also possible run specific tests:
- Unit tests (`bundle exec rspec spec/unit/[file_path]`
- Integration tests (`bundle exec rspec spec/integration/[file_path]`)
- User journey tests (`bundle exec cucumber features/[file_path] -r features/support/env.rb`)
### Checking that all is green
To know that both tests and static analysis is working fine you just have to
run:
`bundle exec rake`
## <a name="technical-debt"></a>Technical Debt
Some of the code in Pacto is commented with the anotations TODO or
FIXME that might point to some potencial technical debt on the source code. If
you are interested to list where are all these, just run:
`bundle exec notes`
## <a name="troubleshooting"></a>Troubleshooting
### Debugging pacto
If you run into some strange behaviour that Pacto might have, you can take
advantage of the debugging capabilities of Pacto. Running the tests with the
environment variable PACTO_DEBUG=true will show (on the standard output) more
details what Pacto is doing behind the scenes.
### Gemfile.lock
Because Pacto is a gem we don't include the Gemfile.lock into the repository
([here is the reason](http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/)).
This could lead to some problems in your daily job as contributor specially
when there is an upgrade in any of the gems that Pacto depends upon. That is
why we recomend you to remove the Gemfile.lock and generate it
(`bundle install`) everytime there are changes on the dependencies.
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
# Specify your gem's dependencies in pacto.gemspec
gemspec name: 'pacto'
gemspec name: 'pacto-server'
# This is only used by Relish tests. Putting it here let's travis
# pre-install so we can speed up the test with `bundle install --local`,
# avoiding Aruba timeouts.
gem 'excon'
gem 'octokit'
group :samples do
gem 'grape'
gem 'grape-swagger'
gem 'puma'
gem 'rake'
gem 'pry'
gem 'rack'
end
================================================
FILE: Guardfile
================================================
# vim: syntax=ruby filetype=ruby
guard :rubocop, all_on_start: false do
watch(/.+\.rb$/)
watch(/\.gemspec$/)
watch('Guardfile')
watch('Rakefile')
watch('.rubocop.yml') { '.' }
watch('.rubocop-todo.yml') { '.' }
end
group :tests, halt_on_fail: true do
guard :rspec, cmd: 'bundle exec rspec' do
# Unit tests
watch(%r{^spec/unit/.+_spec\.rb$})
watch(/^lib\/(.+)\.rb$/) { |_m| 'spec/unit/#{m[1]}_spec.rb' }
watch('spec/spec_helper.rb') { 'spec/unit' }
watch('spec/unit/spec_helper.rb') { 'spec/unit' }
watch(%r{^spec/unit/data/.+\.json$}) { 'spec/unit' }
# Integration tests
watch(%r{^spec/integration/.+_spec\.rb$})
watch(%r{^spec/integration/utils/.+\.rb$}) { 'spec/integration' }
watch(/^lib\/.+\.rb$/) { 'spec/integration' }
watch('spec/spec_helper.rb') { 'spec/integration' }
watch('spec/integration/spec_helper.rb') { 'spec/integration' }
watch(%r{^spec/integration/data/.+\.json$}) { 'spec/integration' }
end
guard :cucumber, cmd: 'bundle exec cucumber', all_on_start: false do
watch(/^features\/.+\.feature$/)
watch(%r{^features/support/.+$}) { 'features' }
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |_m| Dir[File.join('**/#{m[1]}.feature')][0] || 'features' }
end
end
================================================
FILE: LICENSE.txt
================================================
Copyright (c) 2013 ThoughtWorks Brasil & Abril Midia
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Procfile
================================================
sample_apis: sh -c 'bundle exec rackup -s puma -o localhost -p $PORT sample_apis/config.ru'
================================================
FILE: README.md
================================================
[](http://badge.fury.io/rb/pacto)
[](https://travis-ci.org/thoughtworks/pacto)
[](https://codeclimate.com/github/thoughtworks/pacto)
[](https://gemnasium.com/thoughtworks/pacto)
[](https://coveralls.io/r/thoughtworks/pacto)
**Pacto is currently INACTIVE. Unfortunately none of the maintainers have had enough time to maintain it. While we feel that Pacto had good ideas, we also feel that a lot has changed since Pacto was first conceived (including the [OpenAPIs initiative](https://openapis.org/)) and that too much work would be required to maintain & update Pacto. Instead, we encourage others to use other projects that have focused on Consumer-Driven Contracts, like [Pact](https://github.com/realestate-com-au/pact), or to write their own Pacto-inspired project.**
# [INACTIVE] Pacto
Pacto is a judge that arbitrates contract disputes between a **service provider** and one or more **consumers**. In other words, it is a framework for [Integration Contract Testing](http://martinfowler.com/bliki/IntegrationContractTest.html), and helps guide service evolution patterns like [Consumer-Driven Contracts](http://thoughtworks.github.io/pacto/patterns/cdc/) or [Documentation-Driven Contracts](http://thoughtworks.github.io/pacto/patterns/documentation_driven/).
Pacto considers two major terms in order decide if there has been a breach of contract: the **request clause** and the **response clause**.
The **request clause** defines what information must be sent by the **consumer** to the **provider** in order to compel them to render a service. The request clause often describes the required HTTP request headers like `Content-Type`, the required parameters, and the required request body (defined in [json-schema](http://json-schema.org/)) when applicable. Providers are not held liable for failing to deliver services for invalid requests.
The **response clause** defines what information must be returned by the **provider** to the **consumer** in order to successfully complete the transaction. This commonly includes HTTP response headers like `Location` as well as the required response body (also defined in [json-schema](http://json-schema.org/)).
## Test Doubles
The consumer may also enter into an agreement with **test doubles** like [WebMock](http://robots.thoughtbot.com/how-to-stub-external-services-in-tests), [vcr](https://github.com/vcr/vcr) or [mountebank](http://www.mbtest.org/). The services delivered by the **test doubles** for the purposes of development and testing will be held to the same conditions as the service the final services rendered by the parent **provider**. This prevents misrepresentation of sample services as realistic, leading to damages during late integration.
Pacto can provide a [**test double**](#stubbing) if you cannot afford your own.
## Due Diligence
Pacto usually makes it clear if the **consumer** or **provider** is at fault, but if a contract is too vague Pacto cannot assign blame, and if it is too specific the matter may become litigious.
Pacto can provide a [**contract writer**](#generating) that tries to strike a balance, but you may still need to adjust the terms.
## Implied Terms
- Pacto only arbitrates contracts for JSON services.
- Pacto requires Ruby 1.9.3 or newer, though you can use [Polyglot Testing](http://thoughtworks.github.io/pacto/patterns/polyglot/) techniques to support older Rubies and non-Ruby projects.
## Roadmap
- The **test double** provided by Pacto is only semi-competent. It handles simple cases, but we expect to find a more capable replacement in the near future.
- Pacto will provide additional Contract Writers for converting from apiblueprint, WADL, or other documentation formats in the future. It's part of our goal to support [Documentation-Driven Contracts](http://thoughtworks.github.io/pacto/patterns/documentation_driven/)
- Pacto reserves the right to consider other clauses in the future, like security and compliance to industry specifications.
## Usage
**See also: http://thoughtworks.github.io/pacto/usage/**
Pacto can perform three activities: generating, validating, or stubbing services. You can do each of these activities against either live or stubbed services.
You can also use [Pacto Server](#pacto-server-non-ruby-usage) if you are working with non-Ruby projects.
### Configuration
In order to start with Pacto, you just need to require it and optionally customize the default [Configuration](docs/configuration.md). For example:
```ruby
require 'pacto'
Pacto.configure do |config|
config.contracts_path = 'contracts'
end
```
### Generating
The easiest way to get started with Pacto is to run a suite of live tests and tell Pacto to generate the contracts:
```ruby
Pacto.generate!
# run your tests
```
If you're using the same configuration as above, this will produce Contracts in the contracts/ folder.
We know we cannot generate perfect Contracts, especially if we only have one sample request. So we do our best to give you a good starting point, but you will likely want to customize the contract so the validation is more strict in some places and less strict in others.
### Contract Lists
In order to stub or validate a group of contracts you need to create a ContractList.
A ContractList represent a collection of endpoints attached to the same host.
```ruby
require 'pacto'
default_contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
authentication_contracts = Pacto.load_contracts('contracts/auth', 'http://example.com')
legacy_contracts = Pacto.load_contracts('contracts/legacy', 'http://example.com')
```
### Validating
Once you have a ContractList, you can validate all the contracts against the live host.
```ruby
contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.simulate_consumers
```
This method will hit the real endpoint, with a request created based on the request part of the contract.
The response will be compared against the response part of the contract and any structural difference will
generate a validation error.
Running this in your build pipeline will ensure that your contracts actually match the real world, and that
you can run your acceptance tests against the contract stubs without worries.
### Stubbing
To generate stubs based on a ContractList you can run:
```ruby
contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.stub_providers
```
This method uses webmock to ensure that whenever you make a request against one of contracts you actually get a stubbed response,
based on the default values specified on the contract, instead of hitting the real provider.
You can override any default value on the contracts by providing a hash of optional values to the stub_providers method. This
will override the keys for every contract in the list
```ruby
contracts = Pacto.load_contracts('contracts/services', 'http://example.com')
contracts.stub_providers(request_id: 14, name: "Marcos")
```
## Pacto Server (non-Ruby usage)
**See also: http://thoughtworks.github.io/pacto/patterns/polyglot/**
We've bundled a small server that embeds pacto so you can use it for non-Ruby projects. If you want to take advantage of the full features, you'll still need to use Ruby (usually rspec) to drive your API testing. You can run just the server in order to stub or to write validation issues to a log, but you won't have access to the full API fail your tests if there were validation problems.
### Command-line
The command-line version of the server will show you all the options. These same options are used when you launch the server from within rspec. In order to see the options:
`bundle exec pacto-server --help`
Some examples:
```sh
$ bundle exec pacto-server -sv --generate
# Runs a server that will generate Contracts for each request received
$ bundle exec pacto-server -sv --stub --validate
# Runs the server that provides stubs and checks them against Contracts
$ bundle exec pacto-server -sv --live --validate --host
# Runs the server that acts as a proxy to http://example.com, validating each request/response against a Contract
```
### RSpec test helper
You can also launch a server from within an rspec test. The server does start up an external port and runs asynchronously so it doens't block your main test thread from kicking off your consumer code. This gives you an externally accessable server that non-Ruby clients can hit, but still gives you the full Pacto API in order to validate and spy on HTTP interactions.
Example usage of the rspec test helper:
```ruby
require 'rspec'
require 'pacto/rspec'
require 'pacto/test_helper'
describe 'my consumer' do
include Pacto::TestHelper
it 'calls a service' do
with_pacto(
:port => 5000,
:directory => '../spec/integration/data',
:stub => true) do |pacto_endpoint|
# call your code
system "curl #{pacto_endpoint}/echo"
# check results
expect(Pacto).to have_validated(:get, 'https://localhost/echo')
end
end
end
```
## Rake Tasks
Pacto includes a few Rake tasks to help with common tasks. If you want to use these tasks, just add this top your Rakefile:
```ruby
require 'pacto/rake_task'
```
This should add several new tasks to you project:
```sh
rake pacto:generate[input_dir,output_dir,host] # Generates contracts from partial contracts
rake pacto:meta_validate[dir] # Validates a directory of contract definitions
rake pacto:validate[host,dir] # Validates all contracts in a given directory against a given host
```
The pacto:generate task will take partially defined Contracts and generate the missing pieces. See [Generate](docs/generation.md) for more details.
The pacto:meta_validate task makes sure that your Contracts are valid. It only checks the Contracts, not the services that implement them.
The pacto:validate task sends a request to an actual provider and ensures their response complies with the Contract.
## Contracts
Pacto works by associating a service with a Contract. The Contract is a JSON description of the service that uses json-schema to describe the response body. You don't need to write your contracts by hand. In fact, we recommend generating a Contract from your documentation or a service.
A contract is composed of a request that has:
- Method: the method of the HTTP request (e.g. GET, POST, PUT, DELETE);
- Path: the relative path (without host) of the provider's endpoint;
- Headers: headers sent in the HTTP request;
- Params: any data or parameters of the HTTP request (e.g. query string for GET, body for POST).
And a response has that has:
- Status: the HTTP response status code (e.g. 200, 404, 500);
- Headers: the HTTP response headers;
- Body: a JSON Schema defining the expected structure of the HTTP response body.
Below is an example contract for a GET request
to the /hello_world endpoint of a provider:
```json
{
"request": {
"method": "GET",
"path": "/hello_world",
"headers": {
"Accept": "application/json"
},
"params": {}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"description": "A simple response",
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
```
## Constraints
- Pacto only works with JSON services
- Pacto requires Ruby 1.9.3 or newer (though you can older Rubies or non-Ruby projects with a [Pacto Server](#pacto-server-non-ruby-usage))
- Pacto cannot currently specify multiple acceptable status codes (e.g. 200 or 201)
## Contributing
Read the [CONTRIBUTING.md](CONTRIBUTING.md) file.
================================================
FILE: Rakefile
================================================
require 'rspec/core/rake_task'
require 'cucumber'
require 'cucumber/rake/task'
require 'coveralls/rake/task'
require 'rubocop/rake_task'
require 'rake/notes/rake_task'
require 'rake/packagetask'
Dir.glob('tasks/*.rake').each { |r| import r }
Coveralls::RakeTask.new
require 'pacto/rake_task' # FIXME: This require turns on WebMock
WebMock.allow_net_connect!
RuboCop::RakeTask.new(:rubocop) do |task|
task.fail_on_error = true
end
Cucumber::Rake::Task.new(:journeys) do |t|
t.cucumber_opts = 'features --format progress'
end
RSpec::Core::RakeTask.new(:unit) do |t|
t.pattern = 'spec/unit/**/*_spec.rb'
end
RSpec::Core::RakeTask.new(:integration) do |t|
t.pattern = 'spec/integration/**/*_spec.rb'
end
task default: [:unit, :integration, :journeys, :samples, :rubocop, 'coveralls:push']
%w(unit integration journeys samples).each do |taskname|
task taskname => 'smoke_test_services'
end
desc 'Run the samples'
task :samples do
FileUtils.rm_rf('samples/tmp')
sh 'bundle exec polytrix exec --solo=samples --solo-glob="*.{rb,sh}"'
sh 'bundle exec polytrix generate code2doc --solo=samples --solo-glob="*.{rb,sh}"'
end
desc 'Build gems into the pkg directory'
task :build do
FileUtils.rm_rf('pkg')
Dir['*.gemspec'].each do |gemspec|
system "gem build #{gemspec}"
end
FileUtils.mkdir_p('pkg')
FileUtils.mv(Dir['*.gem'], 'pkg')
end
Rake::PackageTask.new('pacto_docs', Pacto::VERSION) do |p|
p.need_zip = true
p.need_tar = true
p.package_files.include('docs/**/*')
end
def changelog
changelog = File.read('CHANGELOG').split("\n\n\n", 2).first
confirm 'Does the CHANGELOG look correct? ', changelog
end
def confirm(question, data)
puts 'Please confirm...'
puts data
print question
abort 'Aborted' unless $stdin.gets.strip == 'y'
puts 'Confirmed'
data
end
desc 'Make sure the sample services are running'
task :smoke_test_services do
require 'faraday'
begin
retryable(tries: 5, sleep: 1) do
Faraday.get('http://localhost:5000/api/ping')
end
rescue
abort 'Could not connect to the demo services, please start them with `foreman start`'
end
end
# Retries a given block a specified number of times in the
# event the specified exception is raised. If the retries
# run out, the final exception is raised.
#
# This code is slightly adapted from https://github.com/mitchellh/vagrant/blob/master/lib/vagrant/util/retryable.rb,
# which is in turn adapted slightly from the following blog post:
# http://blog.codefront.net/2008/01/14/retrying-code-blocks-in-ruby-on-exceptions-whatever/
def retryable(opts = nil)
opts = { tries: 1, on: Exception }.merge(opts || {})
begin
return yield
rescue *opts[:on] => e
if (opts[:tries] -= 1) > 0
$stderr.puts("Retryable exception raised: #{e.inspect}")
sleep opts[:sleep].to_f if opts[:sleep]
retry
end
raise
end
end
================================================
FILE: TODO.md
================================================
# TODO
# v0.4
## Thor
- Kill rake tasks, replace w/ pacto binary
- Split Pacto server to separate repo??
## Swagger converter
- Legacy contracts -> Swagger
## Swagger concepts not yet supported by Pacto
- Support schemes (multiple)
- Support multiple report types
- Validate parameters
- Support Swagger formats/serializations
- Support Swagger examples, or extension for examples
## Documentation
- Polytrix samples -> docs
# v0.5
## Swagger
- Support multiple media types (not just JSON)
- Extension: templates for more advanced stubbing
- Patterns: detect creation, auto-delete
- Configure multiple producers: pacto server w/ multiple ports
# v0.6
## Nice to have
# Someday
- Pretty output for hash difference (using something like [hashdiff](https://github.com/liufengyun/hashdiff)).
- A default header in the response marking the response as "mocked"
## Assumptions
- JSON Schema references are stored in the 'definitions' attribute, in the schema's root element.
================================================
FILE: appveyor.yml
================================================
version: 0.0.{build}
init:
- choco install openssl.light
- gem install bundler --quiet --no-ri --no-rdoc
- gem install foreman --quiet --no-ri --no-rdoc
install:
- bundle install
build: off
test_script:
- START /B foreman start
- bundle exec rake
================================================
FILE: bin/pacto
================================================
#!/usr/bin/env ruby
require 'pacto/cli'
Pacto::CLI::Main.start
================================================
FILE: bin/pacto-server
================================================
#!/usr/bin/env ruby
require 'pacto/server/cli'
Pacto::Server::CLI.start
================================================
FILE: changelog.md
================================================
## 0.3.2
*New Features:*
- #105: Add pacto-server for non-ruby tests. Use the pacto-server gem.
*Breaking Changes:*
- #107: Change default URI pattern to be less greedy.
/magazine will now not match also /magazine/last_edition.
query parameters after ? are still a match (ie /magazine?lastest=true)
*Bug Fixes:*
- #106: Remove dead, undocumented tag feature
## 0.3.1
*Enhancements:*
- #103: Display file URI instead of meaningless schema identifier in messages
*Bug Fixes:*
- #102: - Fix rake pacto:generate task
## 0.3.0
First stable release
================================================
FILE: docs/configuration.md
================================================
Just require pacto to add it to your project.
```rb
require 'pacto'
```
Pacto will disable live connections, so you will get an error if
your code unexpectedly calls an service that was not stubbed. If you
want to re-enable connections, run `WebMock.allow_net_connect!`
```rb
WebMock.allow_net_connect!
```
Pacto can be configured via a block:
```rb
Pacto.configure do |c|
```
Path for loading/storing contracts.
```rb
c.contracts_path = 'contracts'
```
If the request matching should be strict (especially regarding HTTP Headers).
```rb
c.strict_matchers = true
```
You can set the Ruby Logger used by Pacto.
```rb
c.logger = Pacto::Logger::SimpleLogger.instance
```
(Deprecated) You can specify a callback for post-processing responses. Note that only one hook
can be active, and specifying your own will disable ERB post-processing.
```rb
c.register_hook do |_contracts, request, _response|
puts "Received #{request}"
end
```
Options to pass to the [json-schema-generator](https://github.com/maxlinc/json-schema-generator) while generating contracts.
```rb
c.generator_options = { schema_version: 'draft3' }
end
```
You can also do inline configuration. This example tells the json-schema-generator to store default values in the schema.
```rb
Pacto.configuration.generator_options = { defaults: true }
```
If you're using Pacto's rspec matchers you might want to configure a reset between each scenario
```rb
require 'pacto/rspec'
RSpec.configure do |c|
c.after(:each) { Pacto.clear! }
end
```
================================================
FILE: docs/consumer.md
================================================
```rb
require 'pacto'
Pacto.load_contracts 'contracts', 'http://localhost:5000'
WebMock.allow_net_connect!
interactions = Pacto.simulate_consumer :my_client do
request 'Ping'
request 'Echo', body: ->(body) { body.reverse },
headers: (proc do |headers|
headers['Content-Type'] = 'text/json'
headers['Accept'] = 'none'
headers
end)
end
puts interactions
```
================================================
FILE: docs/cops.md
================================================
```rb
require 'pacto'
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
Pacto.validate!
```
You can create a custom cop that investigates the request/response and sees if it complies with a
contract. The cop should return a list of citations if it finds any problems.
```rb
class MyCustomCop
def investigate(_request, _response, contract)
citations = []
citations << 'Contract must have a request schema' if contract.request.schema.empty?
citations << 'Contract must have a response schema' if contract.response.schema.empty?
citations
end
end
Pacto::Cops.active_cops << MyCustomCop.new
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
contracts.stub_providers
puts contracts.simulate_consumers
```
Or you can completely replace the default set of validators
```rb
Pacto::Cops.registered_cops.clear
Pacto::Cops.register_cop Pacto::Cops::ResponseBodyCop
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
puts contracts.simulate_consumers
```
================================================
FILE: docs/forensics.md
================================================
Pacto has a few RSpec matchers to help you ensure a **consumer** and **producer** are
interacting properly. First, let's setup the rspec suite.
```rb
require 'rspec/autorun' # Not generally needed
require 'pacto/rspec'
WebMock.allow_net_connect!
Pacto.validate!
Pacto.load_contracts('contracts', 'http://localhost:5000').stub_providers
```
It's usually a good idea to reset Pacto between each scenario. `Pacto.reset` just clears the
data and metrics about which services were called. `Pacto.clear!` also resets all configuration
and plugins.
```rb
RSpec.configure do |c|
c.after(:each) { Pacto.reset }
end
```
Pacto provides some RSpec matchers related to contract testing, like making sure
Pacto didn't received any unrecognized requests (`have_unmatched_requests`) and that
the HTTP requests matched up with the terms of the contract (`have_failed_investigations`).
```rb
describe Faraday do
let(:connection) { described_class.new(url: 'http://localhost:5000') }
it 'passes contract tests' do
connection.get '/api/ping'
expect(Pacto).to_not have_failed_investigations
expect(Pacto).to_not have_unmatched_requests
end
end
```
There are also some matchers for collaboration testing, so you can make sure each scenario is
calling the expected services and sending the right type of data.
```rb
describe Faraday do
let(:connection) { described_class.new(url: 'http://localhost:5000') }
before(:each) do
connection.get '/api/ping'
connection.post do |req|
req.url '/api/echo'
req.headers['Content-Type'] = 'application/json'
req.body = '{"foo": "bar"}'
end
end
it 'calls the ping service' do
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping').against_contract('Ping')
end
it 'sends data to the echo service' do
expect(Pacto).to have_investigated('Ping').with_response(body: hash_including('ping' => 'pong - from the example!'))
expect(Pacto).to have_investigated('Echo').with_request(body: hash_including('foo' => 'bar'))
echoed_body = { 'foo' => 'bar' }
expect(Pacto).to have_investigated('Echo').with_request(body: echoed_body).with_response(body: echoed_body)
end
end
```
================================================
FILE: docs/generation.md
================================================
Some generation related [configuration](configuration.rb).
```rb
require 'pacto'
WebMock.allow_net_connect!
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
WebMock.allow_net_connect!
```
Once we call `Pacto.generate!`, Pacto will record contracts for all requests it detects.
```rb
Pacto.generate!
```
Now, if we run any code that makes an HTTP call (using an
[HTTP library supported by WebMock](https://github.com/bblimke/webmock#supported-http-libraries))
then Pacto will generate a Contract based on the HTTP request/response.
This code snippet will generate a Contract and save it to `contracts/samples/contracts/localhost/api/ping.json`.
```rb
require 'faraday'
conn = Faraday.new(url: 'http://localhost:5000')
response = conn.get '/api/ping'
```
We're getting back real data from GitHub, so this should be the actual file encoding.
```rb
puts response.body
```
The generated contract will contain expectations based on the request/response we observed,
including a best-guess at an appropriate json-schema. Our heuristics certainly aren't foolproof,
so you might want to customize schema!
Here's another sample that sends a post request.
```rb
conn.post do |req|
req.url '/api/echo'
req.headers['Content-Type'] = 'application/json'
req.body = '{"red fish": "blue fish"}'
end
```
You can provide hints to Pacto to help it generate contracts. For example, Pacto doesn't have
a good way to know a good name and correct URI template for the service. That means that Pacto
will not know if two similar requests are for the same service or two different services, and
will be forced to give names based on the URI that are not good display names.
The hint below tells Pacto that requests to http://localhost:5000/album/1/cover and http://localhost:5000/album/2/cover
are both going to the same service, which is known as "Get Album Cover". This hint will cause Pacto to
generate a Contract for "Get Album Cover" and save it to `contracts/get_album_cover.json`, rather than two
contracts that are stored at `contracts/localhost/album/1/cover.json` and `contracts/localhost/album/2/cover.json`.
```rb
Pacto::Generator.configure do |c|
c.hint 'Get Album Cover', http_method: :get, host: 'http://localhost:5000', path: '/api/album/{id}/cover'
end
conn.get '/api/album/1/cover'
conn.get '/api/album/2/cover'
```
================================================
FILE: docs/rake_tasks.md
================================================
# Rake tasks
## This is a test!
[That](www.google.com) markdown works
```sh
bundle exec rake pacto:meta_validate['contracts']
bundle exec rake pacto:validate['http://localhost:5000','contracts']
```
================================================
FILE: docs/rspec.md
================================================
================================================
FILE: docs/samples.md
================================================
# Overview
Welcome to the Pacto usage samples!
This document gives a quick overview of the main features.
You can browse the Table of Contents (upper right corner) to view additional samples.
In addition to this document, here are some highlighted samples:
<ul>
<li><a href="configuration">Configuration</a>: Shows all available configuration options</li>
<li><a href="generation">Generation</a>: More details on generation</li>
<li><a href="rspec">RSpec</a>: More samples for RSpec expectations</li>
</ul>
You can also find other samples using the Table of Content (upper right corner), including sample contracts.
# Getting started
Once you've installed the Pacto gem, you just require it. If you want, you can also require the Pacto rspec expectations.
```rb
require 'pacto'
require 'pacto/rspec'
```
Pacto will disable live connections, so you will get an error if
your code unexpectedly calls an service that was not stubbed. If you
want to re-enable connections, run `WebMock.allow_net_connect!`
```rb
WebMock.allow_net_connect!
```
Pacto can be configured via a block. The `contracts_path` option tells Pacto where it should load or save contracts. See the [Configuration](configuration.html) for all the available options.
```rb
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
```
# Generating a Contract
Calling `Pacto.generate!` enables contract generation.
Pacto.generate!
Now, if we run any code that makes an HTTP call (using an
[HTTP library supported by WebMock](https://github.com/bblimke/webmock#supported-http-libraries))
then Pacto will generate a Contract based on the HTTP request/response.
We're using the sample APIs in the sample_apis directory.
```rb
require 'faraday'
conn = Faraday.new(url: 'http://localhost:5000')
response = conn.get '/api/ping'
```
This is the real request, so you should see {"ping":"pong"}
```rb
puts response.body
```
# Testing providers by simulating consumers
The generated contract will contain expectations based on the request/response we observed,
including a best-guess at an appropriate json-schema. Our heuristics certainly aren't foolproof,
so you might want to modify the output!
We can load the contract and validate it, by sending a new request and making sure
the response matches the JSON schema. Obviously it will pass since we just recorded it,
but if the service has made a change, or if you alter the contract with new expectations,
then you will see a contract investigation message.
```rb
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
contracts.simulate_consumers
```
# Stubbing providers for consumer testing
We can also use Pacto to stub the service based on the contract.
```rb
contracts.stub_providers
```
The stubbed data won't be very realistic, the default behavior is to return the simplest data
that complies with the schema. That basically means that you'll have "bar" for every string.
```rb
response = conn.get '/api/ping'
```
You're now getting stubbed data. You should see {"ping":"bar"} unless you recorded with
the `defaults` option enabled, in which case you will still seee {"ping":"pong"}.
```rb
puts response.body
```
# Collaboration tests with RSpec
Pacto comes with rspec matchers
```rb
require 'pacto/rspec'
```
It's probably a good idea to reset Pacto between each rspec scenario
```rb
RSpec.configure do |c|
c.after(:each) { Pacto.clear! }
end
```
Load your contracts, and stub them if you'd like.
```rb
Pacto.load_contracts('contracts', 'http://localhost:5000').stub_providers
```
You can turn on investigation mode so Pacto will detect and validate HTTP requests.
```rb
Pacto.validate!
describe 'my_code' do
it 'calls a service' do
conn = Faraday.new(url: 'http://localhost:5000')
response = conn.get '/api/ping'
```
The have_validated matcher makes sure that Pacto received and successfully validated a request
```rb
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping')
end
end
```
================================================
FILE: docs/server.md
================================================
```rb
require 'pacto/rspec'
require 'pacto/test_helper'
describe 'ping service' do
include Pacto::TestHelper
it 'pongs' do
with_pacto(
port: 6000,
backend_host: 'http://localhost:5000',
live: true,
stub: false,
generate: false,
directory: 'contracts'
) do |pacto_endpoint|
```
call your code
```rb
system "curl #{pacto_endpoint}/api/ping"
end
```
check citations
```rb
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping')
end
end
```
================================================
FILE: docs/server_cli.md
================================================
# Standalone server
You can run Pacto as a server in order to test non-Ruby projects. In order to get the full set
of options, run:
```sh
bundle exec pacto-server -h
```
You probably want to run with the -sv option, which will display verbose output to stdout. You can
run server that proxies to a live endpoint:
```sh
bundle exec pacto-server -sv --port 9000 --live http://example.com &
bundle exec pacto-server -sv --port 9001 --stub &
pkill -f pacto-server
```
================================================
FILE: docs/stenographer.md
================================================
```rb
require 'pacto'
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
contracts.stub_providers
Pacto.simulate_consumer do
request 'Echo', values: nil, response: { status: 200 } # 0 contract violations
request 'Ping', values: nil, response: { status: 200 } # 0 contract violations
request 'Unknown (http://localhost:8000/404)', values: nil, response: { status: 500 } # 0 contract violations
end
Pacto.simulate_consumer :my_consumer do
playback 'pacto_stenographer.log'
end
```
================================================
FILE: features/configuration/strict_matchers.feature
================================================
Feature: Strict Matching
By default, Pacto matches requests to contracts (and stubs) using exact request paths, parameters, and headers. This strict behavior is useful for Consumer-Driven Contracts.
You can use less strict matching so the same contract can match multiple similar requests. This is useful for matching contracts with resource identifiers in the path. Any placeholder in the path (like :id in /beers/:id) is considered a resource identifier and will match any alphanumeric combination.
You can change the default behavior to the behavior that allows placeholders and ignores headers or request parameters by setting the strict_matchers configuration option:
```ruby
Pacto.configure do |config|
config.strict_matchers = false
end
```
Background:
Given a file named "contracts/hello_contract.json" with:
"""json
{
"request": {
"http_method": "GET",
"path": "/hello/{id}",
"headers": {
"Accept": "application/json"
},
"params": {}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"schema": {
"type": "object",
"required": true,
"properties": {
"message": { "type": "string", "required": true, "default": "Hello, world!" }
}
}
}
}
"""
Given a file named "requests.rb" with:
"""ruby
require 'pacto'
strict = ARGV[0] == "true"
puts "Pacto.configuration.strict_matchers = #{strict}"
puts
Pacto.configure do |config|
config.strict_matchers = strict
end
Pacto.load_contracts('contracts', 'http://dummyprovider.com').stub_providers
def response url, headers
begin
response = Faraday.get(url) do |req|
req.headers = headers[:headers]
end
response.body
rescue WebMock::NetConnectNotAllowedError => e
e.class
end
end
print 'Exact: '
puts response URI.encode('http://dummyprovider.com/hello/{id}'), headers: {'Accept' => 'application/json' }
print 'Wrong headers: '
puts response 'http://dummyprovider.com/hello/123', headers: {'Content-Type' => 'application/json' }
print 'ID placeholder: '
puts response 'http://dummyprovider.com/hello/123', headers: {'Accept' => 'application/json' }
"""
Scenario: Default (strict) behavior
When I run `bundle exec ruby requests.rb true`
Then the stdout should contain:
"""
Pacto.configuration.strict_matchers = true
Exact: {"message":"Hello, world!"}
Wrong headers: WebMock::NetConnectNotAllowedError
ID placeholder: {"message":"Hello, world!"}
"""
Scenario: Non-strict matching
When I run `bundle exec ruby requests.rb false`
Then the stdout should contain:
"""
Pacto.configuration.strict_matchers = false
Exact: {"message":"Hello, world!"}
Wrong headers: {"message":"Hello, world!"}
ID placeholder: {"message":"Hello, world!"}
"""
================================================
FILE: features/evolve/README.md
================================================
## Consumer-Driven Contract Recommendations
If you are using Pacto for Consumer-Driven Contracts, we recommend avoiding the advanced features so you'll test with the strictest Contract possible. We recommend:
- Do not use templating, let Pacto use the json-generator
- Use strict request matching
- Use multiple contracts for the same service to capture attributes that are required in some situations but not others
The host address is intentionally left out of the request specification so that we can validate a contract against any provider.
It also reinforces the fact that a contract defines the expectation of a consumer, and not the implementation of any specific provider.
================================================
FILE: features/evolve/existing_services.feature
================================================
Feature: Existing services journey
Scenario: Generating the contracts
Given I have a Rakefile
Given an existing set of services
When I execute:
"""ruby
require 'pacto'
Pacto.configure do | c |
c.contracts_path = 'contracts'
end
Pacto.generate!
Faraday.get 'http://www.example.com/service1'
Faraday.get 'http://www.example.com/service2'
"""
Then the following files should exist:
| contracts/www.example.com/service1.json |
| contracts/www.example.com/service2.json |
@no-clobber
Scenario: Ensuring all contracts are valid
When I successfully run `bundle exec rake pacto:meta_validate['contracts']`
Then the stdout should contain exactly:
"""
validated contracts/www.example.com/service1.json
validated contracts/www.example.com/service2.json
All contracts successfully meta-validated
"""
# TODO: find where Webmock is being called with and an empty :with
# and update it, to not use with so we can upgrade Webmock past 1.20.2
@no-clobber
Scenario: Stubbing with the contracts
Given a file named "test.rb" with:
"""ruby
require 'pacto'
Pacto.configure do | c |
c.contracts_path = 'contracts'
end
contracts = Pacto.load_contracts('contracts/www.example.com/', 'http://www.example.com')
contracts.stub_providers
puts Faraday.get('http://www.example.com/service1').body
puts Faraday.get('http://www.example.com/service2').body
"""
When I successfully run `bundle exec ruby test.rb`
Then the stdout should contain exactly:
"""
{"thoughtworks":"pacto"}
{"service2":["thoughtworks","pacto"]}
"""
@no-clobber
Scenario: Expecting a change
When I make replacements in "contracts/www.example.com/service1.json":
| pattern | replacement |
| string | integer |
When I execute:
"""ruby
require 'pacto'
Pacto.stop_generating!
Pacto.configure do | c |
c.contracts_path = 'contracts'
end
Pacto.load_contracts('contracts', 'http://www.example.com').stub_providers
Pacto.validate!
Faraday.get 'http://www.example.com/service1'
Faraday.get 'http://www.example.com/service2'
"""
Then the stdout should contain exactly:
"""
"""
================================================
FILE: features/generate/README.md
================================================
We know - json-schema can get pretty verbose! It's a powerful tool, but writing the entire Contract by hand for a complex service is a painstaking task. We've created a simple generator to speed this process up. You can invoke it programmatically, or with the provided rake task.
The basic generate we've bundled with Pacto completes partially defined Contracts - Contracts that have a request defined but no response. We haven't bundled any other generates, but you could use the API to generate from other sources, like existing [VCR](https://github.com/vcr/vcr) cassettes, [apiblueprint](http://apiblueprint.org/), or [WADL](https://wadl.java.net/). If you want some help or ideas, try the [pacto mailing-list](https://groups.google.com/forum/#!forum/pacto-gem).
Note: Request headers are only recorded if they are in the response's [Vary header](http://www.subbu.org/blog/2007/12/vary-header-for-restful-applications), so make sure your services return a proper Vary for best results!
================================================
FILE: features/generate/generation.feature
================================================
@needs_server
Feature: Contract Generation
We know - json-schema can get pretty verbose! It's a powerful tool, but writing the entire Contract by hand for a complex service is a painstaking task. We've created a simple generator to speed this process up. You can invoke it programmatically, or with the provided rake task.
You just need to create a partial Contract that only describes the request. The generator will then execute the request, and use the response to generate a full Contract.
Remember, we only record request headers if they are in the response's [Vary header](http://www.subbu.org/blog/2007/12/vary-header-for-restful-applications), so make sure your services return a proper Vary for best results!
Background:
Given a file named "requests/my_contract.json" with:
"""
{
"request": {
"http_method": "GET",
"path": "/api/ping",
"headers": {
"Accept": "application/json"
}
},
"response": {
"status": 200,
"schema": {
"required": true
}
}
}
"""
Scenario: Generating a contract using the rake task
Given a directory named "contracts"
When I successfully run `bundle exec rake pacto:generate['tmp/aruba/requests','tmp/aruba/contracts','http://localhost:5000']`
Then the stdout should contain "Successfully generated all contracts"
Scenario: Generating a contract programmatically
Given a file named "generate.rb" with:
"""ruby
require 'pacto'
Pacto.configuration.generator_options[:no_examples] = true
WebMock.allow_net_connect!
generator = Pacto::Generator.contract_generator
contract = generator.generate_from_partial_contract('requests/my_contract.json', 'http://localhost:5000')
puts contract
"""
When I successfully run `bundle exec ruby generate.rb`
Then the stdout should match this contract:
"""json
{
"name": "/api/ping",
"request": {
"headers": {
},
"http_method": "get",
"path": "/api/ping"
},
"response": {
"headers": {
"Content-Type": "application/json"
},
"status": 200,
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"description": "Generated from requests/my_contract.json with shasum 210fa3b144ef2db8d1c160c4d9e8d8bf738ed851",
"type": "object",
"required": true,
"properties": {
"ping": {
"type": "string",
"required": true
}
}
}
}
}
"""
================================================
FILE: features/steps/pacto_steps.rb
================================================
# -*- encoding : utf-8 -*-
Given(/^Pacto is configured with:$/) do |string|
steps %(
Given a file named "pacto_config.rb" with:
"""ruby
#{string}
"""
)
end
Given(/^I have a Rakefile$/) do
steps %(
Given a file named "Rakefile" with:
"""ruby
require 'pacto/rake_task'
"""
)
end
When(/^I request "(.*?)"$/) do |url|
steps %{
Given a file named "request.rb" with:
"""ruby
require 'pacto'
require_relative 'pacto_config'
require 'faraday'
resp = Faraday.get('#{url}') do |req|
req.headers = { 'Accept' => 'application/json' }
end
puts resp.body
"""
When I run `bundle exec ruby request.rb`
}
end
Given(/^an existing set of services$/) do
WebMock.stub_request(:get, 'www.example.com/service1').to_return(body: { 'thoughtworks' => 'pacto' }.to_json)
WebMock.stub_request(:post, 'www.example.com/service1').with(body: 'thoughtworks').to_return(body: 'pacto')
WebMock.stub_request(:get, 'www.example.com/service2').to_return(body: { 'service2' => %w(thoughtworks pacto) }.to_json)
WebMock.stub_request(:post, 'www.example.com/service2').with(body: 'thoughtworks').to_return(body: 'pacto')
end
When(/^I execute:$/) do |script|
FileUtils.mkdir_p 'tmp/aruba'
Dir.chdir 'tmp/aruba' do
begin
script = <<-eof
require 'stringio'
begin $stdout = StringIO.new
#{ script }
$stdout.string
ensure
$stdout = STDOUT
end
eof
eval(script) # rubocop:disable Eval
# It's just for testing...
rescue SyntaxError => e
raise e
end
end
end
When(/^I make replacements in "([^"]*)":$/) do |file_name, replacements|
Dir.chdir 'tmp/aruba' do
content = File.read file_name
replacements.rows.each do | pattern, replacement |
content.gsub! Regexp.new(pattern), replacement
end
File.open(file_name, 'w') { |file| file.write content }
end
end
Then(/^the stdout should match this contract:$/) do |expected_contract|
actual_contract = all_stdout
expect(actual_contract).to be_json_eql(expected_contract).excluding('description')
end
================================================
FILE: features/stub/README.md
================================================
You can write your own stubs and use Pacto to [validate](https://www.relishapp.com/maxlinc/pacto/docs/validate) them, or you can just let Pacto create stubs for you.
================================================
FILE: features/stub/templates.feature
================================================
Feature: Templating
If you want to create more dynamic stubs, you can use Pacto templating. Currently the only supported templating mechanism is to use ERB in the "default" attributes of the json-schema.
Background:
Given Pacto is configured with:
"""ruby
Pacto.configure do |c|
c.register_hook Pacto::Hooks::ERBHook.new
end
Pacto.load_contracts('contracts', 'http://example.com').stub_providers
"""
Given a file named "contracts/template.json" with:
"""json
{
"request": {
"http_method": "GET",
"path": "/hello",
"headers": {
"Accept": "application/json"
},
"params": {}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"schema": {
"type": "object",
"required": true,
"properties": {
"message": { "type": "string", "required": true,
"default": "<%= 'Hello, world!'.reverse %>"
}
}
}
}
}
"""
Scenario: ERB Template
When I request "http://example.com/hello"
Then the stdout should contain:
"""
{"message":"!dlrow ,olleH"}
"""
================================================
FILE: features/support/env.rb
================================================
# -*- encoding : utf-8 -*-
require_relative '../../spec/coveralls_helper'
require 'aruba'
require 'aruba/cucumber'
require 'json_spec/cucumber'
require 'aruba/jruby' if RUBY_PLATFORM == 'java'
require 'pacto/test_helper'
Pacto.configuration.hide_deprecations = true
Before do
# Given I successfully run `bundle install` can take a while.
@aruba_timeout_seconds = RUBY_PLATFORM == 'java' ? 60 : 10
end
class PactoWorld
include Pacto::TestHelper
end
World do
PactoWorld.new
end
Around do | _scenario, block |
WebMock.allow_net_connect!
world = self || PactoWorld.new
world.with_pacto(port: 8000, live: true, backend_host: 'http://localhost:5000') do
block.call
end
end
================================================
FILE: features/validate/README.md
================================================
You can use Pacto to do Integration Contract Testing - making sure your service and any test double that simulates the service are similar. If you generate your Contracts from documentation, you can be fairly confident that all three - live services, test doubles, and documentation - are in sync.
================================================
FILE: features/validate/meta_validation.feature
================================================
Feature: Meta-validation
Meta-validation ensures that a Contract file matches the Contract format. It does not validation actual responses, just the Contract itself.
You can easily do meta-validation with the Rake task pacto:meta_validate, or programmatically.
Background:
Given a file named "contracts/my_contract.json" with:
"""
{
"request": {
"http_method": "GET",
"path": "/hello_world",
"headers": {
"Accept": "application/json"
},
"params": {}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"schema": {
"description": "A simple response",
"type": "object",
"required": ["message"],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
"""
Scenario: Meta-validation via a rake task
When I successfully run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
Then the stdout should contain "All contracts successfully meta-validated"
# The tests from here down should probably be specs instead of relish
Scenario: Meta-validation of an invalid contract
Given a file named "contracts/my_contract.json" with:
"""
{"request": "yes"}
"""
When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
Then the exit status should be 1
And the stdout should contain "did not match the following type"
Scenario: Meta-validation of a contract with empty request and response
Given a file named "contracts/my_contract.json" with:
"""
{"request": {}, "response": {}}
"""
When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
Then the exit status should be 1
And the stdout should contain "did not contain a required property"
Scenario: Meta-validation of a contracts response body
Given a file named "contracts/my_contract.json" with:
"""
{
"request": {
"http_method": "GET",
"path": "/hello_world"
},
"response": {
"status": 200,
"schema": {
"required": "anystring"
}
}
}
"""
When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
Then the exit status should be 1
And the stdout should contain "did not match the following type"
================================================
FILE: features/validate/validation.feature
================================================
Feature: Validation
Validation ensures that a external service conform to a Contract.
Scenario: Validation via a rake task
Given a file named "contracts/simple_contract.json" with:
"""
{
"request": {
"http_method": "GET",
"path": "/api/hello",
"headers": { "Accept": "application/json" },
"params": {}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"schema": {
"description": "A simple response",
"type": "object",
"properties": {
"message": { "type": "string" }
}
}
}
}
"""
When I successfully run `bundle exec rake pacto:validate['http://localhost:5000','tmp/aruba/contracts/simple_contract.json']`
Then the stdout should contain:
""""
Validating contracts against host http://localhost:5000
OK! simple_contract.json
1 valid contract
"""
================================================
FILE: lib/pacto/actor.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class Actor
end
end
================================================
FILE: lib/pacto/actors/from_examples.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Actors
class FirstExampleSelector
def self.select(examples, _values)
Hashie::Mash.new examples.values.first
end
end
class RandomExampleSelector
def self.select(examples, _values)
Hashie::Mash.new examples.values.sample
end
end
class NamedExampleSelector
def self.select(examples, values)
name = values[:example_name]
if name.nil?
RandomExampleSelector.select(examples, values)
else
Hashie::Mash.new examples[name]
end
end
end
class FromExamples < Actor
def initialize(fallback_actor = JSONGenerator.new, selector = Pacto::Actors::FirstExampleSelector)
@fallback_actor = fallback_actor
@selector = selector
end
def build_request(contract, values = {})
request_values = (values || {}).dup
if contract.examples?
example = @selector.select(contract.examples, values)
data = contract.request.to_hash
request_values.merge! example_uri_values(contract)
data['uri'] = contract.request.uri(request_values)
data['body'] = example.request.body
data['method'] = contract.request.http_method
Pacto::PactoRequest.new(data)
else
@fallback_actor.build_request contract, values
end
end
def build_response(contract, values = {})
if contract.examples?
example = @selector.select(contract.examples, values)
data = contract.response.to_hash
data['body'] = example.response.body
Pacto::PactoResponse.new(data)
else
@fallback_actor.build_response contract, values
end
end
def example_uri_values(contract)
uri_template = contract.request.pattern.uri_template
if contract.examples && contract.examples.values.first[:request][:uri]
example_uri = contract.examples.values.first[:request][:uri]
uri_template.extract example_uri
else
{}
end
end
end
end
end
================================================
FILE: lib/pacto/actors/json_generator.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Actors
class JSONGenerator < Actor
def build_request(contract, values = {})
data = contract.request.to_hash
data['uri'] = contract.request.uri(values)
data['body'] = JSON::Generator.generate(data['schema']) if data['schema']
data['method'] = contract.request.http_method
Pacto::PactoRequest.new(data)
end
def build_response(contract, _values = {})
data = contract.response.to_hash
data['body'] = JSON::Generator.generate(data['schema'])
Pacto::PactoResponse.new(data)
end
end
end
end
================================================
FILE: lib/pacto/body_parsing.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Handlers
autoload :JSONHandler, 'pacto/handlers/json_handler'
autoload :TextHandler, 'pacto/handlers/text_handler'
autoload :XMLHandler, 'pacto/handlers/xml_handler'
end
module BodyParsing
def raw_body
return nil if body.nil?
return body if body.respond_to? :to_str
body_handler.raw(body)
end
def parsed_body
return nil if body.nil?
body_handler.parse(body)
end
def content_type
headers['Content-Type']
end
def body_handler
case content_type
when /\bjson$/
Pacto::Handlers::JSONHandler
when /\btext$/
Pacto::Handlers::TextHandler
# No XML support - yet
# when /\bxml$/
# XMLHandler
else
# JSON is still the default
Pacto::Handlers::JSONHandler
end
end
end
end
================================================
FILE: lib/pacto/cli/helpers.rb
================================================
module Pacto
module CLI
module Helpers
def each_contract(*contracts)
[*contracts].each do |contract|
if File.file? contract
yield contract
else # Should we assume it's a dir, or also support glob patterns?
contracts = Dir[File.join(contract, '**/*{.json.erb,.json}')]
fail Pacto::UI.colorize("No contracts found in directory #{contract}", :yellow) if contracts.empty?
contracts.sort.each do |contract_file|
yield contract_file
end
end
end
end
end
end
end
================================================
FILE: lib/pacto/cli.rb
================================================
require 'thor'
require 'pacto'
require 'pacto/cli/helpers'
module Pacto
module CLI
class Main < Thor
include Pacto::CLI::Helpers
desc 'meta_validate [CONTRACTS...]', 'Validates a directory of contract definitions'
def meta_validate(*contracts)
invalid = []
each_contract(*contracts) do |contract_file|
begin
Pacto.validate_contract(contract_file)
say_status :validated, contract_file
rescue InvalidContract => exception
invalid << contract_file
shell.say_status :invalid, contract_file, :red
exception.errors.each do |error|
say set_color(" Error: #{error}", :red)
end
end
end
abort "The following contracts were invalid: #{invalid.join(',')}" unless invalid.empty?
say 'All contracts successfully meta-validated'
end
desc 'validate [CONTRACTS...]', 'Validates all contracts in a given directory against a given host'
method_option :host, type: :string, desc: 'Override host in contracts for validation'
def validate(*contracts)
host = options[:host]
WebMock.allow_net_connect!
banner = 'Validating contracts'
banner << " against host #{host}" unless host.nil?
say banner
invalid_contracts = []
tested_contracts = []
each_contract(*contracts) do |contract_file|
tested_contracts << contract_file
invalid_contracts << contract_file unless contract_is_valid?(contract_file, host)
end
validation_summary(tested_contracts, invalid_contracts)
end
private
def validation_summary(contracts, invalid_contracts)
if invalid_contracts.empty?
say set_color("#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}", :green)
else
abort set_color("#{invalid_contracts.size} of #{contracts.size} failed. Check output for detailed error messages.", :red)
end
end
def contract_is_valid?(contract_file, host)
name = File.split(contract_file).last
contract = Pacto.load_contract(contract_file, host)
investigation = contract.simulate_request
if investigation.successful?
say_status 'OK!', name
true
else
say_status 'FAILED!', name, :red
say set_color(investigation.summary, :red)
say set_color(investigation.to_s, :red)
false
end
end
end
end
end
================================================
FILE: lib/pacto/consumer/faraday_driver.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class Consumer
class FaradayDriver
include Pacto::Logger
# Sends a Pacto::PactoRequest
def execute(req)
conn_options = { url: req.uri.site }
conn_options[:proxy] = Pacto.configuration.proxy if Pacto.configuration.proxy
conn = Faraday.new(conn_options) do |faraday|
faraday.response :logger if Pacto.configuration.logger.level == :debug
faraday.adapter Faraday.default_adapter
end
response = conn.send(req.method) do |faraday_request|
faraday_request.url(req.uri.path, req.uri.query_values)
faraday_request.headers = req.headers
faraday_request.body = req.raw_body
end
faraday_to_pacto_response response
end
private
# This belongs in an adapter
def faraday_to_pacto_response(faraday_response)
data = {
status: faraday_response.status,
headers: faraday_response.headers,
body: faraday_response.body
}
Pacto::PactoResponse.new(data)
end
end
end
end
================================================
FILE: lib/pacto/consumer.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
def self.consumers
@consumers ||= {}
end
def self.simulate_consumer(consumer_name = :consumer, &block)
consumers[consumer_name] ||= Consumer.new(consumer_name)
consumers[consumer_name].simulate(&block)
end
class Consumer
include Logger
include Resettable
def initialize(name = :consumer)
@name = name
end
def simulate(&block)
instance_eval(&block)
end
def playback(stenographer_script)
script = File.read(stenographer_script)
instance_eval script, stenographer_script
end
def self.reset!
Pacto.consumers.clear
end
def actor
@actor ||= Pacto::Actors::FromExamples.new
end
def actor=(actor)
fail ArgumentError, 'The actor must respond to :build_request' unless actor.respond_to? :build_request
@actor = actor
end
def request(contract, data = {})
contract = Pacto.contract_registry.find_by_name(contract) if contract.is_a? String
logger.info "Sending request to #{contract.name.inspect}"
logger.info " with #{data.inspect}"
reenact(contract, data)
rescue ContractNotFound => e
logger.warn "Ignoring request: #{e.message}"
end
def reenact(contract, data = {})
request = build_request contract, data
response = driver.execute request
[request, response]
end
# Returns the current driver
def driver
@driver ||= Pacto::Consumer::FaradayDriver.new
end
# Changes the driver
def driver=(driver)
fail ArgumentError, 'The driver must respond to :execute' unless driver.respond_to? :execute
@driver = driver
end
# @api private
def build_request(contract, data = {})
actor.build_request(contract, data[:values]).tap do |request|
if data[:headers] && data[:headers].respond_to?(:call)
request.headers = data[:headers].call(request.headers)
end
if data[:body] && data[:body].respond_to?(:call)
request.body = data[:body].call(request.body)
end
end
end
end
end
================================================
FILE: lib/pacto/contract.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Contract
include Logger
attr_reader :id
attr_reader :file
attr_reader :request
attr_reader :response
attr_reader :values
attr_reader :examples
attr_reader :name
attr_writer :adapter
attr_writer :consumer
attr_writer :provider
def adapter
@adapter ||= Pacto.configuration.adapter
end
def consumer
@consumer ||= Pacto.configuration.default_consumer
end
def provider
@provider ||= Pacto.configuration.default_provider
end
def examples?
examples && !examples.empty?
end
def stub_contract!(values = {})
self.values = values
adapter.stub_request!(self)
end
def simulate_request
pacto_request, pacto_response = execute
validate_response pacto_request, pacto_response
end
# Should this be deprecated?
def validate_response(request, response)
Pacto::Cops.perform_investigation request, response, self
end
def matches?(request_signature)
request_pattern.matches? request_signature
end
def request_pattern
request.pattern
end
def response_for(pacto_request)
provider.response_for self, pacto_request
end
def execute(additional_values = {})
# FIXME: Do we really need to store on the Contract, or just as a param for #stub_contact! and #execute?
full_values = values.merge(additional_values)
consumer.reenact(self, full_values)
end
end
end
================================================
FILE: lib/pacto/contract_factory.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class ContractFactory
include Singleton
include Logger
def initialize
@factories = {}
end
def add_factory(format, factory)
@factories[format.to_sym] = factory
end
def remove_factory(format)
@factories.delete format
end
def build(contract_files, host, format = :legacy)
factory = @factories[format.to_sym]
fail "No Contract factory registered for #{format}" if factory.nil?
contract_files.map { |file| factory.build_from_file(file, host) }.flatten
end
def load_contracts(contracts_path, host, format = :legacy)
factory = @factories[format.to_sym]
files = factory.files_for(contracts_path)
contracts = ContractFactory.build(files, host, format)
contracts
end
class << self
extend Forwardable
def_delegators :instance, *ContractFactory.instance_methods(false)
end
end
end
require 'pacto/formats/legacy/contract_factory'
require 'pacto/formats/swagger/contract_factory'
================================================
FILE: lib/pacto/contract_files.rb
================================================
# -*- encoding : utf-8 -*-
require 'pathname'
module Pacto
class ContractFiles
def self.for(path)
full_path = Pathname.new(path).realpath
if full_path.directory?
all_json_files = "#{full_path}/**/*.json"
Dir.glob(all_json_files).map do |f|
Pathname.new(f)
end
else
[full_path]
end
end
end
end
================================================
FILE: lib/pacto/contract_set.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class ContractSet < Set
def stub_providers(values = {})
each { |contract| contract.stub_contract!(values) }
end
def simulate_consumers
map(&:simulate_request)
end
end
end
================================================
FILE: lib/pacto/cops/body_cop.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
class BodyCop
KNOWN_CLAUSES = [:request, :response]
def self.validates(clause)
fail ArgumentError, "Unknown clause: #{clause}" unless KNOWN_CLAUSES.include? clause
@clause = clause
end
def self.investigate(request, response, contract)
# eval "is a security risk" and local_variable_get is ruby 2.1+ only, so...
body = { request: request, response: response }[@clause].body
schema = contract.send(@clause).schema
if schema && !schema.empty?
schema['id'] = contract.file unless schema.key? 'id'
JSON::Validator.fully_validate(schema, body, version: :draft3)
end || []
end
end
end
end
================================================
FILE: lib/pacto/cops/request_body_cop.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
class RequestBodyCop < BodyCop
validates :request
end
end
end
Pacto::Cops.register_cop Pacto::Cops::RequestBodyCop
================================================
FILE: lib/pacto/cops/response_body_cop.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
class ResponseBodyCop < BodyCop
validates :response
end
end
end
Pacto::Cops.register_cop Pacto::Cops::ResponseBodyCop
================================================
FILE: lib/pacto/cops/response_header_cop.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
class ResponseHeaderCop
def self.investigate(_request, response, contract)
expected_headers = contract.response.headers
actual_headers = response.headers
actual_headers = Pacto::Extensions.normalize_header_keys actual_headers
headers_to_validate = Pacto::Extensions.normalize_header_keys expected_headers
headers_to_validate.map do |expected_header, expected_value|
if actual_headers.key? expected_header
actual_value = actual_headers[expected_header]
HeaderValidatorMap[expected_header].call(expected_value, actual_value)
else
"Missing expected response header: #{expected_header}"
end
end.compact
end
private
HeaderValidatorMap = Hash.new do |_map, key|
proc do |expected_value, actual_value|
unless expected_value.eql? actual_value
"Invalid response header #{key}: expected #{expected_value.inspect} but received #{actual_value.inspect}"
end
end
end
HeaderValidatorMap['Location'] = proc do |expected_value, actual_value|
location_template = Addressable::Template.new(expected_value)
if location_template.match(Addressable::URI.parse(actual_value))
nil
else
"Invalid response header Location: expected URI #{actual_value} to match URI Template #{location_template.pattern}"
end
end
end
end
end
Pacto::Cops.register_cop Pacto::Cops::ResponseHeaderCop
================================================
FILE: lib/pacto/cops/response_status_cop.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
class ResponseStatusCop
def self.investigate(_request, response, contract)
expected_status = contract.response.status
actual_status = response.status
errors = []
if expected_status != actual_status
errors << "Invalid status: expected #{expected_status} but got #{actual_status}"
end
errors
end
end
end
end
Pacto::Cops.register_cop Pacto::Cops::ResponseStatusCop
================================================
FILE: lib/pacto/cops.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Cops
extend Pacto::Resettable
class << self
def reset!
@active_cops = nil
end
def register_cop(cop)
fail TypeError "#{cop} does not respond to investigate" unless cop.respond_to? :investigate
registered_cops << cop
end
def registered_cops
@registered_cops ||= Set.new
end
def active_cops
@active_cops ||= registered_cops.dup
end
def investigate(request_signature, pacto_response)
return unless Pacto.validating?
contract = Pacto.contracts_for(request_signature).first
if contract
investigation = perform_investigation request_signature, pacto_response, contract
else
investigation = Investigation.new request_signature, pacto_response
end
Pacto::InvestigationRegistry.instance.register_investigation investigation
end
def perform_investigation(request, response, contract)
citations = []
active_cops.map do | cop |
citations.concat cop.investigate(request, response, contract)
end
Investigation.new(request, response, contract, citations.compact)
end
end
end
end
================================================
FILE: lib/pacto/core/configuration.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class Configuration
attr_accessor :adapter, :strict_matchers,
:contracts_path, :logger, :generator_options,
:hide_deprecations, :default_consumer, :default_provider,
:stenographer_log_file, :color, :proxy
attr_reader :hook
def initialize # rubocop:disable Metrics/MethodLength
@middleware = Pacto::Core::HTTPMiddleware.new
@middleware.add_observer Pacto::Cops, :investigate
@generator = Pacto::Generator.contract_generator
@middleware.add_observer @generator, :generate
@default_consumer = Pacto::Consumer.new
@default_provider = Pacto::Provider.new
@adapter = Stubs::WebMockAdapter.new(@middleware)
@strict_matchers = true
@contracts_path = '.'
@hook = Hook.new {}
@generator_options = { schema_version: 'draft3' }
@color = $stdout.tty?
@proxy = ENV['PACTO_PROXY']
end
def logger
@logger ||= new_simple_logger
end
def stenographer_log_file
@stenographer_log_file ||= File.expand_path('pacto_stenographer.log')
end
def register_hook(hook = nil, &block)
if block_given?
@hook = Hook.new(&block)
else
fail 'Expected a Pacto::Hook' unless hook.is_a? Hook
@hook = hook
end
end
private
def new_simple_logger
Logger::SimpleLogger.instance.tap do | logger |
if ENV['PACTO_DEBUG']
logger.level = :debug
else
logger.level = :default
end
end
end
end
end
================================================
FILE: lib/pacto/core/contract_registry.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class ContractNotFound < StandardError; end
class ContractRegistry < Set
include Logger
def register(contract)
fail ArgumentError, 'expected a Pacto::Contract' unless contract.is_a? Contract
logger.debug "Registering #{contract.request_pattern} as #{contract.name}"
add contract
end
def find_by_name(name)
contract = select { |c| c.name == name }.first
fail ContractNotFound, "No contract found for #{name}" unless contract
contract
end
def contracts_for(request_signature)
select { |c| c.matches? request_signature }
end
end
end
================================================
FILE: lib/pacto/core/hook.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class Hook
def initialize(&block)
@hook = block
end
def process(contracts, request_signature, response)
@hook.call contracts, request_signature, response
end
end
end
================================================
FILE: lib/pacto/core/http_middleware.rb
================================================
# -*- encoding : utf-8 -*-
require 'observer'
module Pacto
module Core
class HTTPMiddleware
include Logger
include Observable
def process(request, response)
contracts = Pacto.contracts_for request
Pacto.configuration.hook.process contracts, request, response
changed
begin
notify_observers request, response
rescue StandardError => e
logger.error Pacto::Errors.formatted_trace(e)
end
end
end
end
end
================================================
FILE: lib/pacto/core/investigation_registry.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class InvestigationRegistry
include Singleton
include Logger
include Resettable
attr_reader :investigations
def initialize
@investigations = []
end
def self.reset!
instance.reset!
end
def reset!
@investigations.clear
@stenographer = nil
end
def validated?(request_pattern)
matched_investigations = @investigations.select do |investigation|
request_pattern.matches? investigation.request
end
matched_investigations unless matched_investigations.empty?
end
def register_investigation(investigation)
@investigations << investigation
stenographer.log_investigation investigation
logger.info "Detected #{investigation.summary}"
logger.debug(investigation.to_s) unless investigation.successful?
investigation
end
def unmatched_investigations
@investigations.select do |investigation|
investigation.contract.nil?
end
end
def failed_investigations
@investigations.select do |investigation|
!investigation.successful?
end
end
protected
def stenographer
@stenographer ||= create_stenographer
end
def create_stenographer
stenographer_log = File.open(Pacto.configuration.stenographer_log_file, 'a+')
Pacto::Observers::Stenographer.new stenographer_log
end
end
end
================================================
FILE: lib/pacto/core/modes.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class << self
def generate!
modes << :generate
end
def stop_generating!
modes.delete :generate
end
def generating?
modes.include? :generate
end
def validate!
modes << :validate
end
def stop_validating!
modes.delete :validate
end
def validating?
modes.include? :validate
end
private
def modes
@modes ||= []
end
end
end
================================================
FILE: lib/pacto/core/pacto_request.rb
================================================
# -*- encoding : utf-8 -*-
require 'hashie/mash'
module Pacto
class PactoRequest
# FIXME: Need case insensitive header lookup, but case-sensitive storage
attr_accessor :headers, :body, :method, :uri
include BodyParsing
def initialize(data)
mash = Hashie::Mash.new data
@headers = mash.headers.nil? ? {} : mash.headers
@body = mash.body
@method = mash[:method]
@uri = mash.uri
normalize
end
def to_hash
{
method: method,
uri: uri,
headers: headers,
body: body
}
end
def to_s
string = Pacto::UI.colorize_method(method)
string << " #{relative_uri}"
string << " with body (#{raw_body.bytesize} bytes)" if raw_body
string
end
def relative_uri
uri.to_s.tap do |s|
s.slice!(uri.normalized_site)
end
end
def normalize
@method = @method.to_s.downcase.to_sym
@uri = @uri.normalize if @uri
end
end
end
================================================
FILE: lib/pacto/core/pacto_response.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class PactoResponse
# FIXME: Need case insensitive header lookup, but case-sensitive storage
attr_accessor :headers, :body, :status, :parsed_body
attr_reader :parsed_body
include BodyParsing
def initialize(data)
mash = Hashie::Mash.new data
@headers = mash.headers.nil? ? {} : mash.headers
@body = mash.body
@status = mash.status.to_i
end
def to_hash
{
status: status,
headers: headers,
body: body
}
end
def to_s
string = "STATUS: #{status}"
string << " with body (#{raw_body.bytesize} bytes)" if raw_body
string
end
end
end
================================================
FILE: lib/pacto/dash.rb
================================================
# -*- encoding : utf-8 -*-
require 'hashie'
module Pacto
class Dash < Hashie::Dash
include Hashie::Extensions::Coercion
include Hashie::Extensions::Dash::IndifferentAccess
end
end
================================================
FILE: lib/pacto/erb_processor.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class ERBProcessor
include Logger
def process(contract, values = {})
erb = ERB.new(contract)
erb_result = erb.result hash_binding(values)
logger.debug "Processed contract: #{erb_result.inspect}"
erb_result
end
private
def hash_binding(values)
namespace = OpenStruct.new(values)
namespace.instance_eval { binding }
end
end
end
================================================
FILE: lib/pacto/errors.rb
================================================
module Pacto
class InvalidContract < ArgumentError
attr_reader :errors
def initialize(errors)
@errors = errors
end
def message
@errors.join "\n"
end
end
module Errors
# Creates an array of strings, representing a formatted exception,
# containing backtrace and nested exception info as necessary, that can
# be viewed by a human.
#
# For example:
#
# ------Exception-------
# Class: Crosstest::StandardError
# Message: Failure starting the party
# ---Nested Exception---
# Class: IOError
# Message: not enough directories for a party
# ------Backtrace-------
# nil
# ----------------------
#
# @param exception [::StandardError] an exception
# @return [Array<String>] a formatted message
def self.formatted_trace(exception)
arr = formatted_exception(exception).dup
last = arr.pop
if exception.respond_to?(:original) && exception.original
arr += formatted_exception(exception.original, 'Nested Exception')
last = arr.pop
end
arr += ['Backtrace'.center(22, '-'), exception.backtrace, last].flatten
arr
end
# Creates an array of strings, representing a formatted exception that
# can be viewed by a human. Thanks to MiniTest for the inspiration
# upon which this output has been designed.
#
# For example:
#
# ------Exception-------
# Class: Crosstest::StandardError
# Message: I have failed you
# ----------------------
#
# @param exception [::StandardError] an exception
# @param title [String] a custom title for the message
# (default: `"Exception"`)
# @return [Array<String>] a formatted message
def self.formatted_exception(exception, title = 'Exception')
[
title.center(22, '-'),
"Class: #{exception.class}",
"Message: #{exception.message}",
''.center(22, '-')
]
end
end
end
================================================
FILE: lib/pacto/extensions.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Extensions
# Adapted from Faraday
HeaderKeyMap = Hash.new do |map, key|
split_char = key.to_s.include?('-') ? '-' : '_'
map[key] = key.to_s.split(split_char). # :user_agent => %w(user agent)
each(&:capitalize!). # => %w(User Agent)
join('-') # => "User-Agent"
end
HeaderKeyMap[:etag] = 'ETag'
def self.normalize_header_keys(headers)
headers.each_with_object({}) do |(key, value), normalized|
normalized[HeaderKeyMap[key]] = value
end
end
end
end
================================================
FILE: lib/pacto/forensics/investigation_filter.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Forensics
class FilterExhaustedError < StandardError
attr_reader :suspects
def initialize(msg, filter, suspects = [])
@suspects = suspects
if filter.respond_to? :description
msg = "#{msg} #{filter.description}"
else
msg = "#{msg} #{filter}"
end
super(msg)
end
end
class InvestigationFilter
# CaseEquality makes sense for some of the rspec matchers and compound matching behavior
# rubocop:disable Style/CaseEquality
attr_reader :investigations, :filtered_investigations
def initialize(investigations, track_suspects = true)
investigations ||= []
@investigations = investigations.dup
@filtered_investigations = @investigations.dup
@track_suspects = track_suspects
end
def with_name(contract_name)
@filtered_investigations.keep_if do |investigation|
return false if investigation.contract.nil?
contract_name === investigation.contract.name
end
self
end
def with_request(request_constraints)
return self if request_constraints.nil?
[:headers, :body].each do |section|
filter_request_section(section, request_constraints[section])
end
self
end
def with_response(response_constraints)
return self if response_constraints.nil?
[:headers, :body].each do |section|
filter_response_section(section, response_constraints[section])
end
self
end
def successful_investigations
@filtered_investigations.select(&:successful?)
end
def unsuccessful_investigations
@filtered_investigations - successful_investigations
end
protected
def filter_request_section(section, filter)
suspects = []
section = :parsed_body if section == :body
@filtered_investigations.keep_if do |investigation|
candidate = investigation.request.send(section)
suspects << candidate if @track_suspects
filter === candidate
end if filter
fail FilterExhaustedError.new("no requests matched #{section}", filter, suspects) if @filtered_investigations.empty?
end
def filter_response_section(section, filter)
section = :parsed_body if section == :body
suspects = []
@filtered_investigations.keep_if do |investigation|
candidate = investigation.response.send(section)
suspects << candidate if @track_suspects
filter === candidate
end if filter
fail FilterExhaustedError.new("no responses matched #{section}", filter, suspects) if @filtered_investigations.empty?
end
# rubocop:enable Style/CaseEquality
end
end
end
================================================
FILE: lib/pacto/forensics/investigation_matcher.rb
================================================
# -*- encoding : utf-8 -*-
RSpec::Matchers.define :have_investigated do |service_name|
match do
investigations = Pacto::InvestigationRegistry.instance.investigations
@service_name = service_name
begin
@investigation_filter = Pacto::Forensics::InvestigationFilter.new(investigations)
@investigation_filter.with_name(@service_name)
.with_request(@request_constraints)
.with_response(@response_constraints)
@matched_investigations = @investigation_filter.filtered_investigations
@unsuccessful_investigations = @investigation_filter.unsuccessful_investigations
!@matched_investigations.empty? && (@allow_citations || @unsuccessful_investigations.empty?)
rescue Pacto::Forensics::FilterExhaustedError => e
@filter_error = e
false
end
end
def describe(obj)
obj.respond_to?(:description) ? obj.description : obj.to_s
end
description do
buffer = StringIO.new
buffer.puts "to have investigated #{@service_name}"
if @request_constraints
buffer.puts ' with request matching'
@request_constraints.each do |k, v|
buffer.puts " #{k}: #{describe(v)}"
end
end
buffer.puts ' and' if @request_constraints && @response_constraints
if @response_constraint
buffer.puts ' with response matching'
@request_constraints.each do |k, v|
buffer.puts " #{k}: #{describe(v)}"
end
end
buffer.string
end
chain :with_request do |request_constraints|
@request_constraints = request_constraints
end
chain :with_response do |response_constraints|
@response_constraints = response_constraints
end
chain :allow_citations do
@allow_citations = true
end
failure_message do | group |
buffer = StringIO.new
buffer.puts "expected #{group} " + description
if @filter_error
buffer.puts "but #{@filter_error.message}"
unless @filter_error.suspects.empty?
buffer.puts ' suspects:'
@filter_error.suspects.each do |suspect|
buffer.puts " #{suspect}"
end
end
elsif @matched_investigations.empty?
investigated_services = @investigation_filter.investigations.map(&:contract).compact.map(&:name).uniq
buffer.puts "but it was not among the services investigated: #{investigated_services}"
elsif @unsuccessful_investigations
buffer.puts 'but investigation errors were found:'
@unsuccessful_investigations.each do |investigation|
buffer.puts " #{investigation}"
end
end
buffer.string
end
end
================================================
FILE: lib/pacto/formats/legacy/contract.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto/formats/legacy/request_clause'
require 'pacto/formats/legacy/response_clause'
module Pacto
module Formats
module Legacy
class Contract < Pacto::Dash
include Pacto::Contract
property :id
property :file
property :request, required: true
# Although I'd like response to be required, it complicates
# the partial contracts used the rake generation task...
# yet another reason I'd like to deprecate that feature
property :response # , required: true
property :values, default: {}
# Gotta figure out how to use test doubles w/ coercion
coerce_key :request, RequestClause
coerce_key :response, ResponseClause
property :examples
property :name, required: true
property :adapter, default: proc { Pacto.configuration.adapter }
property :consumer, default: proc { Pacto.configuration.default_consumer }
property :provider, default: proc { Pacto.configuration.default_provider }
def initialize(opts)
skip_freeze = opts.delete(:skip_freeze)
if opts[:file]
opts[:file] = Addressable::URI.convert_path(File.expand_path(opts[:file])).to_s
opts[:name] ||= opts[:file]
end
opts[:id] ||= (opts[:summary] || opts[:file])
super
freeze unless skip_freeze
end
def freeze
(keys.map(&:to_sym) - [:values, :adapter, :consumer, :provider]).each do | key |
send(key).freeze
end
self
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/contract_builder.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Legacy
class ContractBuilder < Hashie::Dash # rubocop:disable Metrics/ClassLength
extend Forwardable
attr_accessor :source
def initialize(options = {})
@schema_generator = options[:schema_generator] ||= JSON::SchemaGenerator
@filters = options[:filters] ||= Generator::Filters.new
@data = { request: {}, response: {}, examples: {} }
@source = 'Pacto' # Currently used by JSONSchemaGeneator, but not really useful
end
def name=(name)
@data[:name] = name
end
def add_example(name, pacto_request, pacto_response)
@data[:examples][name] ||= {}
@data[:examples][name][:request] = clean(pacto_request.to_hash)
@data[:examples][name][:response] = clean(pacto_response.to_hash)
self
end
def infer_all
# infer_file # The target file is being chosen inferred by the Generator
infer_name
infer_schemas
end
def infer_name
if @data[:examples].empty?
@data[:name] = @data[:request][:path] if @data[:request]
return self
end
example, hint = example_and_hint
@data[:name] = hint.nil? ? PactoRequest.new(example[:request]).uri.path : hint.service_name
self
end
def infer_schemas
return self if @data[:examples].empty?
# TODO: It'd be awesome if we could infer across all examples
example, _hint = example_and_hint
sample_request_body = example[:request][:body]
sample_response_body = example[:response][:body]
@data[:request][:schema] = generate_schema(sample_request_body) if sample_request_body && !sample_request_body.empty?
@data[:response][:schema] = generate_schema(sample_response_body) if sample_response_body && !sample_response_body.empty?
self
end
def without_examples
@export_examples = false
self
end
def generate_contract(request, response)
generate_request(request, response)
generate_response(request, response)
infer_all
self
end
def generate_request(request, response)
hint = hint_for(request)
request = clean(
headers: @filters.filter_request_headers(request, response),
http_method: request.method,
params: request.uri.query_values,
path: hint.nil? ? request.uri.path : hint.path
)
@data[:request] = request
self
end
def generate_response(request, response)
response = clean(
headers: @filters.filter_response_headers(request, response),
status: response.status
)
@data[:response] = response
self
end
def build_hash
instance_eval(&block) if block_given?
@final_data = @data.dup
@final_data.delete(:examples) if exclude_examples?
clean(@final_data)
end
def build(&block)
Contract.new build_hash(&block)
end
protected
def example_and_hint
example = @data[:examples].values.first
example_request = PactoRequest.new example[:request]
[example, Pacto::Generator.hint_for(example_request)]
end
def exclude_examples?
@export_examples == false
end
def generate_schema(body, generator_options = Pacto.configuration.generator_options)
return if body.nil? || body.empty?
body_schema = @schema_generator.generate @source, body, generator_options
MultiJson.load(body_schema)
end
def clean(data)
data.delete_if { |_k, v| v.nil? }
end
def hint_for(pacto_request)
Pacto::Generator.hint_for(pacto_request)
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/contract_factory.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto/formats/legacy/contract'
module Pacto
module Formats
module Legacy
# Builds {Pacto::Formats::Legacy::Contract} instances from Pacto's legacy Contract format.
class ContractFactory
attr_reader :schema
def initialize(options = {})
@schema = options[:schema] || MetaSchema.new
end
def build_from_file(contract_path, host)
contract_definition = File.read(contract_path)
definition = JSON.parse(contract_definition)
schema.validate definition
definition['request'].merge!('host' => host)
body_to_schema(definition, 'request', contract_path)
body_to_schema(definition, 'response', contract_path)
method_to_http_method(definition, contract_path)
request = RequestClause.new(definition['request'])
response = ResponseClause.new(definition['response'])
Contract.new(request: request, response: response, file: contract_path, name: definition['name'], examples: definition['examples'])
end
def files_for(contracts_dir)
full_path = Pathname.new(contracts_dir).realpath
if full_path.directory?
all_json_files = "#{full_path}/**/*.json"
Dir.glob(all_json_files).map do |f|
Pathname.new(f)
end
else
[full_path]
end
end
private
def body_to_schema(definition, section, file)
schema = definition[section].delete 'body'
return nil unless schema
Pacto::UI.deprecation "Contract format deprecation: #{section}:body will be moved to #{section}:schema (#{file})"
definition[section]['schema'] = schema
end
def method_to_http_method(definition, file)
method = definition['request'].delete 'method'
return nil unless method
Pacto::UI.deprecation "Contract format deprecation: request:method will be moved to request:http_method (#{file})"
definition['request']['http_method'] = method
end
Pacto::ContractFactory.add_factory(:legacy, ContractFactory.new)
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/contract_generator.rb
================================================
# -*- encoding : utf-8 -*-
require 'json/schema_generator'
require 'pacto/formats/legacy/contract_builder'
require 'pacto/formats/legacy/generator/filters'
module Pacto
module Formats
module Legacy
class ContractGenerator
include Logger
def initialize(_schema_version = 'draft3',
schema_generator = JSON::SchemaGenerator,
validator = Pacto::MetaSchema.new,
filters = Generator::Filters.new,
consumer = Pacto::Consumer.new)
@contract_builder = ContractBuilder.new(schema_generator: schema_generator, filters: filters)
@consumer = consumer
@validator = validator
end
def generate(pacto_request, pacto_response)
return unless Pacto.generating?
logger.debug("Generating Contract for #{pacto_request}, #{pacto_response}")
begin
contract_file = load_contract_file(pacto_request)
unless File.exist? contract_file
uri = URI(pacto_request.uri)
FileUtils.mkdir_p(File.dirname contract_file)
raw_contract = save(uri, pacto_request, pacto_response)
File.write(contract_file, raw_contract)
logger.debug("Generating #{contract_file}")
Pacto.load_contract contract_file, uri.host
end
rescue => e
raise StandardError, "Error while generating Contract #{contract_file}: #{e.message}", e.backtrace
end
end
def generate_from_partial_contract(request_file, host)
contract = Pacto.load_contract request_file, host
request, response = @consumer.request(contract)
save(request_file, request, response)
end
def save(source, request, response)
@contract_builder.source = source
# TODO: Get rid of the generate_contract call, just use add_example/infer_all
@contract_builder.add_example('default', request, response).generate_contract(request, response) # .infer_all
@contract_builder.without_examples if Pacto.configuration.generator_options[:no_examples]
contract = @contract_builder.build_hash
pretty_contract = MultiJson.encode(contract, pretty: true)
# This is because of a discrepency w/ jruby vs MRI pretty json
pretty_contract.gsub!(/^$\n/, '')
@validator.validate pretty_contract
pretty_contract
end
private
def load_contract_file(pacto_request)
hint = Pacto::Generator.hint_for(pacto_request)
if hint.nil?
uri = URI(pacto_request.uri)
path = uri.path
basename = File.basename(path, '.json') + '.json'
File.join(Pacto.configuration.contracts_path, uri.host, File.dirname(path), basename)
else
File.expand_path(hint.target_file, Pacto.configuration.contracts_path)
end
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/generator/filters.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Legacy
module Generator
class Filters
CONNECTION_CONTROL_HEADERS = %w(
Via
Server
Connection
Transfer-Encoding
Content-Length
)
FRESHNESS_HEADERS =
%w(
Date
Last-Modified
ETag
)
HEADERS_TO_FILTER = CONNECTION_CONTROL_HEADERS + FRESHNESS_HEADERS
def filter_request_headers(request, response)
# FIXME: Do we need to handle all these cases in real situations, or just because of stubbing?
vary_headers = response.headers['vary'] || response.headers['Vary'] || []
vary_headers = [vary_headers] if vary_headers.is_a? String
vary_headers = vary_headers.map do |h|
h.split(',').map(&:strip)
end.flatten
request.headers.select do |header|
vary_headers.map(&:downcase).include? header.downcase
end
end
def filter_response_headers(_request, response)
Pacto::Extensions.normalize_header_keys(response.headers).reject do |header|
(HEADERS_TO_FILTER.include? header) || (header.start_with?('X-'))
end
end
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/generator_hint.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Legacy
class GeneratorHint < Pacto::Dash
extend Forwardable
property :request_clause
coerce_key :request_clause, RequestClause
property :service_name, required: true
property :target_file
def_delegators :request_clause, *RequestClause::Data.properties.map(&:to_sym)
def initialize(data)
data[:request_clause] = RequestClause::Data.properties.each_with_object({}) do | prop, hash |
hash[prop] = data.delete prop
end
super
self.target_file ||= "#{slugify(service_name)}.json"
end
def matches?(pacto_request)
return false if pacto_request.nil?
Pacto::RequestPattern.for(request_clause).matches?(pacto_request)
end
private
def slugify(path)
path.downcase.gsub(' ', '_')
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/request_clause.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Legacy
class RequestClause < Pacto::Dash
include Pacto::RequestClause
extend Forwardable
attr_reader :data
def_delegators :data, :to_hash
def_delegators :data, :host, :http_method, :schema, :path, :headers, :params
def_delegators :data, :host=, :http_method=, :schema=, :path=, :headers=, :params=
class Data < Pacto::Dash
property :host # required?
property :http_method, required: true
property :schema, default: {}
property :path, default: '/'
property :headers, default: {}
property :params, default: {}
end
def initialize(data)
skip_freeze = data.delete(:skip_freeze)
mash = Hashie::Mash.new data
mash['http_method'] = normalize(mash['http_method'])
@data = Data.new(mash)
freeze unless skip_freeze
super({})
@pattern = Pacto::RequestPattern.for(self)
end
def freeze
@data.freeze
self
end
end
end
end
end
================================================
FILE: lib/pacto/formats/legacy/response_clause.rb
================================================
module Pacto
module Formats
module Legacy
class ResponseClause
include Pacto::ResponseClause
extend Forwardable
attr_reader :data
def_delegators :data, :to_hash
def_delegators :data, :status, :headers, :schema
def_delegators :data, :status=, :headers=, :schema=
class Data < Pacto::Dash
property :status
property :headers, default: {}
property :schema, default: {}
end
def initialize(data)
skip_freeze = data.delete(:skip_freeze)
@data = Data.new(data)
freeze unless skip_freeze
end
def freeze
@data.freeze
self
end
end
end
end
end
================================================
FILE: lib/pacto/formats/swagger/contract.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto/formats/swagger/request_clause'
require 'pacto/formats/swagger/response_clause'
module Pacto
module Formats
module Swagger
class Contract < Pacto::Dash
include Pacto::Contract
attr_reader :swagger_api_operation
property :id
property :file
property :request, required: true
# Although I'd like response to be required, it complicates
# the partial contracts used the rake generation task...
# yet another reason I'd like to deprecate that feature
property :response # , required: true
property :values, default: {}
# Gotta figure out how to use test doubles w/ coercion
coerce_key :request, RequestClause
coerce_key :response, ResponseClause
property :examples
property :name, required: true
property :adapter, default: proc { Pacto.configuration.adapter }
property :consumer, default: proc { Pacto.configuration.default_consumer }
property :provider, default: proc { Pacto.configuration.default_provider }
def initialize(swagger_api_operation, base_data = {}) # rubocop:disable Metrics/MethodLength
if base_data[:file]
base_data[:file] = Addressable::URI.convert_path(File.expand_path(base_data[:file])).to_s
base_data[:name] ||= base_data[:file]
end
base_data[:id] ||= (base_data[:summary] || base_data[:file])
@swagger_api_operation = swagger_api_operation
host = base_data.delete(:host) || swagger_api_operation.host
default_response = swagger_api_operation.default_response
request_clause = Pacto::Formats::Swagger::RequestClause.new(swagger_api_operation, host: host)
if default_response.nil?
logger.warn("No response defined for #{swagger_api_operation.full_name}")
response_clause = ResponseClause.new(status: 200)
else
response_clause = ResponseClause.new(default_response)
end
examples = build_examples(default_response)
super base_data.merge(
id: swagger_api_operation.operationId,
name: swagger_api_operation.full_name,
request: request_clause, response: response_clause,
examples: examples
)
end
private
def build_examples(response)
return nil if response.nil? || response.examples.nil? || response.examples.empty?
if response.examples.empty?
response_body = nil
else
response_body = response.examples.values.first
end
{
default: {
request: {}, # Swagger doesn't have a clear way to capture request examples
response: {
body: response_body
}
}
}
rescue => e # FIXME: Only parsing errors?
logger.warn("Error while trying to parse response example for #{swagger_api_operation.full_name}")
logger.debug(" Error details: #{e.inspect}")
nil
end
end
end
end
end
================================================
FILE: lib/pacto/formats/swagger/contract_factory.rb
================================================
# -*- encoding : utf-8 -*-
require 'swagger'
require 'pacto/formats/swagger/contract'
module Pacto
module Formats
module Swagger
# Builds {Pacto::Formats::Swagger::Contract} instances from Swagger documents
class ContractFactory
include Logger
def load_hints(_contract_path, _host = nil)
fail NotImplementedError, 'Contract generation from hints is not currently supported for Swagger'
end
def build_from_file(contract_path, host = nil)
app = ::Swagger.load(contract_path)
app.operations.map do |op|
Contract.new(op,
file: contract_path,
host: host
)
end
rescue ArgumentError => e
logger.error(e)
raise "Could not load #{contract_path}: #{e.message}"
end
def files_for(contracts_dir)
full_path = Pathname.new(contracts_dir).realpath
if full_path.directory?
all_json_files = "#{full_path}/**/*.{json,yaml,yml}"
Dir.glob(all_json_files).map do |f|
Pathname.new(f)
end
else
[full_path]
end
end
end
Pacto::ContractFactory.add_factory(:swagger, ContractFactory.new)
end
end
end
================================================
FILE: lib/pacto/formats/swagger/request_clause.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Swagger
class RequestClause
include Pacto::RequestClause
extend Forwardable
attr_writer :host
attr_reader :swagger_api_operation
def_delegator :swagger_api_operation, :verb, :http_method
def_delegators :swagger_api_operation, :path
def initialize(swagger_api_operation, base_data = {})
@swagger_api_operation = swagger_api_operation
@host = base_data[:host] || swagger_api_operation.host
@pattern = Pacto::RequestPattern.for(self)
end
def schema
return nil if body_parameter.nil?
return nil if body_parameter.schema.nil?
body_parameter.schema.parse
end
def params
return {} if swagger_api_operation.parameters.nil?
swagger_api_operation.parameters.select { |p| p.in == 'query' }
end
def headers
return {} if swagger_api_operation.parameters.nil?
swagger_api_operation.parameters.select { |p| p.in == 'header' }
end
def to_hash
[:http_method, :schema, :path, :headers, :params].each_with_object({}) do | key, hash |
hash[key.to_s] = send key
end
end
private
def body_parameter
return nil if swagger_api_operation.parameters.nil?
swagger_api_operation.parameters.find { |p| p.in == 'body' }
end
end
end
end
end
================================================
FILE: lib/pacto/formats/swagger/response_clause.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Formats
module Swagger
class ResponseClause
extend Forwardable
include Pacto::ResponseClause
attr_reader :swagger_response
def_delegators :swagger_response, :schema
def initialize(swagger_response, _base_data = {})
@swagger_response = swagger_response
end
def status
swagger_response.status_code || 200
end
def headers
swagger_response.headers || {}
end
def schema
return nil unless swagger_response.schema
swagger_response.schema.parse
end
end
end
end
end
================================================
FILE: lib/pacto/generator.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto/formats/legacy/contract_generator'
require 'pacto/formats/legacy/generator_hint'
module Pacto
module Generator
include Logger
class << self
# Factory method to return the active contract generator implementation
def contract_generator
Pacto::Formats::Legacy::ContractGenerator.new
end
# Factory method to return the active contract generator implementation
def schema_generator
JSON::SchemaGenerator
end
def configuration
@configuration ||= Configuration.new
end
def configure
yield(configuration)
end
def hint_for(pacto_request)
configuration.hints.find { |hint| hint.matches? pacto_request }
end
end
class Configuration
attr_reader :hints
def initialize
@hints = Set.new
end
def hint(name, hint_data)
@hints << Formats::Legacy::GeneratorHint.new(hint_data.merge(service_name: name))
end
end
end
end
================================================
FILE: lib/pacto/handlers/json_handler.rb
================================================
require 'json'
module Pacto
module Handlers
module JSONHandler
class << self
def raw(body)
JSON.dump(body)
end
def parse(body)
JSON.parse(body)
end
# TODO: Something like validate(contract, body)
end
end
end
end
================================================
FILE: lib/pacto/handlers/text_handler.rb
================================================
module Pacto
module Handlers
module TextHandler
class << self
def raw(body)
body.to_s
end
def parse(body)
body.to_s
end
# TODO: Something like validate(contract, body)
end
end
end
end
================================================
FILE: lib/pacto/hooks/erb_hook.rb
================================================
# -*- encoding : utf-8 -*-
require_relative '../erb_processor'
module Pacto
module Hooks
class ERBHook < Pacto::Hook
def initialize
@processor = ERBProcessor.new
end
def process(contracts, request_signature, response)
bound_values = contracts.empty? ? {} : contracts.first.values
bound_values.merge!(req: { 'HEADERS' => request_signature.headers })
response.body = @processor.process response.body, bound_values
response.body
end
end
end
end
================================================
FILE: lib/pacto/investigation.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class Investigation
include Logger
attr_reader :request, :response, :contract, :citations
def initialize(request, response, contract = nil, citations = nil)
@request = request
@response = response
@contract = contract
@citations = citations || []
end
def successful?
@citations.empty?
end
def against_contract?(contract_pattern)
return nil if @contract.nil?
case contract_pattern
when String
@contract if @contract.file.eql? contract_pattern
when Regexp
@contract if @contract.file =~ contract_pattern
end
end
def to_s
contract_name = @contract.nil? ? 'nil' : contract.name
citation_string = Pacto::UI.colorize(@citations.join("\n\t\t"), :red)
''"
Investigation:
\tContract: #{contract_name}
\tRequest: #{@request}
\tCitations: \n\t\t#{citation_string}
"''
end
def summary
if @contract.nil?
"Missing contract for services provided by #{@request.uri.host}"
else
status = successful? ? 'successful' : 'unsuccessful'
"#{status} investigation of #{@contract.name}"
end
end
end
end
================================================
FILE: lib/pacto/logger.rb
================================================
# -*- encoding : utf-8 -*-
require 'forwardable'
module Pacto
module Logger
def logger
Pacto.configuration.logger
end
class SimpleLogger
include Singleton
extend Forwardable
def_delegators :@log, :debug, :info, :warn, :error, :fatal
def initialize
log ::Logger.new STDOUT
end
def log(log)
@log = log
@log.level = default_level
@log.progname = 'Pacto'
end
def level=(level)
@log.level = log_levels.fetch(level, default_level)
end
def level
log_levels.key @log.level
end
private
def default_level
::Logger::ERROR
end
def log_levels
{
debug: ::Logger::DEBUG,
info: ::Logger::INFO,
warn: ::Logger::WARN,
error: ::Logger::ERROR,
fatal: ::Logger::FATAL
}
end
end
end
end
================================================
FILE: lib/pacto/meta_schema.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class MetaSchema
attr_accessor :schema, :engine
def initialize(engine = JSON::Validator)
@schema = File.join(File.dirname(File.expand_path(__FILE__)), '../../resources/contract_schema.json')
base_schemas = ['../../resources/draft-03.json', '../../resources/draft-04.json']
validatable = false
base_schemas.each do |base_schema|
base_schema_file = File.join(File.dirname(File.expand_path(__FILE__)), base_schema)
# This has a side-effect of caching local schemas, so we don't
# look up json-schemas over HTTP.
validatable ||= JSON::Validator.validate(base_schema_file, @schema)
end
fail 'Could not validate metaschema against any known version of json-schema' unless validatable
@engine = engine
end
def validate(definition)
errors = engine.fully_validate(schema, definition)
fail InvalidContract, errors unless errors.empty?
end
end
end
================================================
FILE: lib/pacto/observers/stenographer.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Observers
class Stenographer
def initialize(output)
@output = output
end
def log_investigation(investigation)
return if @output.nil?
contract = investigation.contract
request = investigation.request
response = investigation.response
name = name_for(contract, request)
values = values_for(contract, request)
msg = "request #{name.inspect}, values: #{values.inspect}, response: {status: #{response.status}} # #{number_of_citations(investigation)} contract violations"
@output.puts msg
@output.flush
end
protected
def name_for(contract, request)
return "Unknown (#{request.uri})" if contract.nil?
contract.name
end
def number_of_citations(investigation)
return 0 if investigation.nil?
return 0 if investigation.citations.nil?
investigation.citations.size.to_s
end
def values_for(_contract, request)
# FIXME: Extract vars w/ URI::Template
request.uri.query_values
end
end
end
end
================================================
FILE: lib/pacto/provider.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
def self.providers
@providers ||= {}
end
class Provider
include Resettable
def self.reset!
Pacto.providers.clear
end
def actor
@actor ||= Pacto::Actors::FromExamples.new
end
def actor=(actor)
fail ArgumentError, 'The actor must respond to :build_response' unless actor.respond_to? :build_response
@actor = actor
end
def response_for(contract, data = {})
actor.build_response contract, data
end
end
end
================================================
FILE: lib/pacto/rake_task.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto'
require 'thor'
require 'pacto/cli'
require 'pacto/cli/helpers'
# FIXME: RakeTask is a huge class, refactor this please
# rubocop:disable ClassLength
module Pacto
class RakeTask
extend Forwardable
include Thor::Actions
include Rake::DSL
include Pacto::CLI::Helpers
def initialize
@exit_with_error = false
@cli = Pacto::CLI::Main.new
end
def run(task, args, opts = {})
Pacto::CLI::Main.new([], opts).public_send(task, *args)
end
def install
desc 'Tasks for Pacto gem'
namespace :pacto do
validate_task
generate_task
meta_validate
end
end
def validate_task
desc 'Validates all contracts in a given directory against a given host'
task :validate, :host, :dir do |_t, args|
opts = args.to_hash
dir = opts.delete :dir
run(:validate, dir, opts)
end
end
def generate_task
desc 'Generates contracts from partial contracts'
task :generate, :input_dir, :output_dir, :host do |_t, args|
if args.to_a.size < 3
fail Pacto::UI.colorize('USAGE: rake pacto:generate[<request_contract_dir>, <output_dir>, <record_host>]', :yellow)
end
generate_contracts(args[:input_dir], args[:output_dir], args[:host])
end
end
def meta_validate
desc 'Validates a directory of contract definitions'
task :meta_validate, :dir do |_t, args|
run(:meta_validate, *args)
end
end
# rubocop:enable MethodLength
# FIXME: generate_contracts is a big method =(. Needs refactoring
# rubocop:disable MethodLength
def generate_contracts(input_dir, output_dir, host)
WebMock.allow_net_connect!
generator = Pacto::Generator.contract_generator
puts "Generating contracts from partial contracts in #{input_dir} and recording to #{output_dir}\n\n"
failed_contracts = []
each_contract(input_dir) do |contract_file|
begin
contract = generator.generate_from_partial_contract(contract_file, host)
output_file = File.expand_path(File.basename(contract_file), output_dir)
output_file = File.open(output_file, 'wb')
output_file.write contract
output_file.flush
output_file.close
rescue InvalidContract => e
failed_contracts << contract_file
puts Pacto::UI.colorize(e.message, :red)
end
end
if failed_contracts.empty?
puts Pacto::UI.colorize('Successfully generated all contracts', :green)
else
fail Pacto::UI.colorize("The following contracts could not be generated: #{failed_contracts.join ','}", :red)
end
end
# rubocop:enable MethodLength
end
end
# rubocop:enable ClassLength
Pacto::RakeTask.new.install
================================================
FILE: lib/pacto/request_clause.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module RequestClause
include Logger
attr_reader :host
attr_reader :http_method
attr_reader :schema
attr_reader :path
attr_reader :headers
attr_reader :params
attr_reader :pattern
def http_method=(method)
normalize(method)
end
def uri(values = {})
values ||= {}
uri_template = pattern.uri_template
missing_keys = uri_template.keys.map(&:to_sym) - values.keys.map(&:to_sym)
values[:scheme] = 'http' if missing_keys.delete(:scheme)
values[:server] = 'localhost' if missing_keys.delete(:server)
logger.warn "Missing keys for building a complete URL: #{missing_keys.inspect}" unless missing_keys.empty?
Addressable::URI.heuristic_parse(uri_template.expand(values)).tap do |uri|
uri.query_values = params unless params.nil? || params.empty?
end
end
private
def normalize(method)
method.to_s.downcase.to_sym
end
end
end
================================================
FILE: lib/pacto/request_pattern.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class RequestPattern < WebMock::RequestPattern
attr_accessor :uri_template
def self.for(base_request)
new(base_request.http_method, UriPattern.for(base_request))
end
def initialize(http_method, uri_template)
@uri_template = uri_template
super
end
def to_s
string = Pacto::UI.colorize_method(@method_pattern.to_s)
string << " #{@uri_pattern}"
# WebMock includes this info, but I don't think we should. Pacto should match on URIs only and then validate the rest...
# string << " with body #{@body_pattern.to_s}" if @body_pattern
# string << " with headers #{@headers_pattern.to_s}" if @headers_pattern
# string << " with given block" if @with_block
string
end
end
end
================================================
FILE: lib/pacto/resettable.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
# Included this module so that Pacto::Resettable.reset_all will call your class/module's self.reset! method.
module Resettable
def self.resettables
@resettables ||= []
end
def self.extended(base)
resettables << base
end
def self.included(base)
resettables << base
end
def self.reset_all
resettables.each(&:reset!)
true
end
end
end
================================================
FILE: lib/pacto/response_clause.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module ResponseClause
attr_reader :status
attr_reader :headers
attr_reader :schema
end
end
================================================
FILE: lib/pacto/rspec.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto'
begin
require 'rspec/core'
require 'rspec/expectations'
rescue LoadError
raise 'pacto/rspec requires rspec 2 or later'
end
require 'pacto/forensics/investigation_filter'
require 'pacto/forensics/investigation_matcher'
RSpec::Matchers.define :have_unmatched_requests do |_method, _uri|
match do
@unmatched_investigations = Pacto::InvestigationRegistry.instance.unmatched_investigations
!@unmatched_investigations.empty?
end
failure_message do
'Expected Pacto to have not matched all requests to a Contract, but all requests were matched.'
end
failure_message_when_negated do
unmatched_requests = @unmatched_investigations.map(&:request).join("\n ")
"Expected Pacto to have matched all requests to a Contract, but the following requests were not matched: \n #{unmatched_requests}"
end
end
RSpec::Matchers.define :have_failed_investigations do |_method, _uri|
match do
@failed_investigations = Pacto::InvestigationRegistry.instance.failed_investigations
!@failed_investigations.empty?
end
failure_message do
'Expected Pacto to have found investigation problems, but none were found.'
end
failure_message_when_negated do
"Expected Pacto to have successfully validated all requests, but the following issues were found: #{@failed_investigations}"
end
end
RSpec::Matchers.define :have_validated do |method, uri|
match do
@request_pattern = Pacto::RequestPattern.new(method, uri)
@request_pattern.with(@options) if @options
validated? @request_pattern
end
chain :against_contract do |contract|
@contract = contract
end
chain :with do |options|
@options = options
end
def validated?(_request_pattern)
@matching_investigations = Pacto::InvestigationRegistry.instance.validated? @request_pattern
validated = !@matching_investigations.nil?
validated && successfully? && contract_matches?
end
def investigation_citations
@investigation_citations ||= @matching_investigations.map(&:citations).flatten.compact
end
def successfully?
@matching_investigations.map(&:successful?).uniq.eql? [true]
end
def contract_matches?
if @contract
validated_contracts = @matching_investigations.map(&:contract).compact
# Is there a better option than case equality for string & regex support?
validated_contracts.any? do |contract|
@contract === contract.file || @contract === contract.name # rubocop:disable CaseEquality
end
else
true
end
end
failure_message do
buffer = StringIO.new
buffer.puts "expected Pacto to have validated #{@request_pattern}"
if @matching_investigations.nil? || @matching_investigations.empty?
buffer.puts ' but no matching request was received'
buffer.puts ' received:'
buffer.puts "#{WebMock::RequestRegistry.instance}"
elsif @matching_investigations.map(&:contract).compact.empty?
buffer.puts ' but a matching Contract was not found'
elsif !successfully?
buffer.puts ' but investigation errors were found:'
buffer.print ' '
buffer.puts investigation_citations.join "\n "
# investigation_citations.each do |investigation_result|
# buffer.puts " #{investigation_result}"
# end
elsif @contract
validated_against = @matching_investigations.map { |v| v.against_contract? @contract }.compact.join ','
buffer.puts " against Contract #{@contract}"
buffer.puts " but it was validated against #{validated_against}"
end
buffer.string
end
end
================================================
FILE: lib/pacto/server/cli.rb
================================================
require 'thor'
require 'pacto/server'
module Pacto
module Server
class CLI < Thor
class << self
DEFAULTS = {
stdout: true,
log_file: 'pacto.log',
# :config => 'pacto/config/pacto_server.rb',
strict: false,
stub: true,
live: false,
generate: false,
verbose: true,
validate: true,
directory: File.join(Dir.pwd, 'contracts'),
port: 9000,
format: :legacy,
stenographer_log_file: File.expand_path('pacto_stenographer.log', Dir.pwd),
strip_port: true
}
def server_options
method_option :port, default: 4567, desc: 'The port to run the server on'
method_option :directory, default: DEFAULTS[:directory], desc: 'The directory containing contracts'
method_option :strict, default: DEFAULTS[:strict], desc: 'Whether Pacto should use strict matching or not'
method_option :format, default: DEFAULTS[:format], desc: 'The contract format to use'
method_option :strip_port, default: DEFAULTS[:strip_port], desc: 'If pacto should remove the port from URLs before forwarding'
end
end
desc 'stub [CONTRACTS...]', 'Launches a stub server for a set of contracts'
method_option :port, type: :numeric, desc: 'The port to listen on', default: 3000
method_option :spy, type: :boolean, desc: 'Display traffic received by Pacto'
server_options
def stub(*_contracts)
setup_interrupt
server_options = @options.dup
server_options[:stub] = true
Pacto::Server::HTTP.run('0.0.0.0', options.port, server_options)
end
desc 'proxy [CONTRACTS...]', 'Launches an intercepting proxy server for a set of contracts'
method_option :to, type: :string, desc: 'The target host for forwarded requests'
method_option :port, type: :numeric, desc: 'The port to listen on', default: 3000
method_option :spy, type: :boolean, desc: 'Display traffic received by Pacto'
def proxy(*_contracts)
setup_interrupt
server_options = @options.dup
server_options[:live] = true
Pacto::Server::HTTP.run('0.0.0.0', options.port, server_options)
end
private
def setup_interrupt
trap('INT') do
say 'Exiting...'
exit
end
end
end
end
end
================================================
FILE: lib/pacto/server/config.rb
================================================
# -*- encoding : utf-8 -*-
Pacto::Server::Settings::OptionHandler.new(port, logger, config).handle(options)
================================================
FILE: lib/pacto/server/proxy.rb
================================================
module Pacto
module Server
module Proxy
def proxy_request(pacto_request)
prepare_to_forward(pacto_request)
pacto_response = forward(pacto_request)
prepare_to_respond(pacto_response)
pacto_response.body = rewrite(pacto_response.body)
pacto_response
end
def prepare_to_forward(pacto_request)
host = host_for(pacto_request)
fail 'Could not determine request host' if host.nil?
host.gsub!('.dev', '.com') if settings[:strip_dev]
scheme, host = host.split('://')
host, scheme = scheme, host if host.nil?
host, _port = host.split(':')
scheme ||= 'https'
pacto_request.uri = Addressable::URI.heuristic_parse("#{scheme}://#{host}#{pacto_request.uri}")
# FIXME: We're stripping accept-encoding and transfer-encoding rather than dealing with the encodings
pacto_request.headers.delete_if { |k, _v| %w(host content-length accept-encoding transfer-encoding).include? k.downcase }
end
def rewrite(body)
return body unless settings[:strip_dev]
# FIXME: This is pretty hacky and needs to be rethought, but here to support hypermedia APIs
# This rewrites the response body so that URLs that may link to other services are rewritten
# to also passs through the Pacto server.
body.gsub('.com', ".dev:#{settings[:port]}").gsub(/https\:([\w\-\.\\\/]+).dev/, 'http:\1.dev')
end
def forward(pacto_request)
Pacto::Consumer::FaradayDriver.new.execute(pacto_request)
end
def prepare_to_respond(pacto_response)
pacto_response.headers.delete_if { |k, _v| %w(connection content-encoding content-length transfer-encoding).include? k.downcase }
end
private
def host_for(pacto_request)
# FIXME: Need case insensitive fetch for headers
pacto_request.uri.site || pacto_request.headers.find { |key, _| key.downcase == 'host' }[1]
end
end
end
end
================================================
FILE: lib/pacto/server/settings.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Server
module Settings
def options_parser(opts, options) # rubocop:disable MethodLength
options[:format] ||= :legacy
options[:strict] ||= false
options[:directory] ||= File.expand_path('contracts', @original_pwd)
options[:config] ||= File.expand_path('../config.rb', __FILE__)
options[:stenographer_log_file] ||= File.expand_path('pacto_stenographer.log', @original_pwd)
options[:strip_port] ||= true
opts.on('-l', '--live', 'Send requests to live services (instead of stubs)') { |_val| options[:live] = true }
opts.on('-f', '--format FORMAT', 'Contract format') { |val| options[:format] = val }
opts.on('--stub', 'Stub responses based on contracts') { |_val| options[:stub] = true }
opts.on('-g', '--generate', 'Generate Contracts from requests') { |_val| options[:generate] = true }
opts.on('-V', '--validate', 'Validate requests/responses against Contracts') { |_val| options[:validate] = true }
opts.on('-m', '--match-strict', 'Enforce strict request matching rules') { |_val| options[:strict] = true }
opts.on('-x', '--contracts_dir DIR', 'Directory that contains the contracts to be registered') { |val| options[:directory] = File.expand_path(val, @original_pwd) }
opts.on('-H', '--host HOST', 'Host of the real service, for generating or validating live requests') { |val| options[:backend_host] = val }
opts.on('-r', '--recursive-loading', 'Load contracts from folders named after the host to be stubbed') { |_val| options[:recursive_loading] = true }
opts.on('--strip-port', 'Strip the port from the request URI to build the proxied URI') { |_val| options[:strip_port] = true }
opts.on('--strip-dev', 'Strip .dev from the request domain to build the proxied URI') { |_val| options[:strip_dev] = true }
opts.on('--stenographer-log-file', 'Location for the stenographer log file') { |val| options[:stenographer_log_file] = val }
opts.on('--log-level [LEVEL]', [:debug, :info, :warn, :error, :fatal], 'Pacto log level ( debug, info, warn, error or fatal)') { |val| options[:pacto_log_level] = val }
end
class OptionHandler
attr_reader :port, :logger, :config, :options
def initialize(port, logger, config = {})
@port, @logger, @config = port, logger, config
end
def token_map
if File.readable? '.tokens.json'
MultiJson.load(File.read '.tokens.json')
else
{}
end
end
def prepare_contracts(contracts)
contracts.stub_providers if options[:stub]
end
def handle(options) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
@options = options
config[:backend_host] = options[:backend_host]
config[:strip_port] = options[:strip_port]
config[:strip_dev] = options[:strip_dev]
config[:port] = port
contracts_path = options[:directory] || File.expand_path('contracts', Dir.pwd)
Pacto.configure do |pacto_config|
pacto_config.logger = options[:pacto_logger] || logger
pacto_config.loggerl.log_level = config[:pacto_log_level] if config[:pacto_log_level]
pacto_config.contracts_path = contracts_path
pacto_config.strict_matchers = options[:strict]
pacto_config.generator_options = {
schema_version: :draft3,
token_map: token_map
}
pacto_config.stenographer_log_file = options[:stenographer_log_file]
end
if options[:generate]
Pacto.generate!
logger.info 'Pacto generation mode enabled'
end
if options[:recursive_loading]
Dir["#{contracts_path}/*"].each do |host_dir|
host = File.basename host_dir
prepare_contracts Pacto.load_contracts(host_dir, "https://#{host}", options[:format])
end
else
host_pattern = options[:backend_host] || '{scheme}://{server}'
if File.exist? contracts_path
prepare_contracts Pacto.load_contracts(contracts_path, host_pattern, options[:format])
end
end
Pacto.validate! if options[:validate]
if options[:live]
# WebMock.reset!
WebMock.allow_net_connect!
end
config
end
end
end
end
end
================================================
FILE: lib/pacto/server.rb
================================================
# -*- encoding : utf-8 -*-
require 'reel'
require 'pacto'
require_relative 'server/settings'
require_relative 'server/proxy'
module Pacto
module Server
class HTTP < Reel::Server::HTTP
attr_reader :settings, :logger
include Proxy
def initialize(host = '127.0.0.1', port = 3000, options = {})
@logger = options[:pacto_logger] || Pacto.configuration.logger
@settings = Settings::OptionHandler.new(port, @logger).handle(options)
logger.info "Pacto Server starting on #{host}:#{port}"
super(host, port, spy: options[:spy], &method(:on_connection))
end
def on_connection(connection)
# Support multiple keep-alive requests per connection
connection.each_request do |reel_request|
begin
pacto_request = # exclusive do
Pacto::PactoRequest.new(
headers: reel_request.headers, body: reel_request.read,
method: reel_request.method, uri: Addressable::URI.heuristic_parse(reel_request.uri)
)
# end
pacto_response = proxy_request(pacto_request)
reel_response = ::Reel::Response.new(pacto_response.status, pacto_response.headers, pacto_response.body)
reel_request.respond(reel_response)
rescue WebMock::NetConnectNotAllowedError, Faraday::ConnectionFailed => e
reel_request.respond 502, e.message
rescue => e
reel_request.respond 500, Pacto::Errors.formatted_trace(e)
end
end
end
end
end
end
================================================
FILE: lib/pacto/stubs/uri_pattern.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class UriPattern
class << self
def for(request, strict = Pacto.configuration.strict_matchers)
fail_deprecations(request)
build_template_uri_pattern(request, strict)
end
def build_template_uri_pattern(request, strict)
path = request.path.respond_to?(:pattern) ? request.path.pattern : request.path
host = request.host
host ||= '{server}'
scheme, host = host.split('://') if host.include?('://')
scheme ||= '{scheme}'
if strict
Addressable::Template.new("#{scheme}://#{host}#{path}")
else
Addressable::Template.new("#{scheme}://#{host}#{path}{?anyvars*}")
end
end
def fail_deprecations(request)
return if request.path.is_a? Addressable::Template
return if request.path == (corrected_path = request.path.gsub(/\/:(\w+)/, '/{\\1}'))
fail "please change path #{request.path} to uri template: #{corrected_path} - old syntax no longer supported"
end
end
end
end
================================================
FILE: lib/pacto/stubs/webmock_adapter.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
module Adapters
module WebMock
class PactoRequest < Pacto::PactoRequest
extend Forwardable
def_delegators :@webmock_request_signature, :headers, :method, :body, :uri, :to_s, :inspect
def initialize(webmock_request_signature)
@webmock_request_signature = webmock_request_signature
end
def params
@webmock_request_signature.uri.query_values
end
def path
@webmock_request_signature.uri.path
end
end
class PactoResponse < Pacto::PactoResponse
extend Forwardable
def_delegators :@webmock_response, :body, :body=, :headers=, :status=, :to_s, :inspect
def initialize(webmock_response)
@webmock_response = webmock_response
end
def headers
@webmock_response.headers || {}
end
def status
status, _ = @webmock_response.status
status
end
end
end
end
module Stubs
class WebMockAdapter
include Resettable
def initialize(middleware)
@middleware = middleware
WebMock.after_request do |webmock_request_signature, webmock_response|
process_hooks webmock_request_signature, webmock_response
end
end
def stub_request!(contract)
request_clause = contract.request
uri_pattern = UriPattern.for(request_clause)
stub = WebMock.stub_request(request_clause.http_method, uri_pattern)
if Pacto.configuration.strict_matchers
with_opts = strict_details(request_clause)
stub.request_pattern.with(with_opts) unless with_opts.empty?
end
stub.to_return do |request|
pacto_request = Pacto::Adapters::WebMock::PactoRequest.new request
response = contract.response_for pacto_request
{
status: response.status,
headers: response.headers,
body: format_body(response.body)
}
end
end
def self.reset!
WebMock.reset!
WebMock.reset_callbacks
end
def process_hooks(webmock_request_signature, webmock_response)
pacto_request = Pacto::Adapters::WebMock::PactoRequest.new webmock_request_signature
pacto_response = Pacto::Adapters::WebMock::PactoResponse.new webmock_response
@middleware.process pacto_request, pacto_response
end
private
def format_body(body)
if body.is_a?(Hash) || body.is_a?(Array)
body.to_json
else
body
end
end
def strict_details(request)
{}.tap do |details|
details[webmock_params_key(request)] = request.params unless request.params.empty?
details[:headers] = request.headers unless request.headers.empty?
end
end
def webmock_params_key(request)
request.http_method == :get ? :query : :body
end
end
end
end
================================================
FILE: lib/pacto/test_helper.rb
================================================
# -*- encoding : utf-8 -*-
begin
require 'pacto'
require 'pacto/server'
rescue LoadError
raise 'pacto/test_helper requires the pacto-server gem'
end
module Pacto
module TestHelper
DEFAULT_ARGS = {
stdout: true,
log_file: 'pacto.log',
# :config => 'pacto/config/pacto_server.rb',
strict: false,
stub: true,
live: false,
generate: false,
verbose: true,
validate: true,
directory: File.join(Dir.pwd, 'contracts'),
port: 9000,
format: :legacy,
stenographer_log_file: File.expand_path('pacto_stenographer.log', Dir.pwd),
strip_port: true
}
def with_pacto(args = {})
start_index = ::Pacto::InvestigationRegistry.instance.investigations.size
::Pacto::InvestigationRegistry.instance.investigations.clear
args = DEFAULT_ARGS.merge(args)
args[:spy] = args[:verbose]
server = Pacto::Server::HTTP.supervise('0.0.0.0', args[:port], args)
yield "http://localhost:#{args[:port]}"
::Pacto::InvestigationRegistry.instance.investigations[start_index, -1]
ensure
server.terminate unless server.nil?
end
end
end
================================================
FILE: lib/pacto/ui.rb
================================================
# -*- encoding : utf-8 -*-
require 'thor'
module Pacto
module UI
# Colors for HTTP Methods, intended to match colors of Swagger-UI (as close as possible with ANSI Colors)
METHOD_COLORS = {
'POST' => :green,
'PUT' => :yellow,
'DELETE' => :red,
'GET' => :blue,
'PATCH' => :yellow,
'HEAD' => :green
}
def self.shell
@shell ||= Thor::Shell::Color.new
end
def self.deprecation(msg)
$stderr.puts colorize(msg, :yellow) unless Pacto.configuration.hide_deprecations
end
def self.colorize(msg, color)
return msg unless Pacto.configuration.color
shell.set_color(msg, color)
end
def self.colorize_method(method)
method_string = method.to_s.upcase
color = METHOD_COLORS[method_string] || :red # red for unknown methods
colorize(method_string, color)
end
end
end
================================================
FILE: lib/pacto/uri.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
class URI
def self.for(host, path, params = {})
Addressable::URI.heuristic_parse("#{host}#{path}").tap do |uri|
uri.query_values = params unless params.nil? || params.empty?
end
end
end
end
================================================
FILE: lib/pacto/version.rb
================================================
# -*- encoding : utf-8 -*-
module Pacto
VERSION = '0.4.0.rc3'
end
================================================
FILE: lib/pacto.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto/version'
require 'addressable/template'
require 'swagger'
require 'middleware'
require 'faraday'
require 'multi_json'
require 'json-schema'
require 'json-generator'
require 'webmock'
require 'ostruct'
require 'erb'
require 'logger'
# FIXME: There's soo much stuff here! I'd both like to re-roganize and to use autoloading.
require 'pacto/errors'
require 'pacto/dash'
require 'pacto/resettable'
require 'pacto/logger'
require 'pacto/ui'
require 'pacto/request_pattern'
require 'pacto/core/http_middleware'
require 'pacto/consumer/faraday_driver'
require 'pacto/actor'
require 'pacto/consumer'
require 'pacto/provider'
require 'pacto/actors/json_generator'
require 'pacto/actors/from_examples'
require 'pacto/body_parsing'
require 'pacto/core/pacto_request'
require 'pacto/core/pacto_response'
require 'pacto/core/contract_registry'
require 'pacto/core/investigation_registry'
require 'pacto/core/configuration'
require 'pacto/core/modes'
require 'pacto/core/hook'
require 'pacto/extensions'
require 'pacto/request_clause'
require 'pacto/response_clause'
require 'pacto/stubs/webmock_adapter'
require 'pacto/stubs/uri_pattern'
require 'pacto/contract'
require 'pacto/cops'
require 'pacto/meta_schema'
require 'pacto/contract_factory'
require 'pacto/investigation'
require 'pacto/hooks/erb_hook'
require 'pacto/observers/stenographer'
require 'pacto/generator'
require 'pacto/contract_files'
require 'pacto/contract_set'
require 'pacto/uri'
# Cops
require 'pacto/cops/body_cop'
require 'pacto/cops/request_body_cop'
require 'pacto/cops/response_body_cop'
require 'pacto/cops/response_status_cop'
require 'pacto/cops/response_header_cop'
module Pacto
class << self
def configuration
@configuration ||= Configuration.new
end
def contract_registry
@registry ||= ContractRegistry.new
end
# Resets data and metrics only. It usually makes sense to call this between test scenarios.
def reset
Pacto::InvestigationRegistry.instance.reset!
# Pacto::Resettable.reset_all
end
# Resets but also clears configuration, loaded contracts, and plugins.
def clear!
Pacto::Resettable.reset_all
@modes = nil
@configuration = nil
@registry = nil
end
def configure
yield(configuration)
end
def contracts_for(request_signature)
contract_registry.contracts_for(request_signature)
end
# @throws Pacto::InvalidContract
def validate_contract(contract)
Pacto::MetaSchema.new.validate contract
true
end
def load_contract(contract_path, host, format = :legacy)
load_contracts(contract_path, host, format).first
end
def load_contracts(contracts_path, host, format = :legacy)
contracts = ContractFactory.load_contracts(contracts_path, host, format)
contracts.each do |contract|
contract_registry.register(contract)
end
ContractSet.new(contracts)
end
end
end
================================================
FILE: pacto-server.gemspec
================================================
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'pacto/version'
Gem::Specification.new do |gem|
gem.name = 'pacto-server'
gem.version = Pacto::VERSION
gem.authors = ['ThoughtWorks']
gem.email = ['pacto-gem@googlegroups.com']
gem.description = "Pacto Server let's you run Pacto as a standalone server to arbitrate contract disputes between a service provider and one or more consumers in any programming language. It's Pacto beyond Ruby"
gem.summary = 'Polyglot Integration Contract Testing server'
gem.homepage = 'http://thoughtworks.github.io/pacto/'
gem.license = 'MIT'
gem.files = `git ls-files -- bin/pacto-server lib/pacto/server.rb lib/pacto/server`.split($/) # rubocop:disable SpecialGlobalVars
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
gem.require_paths = ['lib']
gem.add_dependency 'pacto', Pacto::VERSION
gem.add_dependency 'reel', '~> 0.5'
end
================================================
FILE: pacto.gemspec
================================================
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'pacto/version'
plugin_files = Dir['pacto-*.gemspec'].map do |gemspec|
eval(File.read(gemspec)).files # rubocop:disable Eval
end.flatten.uniq
Gem::Specification.new do |gem|
gem.name = 'pacto'
gem.version = Pacto::VERSION
gem.authors = ['ThoughtWorks & Abril']
gem.email = ['pacto-gem@googlegroups.com']
gem.description = 'Pacto is a judge that arbitrates contract disputes between a service provider and one or more consumers. In other words, it is a framework for Integration Contract Testing, and helps guide service evolution patterns like Consumer-Driven Contracts or Documentation-Driven Contracts.'
gem.summary = 'Integration Contract Testing framework'
gem.homepage = 'http://thoughtworks.github.io/pacto/'
gem.license = 'MIT'
gem.files = `git ls-files`.split($/) - plugin_files # rubocop:disable SpecialGlobalVars
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
gem.require_paths = ['lib']
gem.add_dependency 'webmock', '~> 1.18'
gem.add_dependency 'swagger-core', '~> 0.2', '>= 0.2.1'
gem.add_dependency 'middleware', '~> 0.1'
gem.add_dependency 'multi_json', '~> 1.8'
gem.add_dependency 'json-schema', '~> 2.0'
gem.add_dependency 'json-generator', '~> 0.0', '>= 0.0.5'
gem.add_dependency 'hashie', '~> 3.0'
gem.add_dependency 'faraday', '~> 0.9'
gem.add_dependency 'addressable', '~> 2.3'
gem.add_dependency 'json-schema-generator', '~> 0.0', '>= 0.0.7'
gem.add_dependency 'thor', '~> 0.19'
gem.add_development_dependency 'polytrix', '~> 0.1', '>= 0.1.4'
gem.add_development_dependency 'coveralls', '~> 0'
gem.add_development_dependency 'fabrication', '~> 2.11'
gem.add_development_dependency 'rake', '~> 10.0'
gem.add_development_dependency 'rake-notes', '~> 0'
gem.add_development_dependency 'rspec', '~> 3.0'
gem.add_development_dependency 'aruba', '~> 0'
gem.add_development_dependency 'json_spec', '~> 1.0'
# Only required to push documentation, and not easily installed on Windows
# gem.add_development_dependency 'relish'
gem.add_development_dependency 'guard-rspec', '~> 4.2'
# FIXME: Rubocop upgrade needed... rubocop -a will do most of the work
gem.add_development_dependency 'rubocop', '~> 0.23', '< 0.27.0'
gem.add_development_dependency 'rubocop-rspec', '~> 1.0.rc3'
gem.add_development_dependency 'guard-rubocop', '~> 1.0'
gem.add_development_dependency 'guard-cucumber', '~> 1.4'
gem.add_development_dependency 'rb-fsevent', '~> 0' if RUBY_PLATFORM =~ /darwin/i
gem.add_development_dependency 'terminal-notifier-guard', '~> 1.5' if RUBY_PLATFORM =~ /darwin/i
end
================================================
FILE: resources/contract_schema.json
================================================
{
"title": "Example Schema",
"type": "object",
"required": ["request", "response"],
"definitions": {
"subschema": {
"anyOf": [
{ "$ref": "http://json-schema.org/draft-03/schema#" },
{ "$ref": "http://json-schema.org/draft-04/schema#" }
]
}
},
"properties": {
"name": {
"type": "string"
},
"request": {
"type": "object",
"required": ["path"],
"properties": {
"method": {
"_deprecated": true,
"type": "string"
},
"http_method": {
"type": "string"
},
"path": {
"type": "string"
},
"headers": {
"type": "object"
},
"params": {
"type": "object"
},
"body": {
"description": "body is deprecated, use schema",
"$ref": "#/definitions/subschema"
},
"schema": {
"$ref": "#/definitions/subschema"
}
}
},
"response": {
"type": "object",
"required": ["status"],
"properties": {
"status":{
"type": "integer"
},
"body": {
"description": "body is deprecated, use schema",
"$ref": "#/definitions/subschema"
},
"schema": {
"$ref": "#/definitions/subschema"
}
}
},
"examples": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["request", "response"],
"properties": {
"request": {
},
"response": {
}
}
}
}
}
}
================================================
FILE: resources/draft-03.json
================================================
{
"$schema" : "http://json-schema.org/draft-03/schema#",
"id" : "http://json-schema.org/draft-03/schema#",
"type" : "object",
"properties" : {
"type" : {
"type" : ["string", "array"],
"items" : {
"type" : ["string", {"$ref" : "#"}]
},
"uniqueItems" : true,
"default" : "any"
},
"properties" : {
"type" : "object",
"additionalProperties" : {"$ref" : "#"},
"default" : {}
},
"patternProperties" : {
"type" : "object",
"additionalProperties" : {"$ref" : "#"},
"default" : {}
},
"additionalProperties" : {
"type" : [{"$ref" : "#"}, "boolean"],
"default" : {}
},
"items" : {
"type" : [{"$ref" : "#"}, "array"],
"items" : {"$ref" : "#"},
"default" : {}
},
"additionalItems" : {
"type" : [{"$ref" : "#"}, "boolean"],
"default" : {}
},
"required" : {
"type" : "boolean",
"default" : false
},
"dependencies" : {
"type" : "object",
"additionalProperties" : {
"type" : ["string", "array", {"$ref" : "#"}],
"items" : {
"type" : "string"
}
},
"default" : {}
},
"minimum" : {
"type" : "number"
},
"maximum" : {
"type" : "number"
},
"exclusiveMinimum" : {
"type" : "boolean",
"default" : false
},
"exclusiveMaximum" : {
"type" : "boolean",
"default" : false
},
"minItems" : {
"type" : "integer",
"minimum" : 0,
"default" : 0
},
"maxItems" : {
"type" : "integer",
"minimum" : 0
},
"uniqueItems" : {
"type" : "boolean",
"default" : false
},
"pattern" : {
"type" : "string",
"format" : "regex"
},
"minLength" : {
"type" : "integer",
"minimum" : 0,
"default" : 0
},
"maxLength" : {
"type" : "integer"
},
"enum" : {
"type" : "array",
"minItems" : 1,
"uniqueItems" : true
},
"default" : {
"type" : "any"
},
"title" : {
"type" : "string"
},
"description" : {
"type" : "string"
},
"format" : {
"type" : "string"
},
"divisibleBy" : {
"type" : "number",
"minimum" : 0,
"exclusiveMinimum" : true,
"default" : 1
},
"disallow" : {
"type" : ["string", "array"],
"items" : {
"type" : ["string", {"$ref" : "#"}]
},
"uniqueItems" : true
},
"extends" : {
"type" : [{"$ref" : "#"}, "array"],
"items" : {"$ref" : "#"},
"default" : {}
},
"id" : {
"type" : "string",
"format" : "uri"
},
"$ref" : {
"type" : "string",
"format" : "uri"
},
"$schema" : {
"type" : "string",
"format" : "uri"
}
},
"dependencies" : {
"exclusiveMinimum" : "minimum",
"exclusiveMaximum" : "maximum"
},
"default" : {}
}
================================================
FILE: resources/draft-04.json
================================================
{
"id": "http://json-schema.org/draft-04/schema#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
================================================
FILE: sample_apis/album/cover_api.rb
================================================
# -*- encoding : utf-8 -*-
module AlbumServices
class Cover < Grape::API
format :json
desc 'Ping'
namespace :album do
get ':id/cover' do
{ cover: 'image' }
end
end
end
end
================================================
FILE: sample_apis/config.ru
================================================
require 'grape'
require 'grape-swagger'
require 'json'
Dir[File.expand_path('../**/*_api.rb', __FILE__)].each do |f|
puts "Requiring #{f}"
require f
end
module DummyServices
class API < Grape::API
prefix 'api'
format :json
mount DummyServices::Hello
mount DummyServices::Ping
mount DummyServices::Echo
mount DummyServices::Files
mount DummyServices::Reverse
mount AlbumServices::Cover
add_swagger_documentation # api_version: 'v1'
end
end
DummyServices::API.routes.each do |route|
p route
end
run DummyServices::API
================================================
FILE: sample_apis/echo_api.rb
================================================
# -*- encoding : utf-8 -*-
# This illustrates simple get w/ params and post w/ body services
# It also illustrates having two services w/ the same endpoint (just different HTTP methods)
module DummyServices
class Echo < Grape::API
format :json
content_type :txt, 'text/plain'
helpers do
def echo(message)
error!('Bad Request', 400) unless message
message
end
end
# curl localhost:5000/api/echo --get --data-urlencode 'msg={"one fish": "two fish"}' -vv
get '/echo' do
echo params[:msg]
end
# curl localhost:5000/api/echo -H 'Content-Type: text/plain' -d '{"red fish": "blue fish"}' -vv
post '/echo' do
echo env['api.request.body']
end
end
end
================================================
FILE: sample_apis/files_api.rb
================================================
# -*- encoding : utf-8 -*-
# This example should illustrate
# - Authentication
# - Expect: 100-continue
# - Binary data
# - Content negotiation
# - Etags
# - Collections
module DummyServices
class PartialRequestException < StandardError
attr_reader :http_status, :msg
def initialize(http_status, msg)
@http_status = http_status
@msg = msg
end
end
class Files < Grape::API
format :json
content_type :binary, 'application/octet-stream'
content_type :pdf, 'application/pdf'
before do
error!('Unauthorized', 401) unless env['HTTP_X_AUTH_TOKEN'] == '12345'
if env['HTTP_EXPECT'] == '100-continue'
# Can't use Content-Type because Grape tries to handle it, causing problems
case env['CONTENT_TYPE']
when 'application/pdf'
fail DummyServices::PartialRequestException.new(100, 'Continue')
when 'application/webm'
fail DummyServices::PartialRequestException.new(415, 'Unsupported Media Type')
else
fail DummyServices::PartialRequestException.new(417, 'Expectation Failed')
end
end
end
rescue_from DummyServices::PartialRequestException do |e|
Rack::Response.new([], e.http_status, {}).finish
end
namespace '/files' do
# curl localhost:5000/api/files/myfile.txt -H 'X-Auth-Token: 12345' -d @myfile.txt -vv
put ':name' do
params[:name]
end
end
end
end
================================================
FILE: sample_apis/hello_api.rb
================================================
# -*- encoding : utf-8 -*-
# This illustrates a simple get service
module DummyServices
class Hello < Grape::API
format :json
content_type :json, 'application/json'
desc 'Hello'
get '/hello' do
header 'Vary', 'Accept'
{ message: 'Hello World!' }
end
end
end
================================================
FILE: sample_apis/ping_api.rb
================================================
# -*- encoding : utf-8 -*-
# This illustrates a simple get service
module DummyServices
class Ping < Grape::API
format :json
desc 'Ping'
get '/ping' do
{ ping: 'pong' }
end
end
end
================================================
FILE: sample_apis/reverse_api.rb
================================================
# -*- encoding : utf-8 -*-
# This illustrates simple get w/ params and post w/ body services
# It also illustrates having two services w/ the same endpoint (just different HTTP methods)
module DummyServices
class Reverse < Grape::API
format :txt
helpers do
def echo(message)
error!('Bad Request', 400) unless message
message
end
end
# curl localhost:5000/api/echo -H 'Content-Type: application/json' -d '{"red fish": "blue fish"}' -vv
post '/reverse' do
echo(env['api.request.body']).reverse
end
end
end
================================================
FILE: sample_apis/user_api.rb
================================================
# -*- encoding : utf-8 -*-
# A siple JSON service to demonstrate request/response bodies
require 'securerandom'
module DummyServices
class Echo < Grape::API
format :json
post '/users' do
user = env['api.request.body']
user[:id] = SecureRandom.uuid
user
end
end
end
================================================
FILE: samples/README.md
================================================
Welcome to the Pacto usage samples!
We have a listing of [sample contracts](contracts/README.html).
Highlighted samples:
- *[Configuration](configuration.html)*: Shows the available Pacto configuration
- *[Generation](generation.html)*: Shows how to generate Contracts
- *[RSpec](rspec.html)*: Shows the usage of RSpec expectations for collaboration tests
See the Table of Contents (upper right corner) for a full list of available samples.
================================================
FILE: samples/Rakefile
================================================
require 'pacto/rake_task' # FIXME: This require turns on WebMock
WebMock.allow_net_connect!
================================================
FILE: samples/configuration.rb
================================================
# -*- encoding : utf-8 -*-
# Just require pacto to add it to your project.
require 'pacto'
# Pacto will disable live connections, so you will get an error if
# your code unexpectedly calls an service that was not stubbed. If you
# want to re-enable connections, run `WebMock.allow_net_connect!`
WebMock.allow_net_connect!
# Pacto can be configured via a block:
Pacto.configure do |c|
# Path for loading/storing contracts.
c.contracts_path = 'contracts'
# If the request matching should be strict (especially regarding HTTP Headers).
c.strict_matchers = true
# You can set the Ruby Logger used by Pacto.
c.logger = Pacto::Logger::SimpleLogger.instance
# (Deprecated) You can specify a callback for post-processing responses. Note that only one hook
# can be active, and specifying your own will disable ERB post-processing.
c.register_hook do |_contracts, request, _response|
puts "Received #{request}"
end
# Options to pass to the [json-schema-generator](https://github.com/maxlinc/json-schema-generator) while generating contracts.
c.generator_options = { schema_version: 'draft3' }
end
# You can also do inline configuration. This example tells the json-schema-generator to store default values in the schema.
Pacto.configuration.generator_options = { defaults: true }
# If you're using Pacto's rspec matchers you might want to configure a reset between each scenario
require 'pacto/rspec'
RSpec.configure do |c|
c.after(:each) { Pacto.clear! }
end
================================================
FILE: samples/consumer.rb
================================================
# -*- encoding : utf-8 -*-
require 'pacto'
Pacto.load_contracts 'contracts', 'http://localhost:5000'
WebMock.allow_net_connect!
interactions = Pacto.simulate_consumer :my_client do
request 'Ping'
request 'Echo', body: ->(body) { body.reverse },
headers: (proc do |headers|
headers['Content-Type'] = 'text/json'
headers['Accept'] = 'none'
headers
end)
end
puts interactions
================================================
FILE: samples/contracts/README.md
================================================
This folder contains sample contracts.
================================================
FILE: samples/contracts/contract.js
================================================
// Pacto Contracts describe the constraints we want to put on interactions between a consumer and a provider. It sets some expectations about the headers expected for both the request and response, the expected response status code. It also uses [json-schema](http://json-schema.org/) to define the allowable request body (if one should exist) and response body.
{
// The Request section comes first. In this case, we're just describing a simple get request that does not require any parameters or a request body.
"request": {
"headers": {
// A request must exactly match these headers for Pacto to believe the request matches the contract, unless `Pacto.configuration.strict_matchers` is false.
"Accept": "application/vnd.github.beta+json",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
},
// The `method` and `path` are required. The `path` may be an [rfc6570 URI template](http://tools.ietf.org/html/rfc6570) for more flexible matching.
"method": "get",
"path": "/repos/thoughtworks/pacto/readme"
},
"response": {
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Status": "200 OK",
"Cache-Control": "public, max-age=60, s-maxage=60",
"Etag": "\"fc8e78b0a9694de66d47317768b20820\"",
"Vary": "Accept, Accept-Encoding",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Expose-Headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
"Access-Control-Allow-Origin": "*"
},
"status": 200,
"body": {
"$schema": "http://json-schema.org/draft-03/schema#",
"description": "Generated from https://api.github.com/repos/thoughtworks/pacto/readme with shasum 3ae59164c6d9f84c0a81f21fb63e17b3b8ce6894",
"type": "object",
"required": true,
"properties": {
"name": {
"type": "string",
"required": true
},
"path": {
"type": "string",
"required": true
},
"sha": {
"type": "string",
"required": true
},
"size": {
"type": "integer",
"required": true
},
"url": {
"type": "string",
"required": true
},
"html_url": {
"type": "string",
"required": true
},
"git_url": {
"type": "string",
"required": true
},
"type": {
"type": "string",
"required": true
},
"content": {
"type": "string",
"required": true
},
"encoding": {
"type": "string",
"required": true
},
"_links": {
"type": "object",
"required": true,
"properties": {
"self": {
"type": "string",
"required": true
},
"git": {
"type": "string",
"required": true
},
"html": {
"type": "string",
"required": true
}
}
}
}
}
}
}
================================================
FILE: samples/contracts/get_album_cover.json
================================================
{
"request": {
"headers": {
},
"http_method": "get",
"path": "/api/album/{id}/cover"
},
"response": {
"headers": {
"Content-Type": "application/json"
},
"status": 200,
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"description": "Generated from http://localhost:5000/api/album/1/cover with shasum db640385d2b346db760dbfd78058101663197bcf",
"type": "object",
"required": true,
"properties": {
"cover": {
"type": "string",
"required": true
}
}
}
},
"examples": {
"default": {
"request": {
"method": "get",
"uri": "http://localhost:5000/api/album/1/cover",
"headers": {
"User-Agent": "Faraday v0.9.0",
"Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Accept": "*/*"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json",
"Content-Length": "17"
},
"body": "{\"cover\":\"image\"}"
}
}
},
"name": "Get Album Cover"
}
================================================
FILE: samples/contracts/localhost/api/echo.json
================================================
{
"name": "Echo",
"request": {
"headers": {
"Content-Type": "text/plain"
},
"http_method": "post",
"path": "/api/echo",
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"oneOf": [
{ "type": "string", "required": true },
{ "type": "object", "required": true }
]
}
},
"response": {
"status": 201,
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"oneOf": [
{ "type": "string", "required": true },
{ "type": "object", "required": true }
]
}
},
"examples": {
"foo": {
"request": {
"body": "foo"
},
"response": {
"body": "foo"
}
}
}
}
================================================
FILE: samples/contracts/localhost/api/ping.json
================================================
{
"name": "Ping",
"request": {
"headers": {
},
"http_method": "get",
"path": "/api/ping"
},
"response": {
"headers": {
"Content-Type": "application/json"
},
"status": 200,
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"description": "Generated from http://localhost:9292/api/ping with shasum 2cf3478c18e3ce877fb823ed435cb75b4a801aaa",
"type": "object",
"required": true,
"properties": {
"ping": {
"type": "string",
"required": true
}
}
}
},
"examples": {
"default": {
"request": {
},
"response": {
"body": {
"ping": "pong - from the example!"
}
}
}
}
}
================================================
FILE: samples/contracts/user.json
================================================
{
"name": "User",
"request": {
"headers": {
"Content-Type": "application/json"
},
"http_method": "post",
"path": "/api/users",
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"type": "object",
"properties": {
"firstName": {"type": "string", "required": true},
"lastName": {"type": "string", "required": true}
}
}
},
"response": {
"status": 201,
"schema": {
"$schema": "http://json-schema.org/draft-03/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "required": true },
"firstName": {"type": "string", "required": true},
"lastName": {"type": "string", "required": true}
}
}
},
"examples": {
"max": {
"request": {
"headers": {
"Content-Type": "application/json"
},
"body": {
"firstName": "Max",
"lastName": "Lincoln"
}
},
"response": {
"body": {
"id": "026ed411-6d12-4a76-a3c7-19758a872455",
"firstName": "Max",
"lastName": "Lincoln"
}
}
}
}
}
================================================
FILE: samples/cops.rb
================================================
# -*- encoding : utf-8 -*-
require 'rspec'
require 'rspec/autorun'
require 'pacto'
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
Pacto.validate!
# You can create a custom cop that investigates the request/response and sees if it complies with a
# contract. The cop should return a list of citations if it finds any problems.
class MyCustomCop
def investigate(_request, _response, contract)
citations = []
citations << 'Contract must have a request schema' if contract.request.schema.empty?
citations << 'Contract must have a response schema' if contract.response.schema.empty?
citations
end
end
Pacto::Cops.active_cops << MyCustomCop.new
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
contracts.stub_providers
puts contracts.simulate_consumers
# Or you can completely replace the default set of validators
Pacto::Cops.registered_cops.clear
Pacto::Cops.register_cop Pacto::Cops::ResponseBodyCop
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
puts contracts.simulate_consumers
================================================
FILE: samples/forensics.rb
================================================
# -*- encoding : utf-8 -*-
# Pacto has a few RSpec matchers to help you ensure a **consumer** and **producer** are
# interacting properly. First, let's setup the rspec suite.
require 'rspec/autorun' # Not generally needed
require 'pacto/rspec'
WebMock.allow_net_connect!
Pacto.validate!
Pacto.load_contracts('contracts', 'http://localhost:5000').stub_providers
# It's usually a good idea to reset Pacto between each scenario. `Pacto.reset` just clears the
# data and metrics about which services were called. `Pacto.clear!` also resets all configuration
# and plugins.
RSpec.configure do |c|
c.after(:each) { Pacto.reset }
end
# Pacto provides some RSpec matchers related to contract testing, like making sure
# Pacto didn't received any unrecognized requests (`have_unmatched_requests`) and that
# the HTTP requests matched up with the terms of the contract (`have_failed_investigations`).
describe Faraday do
let(:connection) { described_class.new(url: 'http://localhost:5000') }
it 'passes contract tests' do
connection.get '/api/ping'
expect(Pacto).to_not have_failed_investigations
expect(Pacto).to_not have_unmatched_requests
end
end
# There are also some matchers for collaboration testing, so you can make sure each scenario is
# calling the expected services and sending the right type of data.
describe Faraday do
let(:connection) { described_class.new(url: 'http://localhost:5000') }
before(:each) do
connection.get '/api/ping'
connection.post do |req|
req.url '/api/echo'
req.headers['Content-Type'] = 'application/json'
req.body = '{"foo": "bar"}'
end
end
it 'calls the ping service' do
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping').against_contract('Ping')
end
it 'sends data to the echo service' do
expect(Pacto).to have_investigated('Ping').with_response(body: hash_including('ping' => 'pong - from the example!'))
expect(Pacto).to have_investigated('Echo').with_request(body: hash_including('foo' => 'bar'))
echoed_body = { 'foo' => 'bar' }
expect(Pacto).to have_investigated('Echo').with_request(body: echoed_body).with_response(body: echoed_body)
end
end
================================================
FILE: samples/generation.rb
================================================
# -*- encoding : utf-8 -*-
# Some generation related [configuration](configuration.rb).
require 'pacto'
WebMock.allow_net_connect!
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
WebMock.allow_net_connect!
# Once we call `Pacto.generate!`, Pacto will record contracts for all requests it detects.
Pacto.generate!
# Now, if we run any code that makes an HTTP call (using an
# [HTTP library supported by WebMock](https://github.com/bblimke/webmock#supported-http-libraries))
# then Pacto will generate a Contract based on the HTTP request/response.
#
# This code snippet will generate a Contract and save it to `contracts/samples/contracts/localhost/api/ping.json`.
require 'faraday'
conn = Faraday.new(url: 'http://localhost:5000')
response = conn.get '/api/ping'
# We're getting back real data from GitHub, so this should be the actual file encoding.
puts response.body
# The generated contract will contain expectations based on the request/response we observed,
# including a best-guess at an appropriate json-schema. Our heuristics certainly aren't foolproof,
# so you might want to customize schema!
# Here's another sample that sends a post request.
conn.post do |req|
req.url '/api/echo'
req.headers['Content-Type'] = 'application/json'
req.body = '{"red fish": "blue fish"}'
end
# You can provide hints to Pacto to help it generate contracts. For example, Pacto doesn't have
# a good way to know a good name and correct URI template for the service. That means that Pacto
# will not know if two similar requests are for the same service or two different services, and
# will be forced to give names based on the URI that are not good display names.
# The hint below tells Pacto that requests to http://localhost:5000/album/1/cover and http://localhost:5000/album/2/cover
# are both going to the same service, which is known as "Get Album Cover". This hint will cause Pacto to
# generate a Contract for "Get Album Cover" and save it to `contracts/get_album_cover.json`, rather than two
# contracts that are stored at `contracts/localhost/album/1/cover.json` and `contracts/localhost/album/2/cover.json`.
Pacto::Generator.configure do |c|
c.hint 'Get Album Cover', http_method: :get, host: 'http://localhost:5000', path: '/api/album/{id}/cover'
end
conn.get '/api/album/1/cover'
conn.get '/api/album/2/cover'
================================================
FILE: samples/rake_tasks.sh
================================================
# # Rake tasks
# ## This is a test!
# [That](www.google.com) markdown works
bundle exec rake pacto:meta_validate['contracts']
bundle exec rake pacto:validate['http://localhost:5000','contracts']
================================================
FILE: samples/rspec.rb
================================================
# -*- encoding : utf-8 -*-
================================================
FILE: samples/samples.rb
================================================
# -*- encoding : utf-8 -*-
# # Overview
# Welcome to the Pacto usage samples!
# This document gives a quick overview of the main features.
#
# You can browse the Table of Contents (upper right corner) to view additional samples.
#
# In addition to this document, here are some highlighted samples:
# <ul>
# <li><a href="configuration">Configuration</a>: Shows all available configuration options</li>
# <li><a href="generation">Generation</a>: More details on generation</li>
# <li><a href="rspec">RSpec</a>: More samples for RSpec expectations</li>
# </ul>
# You can also find other samples using the Table of Content (upper right corner), including sample contracts.
# # Getting started
# Once you've installed the Pacto gem, you just require it. If you want, you can also require the Pacto rspec expectations.
require 'pacto'
require 'pacto/rspec'
# Pacto will disable live connections, so you will get an error if
# your code unexpectedly calls an service that was not stubbed. If you
# want to re-enable connections, run `WebMock.allow_net_connect!`
WebMock.allow_net_connect!
# Pacto can be configured via a block. The `contracts_path` option tells Pacto where it should load or save contracts. See the [Configuration](configuration.html) for all the available options.
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
# # Generating a Contract
# Calling `Pacto.generate!` enables contract generation.
# Pacto.generate!
# Now, if we run any code t
gitextract__ymcm0w_/
├── .gitignore
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── CONTRIBUTING.md
├── Gemfile
├── Guardfile
├── LICENSE.txt
├── Procfile
├── README.md
├── Rakefile
├── TODO.md
├── appveyor.yml
├── bin/
│ ├── pacto
│ └── pacto-server
├── changelog.md
├── docs/
│ ├── configuration.md
│ ├── consumer.md
│ ├── cops.md
│ ├── forensics.md
│ ├── generation.md
│ ├── rake_tasks.md
│ ├── rspec.md
│ ├── samples.md
│ ├── server.md
│ ├── server_cli.md
│ └── stenographer.md
├── features/
│ ├── configuration/
│ │ └── strict_matchers.feature
│ ├── evolve/
│ │ ├── README.md
│ │ └── existing_services.feature
│ ├── generate/
│ │ ├── README.md
│ │ └── generation.feature
│ ├── steps/
│ │ └── pacto_steps.rb
│ ├── stub/
│ │ ├── README.md
│ │ └── templates.feature
│ ├── support/
│ │ └── env.rb
│ └── validate/
│ ├── README.md
│ ├── meta_validation.feature
│ └── validation.feature
├── lib/
│ ├── pacto/
│ │ ├── actor.rb
│ │ ├── actors/
│ │ │ ├── from_examples.rb
│ │ │ └── json_generator.rb
│ │ ├── body_parsing.rb
│ │ ├── cli/
│ │ │ └── helpers.rb
│ │ ├── cli.rb
│ │ ├── consumer/
│ │ │ └── faraday_driver.rb
│ │ ├── consumer.rb
│ │ ├── contract.rb
│ │ ├── contract_factory.rb
│ │ ├── contract_files.rb
│ │ ├── contract_set.rb
│ │ ├── cops/
│ │ │ ├── body_cop.rb
│ │ │ ├── request_body_cop.rb
│ │ │ ├── response_body_cop.rb
│ │ │ ├── response_header_cop.rb
│ │ │ └── response_status_cop.rb
│ │ ├── cops.rb
│ │ ├── core/
│ │ │ ├── configuration.rb
│ │ │ ├── contract_registry.rb
│ │ │ ├── hook.rb
│ │ │ ├── http_middleware.rb
│ │ │ ├── investigation_registry.rb
│ │ │ ├── modes.rb
│ │ │ ├── pacto_request.rb
│ │ │ └── pacto_response.rb
│ │ ├── dash.rb
│ │ ├── erb_processor.rb
│ │ ├── errors.rb
│ │ ├── extensions.rb
│ │ ├── forensics/
│ │ │ ├── investigation_filter.rb
│ │ │ └── investigation_matcher.rb
│ │ ├── formats/
│ │ │ ├── legacy/
│ │ │ │ ├── contract.rb
│ │ │ │ ├── contract_builder.rb
│ │ │ │ ├── contract_factory.rb
│ │ │ │ ├── contract_generator.rb
│ │ │ │ ├── generator/
│ │ │ │ │ └── filters.rb
│ │ │ │ ├── generator_hint.rb
│ │ │ │ ├── request_clause.rb
│ │ │ │ └── response_clause.rb
│ │ │ └── swagger/
│ │ │ ├── contract.rb
│ │ │ ├── contract_factory.rb
│ │ │ ├── request_clause.rb
│ │ │ └── response_clause.rb
│ │ ├── generator.rb
│ │ ├── handlers/
│ │ │ ├── json_handler.rb
│ │ │ └── text_handler.rb
│ │ ├── hooks/
│ │ │ └── erb_hook.rb
│ │ ├── investigation.rb
│ │ ├── logger.rb
│ │ ├── meta_schema.rb
│ │ ├── observers/
│ │ │ └── stenographer.rb
│ │ ├── provider.rb
│ │ ├── rake_task.rb
│ │ ├── request_clause.rb
│ │ ├── request_pattern.rb
│ │ ├── resettable.rb
│ │ ├── response_clause.rb
│ │ ├── rspec.rb
│ │ ├── server/
│ │ │ ├── cli.rb
│ │ │ ├── config.rb
│ │ │ ├── proxy.rb
│ │ │ └── settings.rb
│ │ ├── server.rb
│ │ ├── stubs/
│ │ │ ├── uri_pattern.rb
│ │ │ └── webmock_adapter.rb
│ │ ├── test_helper.rb
│ │ ├── ui.rb
│ │ ├── uri.rb
│ │ └── version.rb
│ └── pacto.rb
├── pacto-server.gemspec
├── pacto.gemspec
├── resources/
│ ├── contract_schema.json
│ ├── draft-03.json
│ └── draft-04.json
├── sample_apis/
│ ├── album/
│ │ └── cover_api.rb
│ ├── config.ru
│ ├── echo_api.rb
│ ├── files_api.rb
│ ├── hello_api.rb
│ ├── ping_api.rb
│ ├── reverse_api.rb
│ └── user_api.rb
├── samples/
│ ├── README.md
│ ├── Rakefile
│ ├── configuration.rb
│ ├── consumer.rb
│ ├── contracts/
│ │ ├── README.md
│ │ ├── contract.js
│ │ ├── get_album_cover.json
│ │ ├── localhost/
│ │ │ └── api/
│ │ │ ├── echo.json
│ │ │ └── ping.json
│ │ └── user.json
│ ├── cops.rb
│ ├── forensics.rb
│ ├── generation.rb
│ ├── rake_tasks.sh
│ ├── rspec.rb
│ ├── samples.rb
│ ├── scripts/
│ │ ├── bootstrap
│ │ └── wrapper
│ ├── server.rb
│ ├── server_cli.sh
│ └── stenographer.rb
├── spec/
│ ├── coveralls_helper.rb
│ ├── fabricators/
│ │ ├── contract_fabricator.rb
│ │ ├── http_fabricator.rb
│ │ └── webmock_fabricator.rb
│ ├── fixtures/
│ │ └── contracts/
│ │ ├── deprecated/
│ │ │ └── deprecated_contract.json
│ │ ├── legacy/
│ │ │ ├── contract.json
│ │ │ ├── contract_with_examples.json
│ │ │ ├── simple_contract.json
│ │ │ ├── strict_contract.json
│ │ │ └── templating_contract.json
│ │ └── swagger/
│ │ └── petstore.yaml
│ ├── integration/
│ │ ├── e2e_spec.rb
│ │ ├── forensics/
│ │ │ └── integration_matcher_spec.rb
│ │ ├── rspec_spec.rb
│ │ └── templating_spec.rb
│ ├── spec_helper.rb
│ └── unit/
│ ├── actors/
│ │ ├── from_examples_spec.rb
│ │ └── json_generator_spec.rb
│ └── pacto/
│ ├── actor_spec.rb
│ ├── configuration_spec.rb
│ ├── consumer/
│ │ └── faraday_driver_spec.rb
│ ├── contract_factory_spec.rb
│ ├── contract_files_spec.rb
│ ├── contract_set_spec.rb
│ ├── contract_spec.rb
│ ├── cops/
│ │ ├── body_cop_spec.rb
│ │ ├── response_header_cop_spec.rb
│ │ └── response_status_cop_spec.rb
│ ├── cops_spec.rb
│ ├── core/
│ │ ├── configuration_spec.rb
│ │ ├── contract_registry_spec.rb
│ │ ├── http_middleware_spec.rb
│ │ ├── investigation_spec.rb
│ │ └── modes_spec.rb
│ ├── erb_processor_spec.rb
│ ├── extensions_spec.rb
│ ├── formats/
│ │ ├── legacy/
│ │ │ ├── contract_builder_spec.rb
│ │ │ ├── contract_factory_spec.rb
│ │ │ ├── contract_generator_spec.rb
│ │ │ ├── contract_spec.rb
│ │ │ ├── generator/
│ │ │ │ └── filters_spec.rb
│ │ │ ├── request_clause_spec.rb
│ │ │ └── response_clause_spec.rb
│ │ └── swagger/
│ │ ├── contract_factory_spec.rb
│ │ └── contract_spec.rb
│ ├── hooks/
│ │ └── erb_hook_spec.rb
│ ├── investigation_registry_spec.rb
│ ├── logger_spec.rb
│ ├── meta_schema_spec.rb
│ ├── pacto_spec.rb
│ ├── request_pattern_spec.rb
│ ├── stubs/
│ │ ├── observers/
│ │ │ └── stenographer_spec.rb
│ │ ├── uri_pattern_spec.rb
│ │ └── webmock_adapter_spec.rb
│ └── uri_spec.rb
└── tasks/
└── release.rake
SYMBOL INDEX (581 symbols across 118 files)
FILE: features/support/env.rb
class PactoWorld (line 16) | class PactoWorld
FILE: lib/pacto.rb
type Pacto (line 62) | module Pacto
function configuration (line 64) | def configuration
function contract_registry (line 68) | def contract_registry
function reset (line 73) | def reset
function clear! (line 79) | def clear!
function configure (line 86) | def configure
function contracts_for (line 90) | def contracts_for(request_signature)
function validate_contract (line 95) | def validate_contract(contract)
function load_contract (line 100) | def load_contract(contract_path, host, format = :legacy)
function load_contracts (line 104) | def load_contracts(contracts_path, host, format = :legacy)
FILE: lib/pacto/actor.rb
type Pacto (line 2) | module Pacto
class Actor (line 3) | class Actor
FILE: lib/pacto/actors/from_examples.rb
type Pacto (line 2) | module Pacto
type Actors (line 3) | module Actors
class FirstExampleSelector (line 4) | class FirstExampleSelector
method select (line 5) | def self.select(examples, _values)
class RandomExampleSelector (line 9) | class RandomExampleSelector
method select (line 10) | def self.select(examples, _values)
class NamedExampleSelector (line 14) | class NamedExampleSelector
method select (line 15) | def self.select(examples, values)
class FromExamples (line 24) | class FromExamples < Actor
method initialize (line 25) | def initialize(fallback_actor = JSONGenerator.new, selector = Pact...
method build_request (line 30) | def build_request(contract, values = {})
method build_response (line 45) | def build_response(contract, values = {})
method example_uri_values (line 56) | def example_uri_values(contract)
FILE: lib/pacto/actors/json_generator.rb
type Pacto (line 2) | module Pacto
type Actors (line 3) | module Actors
class JSONGenerator (line 4) | class JSONGenerator < Actor
method build_request (line 5) | def build_request(contract, values = {})
method build_response (line 13) | def build_response(contract, _values = {})
FILE: lib/pacto/body_parsing.rb
type Pacto (line 3) | module Pacto
type Handlers (line 4) | module Handlers
type BodyParsing (line 9) | module BodyParsing
function raw_body (line 10) | def raw_body
function parsed_body (line 17) | def parsed_body
function content_type (line 23) | def content_type
function body_handler (line 27) | def body_handler
FILE: lib/pacto/cli.rb
type Pacto (line 5) | module Pacto
type CLI (line 6) | module CLI
class Main (line 7) | class Main < Thor
method meta_validate (line 11) | def meta_validate(*contracts)
method validate (line 31) | def validate(*contracts)
method validation_summary (line 50) | def validation_summary(contracts, invalid_contracts)
method contract_is_valid? (line 58) | def contract_is_valid?(contract_file, host)
FILE: lib/pacto/cli/helpers.rb
type Pacto (line 1) | module Pacto
type CLI (line 2) | module CLI
type Helpers (line 3) | module Helpers
function each_contract (line 4) | def each_contract(*contracts)
FILE: lib/pacto/consumer.rb
type Pacto (line 2) | module Pacto
function consumers (line 3) | def self.consumers
function simulate_consumer (line 7) | def self.simulate_consumer(consumer_name = :consumer, &block)
class Consumer (line 12) | class Consumer
method initialize (line 16) | def initialize(name = :consumer)
method simulate (line 20) | def simulate(&block)
method playback (line 24) | def playback(stenographer_script)
method reset! (line 29) | def self.reset!
method actor (line 33) | def actor
method actor= (line 37) | def actor=(actor)
method request (line 42) | def request(contract, data = {})
method reenact (line 51) | def reenact(contract, data = {})
method driver (line 58) | def driver
method driver= (line 63) | def driver=(driver)
method build_request (line 69) | def build_request(contract, data = {})
FILE: lib/pacto/consumer/faraday_driver.rb
type Pacto (line 2) | module Pacto
class Consumer (line 3) | class Consumer
class FaradayDriver (line 4) | class FaradayDriver
method execute (line 7) | def execute(req)
method faraday_to_pacto_response (line 27) | def faraday_to_pacto_response(faraday_response)
FILE: lib/pacto/contract.rb
type Pacto (line 2) | module Pacto
type Contract (line 3) | module Contract
function adapter (line 17) | def adapter
function consumer (line 21) | def consumer
function provider (line 25) | def provider
function examples? (line 29) | def examples?
function stub_contract! (line 33) | def stub_contract!(values = {})
function simulate_request (line 38) | def simulate_request
function validate_response (line 44) | def validate_response(request, response)
function matches? (line 48) | def matches?(request_signature)
function request_pattern (line 52) | def request_pattern
function response_for (line 56) | def response_for(pacto_request)
function execute (line 60) | def execute(additional_values = {})
FILE: lib/pacto/contract_factory.rb
type Pacto (line 2) | module Pacto
class ContractFactory (line 3) | class ContractFactory
method initialize (line 7) | def initialize
method add_factory (line 11) | def add_factory(format, factory)
method remove_factory (line 15) | def remove_factory(format)
method build (line 19) | def build(contract_files, host, format = :legacy)
method load_contracts (line 26) | def load_contracts(contracts_path, host, format = :legacy)
FILE: lib/pacto/contract_files.rb
type Pacto (line 3) | module Pacto
class ContractFiles (line 4) | class ContractFiles
method for (line 5) | def self.for(path)
FILE: lib/pacto/contract_set.rb
type Pacto (line 2) | module Pacto
class ContractSet (line 3) | class ContractSet < Set
method stub_providers (line 4) | def stub_providers(values = {})
method simulate_consumers (line 8) | def simulate_consumers
FILE: lib/pacto/cops.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
function reset! (line 7) | def reset!
function register_cop (line 11) | def register_cop(cop)
function registered_cops (line 16) | def registered_cops
function active_cops (line 20) | def active_cops
function investigate (line 24) | def investigate(request_signature, pacto_response)
function perform_investigation (line 37) | def perform_investigation(request, response, contract)
FILE: lib/pacto/cops/body_cop.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
class BodyCop (line 4) | class BodyCop
method validates (line 7) | def self.validates(clause)
method investigate (line 12) | def self.investigate(request, response, contract)
FILE: lib/pacto/cops/request_body_cop.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
class RequestBodyCop (line 4) | class RequestBodyCop < BodyCop
FILE: lib/pacto/cops/response_body_cop.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
class ResponseBodyCop (line 4) | class ResponseBodyCop < BodyCop
FILE: lib/pacto/cops/response_header_cop.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
class ResponseHeaderCop (line 4) | class ResponseHeaderCop
method investigate (line 5) | def self.investigate(_request, response, contract)
FILE: lib/pacto/cops/response_status_cop.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
class ResponseStatusCop (line 4) | class ResponseStatusCop
method investigate (line 5) | def self.investigate(_request, response, contract)
FILE: lib/pacto/core/configuration.rb
type Pacto (line 2) | module Pacto
class Configuration (line 3) | class Configuration
method initialize (line 10) | def initialize # rubocop:disable Metrics/MethodLength
method logger (line 26) | def logger
method stenographer_log_file (line 30) | def stenographer_log_file
method register_hook (line 34) | def register_hook(hook = nil, &block)
method new_simple_logger (line 45) | def new_simple_logger
FILE: lib/pacto/core/contract_registry.rb
type Pacto (line 2) | module Pacto
class ContractNotFound (line 3) | class ContractNotFound < StandardError; end
class ContractRegistry (line 5) | class ContractRegistry < Set
method register (line 8) | def register(contract)
method find_by_name (line 14) | def find_by_name(name)
method contracts_for (line 20) | def contracts_for(request_signature)
FILE: lib/pacto/core/hook.rb
type Pacto (line 2) | module Pacto
class Hook (line 3) | class Hook
method initialize (line 4) | def initialize(&block)
method process (line 8) | def process(contracts, request_signature, response)
FILE: lib/pacto/core/http_middleware.rb
type Pacto (line 4) | module Pacto
type Core (line 5) | module Core
class HTTPMiddleware (line 6) | class HTTPMiddleware
method process (line 10) | def process(request, response)
FILE: lib/pacto/core/investigation_registry.rb
type Pacto (line 2) | module Pacto
class InvestigationRegistry (line 3) | class InvestigationRegistry
method initialize (line 9) | def initialize
method reset! (line 13) | def self.reset!
method reset! (line 17) | def reset!
method validated? (line 22) | def validated?(request_pattern)
method register_investigation (line 29) | def register_investigation(investigation)
method unmatched_investigations (line 37) | def unmatched_investigations
method failed_investigations (line 43) | def failed_investigations
method stenographer (line 51) | def stenographer
method create_stenographer (line 55) | def create_stenographer
FILE: lib/pacto/core/modes.rb
type Pacto (line 2) | module Pacto
function generate! (line 4) | def generate!
function stop_generating! (line 8) | def stop_generating!
function generating? (line 12) | def generating?
function validate! (line 16) | def validate!
function stop_validating! (line 20) | def stop_validating!
function validating? (line 24) | def validating?
function modes (line 30) | def modes
FILE: lib/pacto/core/pacto_request.rb
type Pacto (line 4) | module Pacto
class PactoRequest (line 5) | class PactoRequest
method initialize (line 11) | def initialize(data)
method to_hash (line 20) | def to_hash
method to_s (line 29) | def to_s
method relative_uri (line 36) | def relative_uri
method normalize (line 42) | def normalize
FILE: lib/pacto/core/pacto_response.rb
type Pacto (line 2) | module Pacto
class PactoResponse (line 3) | class PactoResponse
method initialize (line 10) | def initialize(data)
method to_hash (line 17) | def to_hash
method to_s (line 25) | def to_s
FILE: lib/pacto/dash.rb
type Pacto (line 4) | module Pacto
class Dash (line 5) | class Dash < Hashie::Dash
FILE: lib/pacto/erb_processor.rb
type Pacto (line 2) | module Pacto
class ERBProcessor (line 3) | class ERBProcessor
method process (line 5) | def process(contract, values = {})
method hash_binding (line 14) | def hash_binding(values)
FILE: lib/pacto/errors.rb
type Pacto (line 1) | module Pacto
class InvalidContract (line 2) | class InvalidContract < ArgumentError
method initialize (line 5) | def initialize(errors)
method message (line 9) | def message
type Errors (line 14) | module Errors
function formatted_trace (line 33) | def self.formatted_trace(exception)
function formatted_exception (line 59) | def self.formatted_exception(exception, title = 'Exception')
FILE: lib/pacto/extensions.rb
type Pacto (line 2) | module Pacto
type Extensions (line 3) | module Extensions
function normalize_header_keys (line 13) | def self.normalize_header_keys(headers)
FILE: lib/pacto/forensics/investigation_filter.rb
type Pacto (line 2) | module Pacto
type Forensics (line 3) | module Forensics
class FilterExhaustedError (line 4) | class FilterExhaustedError < StandardError
method initialize (line 7) | def initialize(msg, filter, suspects = [])
class InvestigationFilter (line 18) | class InvestigationFilter
method initialize (line 23) | def initialize(investigations, track_suspects = true)
method with_name (line 30) | def with_name(contract_name)
method with_request (line 39) | def with_request(request_constraints)
method with_response (line 47) | def with_response(response_constraints)
method successful_investigations (line 55) | def successful_investigations
method unsuccessful_investigations (line 59) | def unsuccessful_investigations
method filter_request_section (line 65) | def filter_request_section(section, filter)
method filter_response_section (line 76) | def filter_response_section(section, filter)
FILE: lib/pacto/forensics/investigation_matcher.rb
function describe (line 23) | def describe(obj)
FILE: lib/pacto/formats/legacy/contract.rb
type Pacto (line 5) | module Pacto
type Formats (line 6) | module Formats
type Legacy (line 7) | module Legacy
class Contract (line 8) | class Contract < Pacto::Dash
method initialize (line 28) | def initialize(opts)
method freeze (line 40) | def freeze
FILE: lib/pacto/formats/legacy/contract_builder.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
class ContractBuilder (line 5) | class ContractBuilder < Hashie::Dash # rubocop:disable Metrics/Cla...
method initialize (line 9) | def initialize(options = {})
method name= (line 16) | def name=(name)
method add_example (line 20) | def add_example(name, pacto_request, pacto_response)
method infer_all (line 27) | def infer_all
method infer_name (line 33) | def infer_name
method infer_schemas (line 44) | def infer_schemas
method without_examples (line 56) | def without_examples
method generate_contract (line 61) | def generate_contract(request, response)
method generate_request (line 68) | def generate_request(request, response)
method generate_response (line 80) | def generate_response(request, response)
method build_hash (line 89) | def build_hash
method build (line 96) | def build(&block)
method example_and_hint (line 102) | def example_and_hint
method exclude_examples? (line 108) | def exclude_examples?
method generate_schema (line 112) | def generate_schema(body, generator_options = Pacto.configuratio...
method clean (line 119) | def clean(data)
method hint_for (line 123) | def hint_for(pacto_request)
FILE: lib/pacto/formats/legacy/contract_factory.rb
type Pacto (line 4) | module Pacto
type Formats (line 5) | module Formats
type Legacy (line 6) | module Legacy
class ContractFactory (line 8) | class ContractFactory
method initialize (line 11) | def initialize(options = {})
method build_from_file (line 15) | def build_from_file(contract_path, host)
method files_for (line 28) | def files_for(contracts_dir)
method body_to_schema (line 43) | def body_to_schema(definition, section, file)
method method_to_http_method (line 51) | def method_to_http_method(definition, file)
FILE: lib/pacto/formats/legacy/contract_generator.rb
type Pacto (line 6) | module Pacto
type Formats (line 7) | module Formats
type Legacy (line 8) | module Legacy
class ContractGenerator (line 9) | class ContractGenerator
method initialize (line 12) | def initialize(_schema_version = 'draft3',
method generate (line 22) | def generate(pacto_request, pacto_response)
method generate_from_partial_contract (line 42) | def generate_from_partial_contract(request_file, host)
method save (line 48) | def save(source, request, response)
method load_contract_file (line 63) | def load_contract_file(pacto_request)
FILE: lib/pacto/formats/legacy/generator/filters.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
type Generator (line 5) | module Generator
class Filters (line 6) | class Filters
method filter_request_headers (line 24) | def filter_request_headers(request, response)
method filter_response_headers (line 37) | def filter_response_headers(_request, response)
FILE: lib/pacto/formats/legacy/generator_hint.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
class GeneratorHint (line 5) | class GeneratorHint < Pacto::Dash
method initialize (line 15) | def initialize(data)
method matches? (line 23) | def matches?(pacto_request)
method slugify (line 30) | def slugify(path)
FILE: lib/pacto/formats/legacy/request_clause.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
class RequestClause (line 5) | class RequestClause < Pacto::Dash
class Data (line 13) | class Data < Pacto::Dash
method initialize (line 22) | def initialize(data)
method freeze (line 32) | def freeze
FILE: lib/pacto/formats/legacy/response_clause.rb
type Pacto (line 1) | module Pacto
type Formats (line 2) | module Formats
type Legacy (line 3) | module Legacy
class ResponseClause (line 4) | class ResponseClause
class Data (line 12) | class Data < Pacto::Dash
method initialize (line 18) | def initialize(data)
method freeze (line 24) | def freeze
FILE: lib/pacto/formats/swagger/contract.rb
type Pacto (line 6) | module Pacto
type Formats (line 7) | module Formats
type Swagger (line 8) | module Swagger
class Contract (line 9) | class Contract < Pacto::Dash
method initialize (line 31) | def initialize(swagger_api_operation, base_data = {}) # rubocop:...
method build_examples (line 61) | def build_examples(response)
FILE: lib/pacto/formats/swagger/contract_factory.rb
type Pacto (line 5) | module Pacto
type Formats (line 6) | module Formats
type Swagger (line 7) | module Swagger
class ContractFactory (line 9) | class ContractFactory
method load_hints (line 12) | def load_hints(_contract_path, _host = nil)
method build_from_file (line 16) | def build_from_file(contract_path, host = nil)
method files_for (line 29) | def files_for(contracts_dir)
FILE: lib/pacto/formats/swagger/request_clause.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Swagger (line 4) | module Swagger
class RequestClause (line 5) | class RequestClause
method initialize (line 14) | def initialize(swagger_api_operation, base_data = {})
method schema (line 20) | def schema
method params (line 26) | def params
method headers (line 32) | def headers
method to_hash (line 38) | def to_hash
method body_parameter (line 46) | def body_parameter
FILE: lib/pacto/formats/swagger/response_clause.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Swagger (line 4) | module Swagger
class ResponseClause (line 5) | class ResponseClause
method initialize (line 12) | def initialize(swagger_response, _base_data = {})
method status (line 16) | def status
method headers (line 20) | def headers
method schema (line 24) | def schema
FILE: lib/pacto/generator.rb
type Pacto (line 5) | module Pacto
type Generator (line 6) | module Generator
function contract_generator (line 11) | def contract_generator
function schema_generator (line 16) | def schema_generator
function configuration (line 20) | def configuration
function configure (line 24) | def configure
function hint_for (line 28) | def hint_for(pacto_request)
class Configuration (line 33) | class Configuration
method initialize (line 36) | def initialize
method hint (line 40) | def hint(name, hint_data)
FILE: lib/pacto/handlers/json_handler.rb
type Pacto (line 3) | module Pacto
type Handlers (line 4) | module Handlers
type JSONHandler (line 5) | module JSONHandler
function raw (line 7) | def raw(body)
function parse (line 11) | def parse(body)
FILE: lib/pacto/handlers/text_handler.rb
type Pacto (line 1) | module Pacto
type Handlers (line 2) | module Handlers
type TextHandler (line 3) | module TextHandler
function raw (line 5) | def raw(body)
function parse (line 9) | def parse(body)
FILE: lib/pacto/hooks/erb_hook.rb
type Pacto (line 4) | module Pacto
type Hooks (line 5) | module Hooks
class ERBHook (line 6) | class ERBHook < Pacto::Hook
method initialize (line 7) | def initialize
method process (line 11) | def process(contracts, request_signature, response)
FILE: lib/pacto/investigation.rb
type Pacto (line 2) | module Pacto
class Investigation (line 3) | class Investigation
method initialize (line 7) | def initialize(request, response, contract = nil, citations = nil)
method successful? (line 14) | def successful?
method against_contract? (line 18) | def against_contract?(contract_pattern)
method to_s (line 29) | def to_s
method summary (line 40) | def summary
FILE: lib/pacto/logger.rb
type Pacto (line 4) | module Pacto
type Logger (line 5) | module Logger
function logger (line 6) | def logger
class SimpleLogger (line 10) | class SimpleLogger
method initialize (line 16) | def initialize
method log (line 20) | def log(log)
method level= (line 26) | def level=(level)
method level (line 30) | def level
method default_level (line 36) | def default_level
method log_levels (line 40) | def log_levels
FILE: lib/pacto/meta_schema.rb
type Pacto (line 2) | module Pacto
class MetaSchema (line 3) | class MetaSchema
method initialize (line 6) | def initialize(engine = JSON::Validator)
method validate (line 20) | def validate(definition)
FILE: lib/pacto/observers/stenographer.rb
type Pacto (line 2) | module Pacto
type Observers (line 3) | module Observers
class Stenographer (line 4) | class Stenographer
method initialize (line 5) | def initialize(output)
method log_investigation (line 9) | def log_investigation(investigation)
method name_for (line 25) | def name_for(contract, request)
method number_of_citations (line 30) | def number_of_citations(investigation)
method values_for (line 36) | def values_for(_contract, request)
FILE: lib/pacto/provider.rb
type Pacto (line 2) | module Pacto
function providers (line 3) | def self.providers
class Provider (line 7) | class Provider
method reset! (line 10) | def self.reset!
method actor (line 14) | def actor
method actor= (line 18) | def actor=(actor)
method response_for (line 23) | def response_for(contract, data = {})
FILE: lib/pacto/rake_task.rb
type Pacto (line 9) | module Pacto
class RakeTask (line 10) | class RakeTask
method initialize (line 16) | def initialize
method run (line 21) | def run(task, args, opts = {})
method install (line 25) | def install
method validate_task (line 34) | def validate_task
method generate_task (line 43) | def generate_task
method meta_validate (line 54) | def meta_validate
method generate_contracts (line 65) | def generate_contracts(input_dir, output_dir, host)
FILE: lib/pacto/request_clause.rb
type Pacto (line 2) | module Pacto
type RequestClause (line 3) | module RequestClause
function http_method= (line 13) | def http_method=(method)
function uri (line 17) | def uri(values = {})
function normalize (line 31) | def normalize(method)
FILE: lib/pacto/request_pattern.rb
type Pacto (line 2) | module Pacto
class RequestPattern (line 3) | class RequestPattern < WebMock::RequestPattern
method for (line 6) | def self.for(base_request)
method initialize (line 10) | def initialize(http_method, uri_template)
method to_s (line 15) | def to_s
FILE: lib/pacto/resettable.rb
type Pacto (line 2) | module Pacto
type Resettable (line 4) | module Resettable
function resettables (line 5) | def self.resettables
function extended (line 9) | def self.extended(base)
function included (line 13) | def self.included(base)
function reset_all (line 17) | def self.reset_all
FILE: lib/pacto/response_clause.rb
type Pacto (line 2) | module Pacto
type ResponseClause (line 3) | module ResponseClause
FILE: lib/pacto/rspec.rb
function validated? (line 60) | def validated?(_request_pattern)
function investigation_citations (line 66) | def investigation_citations
function successfully? (line 70) | def successfully?
function contract_matches? (line 74) | def contract_matches?
FILE: lib/pacto/server.rb
type Pacto (line 7) | module Pacto
type Server (line 8) | module Server
class HTTP (line 9) | class HTTP < Reel::Server::HTTP
method initialize (line 13) | def initialize(host = '127.0.0.1', port = 3000, options = {})
method on_connection (line 20) | def on_connection(connection)
FILE: lib/pacto/server/cli.rb
type Pacto (line 4) | module Pacto
type Server (line 5) | module Server
class CLI (line 6) | class CLI < Thor
method server_options (line 25) | def server_options
method stub (line 38) | def stub(*_contracts)
method proxy (line 49) | def proxy(*_contracts)
method setup_interrupt (line 58) | def setup_interrupt
FILE: lib/pacto/server/proxy.rb
type Pacto (line 1) | module Pacto
type Server (line 2) | module Server
type Proxy (line 3) | module Proxy
function proxy_request (line 4) | def proxy_request(pacto_request)
function prepare_to_forward (line 12) | def prepare_to_forward(pacto_request)
function rewrite (line 25) | def rewrite(body)
function forward (line 33) | def forward(pacto_request)
function prepare_to_respond (line 37) | def prepare_to_respond(pacto_response)
function host_for (line 43) | def host_for(pacto_request)
FILE: lib/pacto/server/settings.rb
type Pacto (line 2) | module Pacto
type Server (line 3) | module Server
type Settings (line 4) | module Settings
function options_parser (line 5) | def options_parser(opts, options) # rubocop:disable MethodLength
class OptionHandler (line 28) | class OptionHandler
method initialize (line 31) | def initialize(port, logger, config = {})
method token_map (line 35) | def token_map
method prepare_contracts (line 43) | def prepare_contracts(contracts)
method handle (line 47) | def handle(options) # rubocop:disable Metrics/CyclomaticComplexi...
FILE: lib/pacto/stubs/uri_pattern.rb
type Pacto (line 2) | module Pacto
class UriPattern (line 3) | class UriPattern
method for (line 5) | def for(request, strict = Pacto.configuration.strict_matchers)
method build_template_uri_pattern (line 11) | def build_template_uri_pattern(request, strict)
method fail_deprecations (line 25) | def fail_deprecations(request)
FILE: lib/pacto/stubs/webmock_adapter.rb
type Pacto (line 2) | module Pacto
type Adapters (line 3) | module Adapters
type WebMock (line 4) | module WebMock
class PactoRequest (line 5) | class PactoRequest < Pacto::PactoRequest
method initialize (line 9) | def initialize(webmock_request_signature)
method params (line 13) | def params
method path (line 17) | def path
class PactoResponse (line 22) | class PactoResponse < Pacto::PactoResponse
method initialize (line 26) | def initialize(webmock_response)
method headers (line 30) | def headers
method status (line 34) | def status
type Stubs (line 41) | module Stubs
class WebMockAdapter (line 42) | class WebMockAdapter
method initialize (line 45) | def initialize(middleware)
method stub_request! (line 53) | def stub_request!(contract)
method reset! (line 74) | def self.reset!
method process_hooks (line 79) | def process_hooks(webmock_request_signature, webmock_response)
method format_body (line 87) | def format_body(body)
method strict_details (line 95) | def strict_details(request)
method webmock_params_key (line 102) | def webmock_params_key(request)
FILE: lib/pacto/test_helper.rb
type Pacto (line 9) | module Pacto
type TestHelper (line 10) | module TestHelper
function with_pacto (line 28) | def with_pacto(args = {})
FILE: lib/pacto/ui.rb
type Pacto (line 4) | module Pacto
type UI (line 5) | module UI
function shell (line 16) | def self.shell
function deprecation (line 20) | def self.deprecation(msg)
function colorize (line 24) | def self.colorize(msg, color)
function colorize_method (line 30) | def self.colorize_method(method)
FILE: lib/pacto/uri.rb
type Pacto (line 2) | module Pacto
class URI (line 3) | class URI
method for (line 4) | def self.for(host, path, params = {})
FILE: lib/pacto/version.rb
type Pacto (line 2) | module Pacto
FILE: sample_apis/album/cover_api.rb
type AlbumServices (line 2) | module AlbumServices
class Cover (line 3) | class Cover < Grape::API
FILE: sample_apis/echo_api.rb
type DummyServices (line 4) | module DummyServices
class Echo (line 5) | class Echo < Grape::API
method echo (line 10) | def echo(message)
FILE: sample_apis/files_api.rb
type DummyServices (line 9) | module DummyServices
class PartialRequestException (line 10) | class PartialRequestException < StandardError
method initialize (line 12) | def initialize(http_status, msg)
class Files (line 18) | class Files < Grape::API
FILE: sample_apis/hello_api.rb
type DummyServices (line 3) | module DummyServices
class Hello (line 4) | class Hello < Grape::API
FILE: sample_apis/ping_api.rb
type DummyServices (line 3) | module DummyServices
class Ping (line 4) | class Ping < Grape::API
FILE: sample_apis/reverse_api.rb
type DummyServices (line 4) | module DummyServices
class Reverse (line 5) | class Reverse < Grape::API
method echo (line 9) | def echo(message)
FILE: sample_apis/user_api.rb
type DummyServices (line 6) | module DummyServices
class Echo (line 7) | class Echo < Grape::API
FILE: samples/cops.rb
class MyCustomCop (line 13) | class MyCustomCop
method investigate (line 14) | def investigate(_request, _response, contract)
FILE: spec/integration/e2e_spec.rb
function get_json (line 49) | def get_json(url)
FILE: spec/integration/forensics/integration_matcher_spec.rb
type Pacto (line 4) | module Pacto
function expect_to_raise (line 9) | def expect_to_raise(message_pattern = nil, &blk)
function json_response (line 13) | def json_response(url)
function play_bad_response (line 20) | def play_bad_response
FILE: spec/integration/rspec_spec.rb
function expect_to_raise (line 14) | def expect_to_raise(message_pattern = nil, &blk)
function json_response (line 18) | def json_response(url)
function play_bad_response (line 25) | def play_bad_response
FILE: spec/spec_helper.rb
function default_pacto_format (line 24) | def default_pacto_format
function contracts_folder (line 28) | def contracts_folder(format = default_pacto_format)
function contract_file (line 32) | def contract_file(name, format = default_pacto_format)
function sample_contract (line 38) | def sample_contract
FILE: spec/unit/actors/from_examples_spec.rb
type Pacto (line 2) | module Pacto
type Actors (line 3) | module Actors
FILE: spec/unit/actors/json_generator_spec.rb
type Pacto (line 24) | module Pacto
type Actors (line 25) | module Actors
FILE: spec/unit/pacto/configuration_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/consumer/faraday_driver_spec.rb
type Pacto (line 2) | module Pacto
class Consumer (line 3) | class Consumer
FILE: spec/unit/pacto/contract_factory_spec.rb
type Pacto (line 4) | module Pacto
class CustomContractFactory (line 25) | class CustomContractFactory
method initialize (line 26) | def initialize(dummy_contract)
method build_from_file (line 30) | def build_from_file(_contract_path, _host)
class MultiContractFactory (line 47) | class MultiContractFactory
method initialize (line 48) | def initialize(contracts)
method build_from_file (line 52) | def build_from_file(_contract_path, _host)
FILE: spec/unit/pacto/contract_files_spec.rb
type Pacto (line 5) | module Pacto
FILE: spec/unit/pacto/contract_set_spec.rb
type Pacto (line 4) | module Pacto
FILE: spec/unit/pacto/cops/body_cop_spec.rb
type Pacto (line 97) | module Pacto
type Cops (line 98) | module Cops
FILE: spec/unit/pacto/cops/response_header_cop_spec.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
FILE: spec/unit/pacto/cops/response_status_cop_spec.rb
type Pacto (line 2) | module Pacto
type Cops (line 3) | module Cops
FILE: spec/unit/pacto/cops_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/core/contract_registry_spec.rb
type Pacto (line 4) | module Pacto
function create_contracts (line 40) | def create_contracts(total, matches)
function register_contracts (line 48) | def register_contracts(contracts)
FILE: spec/unit/pacto/core/http_middleware_spec.rb
type Pacto (line 2) | module Pacto
type Core (line 3) | module Core
class FailingObserver (line 9) | class FailingObserver
method raise_error (line 10) | def raise_error(_pacto_request, _pacto_response)
FILE: spec/unit/pacto/core/investigation_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/erb_processor_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/extensions_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/formats/legacy/contract_builder_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
FILE: spec/unit/pacto/formats/legacy/contract_factory_spec.rb
type Pacto (line 4) | module Pacto
type Formats (line 5) | module Formats
type Legacy (line 6) | module Legacy
FILE: spec/unit/pacto/formats/legacy/contract_generator_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
function pretty (line 39) | def pretty(obj)
FILE: spec/unit/pacto/formats/legacy/contract_spec.rb
type Pacto (line 4) | module Pacto
type Formats (line 5) | module Formats
type Legacy (line 6) | module Legacy
FILE: spec/unit/pacto/formats/legacy/generator/filters_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
type Generator (line 5) | module Generator
FILE: spec/unit/pacto/formats/legacy/request_clause_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
FILE: spec/unit/pacto/formats/legacy/response_clause_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Legacy (line 4) | module Legacy
FILE: spec/unit/pacto/formats/swagger/contract_factory_spec.rb
type Pacto (line 2) | module Pacto
type Formats (line 3) | module Formats
type Swagger (line 4) | module Swagger
FILE: spec/unit/pacto/formats/swagger/contract_spec.rb
type Pacto (line 4) | module Pacto
type Formats (line 5) | module Formats
type Swagger (line 6) | module Swagger
FILE: spec/unit/pacto/hooks/erb_hook_spec.rb
function mock_erb (line 55) | def mock_erb(hash)
FILE: spec/unit/pacto/logger_spec.rb
type Pacto (line 2) | module Pacto
type Logger (line 3) | module Logger
FILE: spec/unit/pacto/meta_schema_spec.rb
type Pacto (line 2) | module Pacto
FILE: spec/unit/pacto/pacto_spec.rb
function output (line 11) | def output
function mock_investigation (line 15) | def mock_investigation(errors)
FILE: spec/unit/pacto/request_pattern_spec.rb
type Pacto (line 4) | module Pacto
FILE: spec/unit/pacto/stubs/observers/stenographer_spec.rb
type Pacto (line 4) | module Pacto
type Observers (line 5) | module Observers
FILE: spec/unit/pacto/stubs/uri_pattern_spec.rb
type Pacto (line 4) | module Pacto
FILE: spec/unit/pacto/stubs/webmock_adapter_spec.rb
type Pacto (line 2) | module Pacto
type Stubs (line 3) | module Stubs
FILE: spec/unit/pacto/uri_spec.rb
type Pacto (line 4) | module Pacto
FILE: tasks/release.rake
function github (line 3) | def github
function release_tag (line 7) | def release_tag
function release (line 11) | def release
function changelog (line 15) | def changelog
function confirm (line 20) | def confirm(question, data)
Condensed preview — 200 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (314K chars).
[
{
"path": ".gitignore",
"chars": 228,
"preview": "*.gem\n*.log\n*.pid\n*.rbc\n.bundle\n.config\n.yardoc\nGemfile.lock\nInstalledFiles\n_yardoc\ncoverage\ndoc/\nlib/bundler/man\npkg\nrd"
},
{
"path": ".rspec",
"chars": 31,
"preview": "--colour\n--require spec_helper\n"
},
{
"path": ".rubocop.yml",
"chars": 441,
"preview": "require: rubocop-rspec\n\nDocumentation:\n Enabled: false\n\nDotPosition:\n Enabled: false\n\nLineLength:\n Enabled: false\n\nMe"
},
{
"path": ".travis.yml",
"chars": 227,
"preview": "language: ruby\nrvm:\n - 2.1.0\n - 2.0.0\n - 1.9.3\n # There is a bug in jruby-1.7.15\n # that is blocking testing\n - jr"
},
{
"path": "CONTRIBUTING.md",
"chars": 4324,
"preview": "# Contributing\n\nYou are welcome to contribute to Pacto and this guide will help you to:\n\n- [Setup](#setup) all the neede"
},
{
"path": "Gemfile",
"chars": 437,
"preview": "source 'https://rubygems.org'\n\n# Specify your gem's dependencies in pacto.gemspec\ngemspec name: 'pacto'\ngemspec name: 'p"
},
{
"path": "Guardfile",
"chars": 1365,
"preview": "# vim: syntax=ruby filetype=ruby\n\nguard :rubocop, all_on_start: false do\n watch(/.+\\.rb$/)\n watch(/\\.gemspec$/)\n watc"
},
{
"path": "LICENSE.txt",
"chars": 1089,
"preview": "Copyright (c) 2013 ThoughtWorks Brasil & Abril Midia\n\nMIT License\n\nPermission is hereby granted, free of charge, to any "
},
{
"path": "Procfile",
"chars": 92,
"preview": "sample_apis: sh -c 'bundle exec rackup -s puma -o localhost -p $PORT sample_apis/config.ru'\n"
},
{
"path": "README.md",
"chars": 12067,
"preview": "[](http://badge.fury.io/rb/pacto)\n[.\n\n```rb\nrequire 'pacto'\nWebMock.allow_net_connect!\nPacto.confi"
},
{
"path": "docs/rake_tasks.md",
"chars": 202,
"preview": "# Rake tasks\n## This is a test!\n[That](www.google.com) markdown works\n\n```sh\nbundle exec rake pacto:meta_validate['contr"
},
{
"path": "docs/rspec.md",
"chars": 0,
"preview": ""
},
{
"path": "docs/samples.md",
"chars": 4002,
"preview": "# Overview\nWelcome to the Pacto usage samples!\nThis document gives a quick overview of the main features.\n\nYou can brows"
},
{
"path": "docs/server.md",
"chars": 527,
"preview": "\n```rb\nrequire 'pacto/rspec'\nrequire 'pacto/test_helper'\n\ndescribe 'ping service' do\n include Pacto::TestHelper\n\n it '"
},
{
"path": "docs/server_cli.md",
"chars": 469,
"preview": "# Standalone server\nYou can run Pacto as a server in order to test non-Ruby projects. In order to get the full set\nof op"
},
{
"path": "docs/stenographer.md",
"chars": 571,
"preview": "\n```rb\nrequire 'pacto'\nPacto.configure do |c|\n c.contracts_path = 'contracts'\nend\ncontracts = Pacto.load_contracts('con"
},
{
"path": "features/configuration/strict_matchers.feature",
"chars": 3158,
"preview": "Feature: Strict Matching\n\n By default, Pacto matches requests to contracts (and stubs) using exact request paths, param"
},
{
"path": "features/evolve/README.md",
"chars": 687,
"preview": "## Consumer-Driven Contract Recommendations\n\nIf you are using Pacto for Consumer-Driven Contracts, we recommend avoiding"
},
{
"path": "features/evolve/existing_services.feature",
"chars": 2288,
"preview": "Feature: Existing services journey\n\n Scenario: Generating the contracts\n Given I have a Rakefile\n Given an existi"
},
{
"path": "features/generate/README.md",
"chars": 995,
"preview": "We know - json-schema can get pretty verbose! It's a powerful tool, but writing the entire Contract by hand for a compl"
},
{
"path": "features/generate/generation.feature",
"chars": 2657,
"preview": "@needs_server\nFeature: Contract Generation\n\n We know - json-schema can get pretty verbose! It's a powerful tool, but w"
},
{
"path": "features/steps/pacto_steps.rb",
"chars": 2116,
"preview": "# -*- encoding : utf-8 -*-\nGiven(/^Pacto is configured with:$/) do |string|\n steps %(\n Given a file named \"pacto_con"
},
{
"path": "features/stub/README.md",
"chars": 167,
"preview": "You can write your own stubs and use Pacto to [validate](https://www.relishapp.com/maxlinc/pacto/docs/validate) them, or"
},
{
"path": "features/stub/templates.feature",
"chars": 1292,
"preview": "Feature: Templating\n\n If you want to create more dynamic stubs, you can use Pacto templating. Currently the only suppo"
},
{
"path": "features/support/env.rb",
"chars": 692,
"preview": "# -*- encoding : utf-8 -*-\nrequire_relative '../../spec/coveralls_helper'\nrequire 'aruba'\nrequire 'aruba/cucumber'\nrequi"
},
{
"path": "features/validate/README.md",
"chars": 298,
"preview": "You can use Pacto to do Integration Contract Testing - making sure your service and any test double that simulates the s"
},
{
"path": "features/validate/meta_validation.feature",
"chars": 2658,
"preview": "Feature: Meta-validation\n\n Meta-validation ensures that a Contract file matches the Contract format. It does not valid"
},
{
"path": "features/validate/validation.feature",
"chars": 1087,
"preview": "Feature: Validation\n\n Validation ensures that a external service conform to a Contract.\n\n Scenario: Validation via a r"
},
{
"path": "lib/pacto/actor.rb",
"chars": 64,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Actor\n end\nend\n"
},
{
"path": "lib/pacto/actors/from_examples.rb",
"chars": 2130,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Actors\n class FirstExampleSelector\n def self.select(examples, _"
},
{
"path": "lib/pacto/actors/json_generator.rb",
"chars": 632,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Actors\n class JSONGenerator < Actor\n def build_request(contract"
},
{
"path": "lib/pacto/body_parsing.rb",
"chars": 884,
"preview": "# -*- encoding : utf-8 -*-\n\nmodule Pacto\n module Handlers\n autoload :JSONHandler, 'pacto/handlers/json_handler'\n "
},
{
"path": "lib/pacto/cli/helpers.rb",
"chars": 600,
"preview": "module Pacto\n module CLI\n module Helpers\n def each_contract(*contracts)\n [*contracts].each do |contract|"
},
{
"path": "lib/pacto/cli.rb",
"chars": 2545,
"preview": "require 'thor'\nrequire 'pacto'\nrequire 'pacto/cli/helpers'\n\nmodule Pacto\n module CLI\n class Main < Thor\n includ"
},
{
"path": "lib/pacto/consumer/faraday_driver.rb",
"chars": 1110,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Consumer\n class FaradayDriver\n include Pacto::Logger\n # Sen"
},
{
"path": "lib/pacto/consumer.rb",
"chars": 2112,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n def self.consumers\n @consumers ||= {}\n end\n\n def self.simulate_consumer(c"
},
{
"path": "lib/pacto/contract.rb",
"chars": 1517,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Contract\n include Logger\n\n attr_reader :id\n attr_reader :file\n"
},
{
"path": "lib/pacto/contract_factory.rb",
"chars": 1046,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class ContractFactory\n include Singleton\n include Logger\n\n def initia"
},
{
"path": "lib/pacto/contract_files.rb",
"chars": 373,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pathname'\nmodule Pacto\n class ContractFiles\n def self.for(path)\n full_path "
},
{
"path": "lib/pacto/contract_set.rb",
"chars": 244,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class ContractSet < Set\n def stub_providers(values = {})\n each { |cont"
},
{
"path": "lib/pacto/cops/body_cop.rb",
"chars": 754,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n class BodyCop\n KNOWN_CLAUSES = [:request, :response]\n\n "
},
{
"path": "lib/pacto/cops/request_body_cop.rb",
"chars": 186,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n class RequestBodyCop < BodyCop\n validates :request\n en"
},
{
"path": "lib/pacto/cops/response_body_cop.rb",
"chars": 189,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n class ResponseBodyCop < BodyCop\n validates :response\n "
},
{
"path": "lib/pacto/cops/response_header_cop.rb",
"chars": 1578,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n class ResponseHeaderCop\n def self.investigate(_request, r"
},
{
"path": "lib/pacto/cops/response_status_cop.rb",
"chars": 497,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n class ResponseStatusCop\n def self.investigate(_request, r"
},
{
"path": "lib/pacto/cops.rb",
"chars": 1256,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n extend Pacto::Resettable\n\n class << self\n def reset!\n "
},
{
"path": "lib/pacto/core/configuration.rb",
"chars": 1588,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Configuration\n attr_accessor :adapter, :strict_matchers,\n "
},
{
"path": "lib/pacto/core/contract_registry.rb",
"chars": 650,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class ContractNotFound < StandardError; end\n\n class ContractRegistry < Set\n "
},
{
"path": "lib/pacto/core/hook.rb",
"chars": 239,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Hook\n def initialize(&block)\n @hook = block\n end\n\n def pro"
},
{
"path": "lib/pacto/core/http_middleware.rb",
"chars": 507,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'observer'\n\nmodule Pacto\n module Core\n class HTTPMiddleware\n include Logger\n"
},
{
"path": "lib/pacto/core/investigation_registry.rb",
"chars": 1436,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class InvestigationRegistry\n include Singleton\n include Logger\n inclu"
},
{
"path": "lib/pacto/core/modes.rb",
"chars": 471,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class << self\n def generate!\n modes << :generate\n end\n\n def stop"
},
{
"path": "lib/pacto/core/pacto_request.rb",
"chars": 998,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'hashie/mash'\n\nmodule Pacto\n class PactoRequest\n # FIXME: Need case insensitive h"
},
{
"path": "lib/pacto/core/pacto_response.rb",
"chars": 692,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class PactoResponse\n # FIXME: Need case insensitive header lookup, but case"
},
{
"path": "lib/pacto/dash.rb",
"chars": 193,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'hashie'\n\nmodule Pacto\n class Dash < Hashie::Dash\n include Hashie::Extensions::Co"
},
{
"path": "lib/pacto/erb_processor.rb",
"chars": 432,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class ERBProcessor\n include Logger\n def process(contract, values = {})\n "
},
{
"path": "lib/pacto/errors.rb",
"chars": 2021,
"preview": "module Pacto\n class InvalidContract < ArgumentError\n attr_reader :errors\n\n def initialize(errors)\n @errors ="
},
{
"path": "lib/pacto/extensions.rb",
"chars": 600,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Extensions\n # Adapted from Faraday\n HeaderKeyMap = Hash.new do |m"
},
{
"path": "lib/pacto/forensics/investigation_filter.rb",
"chars": 2861,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Forensics\n class FilterExhaustedError < StandardError\n attr_rea"
},
{
"path": "lib/pacto/forensics/investigation_matcher.rb",
"chars": 2583,
"preview": "# -*- encoding : utf-8 -*-\nRSpec::Matchers.define :have_investigated do |service_name|\n match do\n investigations = P"
},
{
"path": "lib/pacto/formats/legacy/contract.rb",
"chars": 1643,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/formats/legacy/request_clause'\nrequire 'pacto/formats/legacy/response_clause'\n"
},
{
"path": "lib/pacto/formats/legacy/contract_builder.rb",
"chars": 4182,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n class ContractBuilder < Hashie::Dash # "
},
{
"path": "lib/pacto/formats/legacy/contract_factory.rb",
"chars": 2226,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/formats/legacy/contract'\n\nmodule Pacto\n module Formats\n module Legacy\n "
},
{
"path": "lib/pacto/formats/legacy/contract_generator.rb",
"chars": 2982,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'json/schema_generator'\nrequire 'pacto/formats/legacy/contract_builder'\nrequire 'pact"
},
{
"path": "lib/pacto/formats/legacy/generator/filters.rb",
"chars": 1365,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n module Generator\n class Filters\n"
},
{
"path": "lib/pacto/formats/legacy/generator_hint.rb",
"chars": 964,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n class GeneratorHint < Pacto::Dash\n "
},
{
"path": "lib/pacto/formats/legacy/request_clause.rb",
"chars": 1145,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n class RequestClause < Pacto::Dash\n "
},
{
"path": "lib/pacto/formats/legacy/response_clause.rb",
"chars": 735,
"preview": "module Pacto\n module Formats\n module Legacy\n class ResponseClause\n include Pacto::ResponseClause\n "
},
{
"path": "lib/pacto/formats/swagger/contract.rb",
"chars": 3205,
"preview": "# -*- encoding : utf-8 -*-\n\nrequire 'pacto/formats/swagger/request_clause'\nrequire 'pacto/formats/swagger/response_claus"
},
{
"path": "lib/pacto/formats/swagger/contract_factory.rb",
"chars": 1315,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'swagger'\nrequire 'pacto/formats/swagger/contract'\n\nmodule Pacto\n module Formats\n "
},
{
"path": "lib/pacto/formats/swagger/request_clause.rb",
"chars": 1509,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Swagger\n class RequestClause\n include Pa"
},
{
"path": "lib/pacto/formats/swagger/response_clause.rb",
"chars": 679,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Swagger\n class ResponseClause\n extend Fo"
},
{
"path": "lib/pacto/generator.rb",
"chars": 1034,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/formats/legacy/contract_generator'\nrequire 'pacto/formats/legacy/generator_hin"
},
{
"path": "lib/pacto/handlers/json_handler.rb",
"chars": 299,
"preview": "require 'json'\n\nmodule Pacto\n module Handlers\n module JSONHandler\n class << self\n def raw(body)\n "
},
{
"path": "lib/pacto/handlers/text_handler.rb",
"chars": 270,
"preview": "module Pacto\n module Handlers\n module TextHandler\n class << self\n def raw(body)\n body.to_s\n "
},
{
"path": "lib/pacto/hooks/erb_hook.rb",
"chars": 520,
"preview": "# -*- encoding : utf-8 -*-\nrequire_relative '../erb_processor'\n\nmodule Pacto\n module Hooks\n class ERBHook < Pacto::H"
},
{
"path": "lib/pacto/investigation.rb",
"chars": 1241,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Investigation\n include Logger\n attr_reader :request, :response, :c"
},
{
"path": "lib/pacto/logger.rb",
"chars": 921,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'forwardable'\n\nmodule Pacto\n module Logger\n def logger\n Pacto.configuration."
},
{
"path": "lib/pacto/meta_schema.rb",
"chars": 989,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class MetaSchema\n attr_accessor :schema, :engine\n\n def initialize(engine"
},
{
"path": "lib/pacto/observers/stenographer.rb",
"chars": 1144,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Observers\n class Stenographer\n def initialize(output)\n @"
},
{
"path": "lib/pacto/provider.rb",
"chars": 525,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n def self.providers\n @providers ||= {}\n end\n\n class Provider\n include R"
},
{
"path": "lib/pacto/rake_task.rb",
"chars": 2850,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\nrequire 'thor'\nrequire 'pacto/cli'\nrequire 'pacto/cli/helpers'\n\n# FIXME: Rake"
},
{
"path": "lib/pacto/request_clause.rb",
"chars": 989,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module RequestClause\n include Logger\n attr_reader :host\n attr_reader "
},
{
"path": "lib/pacto/request_pattern.rb",
"chars": 801,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class RequestPattern < WebMock::RequestPattern\n attr_accessor :uri_template"
},
{
"path": "lib/pacto/resettable.rb",
"chars": 442,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n # Included this module so that Pacto::Resettable.reset_all will call your clas"
},
{
"path": "lib/pacto/response_clause.rb",
"chars": 147,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module ResponseClause\n attr_reader :status\n attr_reader :headers\n att"
},
{
"path": "lib/pacto/rspec.rb",
"chars": 3610,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\n\nbegin\n require 'rspec/core'\n require 'rspec/expectations'\nrescue LoadError"
},
{
"path": "lib/pacto/server/cli.rb",
"chars": 2412,
"preview": "require 'thor'\nrequire 'pacto/server'\n\nmodule Pacto\n module Server\n class CLI < Thor\n class << self\n DEF"
},
{
"path": "lib/pacto/server/config.rb",
"chars": 108,
"preview": "# -*- encoding : utf-8 -*-\nPacto::Server::Settings::OptionHandler.new(port, logger, config).handle(options)\n"
},
{
"path": "lib/pacto/server/proxy.rb",
"chars": 2000,
"preview": "module Pacto\n module Server\n module Proxy\n def proxy_request(pacto_request)\n prepare_to_forward(pacto_re"
},
{
"path": "lib/pacto/server/settings.rb",
"chars": 4576,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Server\n module Settings\n def options_parser(opts, options) # ru"
},
{
"path": "lib/pacto/server.rb",
"chars": 1568,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'reel'\nrequire 'pacto'\nrequire_relative 'server/settings'\nrequire_relative 'server/pr"
},
{
"path": "lib/pacto/stubs/uri_pattern.rb",
"chars": 1072,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class UriPattern\n class << self\n def for(request, strict = Pacto.confi"
},
{
"path": "lib/pacto/stubs/webmock_adapter.rb",
"chars": 2995,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Adapters\n module WebMock\n class PactoRequest < Pacto::PactoRequ"
},
{
"path": "lib/pacto/test_helper.rb",
"chars": 1156,
"preview": "# -*- encoding : utf-8 -*-\nbegin\n require 'pacto'\n require 'pacto/server'\nrescue LoadError\n raise 'pacto/test_helper "
},
{
"path": "lib/pacto/ui.rb",
"chars": 884,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'thor'\n\nmodule Pacto\n module UI\n # Colors for HTTP Methods, intended to match col"
},
{
"path": "lib/pacto/uri.rb",
"chars": 262,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class URI\n def self.for(host, path, params = {})\n Addressable::URI.heu"
},
{
"path": "lib/pacto/version.rb",
"chars": 68,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n VERSION = '0.4.0.rc3'\nend\n"
},
{
"path": "lib/pacto.rb",
"chars": 2975,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/version'\n\nrequire 'addressable/template'\nrequire 'swagger'\nrequire 'middleware"
},
{
"path": "pacto-server.gemspec",
"chars": 1095,
"preview": "lib = File.expand_path('../lib', __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire 'pacto/versio"
},
{
"path": "pacto.gemspec",
"chars": 2829,
"preview": "lib = File.expand_path('../lib', __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire 'pacto/versio"
},
{
"path": "resources/contract_schema.json",
"chars": 1632,
"preview": "{\n \"title\": \"Example Schema\",\n \"type\": \"object\",\n \"required\": [\"request\", \"response\"],\n \"definitions\": {\n \"subsch"
},
{
"path": "resources/draft-03.json",
"chars": 3113,
"preview": "{\n \"$schema\" : \"http://json-schema.org/draft-03/schema#\",\n \"id\" : \"http://json-schema.org/draft-03/schema#\",\n \"type\" "
},
{
"path": "resources/draft-04.json",
"chars": 4374,
"preview": "{\n \"id\": \"http://json-schema.org/draft-04/schema#\",\n \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n \"de"
},
{
"path": "sample_apis/album/cover_api.rb",
"chars": 212,
"preview": "# -*- encoding : utf-8 -*-\nmodule AlbumServices\n class Cover < Grape::API\n format :json\n desc 'Ping'\n namespac"
},
{
"path": "sample_apis/config.ru",
"chars": 565,
"preview": "require 'grape'\nrequire 'grape-swagger'\nrequire 'json'\nDir[File.expand_path('../**/*_api.rb', __FILE__)].each do |f|\n "
},
{
"path": "sample_apis/echo_api.rb",
"chars": 729,
"preview": "# -*- encoding : utf-8 -*-\n# This illustrates simple get w/ params and post w/ body services\n# It also illustrates havin"
},
{
"path": "sample_apis/files_api.rb",
"chars": 1454,
"preview": "# -*- encoding : utf-8 -*-\n# This example should illustrate\n# - Authentication\n# - Expect: 100-continue\n# - Binary"
},
{
"path": "sample_apis/hello_api.rb",
"chars": 296,
"preview": "# -*- encoding : utf-8 -*-\n# This illustrates a simple get service\nmodule DummyServices\n class Hello < Grape::API\n "
},
{
"path": "sample_apis/ping_api.rb",
"chars": 208,
"preview": "# -*- encoding : utf-8 -*-\n# This illustrates a simple get service\nmodule DummyServices\n class Ping < Grape::API\n f"
},
{
"path": "sample_apis/reverse_api.rb",
"chars": 568,
"preview": "# -*- encoding : utf-8 -*-\n# This illustrates simple get w/ params and post w/ body services\n# It also illustrates havin"
},
{
"path": "sample_apis/user_api.rb",
"chars": 302,
"preview": "# -*- encoding : utf-8 -*-\n# A siple JSON service to demonstrate request/response bodies\n\nrequire 'securerandom'\n\nmodule"
},
{
"path": "samples/README.md",
"chars": 445,
"preview": "Welcome to the Pacto usage samples!\n\nWe have a listing of [sample contracts](contracts/README.html).\n\nHighlighted sample"
},
{
"path": "samples/Rakefile",
"chars": 92,
"preview": "require 'pacto/rake_task' # FIXME: This require turns on WebMock\nWebMock.allow_net_connect!\n"
},
{
"path": "samples/configuration.rb",
"chars": 1488,
"preview": "# -*- encoding : utf-8 -*-\n# Just require pacto to add it to your project.\nrequire 'pacto'\n# Pacto will disable live con"
},
{
"path": "samples/consumer.rb",
"chars": 474,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\nPacto.load_contracts 'contracts', 'http://localhost:5000'\nWebMock.allow_net_c"
},
{
"path": "samples/contracts/README.md",
"chars": 39,
"preview": "This folder contains sample contracts.\n"
},
{
"path": "samples/contracts/contract.js",
"chars": 3209,
"preview": "// Pacto Contracts describe the constraints we want to put on interactions between a consumer and a provider. It sets s"
},
{
"path": "samples/contracts/get_album_cover.json",
"chars": 1146,
"preview": "{\n \"request\": {\n \"headers\": {\n },\n \"http_method\": \"get\",\n \"path\": \"/api/album/{id}/cover\"\n },\n \"response\""
},
{
"path": "samples/contracts/localhost/api/echo.json",
"chars": 736,
"preview": "{\n \"name\": \"Echo\",\n \"request\": {\n \"headers\": {\n \"Content-Type\": \"text/plain\"\n },\n \"http_method\": \"post\","
},
{
"path": "samples/contracts/localhost/api/ping.json",
"chars": 759,
"preview": "{\n \"name\": \"Ping\",\n \"request\": {\n \"headers\": {\n },\n \"http_method\": \"get\",\n \"path\": \"/api/ping\"\n },\n \"res"
},
{
"path": "samples/contracts/user.json",
"chars": 1167,
"preview": "{\n \"name\": \"User\",\n \"request\": {\n \"headers\": {\n \"Content-Type\": \"application/json\"\n },\n \"http_method\": \""
},
{
"path": "samples/cops.rb",
"chars": 1063,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'rspec'\nrequire 'rspec/autorun'\nrequire 'pacto'\n\nPacto.configure do |c|\n c.contracts"
},
{
"path": "samples/forensics.rb",
"chars": 2192,
"preview": "# -*- encoding : utf-8 -*-\n# Pacto has a few RSpec matchers to help you ensure a **consumer** and **producer** are\n# int"
},
{
"path": "samples/generation.rb",
"chars": 2341,
"preview": "# -*- encoding : utf-8 -*-\n# Some generation related [configuration](configuration.rb).\nrequire 'pacto'\nWebMock.allow_ne"
},
{
"path": "samples/rake_tasks.sh",
"chars": 197,
"preview": "# # Rake tasks\n\n# ## This is a test!\n# [That](www.google.com) markdown works\nbundle exec rake pacto:meta_validate['contr"
},
{
"path": "samples/rspec.rb",
"chars": 27,
"preview": "# -*- encoding : utf-8 -*-\n"
},
{
"path": "samples/samples.rb",
"chars": 3972,
"preview": "# -*- encoding : utf-8 -*-\n# # Overview\n# Welcome to the Pacto usage samples!\n# This document gives a quick overview of "
},
{
"path": "samples/scripts/bootstrap",
"chars": 27,
"preview": "#!/bin/bash\nbundle install\n"
},
{
"path": "samples/scripts/wrapper",
"chars": 215,
"preview": "#!/bin/bash\n\n# Polytrix should probably support different wrappers for different langauges\nextension=\"${1##*.}\"\nif [ $ex"
},
{
"path": "samples/server.rb",
"chars": 533,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/rspec'\nrequire 'pacto/test_helper'\n\ndescribe 'ping service' do\n include Pacto"
},
{
"path": "samples/server_cli.sh",
"chars": 439,
"preview": "# # Standalone server\n\n# You can run Pacto as a server in order to test non-Ruby projects. In order to get the full set\n"
},
{
"path": "samples/stenographer.rb",
"chars": 586,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\nPacto.configure do |c|\n c.contracts_path = 'contracts'\nend\ncontracts = Pacto"
},
{
"path": "spec/coveralls_helper.rb",
"chars": 263,
"preview": "# -*- encoding : utf-8 -*-\nunless ENV['NO_COVERAGE']\n require 'simplecov'\n require 'coveralls'\n\n SimpleCov.formatter "
},
{
"path": "spec/fabricators/contract_fabricator.rb",
"chars": 2945,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\nrequire 'hashie/mash'\n\n# Fabricators for contracts or parts of contracts\nunle"
},
{
"path": "spec/fabricators/http_fabricator.rb",
"chars": 1154,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto'\nrequire 'hashie/mash'\n\n# Fabricators for Pacto objects representing HTTP tran"
},
{
"path": "spec/fabricators/webmock_fabricator.rb",
"chars": 805,
"preview": "# -*- encoding : utf-8 -*-\n# Fabricators for WebMock objects\n\nFabricator(:webmock_request_signature, from: WebMock::Requ"
},
{
"path": "spec/fixtures/contracts/deprecated/deprecated_contract.json",
"chars": 476,
"preview": "{\n \"request\": {\n \"method\": \"GET\",\n \"path\": \"/hello/:id\",\n \"headers\": {\n \"Accept\": \"application/json\"\n "
},
{
"path": "spec/fixtures/contracts/legacy/contract.json",
"chars": 435,
"preview": "{\n \"request\": {\n \"http_method\": \"GET\",\n \"path\": \"/hello_world\",\n \"headers\": {\n \"Accept\": \"application/jso"
},
{
"path": "spec/fixtures/contracts/legacy/contract_with_examples.json",
"chars": 927,
"preview": "{\n \"request\": {\n \"headers\": {\n },\n \"http_method\": \"get\",\n \"path\": \"/api/echo\",\n \"schema\": {\n \"type\""
},
{
"path": "spec/fixtures/contracts/legacy/simple_contract.json",
"chars": 512,
"preview": "{\n \"name\": \"Simple Contract\",\n \"request\": {\n \"http_method\": \"GET\",\n \"path\": \"/api/hello\",\n \"headers\": {\n "
},
{
"path": "spec/fixtures/contracts/legacy/strict_contract.json",
"chars": 839,
"preview": "{\n \"name\": \"Strict Contract\",\n \"request\": {\n \"http_method\": \"GET\",\n \"path\": \"/strict\",\n \"headers\": {\n \"A"
},
{
"path": "spec/fixtures/contracts/legacy/templating_contract.json",
"chars": 603,
"preview": "{\n \"request\": {\n \"http_method\": \"GET\",\n \"path\": \"/echo\",\n \"headers\": {\n \"Accept\": \"application/json\",\n "
},
{
"path": "spec/fixtures/contracts/swagger/petstore.yaml",
"chars": 2031,
"preview": "swagger: 2.0\ninfo:\n version: 1.0.0\n title: Swagger Petstore\n license:\n name: MIT\nhost: petstore.swagger.wordnik.co"
},
{
"path": "spec/integration/e2e_spec.rb",
"chars": 1896,
"preview": "# -*- encoding : utf-8 -*-\ndescribe Pacto do\n let(:contract_path) { contract_file 'simple_contract' }\n let(:strict_con"
},
{
"path": "spec/integration/forensics/integration_matcher_spec.rb",
"chars": 3120,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/rspec'\n\nmodule Pacto\n describe '#have_investigated' do\n let(:contract_path"
},
{
"path": "spec/integration/rspec_spec.rb",
"chars": 4354,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'pacto/rspec'\n\ndescribe 'pacto/rspec' do\n let(:contract_path) { contract_file 'simpl"
},
{
"path": "spec/integration/templating_spec.rb",
"chars": 1380,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'securerandom'\n\ndescribe 'Templating' do\n let(:contract_path) { contract_file 'templ"
},
{
"path": "spec/spec_helper.rb",
"chars": 937,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'coveralls_helper'\nrequire 'webmock/rspec'\nrequire 'pacto'\nrequire 'pacto/test_helper"
},
{
"path": "spec/unit/actors/from_examples_spec.rb",
"chars": 2779,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Actors\n describe FromExamples do\n\n let(:fallback) { double('fal"
},
{
"path": "spec/unit/actors/json_generator_spec.rb",
"chars": 3246,
"preview": "# -*- encoding : utf-8 -*-\nRSpec.shared_examples 'uses defaults' do\n it 'uses the default values for the request' do\n "
},
{
"path": "spec/unit/pacto/actor_spec.rb",
"chars": 670,
"preview": "# -*- encoding : utf-8 -*-\nRSpec.shared_examples 'an actor' do\n # let(:contract) { Fabricate(:contract) }\n let(:data) "
},
{
"path": "spec/unit/pacto/configuration_spec.rb",
"chars": 1197,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe Configuration do\n subject(:configuration) { described_class.new }\n"
},
{
"path": "spec/unit/pacto/consumer/faraday_driver_spec.rb",
"chars": 1500,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n class Consumer\n describe FaradayDriver do\n subject(:strategy) { descri"
},
{
"path": "spec/unit/pacto/contract_factory_spec.rb",
"chars": 2499,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n describe ContractFactory do\n let(:host) "
},
{
"path": "spec/unit/pacto/contract_files_spec.rb",
"chars": 1319,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\nrequire 'fileutils'\n\nmodule Pacto\n describe ContractFiles do\n let(:"
},
{
"path": "spec/unit/pacto/contract_set_spec.rb",
"chars": 1069,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n describe ContractSet do\n let(:contract1) { Fabricate"
},
{
"path": "spec/unit/pacto/contract_spec.rb",
"chars": 2343,
"preview": "# -*- encoding : utf-8 -*-\nRSpec.shared_examples 'a contract' do\n before do\n Pacto.configuration.adapter = adapter\n "
},
{
"path": "spec/unit/pacto/cops/body_cop_spec.rb",
"chars": 3593,
"preview": "# -*- encoding : utf-8 -*-\nRSpec.shared_examples 'a body cop' do | section_to_validate |\n subject(:cop) { described_cla"
},
{
"path": "spec/unit/pacto/cops/response_header_cop_spec.rb",
"chars": 3800,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n describe ResponseHeaderCop do\n subject(:cop) { described_"
},
{
"path": "spec/unit/pacto/cops/response_status_cop_spec.rb",
"chars": 834,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Cops\n describe ResponseStatusCop do\n subject(:cop) { described_"
},
{
"path": "spec/unit/pacto/cops_spec.rb",
"chars": 3246,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe Cops do\n let(:investigation_errors) { ['some error', 'another erro"
},
{
"path": "spec/unit/pacto/core/configuration_spec.rb",
"chars": 670,
"preview": "# -*- encoding : utf-8 -*-\ndescribe Pacto do\n describe '.configure' do\n let(:contracts_path) { 'path_to_contracts' }"
},
{
"path": "spec/unit/pacto/core/contract_registry_spec.rb",
"chars": 1557,
"preview": "# -*- encoding : utf-8 -*-\nrequire_relative '../../../../lib/pacto/core/contract_registry'\n\nmodule Pacto\n describe Cont"
},
{
"path": "spec/unit/pacto/core/http_middleware_spec.rb",
"chars": 1401,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Core\n describe HTTPMiddleware do\n subject(:middleware) { Pacto:"
},
{
"path": "spec/unit/pacto/core/investigation_spec.rb",
"chars": 2473,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe Investigation do\n let(:request) { double('request') }\n let(:res"
},
{
"path": "spec/unit/pacto/core/modes_spec.rb",
"chars": 631,
"preview": "# -*- encoding : utf-8 -*-\ndescribe Pacto do\n modes = %w(generate validate)\n modes.each do |mode|\n enable_method = "
},
{
"path": "spec/unit/pacto/erb_processor_spec.rb",
"chars": 643,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe ERBProcessor do\n subject(:processor) { described_class.new }\n\n "
},
{
"path": "spec/unit/pacto/extensions_spec.rb",
"chars": 852,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe Extensions do\n describe '#normalize_header_keys' do\n it 'matc"
},
{
"path": "spec/unit/pacto/formats/legacy/contract_builder_spec.rb",
"chars": 3272,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n describe ContractBuilder do\n let"
},
{
"path": "spec/unit/pacto/formats/legacy/contract_factory_spec.rb",
"chars": 1039,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n module Formats\n module Legacy\n describe Contrac"
},
{
"path": "spec/unit/pacto/formats/legacy/contract_generator_spec.rb",
"chars": 8400,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n describe ContractGenerator do\n l"
},
{
"path": "spec/unit/pacto/formats/legacy/contract_spec.rb",
"chars": 1044,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'unit/pacto/contract_spec'\n\nmodule Pacto\n module Formats\n module Legacy\n des"
},
{
"path": "spec/unit/pacto/formats/legacy/generator/filters_spec.rb",
"chars": 3824,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n module Generator\n describe Filte"
},
{
"path": "spec/unit/pacto/formats/legacy/request_clause_spec.rb",
"chars": 2233,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n describe RequestClause do\n let(:"
},
{
"path": "spec/unit/pacto/formats/legacy/response_clause_spec.rb",
"chars": 1043,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Legacy\n describe ResponseClause do\n let("
},
{
"path": "spec/unit/pacto/formats/swagger/contract_factory_spec.rb",
"chars": 2135,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Formats\n module Swagger\n describe ContractFactory do\n le"
},
{
"path": "spec/unit/pacto/formats/swagger/contract_spec.rb",
"chars": 1153,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'unit/pacto/contract_spec'\n\nmodule Pacto\n module Formats\n module Swagger\n de"
},
{
"path": "spec/unit/pacto/hooks/erb_hook_spec.rb",
"chars": 1692,
"preview": "# -*- encoding : utf-8 -*-\ndescribe Pacto::Hooks::ERBHook do\n describe '#process' do\n let(:req) do\n OpenStruct."
},
{
"path": "spec/unit/pacto/investigation_registry_spec.rb",
"chars": 3544,
"preview": "# -*- encoding : utf-8 -*-\ndescribe Pacto::InvestigationRegistry do\n subject(:registry) { described_class.instance }\n "
},
{
"path": "spec/unit/pacto/logger_spec.rb",
"chars": 1120,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Logger\n describe SimpleLogger do\n before do\n logger.log "
},
{
"path": "spec/unit/pacto/meta_schema_spec.rb",
"chars": 2585,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n describe MetaSchema do\n let(:valid_contract) do\n <<-EOF\n {\n "
},
{
"path": "spec/unit/pacto/pacto_spec.rb",
"chars": 1265,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\ndescribe Pacto do\n around(:each) do |example|\n $stdout = StringIO."
},
{
"path": "spec/unit/pacto/request_pattern_spec.rb",
"chars": 648,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n describe RequestPattern do\n let(:http_method) { :get"
},
{
"path": "spec/unit/pacto/stubs/observers/stenographer_spec.rb",
"chars": 1104,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n module Observers\n describe Stenographer do\n let"
},
{
"path": "spec/unit/pacto/stubs/uri_pattern_spec.rb",
"chars": 2301,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n describe UriPattern do\n context 'with non-strict mat"
},
{
"path": "spec/unit/pacto/stubs/webmock_adapter_spec.rb",
"chars": 4591,
"preview": "# -*- encoding : utf-8 -*-\nmodule Pacto\n module Stubs\n # FIXME: Review this test and see which requests are Pacto vs"
},
{
"path": "spec/unit/pacto/uri_spec.rb",
"chars": 619,
"preview": "# -*- encoding : utf-8 -*-\nrequire 'spec_helper'\n\nmodule Pacto\n describe URI do\n it 'returns the path appended to th"
},
{
"path": "tasks/release.rake",
"chars": 1369,
"preview": "require 'octokit'\n\ndef github\n @client ||= Octokit::Client.new :access_token => ENV['GITHUB_TOKEN']\nend\n\ndef release_ta"
}
]
About this extraction
This page contains the full source code of the thoughtworks/pacto GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 200 files (283.1 KB), approximately 74.7k tokens, and a symbol index with 581 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.