hs-pact-mock_service 3.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +494 -0
  3. data/Gemfile +8 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +137 -0
  6. data/bin/pact-mock-service +3 -0
  7. data/bin/pact-stub-service +3 -0
  8. data/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb +31 -0
  9. data/lib/pact/consumer/mock_service/error_handler.rb +31 -0
  10. data/lib/pact/consumer/mock_service/rack_request_helper.rb +65 -0
  11. data/lib/pact/consumer/mock_service/set_location.rb +28 -0
  12. data/lib/pact/consumer/server.rb +111 -0
  13. data/lib/pact/consumer_contract/consumer_contract_decorator.rb +53 -0
  14. data/lib/pact/consumer_contract/consumer_contract_writer.rb +181 -0
  15. data/lib/pact/consumer_contract/interaction_decorator.rb +40 -0
  16. data/lib/pact/consumer_contract/request_decorator.rb +88 -0
  17. data/lib/pact/consumer_contract/response_decorator.rb +47 -0
  18. data/lib/pact/mock_service/app.rb +85 -0
  19. data/lib/pact/mock_service/app_manager.rb +156 -0
  20. data/lib/pact/mock_service/cli/custom_thor.rb +74 -0
  21. data/lib/pact/mock_service/cli/pidfile.rb +99 -0
  22. data/lib/pact/mock_service/cli.rb +213 -0
  23. data/lib/pact/mock_service/client.rb +79 -0
  24. data/lib/pact/mock_service/control_server/app.rb +42 -0
  25. data/lib/pact/mock_service/control_server/delegator.rb +44 -0
  26. data/lib/pact/mock_service/control_server/index.rb +25 -0
  27. data/lib/pact/mock_service/control_server/mock_service_creator.rb +32 -0
  28. data/lib/pact/mock_service/control_server/mock_services.rb +26 -0
  29. data/lib/pact/mock_service/control_server/require_pacticipant_headers.rb +20 -0
  30. data/lib/pact/mock_service/control_server/run.rb +73 -0
  31. data/lib/pact/mock_service/errors.rb +9 -0
  32. data/lib/pact/mock_service/interaction_decorator.rb +49 -0
  33. data/lib/pact/mock_service/interactions/actual_interactions.rb +36 -0
  34. data/lib/pact/mock_service/interactions/candidate_interactions.rb +15 -0
  35. data/lib/pact/mock_service/interactions/expected_interactions.rb +18 -0
  36. data/lib/pact/mock_service/interactions/interaction_diff_message.rb +45 -0
  37. data/lib/pact/mock_service/interactions/interaction_mismatch.rb +74 -0
  38. data/lib/pact/mock_service/interactions/interactions_filter.rb +66 -0
  39. data/lib/pact/mock_service/interactions/verification.rb +52 -0
  40. data/lib/pact/mock_service/interactions/verified_interactions.rb +20 -0
  41. data/lib/pact/mock_service/logger.rb +40 -0
  42. data/lib/pact/mock_service/request_decorator.rb +36 -0
  43. data/lib/pact/mock_service/request_handlers/base_administration_request_handler.rb +42 -0
  44. data/lib/pact/mock_service/request_handlers/base_request_handler.rb +30 -0
  45. data/lib/pact/mock_service/request_handlers/index_get.rb +23 -0
  46. data/lib/pact/mock_service/request_handlers/interaction_delete.rb +38 -0
  47. data/lib/pact/mock_service/request_handlers/interaction_post.rb +43 -0
  48. data/lib/pact/mock_service/request_handlers/interaction_replay.rb +191 -0
  49. data/lib/pact/mock_service/request_handlers/interactions_put.rb +44 -0
  50. data/lib/pact/mock_service/request_handlers/log_get.rb +27 -0
  51. data/lib/pact/mock_service/request_handlers/missing_interactions_get.rb +33 -0
  52. data/lib/pact/mock_service/request_handlers/options.rb +64 -0
  53. data/lib/pact/mock_service/request_handlers/pact_post.rb +38 -0
  54. data/lib/pact/mock_service/request_handlers/session_delete.rb +32 -0
  55. data/lib/pact/mock_service/request_handlers/verification_get.rb +74 -0
  56. data/lib/pact/mock_service/request_handlers.rb +42 -0
  57. data/lib/pact/mock_service/response_decorator.rb +31 -0
  58. data/lib/pact/mock_service/run.rb +125 -0
  59. data/lib/pact/mock_service/server/respawn.rb +20 -0
  60. data/lib/pact/mock_service/server/spawn.rb +37 -0
  61. data/lib/pact/mock_service/server/wait_for_server_up.rb +44 -0
  62. data/lib/pact/mock_service/server/webrick_request_monkeypatch.rb +15 -0
  63. data/lib/pact/mock_service/session.rb +96 -0
  64. data/lib/pact/mock_service/spawn.rb +86 -0
  65. data/lib/pact/mock_service/version.rb +5 -0
  66. data/lib/pact/mock_service.rb +2 -0
  67. data/lib/pact/stub_service/cli.rb +71 -0
  68. data/lib/pact/support/expand_file_list.rb +26 -0
  69. metadata +399 -0
