pacto-server 0.4.0.rc1 → 0.4.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/pacto-server +2 -7
- data/lib/pacto/server/cli.rb +66 -0
- data/lib/pacto/server/config.rb +1 -50
- data/lib/pacto/server/proxy.rb +49 -0
- data/lib/pacto/server/settings.rb +67 -1
- metadata +9 -36
- data/lib/pacto/server/api.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48d92c082f19c254b64fa04ff72916ca26006864
|
4
|
+
data.tar.gz: dfdbd70436fe3d864b3a6743fdf150be10964e75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 633eeeacc1f2b780fe66504de21391427baea0470d67615b19299d1291aee6483158bd515f0bbbf83eb4b23ec8ac19f3fbbedce84ffb463a8ca4465acd4fcf94
|
7
|
+
data.tar.gz: 596c074835077add5d5cbc4988730e7e821fe6bcce5b09a0180cc265ebba59b95216fb49456ed336a4c9f303666512c00f19181c7d0b95facf4f9783d2800cae
|
data/bin/pacto-server
CHANGED
@@ -1,9 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require '
|
3
|
-
require 'goliath/runner'
|
4
|
-
require 'pacto/server'
|
2
|
+
require 'pacto/server/cli'
|
5
3
|
|
6
|
-
|
7
|
-
runner.log_file = File.expand_path(runner.log_file, Dir.pwd) if runner.log_file
|
8
|
-
runner.app = Goliath::Rack::Builder.build(Pacto::Server::API, runner.api)
|
9
|
-
runner.run
|
4
|
+
Pacto::Server::CLI.start
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'pacto/server'
|
3
|
+
|
4
|
+
module Pacto
|
5
|
+
module Server
|
6
|
+
class CLI < Thor
|
7
|
+
class << self
|
8
|
+
DEFAULTS = {
|
9
|
+
stdout: true,
|
10
|
+
log_file: 'pacto.log',
|
11
|
+
# :config => 'pacto/config/pacto_server.rb',
|
12
|
+
strict: false,
|
13
|
+
stub: true,
|
14
|
+
live: false,
|
15
|
+
generate: false,
|
16
|
+
verbose: true,
|
17
|
+
validate: true,
|
18
|
+
directory: File.join(Dir.pwd, 'contracts'),
|
19
|
+
port: 9000,
|
20
|
+
format: :legacy,
|
21
|
+
stenographer_log_file: File.expand_path('pacto_stenographer.log', Dir.pwd),
|
22
|
+
strip_port: true
|
23
|
+
}
|
24
|
+
|
25
|
+
def server_options
|
26
|
+
method_option :port, default: 4567, desc: 'The port to run the server on'
|
27
|
+
method_option :directory, default: DEFAULTS[:directory], desc: 'The directory containing contracts'
|
28
|
+
method_option :strict, default: DEFAULTS[:strict], desc: 'Whether Pacto should use strict matching or not'
|
29
|
+
method_option :format, default: DEFAULTS[:format], desc: 'The contract format to use'
|
30
|
+
method_option :strip_port, default: DEFAULTS[:strip_port], desc: 'If pacto should remove the port from URLs before forwarding'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'stub [CONTRACTS...]', 'Launches a stub server for a set of contracts'
|
35
|
+
method_option :port, type: :numeric, desc: 'The port to listen on', default: 3000
|
36
|
+
method_option :spy, type: :boolean, desc: 'Display traffic received by Pacto'
|
37
|
+
server_options
|
38
|
+
def stub(*_contracts)
|
39
|
+
setup_interrupt
|
40
|
+
server_options = @options.dup
|
41
|
+
server_options[:stub] = true
|
42
|
+
Pacto::Server::HTTP.run('0.0.0.0', options.port, server_options)
|
43
|
+
end
|
44
|
+
|
45
|
+
desc 'proxy [CONTRACTS...]', 'Launches an intercepting proxy server for a set of contracts'
|
46
|
+
method_option :to, type: :string, desc: 'The target host for forwarded requests'
|
47
|
+
method_option :port, type: :numeric, desc: 'The port to listen on', default: 3000
|
48
|
+
method_option :spy, type: :boolean, desc: 'Display traffic received by Pacto'
|
49
|
+
def proxy(*_contracts)
|
50
|
+
setup_interrupt
|
51
|
+
server_options = @options.dup
|
52
|
+
server_options[:live] = true
|
53
|
+
Pacto::Server::HTTP.run('0.0.0.0', options.port, server_options)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def setup_interrupt
|
59
|
+
trap('INT') do
|
60
|
+
say 'Exiting...'
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/pacto/server/config.rb
CHANGED
@@ -1,51 +1,2 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
-
|
3
|
-
if File.readable? '.tokens.json'
|
4
|
-
MultiJson.load(File.read '.tokens.json')
|
5
|
-
else
|
6
|
-
{}
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
def prepare_contracts(contracts)
|
11
|
-
contracts.stub_providers if options[:stub]
|
12
|
-
end
|
13
|
-
|
14
|
-
config[:backend_host] = options[:backend_host]
|
15
|
-
config[:strip_port] = options[:strip_port]
|
16
|
-
config[:strip_dev] = options[:strip_dev]
|
17
|
-
config[:port] = port
|
18
|
-
contracts_path = options[:directory] || File.expand_path('contracts', Dir.pwd)
|
19
|
-
Pacto.configure do |pacto_config|
|
20
|
-
pacto_config.logger = options[:pacto_logger] || logger
|
21
|
-
pacto_config.loggerl.log_level = config[:pacto_log_level] if config[:pacto_log_level]
|
22
|
-
pacto_config.contracts_path = contracts_path
|
23
|
-
pacto_config.strict_matchers = options[:strict]
|
24
|
-
pacto_config.generator_options = {
|
25
|
-
schema_version: :draft3,
|
26
|
-
token_map: token_map
|
27
|
-
}
|
28
|
-
pacto_config.stenographer_log_file = options[:stenographer_log_file]
|
29
|
-
end
|
30
|
-
|
31
|
-
if options[:generate]
|
32
|
-
Pacto.generate!
|
33
|
-
logger.info 'Pacto generation mode enabled'
|
34
|
-
end
|
35
|
-
|
36
|
-
if options[:recursive_loading]
|
37
|
-
Dir["#{contracts_path}/*"].each do |host_dir|
|
38
|
-
host = File.basename host_dir
|
39
|
-
prepare_contracts Pacto.load_contracts(host_dir, "https://#{host}", options[:format])
|
40
|
-
end
|
41
|
-
else
|
42
|
-
host_pattern = options[:backend_host] || 'https://{server}'
|
43
|
-
prepare_contracts Pacto.load_contracts(contracts_path, host_pattern, options[:format])
|
44
|
-
end
|
45
|
-
|
46
|
-
Pacto.validate! if options[:validate]
|
47
|
-
|
48
|
-
if options[:live]
|
49
|
-
# WebMock.reset!
|
50
|
-
WebMock.allow_net_connect!
|
51
|
-
end
|
2
|
+
Pacto::Server::Settings::OptionHandler.new(port, logger, config).handle(options)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Server
|
3
|
+
module Proxy
|
4
|
+
def proxy_request(pacto_request)
|
5
|
+
prepare_to_forward(pacto_request)
|
6
|
+
pacto_response = forward(pacto_request)
|
7
|
+
prepare_to_respond(pacto_response)
|
8
|
+
pacto_response.body = rewrite(pacto_response.body)
|
9
|
+
pacto_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def prepare_to_forward(pacto_request)
|
13
|
+
host = host_for(pacto_request)
|
14
|
+
fail 'Could not determine request host' if host.nil?
|
15
|
+
host.gsub!('.dev', '.com') if settings[:strip_dev]
|
16
|
+
scheme, host = host.split('://')
|
17
|
+
host, scheme = scheme, host if host.nil?
|
18
|
+
host, _port = host.split(':')
|
19
|
+
scheme ||= 'https'
|
20
|
+
pacto_request.uri = Addressable::URI.heuristic_parse("#{scheme}://#{host}#{pacto_request.uri}")
|
21
|
+
# FIXME: We're stripping accept-encoding and transfer-encoding rather than dealing with the encodings
|
22
|
+
pacto_request.headers.delete_if { |k, _v| %w(host content-length accept-encoding transfer-encoding).include? k.downcase }
|
23
|
+
end
|
24
|
+
|
25
|
+
def rewrite(body)
|
26
|
+
return body unless settings[:strip_dev]
|
27
|
+
# FIXME: This is pretty hacky and needs to be rethought, but here to support hypermedia APIs
|
28
|
+
# This rewrites the response body so that URLs that may link to other services are rewritten
|
29
|
+
# to also passs through the Pacto server.
|
30
|
+
body.gsub('.com', ".dev:#{settings[:port]}").gsub(/https\:([\w\-\.\\\/]+).dev/, 'http:\1.dev')
|
31
|
+
end
|
32
|
+
|
33
|
+
def forward(pacto_request)
|
34
|
+
Pacto::Consumer::FaradayDriver.new.execute(pacto_request)
|
35
|
+
end
|
36
|
+
|
37
|
+
def prepare_to_respond(pacto_response)
|
38
|
+
pacto_response.headers.delete_if { |k, _v| %w(connection content-encoding content-length transfer-encoding).include? k.downcase }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def host_for(pacto_request)
|
44
|
+
# FIXME: Need case insensitive fetch for headers
|
45
|
+
pacto_request.uri.site || pacto_request.headers.find { |key, _| key.downcase == 'host' }[1]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -3,7 +3,7 @@ module Pacto
|
|
3
3
|
module Server
|
4
4
|
module Settings
|
5
5
|
def options_parser(opts, options) # rubocop:disable MethodLength
|
6
|
-
options[:format] ||= :
|
6
|
+
options[:format] ||= :legacy
|
7
7
|
options[:strict] ||= false
|
8
8
|
options[:directory] ||= File.expand_path('contracts', @original_pwd)
|
9
9
|
options[:config] ||= File.expand_path('../config.rb', __FILE__)
|
@@ -24,6 +24,72 @@ module Pacto
|
|
24
24
|
opts.on('--stenographer-log-file', 'Location for the stenographer log file') { |val| options[:stenographer_log_file] = val }
|
25
25
|
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 }
|
26
26
|
end
|
27
|
+
|
28
|
+
class OptionHandler
|
29
|
+
attr_reader :port, :logger, :config, :options
|
30
|
+
|
31
|
+
def initialize(port, logger, config = {})
|
32
|
+
@port, @logger, @config = port, logger, config
|
33
|
+
end
|
34
|
+
|
35
|
+
def token_map
|
36
|
+
if File.readable? '.tokens.json'
|
37
|
+
MultiJson.load(File.read '.tokens.json')
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def prepare_contracts(contracts)
|
44
|
+
contracts.stub_providers if options[:stub]
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle(options) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
48
|
+
@options = options
|
49
|
+
config[:backend_host] = options[:backend_host]
|
50
|
+
config[:strip_port] = options[:strip_port]
|
51
|
+
config[:strip_dev] = options[:strip_dev]
|
52
|
+
config[:port] = port
|
53
|
+
contracts_path = options[:directory] || File.expand_path('contracts', Dir.pwd)
|
54
|
+
Pacto.configure do |pacto_config|
|
55
|
+
pacto_config.logger = options[:pacto_logger] || logger
|
56
|
+
pacto_config.loggerl.log_level = config[:pacto_log_level] if config[:pacto_log_level]
|
57
|
+
pacto_config.contracts_path = contracts_path
|
58
|
+
pacto_config.strict_matchers = options[:strict]
|
59
|
+
pacto_config.generator_options = {
|
60
|
+
schema_version: :draft3,
|
61
|
+
token_map: token_map
|
62
|
+
}
|
63
|
+
pacto_config.stenographer_log_file = options[:stenographer_log_file]
|
64
|
+
end
|
65
|
+
|
66
|
+
if options[:generate]
|
67
|
+
Pacto.generate!
|
68
|
+
logger.info 'Pacto generation mode enabled'
|
69
|
+
end
|
70
|
+
|
71
|
+
if options[:recursive_loading]
|
72
|
+
Dir["#{contracts_path}/*"].each do |host_dir|
|
73
|
+
host = File.basename host_dir
|
74
|
+
prepare_contracts Pacto.load_contracts(host_dir, "https://#{host}", options[:format])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
host_pattern = options[:backend_host] || '{scheme}://{server}'
|
78
|
+
if File.exist? contracts_path
|
79
|
+
prepare_contracts Pacto.load_contracts(contracts_path, host_pattern, options[:format])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Pacto.validate! if options[:validate]
|
84
|
+
|
85
|
+
if options[:live]
|
86
|
+
# WebMock.reset!
|
87
|
+
WebMock.allow_net_connect!
|
88
|
+
end
|
89
|
+
|
90
|
+
config
|
91
|
+
end
|
92
|
+
end
|
27
93
|
end
|
28
94
|
end
|
29
95
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pacto-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.0.
|
4
|
+
version: 0.4.0.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ThoughtWorks
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pacto
|
@@ -16,56 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.4.0.
|
19
|
+
version: 0.4.0.rc2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.4.0.
|
26
|
+
version: 0.4.0.rc2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: reel
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0.5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: em-synchrony
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.0'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '1.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: em-http-request
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '1.1'
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '1.1'
|
40
|
+
version: '0.5'
|
69
41
|
description: Pacto Server let's you run Pacto as a standalone server to arbitrate
|
70
42
|
contract disputes between a service provider and one or more consumers in any programming
|
71
43
|
language. It's Pacto beyond Ruby
|
@@ -77,8 +49,9 @@ extensions: []
|
|
77
49
|
extra_rdoc_files: []
|
78
50
|
files:
|
79
51
|
- bin/pacto-server
|
80
|
-
- lib/pacto/server/
|
52
|
+
- lib/pacto/server/cli.rb
|
81
53
|
- lib/pacto/server/config.rb
|
54
|
+
- lib/pacto/server/proxy.rb
|
82
55
|
- lib/pacto/server/settings.rb
|
83
56
|
homepage: http://thoughtworks.github.io/pacto/
|
84
57
|
licenses:
|
data/lib/pacto/server/api.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
2
|
-
require 'pacto/server/settings'
|
3
|
-
|
4
|
-
module Pacto
|
5
|
-
module Server
|
6
|
-
class API < Goliath::API
|
7
|
-
include Pacto::Server::Settings
|
8
|
-
use ::Rack::ContentLength
|
9
|
-
|
10
|
-
def initialize(*args)
|
11
|
-
@original_pwd = Dir.pwd
|
12
|
-
super
|
13
|
-
end
|
14
|
-
|
15
|
-
def on_headers(env, headers)
|
16
|
-
env.logger.debug 'receiving headers: ' + headers.inspect
|
17
|
-
env['client-headers'] = headers
|
18
|
-
end
|
19
|
-
|
20
|
-
def on_body(env, data)
|
21
|
-
env.logger.debug 'received data: ' + data
|
22
|
-
(env['async-body'] ||= '') << data
|
23
|
-
end
|
24
|
-
|
25
|
-
def response(env)
|
26
|
-
log_request(env)
|
27
|
-
req = prepare_pacto_request(env)
|
28
|
-
env.logger.info "sending: #{req}"
|
29
|
-
resp = Pacto::Consumer::FaradayDriver.new.execute(req)
|
30
|
-
process_pacto_response resp, env
|
31
|
-
rescue => e
|
32
|
-
backtrace = e.backtrace.join("\n")
|
33
|
-
env.logger.warn "responding with error: #{e.message}, backtrace: #{backtrace}"
|
34
|
-
[500, {}, e.message]
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def log_request(env)
|
40
|
-
method = env['REQUEST_METHOD'].upcase
|
41
|
-
env.logger.info "received: #{method} #{env['REQUEST_URI']} with headers #{env['client-headers']}"
|
42
|
-
end
|
43
|
-
|
44
|
-
def prepare_pacto_request(env)
|
45
|
-
PactoRequest.new(
|
46
|
-
body: env['async-body'],
|
47
|
-
headers: filter_request_headers(env),
|
48
|
-
method: env['REQUEST_METHOD'].downcase.to_sym,
|
49
|
-
uri: determine_proxy_uri(env)
|
50
|
-
)
|
51
|
-
end
|
52
|
-
|
53
|
-
def process_pacto_response(resp, _env)
|
54
|
-
code = resp.status
|
55
|
-
safe_response_headers = normalize_headers(resp.headers).reject { |k, _v| %w(connection content-encoding content-length transfer-encoding).include? k.downcase }
|
56
|
-
body = proxy_rewrite(resp.body)
|
57
|
-
env.logger.info "received response: #{resp.inspect}"
|
58
|
-
env.logger.debug "response body: #{body}"
|
59
|
-
[code, safe_response_headers, body]
|
60
|
-
end
|
61
|
-
|
62
|
-
def determine_proxy_uri(env)
|
63
|
-
path = env[Goliath::Request::REQUEST_PATH]
|
64
|
-
if env.config[:backend_host]
|
65
|
-
uri = Addressable::URI.heuristic_parse("#{env.config[:backend_host]}#{path}")
|
66
|
-
else
|
67
|
-
host = env['HTTP_HOST']
|
68
|
-
host.gsub!('.dev', '.com') if env.config[:strip_dev]
|
69
|
-
uri = Addressable::URI.heuristic_parse("https://#{host}#{path}")
|
70
|
-
uri.port = nil if env.config[:strip_port]
|
71
|
-
end
|
72
|
-
uri
|
73
|
-
end
|
74
|
-
|
75
|
-
def filter_request_headers(env)
|
76
|
-
headers = env['client-headers']
|
77
|
-
safe_headers = headers.reject { |k, _v| %w(host content-length transfer-encoding).include? k.downcase }
|
78
|
-
env.logger.debug "filtered headers: #{safe_headers}"
|
79
|
-
safe_headers
|
80
|
-
end
|
81
|
-
|
82
|
-
def port
|
83
|
-
env.config[:port]
|
84
|
-
end
|
85
|
-
|
86
|
-
def normalize_headers(headers)
|
87
|
-
headers.each_with_object({}) do |elem, res|
|
88
|
-
key = elem.first.dup
|
89
|
-
value = elem.last
|
90
|
-
key.gsub!('_', '-')
|
91
|
-
key = key.split('-').map(&:capitalize).join '-'
|
92
|
-
res[key] = value
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def proxy_rewrite(body)
|
97
|
-
# FIXME: How I usually deal with rels, but others may not want this behavior.
|
98
|
-
body.gsub('.com', ".dev:#{port}").gsub(/https\:([\w\-\.\\\/]+).dev/, 'http:\1.dev')
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|