pact 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +3 -3
- data/example/animal-service/Gemfile +14 -0
- data/example/animal-service/Gemfile.lock +67 -0
- data/example/animal-service/Rakefile +3 -0
- data/example/animal-service/spec/service_consumers/pact_helper.rb +24 -0
- data/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb +9 -0
- data/example/zoo-app/Gemfile.lock +2 -15
- data/example/zoo-app/spec/pacts/zoo_app-animal_service.json +21 -6
- data/example/zoo-app/spec/service_providers/animal_service_spec.rb +1 -1
- data/lib/pact/consumer.rb +10 -11
- data/lib/pact/consumer/app_manager.rb +1 -0
- data/lib/pact/consumer/configuration.rb +179 -0
- data/lib/pact/consumer/interaction_builder.rb +1 -2
- data/lib/pact/consumer/mock_service.rb +1 -370
- data/lib/pact/consumer/mock_service/app.rb +70 -0
- data/lib/pact/consumer/mock_service/interaction_delete.rb +28 -0
- data/lib/pact/consumer/mock_service/interaction_list.rb +57 -0
- data/lib/pact/consumer/mock_service/interaction_post.rb +25 -0
- data/lib/pact/consumer/mock_service/interaction_replay.rb +126 -0
- data/lib/pact/consumer/mock_service/missing_interactions_get.rb +26 -0
- data/lib/pact/consumer/mock_service/rack_request_helper.rb +51 -0
- data/lib/pact/consumer/mock_service/startup_poll.rb +22 -0
- data/lib/pact/consumer/mock_service/verification_get.rb +35 -0
- data/lib/pact/consumer/mock_service_client.rb +3 -1
- data/lib/pact/consumer/mock_service_interaction_expectation.rb +33 -0
- data/lib/pact/consumer/request.rb +27 -0
- data/lib/pact/consumer/rspec.rb +1 -4
- data/lib/pact/consumer_contract/consumer_contract.rb +11 -8
- data/lib/pact/consumer_contract/interaction.rb +9 -22
- data/lib/pact/consumer_contract/request.rb +68 -0
- data/lib/pact/consumer_contract/service_consumer.rb +6 -2
- data/lib/pact/consumer_contract/service_provider.rb +6 -2
- data/lib/pact/matchers/index_not_found.rb +20 -0
- data/lib/pact/matchers/matchers.rb +14 -28
- data/lib/pact/matchers/unexpected_index.rb +17 -0
- data/lib/pact/matchers/unexpected_key.rb +17 -0
- data/lib/pact/provider.rb +1 -1
- data/lib/pact/provider/configuration.rb +129 -0
- data/lib/pact/provider/request.rb +59 -0
- data/lib/pact/provider/rspec.rb +4 -5
- data/lib/pact/provider/test_methods.rb +14 -52
- data/lib/pact/shared/dsl.rb +20 -0
- data/lib/pact/shared/key_not_found.rb +24 -0
- data/lib/pact/shared/null_expectation.rb +31 -0
- data/lib/pact/shared/request.rb +68 -0
- data/lib/pact/something_like.rb +7 -1
- data/lib/pact/symbolize_keys.rb +12 -0
- data/lib/pact/tasks.rb +1 -1
- data/lib/pact/tasks/task_helper.rb +23 -0
- data/lib/pact/{verification_task.rb → tasks/verification_task.rb} +4 -4
- data/lib/pact/version.rb +1 -1
- data/lib/tasks/pact.rake +5 -4
- data/pact.gemspec +1 -1
- data/spec/features/consumption_spec.rb +1 -73
- data/spec/features/production_spec.rb +3 -0
- data/spec/integration/consumer_spec.rb +170 -0
- data/spec/integration/pact/consumer_configuration_spec.rb +1 -1
- data/spec/lib/pact/consumer/{dsl_spec.rb → configuration_spec.rb} +10 -9
- data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +2 -2
- data/spec/lib/pact/consumer/interaction_builder_spec.rb +1 -1
- data/spec/lib/pact/consumer/mock_service/interaction_list_spec.rb +66 -0
- data/spec/lib/pact/consumer/mock_service/rack_request_helper_spec.rb +82 -0
- data/spec/lib/pact/consumer/mock_service_interaction_expectation_spec.rb +54 -0
- data/spec/lib/pact/consumer/request_spec.rb +24 -0
- data/spec/lib/pact/consumer_contract/consumer_contract_spec.rb +1 -1
- data/spec/lib/pact/consumer_contract/interaction_spec.rb +0 -34
- data/spec/lib/pact/{request_spec.rb → consumer_contract/request_spec.rb} +45 -121
- data/spec/lib/pact/matchers/matchers_spec.rb +29 -13
- data/spec/lib/pact/provider/configuration_spec.rb +163 -0
- data/spec/lib/pact/provider/request_spec.rb +78 -0
- data/spec/lib/pact/provider/test_methods_spec.rb +0 -30
- data/spec/lib/pact/verification_task_spec.rb +1 -1
- data/spec/spec_helper.rb +3 -0
- data/spec/support/factories.rb +5 -1
- data/spec/support/pact_helper.rb +6 -0
- data/spec/support/shared_examples_for_request.rb +83 -0
- data/spec/support/test_app_fail.json +3 -0
- data/spec/support/test_app_pass.json +18 -1
- metadata +68 -31
- data/lib/pact/consumer/dsl.rb +0 -157
- data/lib/pact/pact_task_helper.rb +0 -21
- data/lib/pact/provider/dsl.rb +0 -115
- data/lib/pact/request.rb +0 -167
- data/spec/lib/pact/consumer/mock_service_spec.rb +0 -143
- data/spec/lib/pact/provider/dsl_spec.rb +0 -179
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module Consumer
|
5
|
+
|
6
|
+
class InteractionDelete
|
7
|
+
|
8
|
+
include RackRequestHelper
|
9
|
+
|
10
|
+
def initialize name, logger, interaction_list
|
11
|
+
@name = name
|
12
|
+
@logger = logger
|
13
|
+
@interaction_list = interaction_list
|
14
|
+
end
|
15
|
+
|
16
|
+
def match? env
|
17
|
+
env['REQUEST_PATH'].start_with?('/interactions') &&
|
18
|
+
env['REQUEST_METHOD'] == 'DELETE'
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond env
|
22
|
+
@interaction_list.clear
|
23
|
+
@logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
|
24
|
+
[200, {}, ['Deleted interactions']]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class InteractionList
|
4
|
+
|
5
|
+
attr_reader :interactions
|
6
|
+
attr_reader :unexpected_requests
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
clear
|
10
|
+
end
|
11
|
+
|
12
|
+
# For testing, sigh
|
13
|
+
def clear
|
14
|
+
@interactions = []
|
15
|
+
@matched_interactions = []
|
16
|
+
@unexpected_requests = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def add interactions
|
20
|
+
@interactions << interactions
|
21
|
+
end
|
22
|
+
|
23
|
+
def register_matched interaction
|
24
|
+
@matched_interactions << interaction
|
25
|
+
end
|
26
|
+
|
27
|
+
def register_unexpected_request request
|
28
|
+
@unexpected_requests << request
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_matched?
|
32
|
+
interaction_diffs.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def missing_interactions
|
36
|
+
@interactions - @matched_interactions
|
37
|
+
end
|
38
|
+
|
39
|
+
def interaction_diffs
|
40
|
+
{
|
41
|
+
:missing_interactions => missing_interactions.collect(&:as_json),
|
42
|
+
:unexpected_requests => unexpected_requests.collect(&:as_json)
|
43
|
+
}.inject({}) do | hash, pair |
|
44
|
+
hash[pair.first] = pair.last if pair.last.any?
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_candidate_interactions actual_request
|
50
|
+
interactions.select do | interaction |
|
51
|
+
interaction.request.matches_route? actual_request
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class InteractionPost
|
4
|
+
|
5
|
+
def initialize name, logger, interaction_list
|
6
|
+
@name = name
|
7
|
+
@logger = logger
|
8
|
+
@interaction_list = interaction_list
|
9
|
+
end
|
10
|
+
|
11
|
+
def match? env
|
12
|
+
env['REQUEST_PATH'] == '/interactions' &&
|
13
|
+
env['REQUEST_METHOD'] == 'POST'
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond env
|
17
|
+
interaction = Interaction.from_hash(JSON.load(env['rack.input'].string))
|
18
|
+
@interaction_list.add interaction
|
19
|
+
@logger.info "Registered expected interaction #{interaction.request.method_and_path} for #{@name}"
|
20
|
+
@logger.ap interaction.as_json
|
21
|
+
[200, {}, ['Added interactions']]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'pact/matchers'
|
2
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Consumer
|
6
|
+
|
7
|
+
class InteractionReplay
|
8
|
+
include Pact::Matchers
|
9
|
+
include RackRequestHelper
|
10
|
+
|
11
|
+
def initialize name, logger, interaction_list
|
12
|
+
@name = name
|
13
|
+
@logger = logger
|
14
|
+
@interaction_list = interaction_list
|
15
|
+
end
|
16
|
+
|
17
|
+
def match? env
|
18
|
+
true # default handler
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond env
|
22
|
+
find_response request_as_hash_from(env)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_response request_hash
|
28
|
+
actual_request = Request::Actual.from_hash(request_hash)
|
29
|
+
@logger.info "#{@name} received request #{actual_request.method_and_path}"
|
30
|
+
@logger.ap actual_request.as_json
|
31
|
+
candidate_interactions = @interaction_list.find_candidate_interactions actual_request
|
32
|
+
matching_interactions = find_matching_interactions actual_request, from: candidate_interactions
|
33
|
+
|
34
|
+
case matching_interactions.size
|
35
|
+
when 0 then handle_unrecognised_request actual_request, candidate_interactions
|
36
|
+
when 1 then handle_matched_interaction matching_interactions.first
|
37
|
+
else
|
38
|
+
handle_more_than_one_matching_interaction actual_request, matching_interactions
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_matching_interactions actual_request, opts
|
43
|
+
candidate_interactions = opts.fetch(:from)
|
44
|
+
candidate_interactions.select do | candidate_interaction |
|
45
|
+
candidate_interaction.request.matches? actual_request
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_matched_interaction interaction
|
50
|
+
@interaction_list.register_matched interaction
|
51
|
+
response = response_from(interaction.response)
|
52
|
+
@logger.info "Found matching response on #{@name}:"
|
53
|
+
@logger.ap interaction.response
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
def multiple_interactions_found_response actual_request, matching_interactions
|
58
|
+
response = {
|
59
|
+
message: "Multiple interaction found for #{actual_request.method_and_path}",
|
60
|
+
matching_interactions: matching_interactions.collect{ | interaction | request_summary_for(interaction) }
|
61
|
+
}
|
62
|
+
[500, {'Content-Type' => 'application/json'}, [response.to_json]]
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_more_than_one_matching_interaction actual_request, matching_interactions
|
66
|
+
@logger.error "Multiple interactions found on #{@name}:"
|
67
|
+
@logger.ap matching_interactions.collect(&:as_json)
|
68
|
+
multiple_interactions_found_response actual_request, matching_interactions
|
69
|
+
end
|
70
|
+
|
71
|
+
def interaction_diffs actual_request, candidate_interactions
|
72
|
+
candidate_interactions.collect do | candidate_interaction |
|
73
|
+
diff = candidate_interaction.request.difference(actual_request)
|
74
|
+
diff_summary_for candidate_interaction, diff
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def diff_summary_for interaction, diff
|
79
|
+
summary = {:description => interaction.description}
|
80
|
+
summary[:provider_state] = interaction.provider_state if interaction.provider_state
|
81
|
+
summary.merge(diff)
|
82
|
+
end
|
83
|
+
|
84
|
+
def request_summary_for interaction
|
85
|
+
summary = {:description => interaction.description}
|
86
|
+
summary[:provider_state] if interaction.provider_state
|
87
|
+
summary[:request] = interaction.request
|
88
|
+
summary
|
89
|
+
end
|
90
|
+
|
91
|
+
def unrecognised_request_response actual_request, interaction_diffs
|
92
|
+
response = {
|
93
|
+
message: "No interaction found for #{actual_request.method_and_path}",
|
94
|
+
interaction_diffs: interaction_diffs
|
95
|
+
}
|
96
|
+
[500, {'Content-Type' => 'application/json'}, [response.to_json]]
|
97
|
+
end
|
98
|
+
|
99
|
+
def log_unrecognised_request_and_interaction_diff actual_request, interaction_diffs, candidate_interactions
|
100
|
+
@logger.error "No interaction found on #{@name} for #{actual_request.method_and_path}"
|
101
|
+
@logger.error 'Interaction diffs for that route:'
|
102
|
+
@logger.ap(interaction_diffs, :error)
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_unrecognised_request actual_request, candidate_interactions
|
106
|
+
@interaction_list.register_unexpected_request actual_request
|
107
|
+
interaction_diffs = interaction_diffs(actual_request, candidate_interactions)
|
108
|
+
log_unrecognised_request_and_interaction_diff actual_request, interaction_diffs, candidate_interactions
|
109
|
+
unrecognised_request_response actual_request, interaction_diffs
|
110
|
+
end
|
111
|
+
|
112
|
+
def response_from response
|
113
|
+
[response['status'], (response['headers'] || {}).to_hash, [render_body(response['body'])]]
|
114
|
+
end
|
115
|
+
|
116
|
+
def render_body body
|
117
|
+
return '' unless body
|
118
|
+
body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
|
119
|
+
end
|
120
|
+
|
121
|
+
def logger_info_ap msg
|
122
|
+
@logger.info msg
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
|
4
|
+
class MissingInteractionsGet
|
5
|
+
include RackRequestHelper
|
6
|
+
|
7
|
+
def initialize name, logger, interaction_list
|
8
|
+
@name = name
|
9
|
+
@logger = logger
|
10
|
+
@interaction_list = interaction_list
|
11
|
+
end
|
12
|
+
|
13
|
+
def match? env
|
14
|
+
env['REQUEST_PATH'].start_with?('/number_of_missing_interactions') &&
|
15
|
+
env['REQUEST_METHOD'] == 'GET'
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond env
|
19
|
+
number_of_missing_interactions = @interaction_list.missing_interactions.size
|
20
|
+
@logger.info "Number of missing interactions for mock \"#{@name}\" = #{number_of_missing_interactions}"
|
21
|
+
[200, {}, ["#{number_of_missing_interactions}"]]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
|
4
|
+
module RackRequestHelper
|
5
|
+
REQUEST_KEYS = {
|
6
|
+
'REQUEST_METHOD' => :method,
|
7
|
+
'REQUEST_PATH' => :path,
|
8
|
+
'QUERY_STRING' => :query,
|
9
|
+
'rack.input' => :body
|
10
|
+
}
|
11
|
+
|
12
|
+
def params_hash env
|
13
|
+
env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
|
14
|
+
end
|
15
|
+
|
16
|
+
def request_as_hash_from env
|
17
|
+
request = env.inject({}) do |memo, (k, v)|
|
18
|
+
request_key = REQUEST_KEYS[k]
|
19
|
+
memo[request_key] = v if request_key
|
20
|
+
memo
|
21
|
+
end
|
22
|
+
|
23
|
+
request[:headers] = headers_from env
|
24
|
+
body_string = request[:body].read
|
25
|
+
|
26
|
+
if body_string.empty?
|
27
|
+
request.delete :body
|
28
|
+
else
|
29
|
+
body_is_json = request[:headers]['Content-Type'] =~ /json/
|
30
|
+
request[:body] = body_is_json ? JSON.parse(body_string) : body_string
|
31
|
+
end
|
32
|
+
request[:method] = request[:method].downcase
|
33
|
+
request
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def headers_from env
|
39
|
+
headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE')}
|
40
|
+
headers.inject({}) do | hash, header |
|
41
|
+
hash[standardise_header(header.first)] = header.last
|
42
|
+
hash
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def standardise_header header
|
47
|
+
header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
|
4
|
+
class StartupPoll
|
5
|
+
|
6
|
+
def initialize name, logger
|
7
|
+
@name = name
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def match? env
|
12
|
+
env['REQUEST_PATH'] == '/index.html' &&
|
13
|
+
env['REQUEST_METHOD'] == 'GET'
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond env
|
17
|
+
@logger.info "#{@name} started up"
|
18
|
+
[200, {}, ['Started up fine']]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class VerificationGet
|
4
|
+
|
5
|
+
include RackRequestHelper
|
6
|
+
|
7
|
+
def initialize name, logger, log_description, interaction_list
|
8
|
+
@name = name
|
9
|
+
@logger = logger
|
10
|
+
@log_description = log_description
|
11
|
+
@interaction_list = interaction_list
|
12
|
+
end
|
13
|
+
|
14
|
+
def match? env
|
15
|
+
env['REQUEST_PATH'].start_with?('/verify') &&
|
16
|
+
env['REQUEST_METHOD'] == 'GET'
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond env
|
20
|
+
if @interaction_list.all_matched?
|
21
|
+
@logger.info "Verifying - interactions matched for example \"#{example_description(env)}\""
|
22
|
+
[200, {}, ['Interactions matched']]
|
23
|
+
else
|
24
|
+
@logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Interaction diffs:"
|
25
|
+
@logger.ap @interaction_list.interaction_diffs, :warn
|
26
|
+
[500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_description env
|
31
|
+
params_hash(env)['example_description']
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'pact/consumer/mock_service_interaction_expectation'
|
2
|
+
|
1
3
|
module Pact
|
2
4
|
module Consumer
|
3
5
|
class MockServiceClient
|
@@ -22,7 +24,7 @@ module Pact
|
|
22
24
|
end
|
23
25
|
|
24
26
|
def add_expected_interaction interaction
|
25
|
-
http.request_post('/interactions', interaction.
|
27
|
+
http.request_post('/interactions', MockServiceInteractionExpectation.new(interaction).to_json)
|
26
28
|
end
|
27
29
|
|
28
30
|
def self.clear_interactions port, example_description
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'pact/reification'
|
2
|
+
|
3
|
+
# Represents the Interaction in the form required by the MockService
|
4
|
+
# The json generated will be posted to the MockService to register the expectation
|
5
|
+
module Pact
|
6
|
+
module Consumer
|
7
|
+
class MockServiceInteractionExpectation
|
8
|
+
|
9
|
+
|
10
|
+
def initialize interaction
|
11
|
+
@interaction = interaction
|
12
|
+
end
|
13
|
+
|
14
|
+
def as_json
|
15
|
+
hash = {:description => interaction.description}
|
16
|
+
hash[:provider_state] = interaction.provider_state if interaction.provider_state
|
17
|
+
options = interaction.request.options.empty? ? {} : { options: interaction.request.options}
|
18
|
+
hash[:request] = interaction.request.as_json.merge(options)
|
19
|
+
hash[:response] = Reification.from_term(interaction.response)
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_json opts = {}
|
24
|
+
as_json.to_json(opts)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :interaction
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'pact/shared/request'
|
2
|
+
require 'pact/shared/key_not_found'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Consumer
|
6
|
+
module Request
|
7
|
+
class Actual < Pact::Request::Base
|
8
|
+
|
9
|
+
def self.from_hash(hash)
|
10
|
+
sym_hash = symbolize_keys hash
|
11
|
+
method = sym_hash.fetch(:method)
|
12
|
+
path = sym_hash.fetch(:path)
|
13
|
+
query = sym_hash.fetch(:query)
|
14
|
+
headers = sym_hash.fetch(:headers)
|
15
|
+
body = sym_hash.fetch(:body, nil)
|
16
|
+
new(method, path, headers, body, query)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def self.key_not_found
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|