@@ -0,0 +1,31 @@
1
+ module Pact
2
+ module Consumer
3
+ class CorsOriginHeaderMiddleware
4
+
5
+ def initialize app, cors_enabled
6
+ @app = app
7
+ @cors_enabled = cors_enabled
8
+ end
9
+
10
+ def call env
11
+ response = @app.call env
12
+ if env['HTTP_X_PACT_MOCK_SERVICE'] || @cors_enabled
13
+ add_cors_header env, response
14
+ else
15
+ response
16
+ end
17
+ end
18
+
19
+ def shutdown
20
+ @app.shutdown
21
+ end
22
+
23
+ private
24
+
25
+ def add_cors_header env, response
26
+ cors_headers = { 'Access-Control-Allow-Origin' => env.fetch('HTTP_ORIGIN','*'), 'Access-Control-Allow-Credentials' => 'true'}
27
+ [response[0], response[1].merge(cors_headers), response[2]]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Pact
2
+ module Consumer
3
+ class MockService
4
+ class ErrorHandler
5
+
6
+ def initialize app, logger
7
+ @app = app
8
+ @logger = logger
9
+ end
10
+
11
+ def call env
12
+ begin
13
+ @app.call(env)
14
+ rescue Pact::Error => e
15
+ @logger.error e.message
16
+ [500, {'Content-Type' => 'application/json'}, [{message: e.message}.to_json]]
17
+ rescue StandardError => e
18
+ message = "Error ocurred in mock service: #{e.class} - #{e.message}"
19
+ @logger.error message
20
+ @logger.error e.backtrace.join("\n")
21
+ [500, {'Content-Type' => 'application/json'}, [{message: message, backtrace: e.backtrace}.to_json]]
22
+ end
23
+ end
24
+
25
+ def shutdown
26
+ @app.shutdown
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ require 'cgi/core'
2
+ require 'pact/consumer_contract/query'
3
+
4
+ module Pact
5
+ module Consumer
6
+
7
+ module RackRequestHelper
8
+ REQUEST_KEYS = {
9
+ 'REQUEST_METHOD' => :method,
10
+ 'PATH_INFO' => :path,
11
+ 'QUERY_STRING' => :query,
12
+ 'rack.input' => :body
13
+ }
14
+
15
+ def params_hash env
16
+ Pact::Query.parse_string(env["QUERY_STRING"])
17
+ end
18
+
19
+ def request_as_hash_from env
20
+ request = env.inject({}) do |memo, (k, v)|
21
+ request_key = REQUEST_KEYS[k]
22
+ memo[request_key] = v if request_key
23
+ memo
24
+ end
25
+
26
+ request[:headers] = headers_from env
27
+ body_string = request[:body].read
28
+
29
+ if body_string.empty?
30
+ request.delete :body
31
+ else
32
+ body_is_json = request[:headers]['Content-Type'] =~ /json/
33
+ request[:body] = body_is_json ? JSON.parse(body_string) : body_string
34
+ end
35
+ request[:method] = request[:method].downcase
36
+ request
37
+ end
38
+
39
+ private
40
+
41
+ def headers_from env
42
+ headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH')}
43
+ dasherized_headers = headers.inject({}) do | hash, header |
44
+ hash[standardise_header(header.first)] = header.last
45
+ hash
46
+ end
47
+ # This header is set in lib/pact/mock_service/server/webrick_request_monkeypatch.rb to allow use to
48
+ # restore the original header names with underscores.
49
+ restore_underscored_header_names(dasherized_headers, (env['X_PACT_UNDERSCORED_HEADER_NAMES'] || '').split(","))
50
+ end
51
+
52
+ def standardise_header header
53
+ header.gsub(/^HTTP_/, '').split(/[_-]/).collect{|word| word[0].upcase + word[1..-1].downcase}.join("-")
54
+ end
55
+
56
+ def restore_underscored_header_names dasherized_headers, original_header_names
57
+ original_header_names.each_with_object(dasherized_headers) do | original_header_name, headers |
58
+ if headers.key?(standardise_header(original_header_name))
59
+ headers[original_header_name] = headers.delete(standardise_header(original_header_name))
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ module Pact
2
+ module Consumer
3
+ class SetLocation
4
+
5
+ LOCATION = 'X-Pact-Mock-Service-Location'.freeze
6
+ HTTP_X_PACT_MOCK_SERVICE = 'HTTP_X_PACT_MOCK_SERVICE'
7
+
8
+ def initialize app, base_url
9
+ @app = app
10
+ @location_header = {LOCATION => base_url}.freeze
11
+ end
12
+
13
+ def call env
14
+ response = @app.call(env)
15
+ env[HTTP_X_PACT_MOCK_SERVICE] ? add_location_header_to_response(response) : response
16
+ end
17
+
18
+ def add_location_header_to_response response
19
+ [response.first, response[1].merge(@location_header), response.last]
20
+ end
21
+
22
+ def shutdown
23
+ @app.shutdown
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'rack'
4
+
5
+ # Copied shamelessly from Capybara
6
+ # Used to run a mock service in a new thread when started by the AppManager or the ControlServer
7
+
8
+ module Pact
9
+ class Server
10
+ class Middleware
11
+ attr_accessor :error
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ if env["PATH_INFO"] == "/__identify__"
19
+ [200, {}, [@app.object_id.to_s]]
20
+ else
21
+ begin
22
+ @app.call(env)
23
+ rescue StandardError => e
24
+ @error = e unless @error
25
+ raise e
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ class << self
32
+ def ports
33
+ @ports ||= {}
34
+ end
35
+ end
36
+
37
+ attr_reader :app, :host, :port, :options
38
+
39
+ def initialize(app, host, port, options = {})
40
+ @app = app
41
+ @middleware = Middleware.new(@app)
42
+ @server_thread = nil
43
+ @host = host
44
+ @port = port
45
+ @options = options
46
+ end
47
+
48
+ def reset_error!
49
+ @middleware.error = nil
50
+ end
51
+
52
+ def error
53
+ @middleware.error
54
+ end
55
+
56
+ def responsive?
57
+ return false if @server_thread && @server_thread.join(0)
58
+ res = get_identity
59
+ if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
60
+ return res.body == @app.object_id.to_s
61
+ end
62
+ rescue SystemCallError
63
+ return false
64
+ rescue EOFError
65
+ return false
66
+ end
67
+
68
+ def run_default_server(app, port)
69
+ require 'rack/handler/webrick'
70
+ Rack::Handler::WEBrick.run(app, **webrick_opts) do |server|
71
+ @port = server[:Port]
72
+ end
73
+ end
74
+
75
+ def get_identity
76
+ return false unless @port
77
+ http = Net::HTTP.new host, @port
78
+ if options[:ssl]
79
+ http.use_ssl = true
80
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
81
+ end
82
+ http.get('/__identify__')
83
+ end
84
+
85
+ def webrick_opts
86
+ opts = { Host: host.nil? ? 'localhost' : host, Port: port.nil? ? 0 : port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0) }
87
+ opts.merge!({
88
+ :SSLCertificate => OpenSSL::X509::Certificate.new(File.open(options[:sslcert]).read) }) if options[:sslcert]
89
+ opts.merge!({
90
+ :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(options[:sslkey]).read) }) if options[:sslkey]
91
+ opts.merge!(ssl_opts) if options[:ssl]
92
+ opts
93
+ end
94
+
95
+ def ssl_opts
96
+ { SSLEnable: true, SSLCertName: [ ["CN", host] ] }
97
+ end
98
+
99
+ def boot
100
+ unless responsive?
101
+ @server_thread = Thread.new { run_default_server(@middleware, @port) }
102
+ Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
103
+ end
104
+ rescue Timeout::Error
105
+ raise "Rack application timed out during boot"
106
+ else
107
+ Pact::Server.ports[@app.object_id] = @port
108
+ self
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,53 @@
1
+ require 'pact/shared/active_support_support'
2
+ require 'pact/consumer_contract/interaction_decorator'
3
+
4
+ module Pact
5
+ class ConsumerContractDecorator
6
+
7
+ include ActiveSupportSupport
8
+
9
+ def initialize consumer_contract, decorator_options = {}
10
+ @consumer_contract = consumer_contract
11
+ @decorator_options = decorator_options
12
+ end
13
+
14
+ def as_json(options = {})
15
+ fix_all_the_things(
16
+ consumer: consumer_contract.consumer.as_json,
17
+ provider: consumer_contract.provider.as_json,
18
+ interactions: sorted_interactions.collect{ |i| InteractionDecorator.new(i, @decorator_options).as_json},
19
+ metadata: {
20
+ pactSpecification: {
21
+ version: pact_specification_version
22
+ }
23
+ }
24
+ )
25
+ end
26
+
27
+ def to_json(options = {})
28
+ as_json.to_json(options)
29
+ end
30
+
31
+ private
32
+
33
+ def sorted_interactions
34
+ # Default order: chronological
35
+ return consumer_contract.writable_interactions if Pact.configuration.pactfile_write_order == :chronological
36
+ # We are supporting only chronological or alphabetical order
37
+ raise NotImplementedError if Pact.configuration.pactfile_write_order != :alphabetical
38
+
39
+ consumer_contract.writable_interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)}
40
+ end
41
+
42
+ def sortable_id interaction
43
+ "#{interaction.description.downcase} #{interaction.response.status} #{(interaction.provider_state || '').downcase}"
44
+ end
45
+
46
+ attr_reader :consumer_contract
47
+
48
+ def pact_specification_version
49
+ version = @decorator_options.fetch(:pact_specification_version)
50
+ "#{version[0]}.0.0" # Only care about the first digit
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,181 @@
1
+ require 'pact/consumer_contract'
2
+ require 'pact/mock_service/interactions/interactions_filter'
3
+ require 'pact/consumer_contract/file_name'
4
+ require 'pact/consumer_contract/pact_file'
5
+ require 'pact/consumer_contract/consumer_contract_decorator'
6
+ require 'pact/shared/active_support_support'
7
+ require 'fileutils'
8
+ require 'filelock'
9
+
10
+ module Pact
11
+
12
+ class ConsumerContractWriterError < StandardError; end
13
+
14
+ class ConsumerContractWriter
15
+
16
+ DEFAULT_PACT_SPECIFICATION_VERSION = '2.0.0'
17
+
18
+ include Pact::FileName
19
+ include Pact::PactFile
20
+ include ActiveSupportSupport
21
+
22
+ def initialize consumer_contract_details, logger
23
+ @logger = logger
24
+ @consumer_contract_details = consumer_contract_details
25
+ @pactfile_write_mode = (consumer_contract_details[:pactfile_write_mode] || :overwrite).to_sym
26
+ @interactions = consumer_contract_details.fetch(:interactions)
27
+ @pact_specification_version = (consumer_contract_details[:pact_specification_version] || DEFAULT_PACT_SPECIFICATION_VERSION).to_s
28
+ @consumer_contract_decorator_class = consumer_contract_details[:consumer_contract_decorator_class] || Pact::ConsumerContractDecorator
29
+ @error_stream = consumer_contract_details[:error_stream] || Pact.configuration.error_stream
30
+ @output_stream = consumer_contract_details[:output_stream] || Pact.configuration.output_stream
31
+ end
32
+
33
+ def consumer_contract
34
+ @consumer_contract ||= Pact::ConsumerContract.new(
35
+ consumer: ServiceConsumer.new(name: consumer_contract_details[:consumer][:name]),
36
+ provider: ServiceProvider.new(name: consumer_contract_details[:provider][:name]),
37
+ interactions: interactions_for_new_consumer_contract)
38
+ end
39
+
40
+ def write
41
+ update_pactfile_if_needed
42
+ pact_json
43
+ end
44
+
45
+ def can_write?
46
+ consumer_name && provider_name && consumer_contract_details[:pact_dir]
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :consumer_contract_details, :pactfile_write_mode, :interactions, :logger, :pact_specification_version, :consumer_contract_decorator_class
52
+ attr_reader :error_stream, :output_stream
53
+
54
+ def update_pactfile_if_needed
55
+ return if pactfile_write_mode == :none
56
+ return if interactions.count == 0
57
+ update_pactfile
58
+ end
59
+
60
+ def update_pactfile
61
+ logger.info log_message
62
+ FileUtils.mkdir_p File.dirname(pactfile_path)
63
+ Filelock(pactfile_path) do | pact_file |
64
+ # must be read after obtaining the lock, and must be read from the yielded file object, otherwise windows freaks out
65
+ @existing_contents = pact_file.read
66
+ new_contents = pact_json
67
+ pact_file.rewind
68
+ pact_file.truncate 0
69
+ pact_file.write new_contents
70
+ pact_file.flush
71
+ pact_file.truncate(pact_file.pos)
72
+ end
73
+ end
74
+
75
+ def pact_json
76
+ @pact_json ||= fix_json_formatting(JSON.pretty_generate(decorated_pact))
77
+ end
78
+
79
+ def decorated_pact
80
+ @decorated_pact ||= consumer_contract_decorator_class.new(consumer_contract, pact_specification_version: pact_specification_version)
81
+ end
82
+
83
+ def interactions_for_new_consumer_contract
84
+ if pactfile_exists? && (updating? || merging?)
85
+ merged_interactions = existing_interactions.dup
86
+ filter = Pact::MockService::Interactions.filter(merged_interactions, pactfile_write_mode)
87
+ interactions.each {|i| filter << i }
88
+ merged_interactions
89
+ else
90
+ interactions
91
+ end
92
+ end
93
+
94
+ def existing_interactions
95
+ interactions = []
96
+ if pactfile_exists? && pactfile_has_contents?
97
+ begin
98
+ interactions = existing_consumer_contract.interactions
99
+ print_updating_warning if updating?
100
+ rescue StandardError => e
101
+ warn_and_stderr "Could not load existing consumer contract from #{pactfile_path} due to #{e}. Creating a new file."
102
+ logger.error e
103
+ logger.error e.backtrace
104
+ end
105
+ end
106
+ interactions
107
+ end
108
+
109
+ def pactfile_exists?
110
+ File.exist?(pactfile_path)
111
+ end
112
+
113
+ def pactfile_has_contents?
114
+ File.size(pactfile_path) != 0
115
+ end
116
+
117
+ def existing_contents
118
+ @existing_contents
119
+ end
120
+
121
+ def existing_consumer_contract
122
+ @existing_consumer_contract ||= Pact::ConsumerContract.from_json(existing_contents)
123
+ end
124
+
125
+ def warn_and_stderr msg
126
+ error_stream.puts msg
127
+ logger.warn msg
128
+ end
129
+
130
+ def info_and_puts msg
131
+ output_stream.puts msg
132
+ logger.info msg
133
+ end
134
+
135
+ def consumer_name
136
+ consumer_contract_details[:consumer][:name]
137
+ end
138
+
139
+ def provider_name
140
+ consumer_contract_details[:provider][:name]
141
+ end
142
+
143
+ def pactfile_path
144
+ raise 'You must specify a consumer and provider name' unless (consumer_name && provider_name)
145
+ file_path consumer_name, provider_name, pact_dir, unique: consumer_contract_details[:unique_pact_file_names]
146
+ end
147
+
148
+ def pact_dir
149
+ unless consumer_contract_details[:pact_dir]
150
+ raise ConsumerContractWriterError.new("Please indicate the directory to write the pact to by specifying the pact_dir field")
151
+ end
152
+ consumer_contract_details[:pact_dir]
153
+ end
154
+
155
+ def updating?
156
+ pactfile_write_mode == :update
157
+ end
158
+
159
+ def merging?
160
+ pactfile_write_mode == :merge
161
+ end
162
+
163
+ def log_message
164
+ if updating?
165
+ "Updating pact for #{provider_name} at #{pactfile_path}"
166
+ elsif merging?
167
+ "Merging interactions into pact for #{provider_name} at #{pactfile_path}"
168
+ else
169
+ "Writing pact for #{provider_name} to #{pactfile_path}"
170
+ end
171
+ end
172
+
173
+ def print_updating_warning
174
+ info_and_puts "*****************************************************************************"
175
+ info_and_puts "Updating existing file .#{pactfile_path.gsub(Dir.pwd, '')} as pactfile_write_mode is :update"
176
+ info_and_puts "Only interactions defined in this test run will be updated."
177
+ info_and_puts "As interactions are identified by description and provider state, pleased note that if either of these have changed, the old interactions won't be removed from the pact file until the specs are next run with :pactfile_write_mode => :overwrite."
178
+ info_and_puts "*****************************************************************************"
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,40 @@
1
+ require 'pact/shared/active_support_support'
2
+ require 'pact/consumer_contract/request_decorator'
3
+ require 'pact/consumer_contract/response_decorator'
4
+
5
+ module Pact
6
+ class InteractionDecorator
7
+
8
+ include ActiveSupportSupport
9
+
10
+ def initialize interaction, decorator_options = {}
11
+ @interaction = interaction
12
+ @decorator_options = decorator_options
13
+ end
14
+
15
+ def as_json options = {}
16
+ hash = { :description => interaction.description }
17
+ hash[:providerState] = interaction.provider_state if interaction.provider_state
18
+ hash[:request] = decorate_request.as_json(options)
19
+ hash[:response] = decorate_response.as_json(options)
20
+ fix_all_the_things hash
21
+ end
22
+
23
+ def to_json(options = {})
24
+ as_json(options).to_json(options)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :interaction
30
+
31
+ def decorate_request
32
+ RequestDecorator.new(interaction.request, @decorator_options)
33
+ end
34
+
35
+ def decorate_response
36
+ ResponseDecorator.new(interaction.response, @decorator_options)
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,88 @@
1
+ require 'pact/reification'
2
+
3
+ module Pact
4
+ class RequestDecorator
5
+
6
+ def initialize request, decorator_options = {}
7
+ @request = request
8
+ @decorator_options = decorator_options
9
+ end
10
+
11
+ def to_json(options = {})
12
+ as_json.to_json(options)
13
+ end
14
+
15
+ def as_json options = {}
16
+ hash = to_hash_without_rules
17
+ include_matching_rules? ? with_matching_rules(hash) : hash
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :request
23
+
24
+ def to_hash_without_rules
25
+ hash = {
26
+ method: request.method,
27
+ path: path
28
+ }
29
+ hash[:query] = query if request.specified?(:query)
30
+ hash[:headers] = headers if request.specified?(:headers)
31
+ hash[:body] = body if request.specified?(:body)
32
+ hash
33
+ end
34
+
35
+ def path
36
+ Pact::Reification.from_term(request.path)
37
+ end
38
+
39
+ def query
40
+ Pact::Reification.from_term(request.query)
41
+ end
42
+
43
+ def headers
44
+ Pact::Reification.from_term(request.headers)
45
+ end
46
+
47
+ # This feels wrong to be checking the class type of the body
48
+ # Do this better somehow.
49
+ def body
50
+ if content_type_is_form && request.body.is_a?(Hash)
51
+ URI.encode_www_form convert_hash_body_to_array_of_arrays
52
+ else
53
+ Pact::Reification.from_term(request.body)
54
+ end
55
+ end
56
+
57
+ def content_type_is_form
58
+ request.content_type? 'application/x-www-form-urlencoded'
59
+ end
60
+
61
+ #This probably belongs somewhere else.
62
+ def convert_hash_body_to_array_of_arrays
63
+ arrays = []
64
+ request.body.keys.each do | key |
65
+ [*request.body[key]].each do | value |
66
+ arrays << [key, value]
67
+ end
68
+ end
69
+
70
+ Pact::Reification.from_term(arrays)
71
+ end
72
+
73
+ def include_matching_rules?
74
+ pact_specification_version && !pact_specification_version.start_with?('1')
75
+ end
76
+
77
+ def with_matching_rules hash
78
+ matching_rules = Pact::MatchingRules.extract request.to_hash
79
+ return hash if matching_rules.empty?
80
+ hash.merge(matchingRules: matching_rules)
81
+ end
82
+
83
+ def pact_specification_version
84
+ @decorator_options[:pact_specification_version]
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,47 @@
1
+ require 'pact/matching_rules'
2
+ require 'pact/term'
3
+
4
+ module Pact
5
+ class ResponseDecorator
6
+
7
+ def initialize response, decorator_options = {}
8
+ @response = response
9
+ @decorator_options = decorator_options
10
+ end
11
+
12
+ def to_json(options = {})
13
+ as_json.to_json(options)
14
+ end
15
+
16
+ def as_json options = {}
17
+ include_matching_rules? ? with_matching_rules(attributes_hash) : attributes_hash
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :response
23
+
24
+ def attributes_hash
25
+ hash = {}
26
+ hash[:status] = response.status if response.specified?(:status)
27
+ hash[:headers] = response.headers if response.specified?(:headers)
28
+ hash[:body] = response.body if response.specified?(:body)
29
+ hash
30
+ end
31
+
32
+ def include_matching_rules?
33
+ pact_specification_version && !pact_specification_version.start_with?('1')
34
+ end
35
+
36
+ def with_matching_rules hash
37
+ matching_rules = Pact::MatchingRules.extract hash
38
+ example = Pact::Reification.from_term hash
39
+ return example if matching_rules.empty?
40
+ example.merge(matchingRules: matching_rules)
41
+ end
42
+
43
+ def pact_specification_version
44
+ @decorator_options[:pact_specification_version]
45
+ end
46
+ end
47
+ end