Skip to content

Commit 5556b81

Browse files
committed
feat(pacts for verification): support querying by POST
1 parent b3fd3aa commit 5556b81

15 files changed

+444
-73
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module PactBroker
2+
module Api
3+
module Contracts
4+
module DryValidationWorkarounds
5+
extend self
6+
7+
# I just cannot seem to get the validation to stop on the first error.
8+
# If one rule fails, they all come back failed, and it's driving me nuts.
9+
# Why on earth would I want that behaviour?
10+
def select_first_message(messages)
11+
messages.each_with_object({}) do | (key, value), new_messages |
12+
new_messages[key] = [value.first]
13+
end
14+
end
15+
16+
def flatten_array_of_hashes(array_of_hashes)
17+
new_messages = array_of_hashes.collect do | index, hash |
18+
hash.values.flatten.collect { | text | "#{text} at index #{index}"}
19+
end.flatten
20+
end
21+
22+
def flatten_indexed_messages(messages)
23+
if messages.values.any?{ | value | is_indexed_structure?(value) }
24+
messages.each_with_object({}) do | (key, value), new_messages |
25+
new_messages[key] = is_indexed_structure?(value) ? flatten_array_of_hashes(value) : value
26+
end
27+
else
28+
messages
29+
end
30+
end
31+
32+
def is_indexed_structure?(thing)
33+
thing.is_a?(Hash) && thing.keys.first.is_a?(Integer)
34+
end
35+
end
36+
end
37+
end
38+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'dry-validation'
2+
require 'pact_broker/hash_refinements'
3+
require 'pact_broker/api/contracts/dry_validation_workarounds'
4+
5+
module PactBroker
6+
module Api
7+
module Contracts
8+
class VerifiablePactsJSONQuerySchema
9+
extend DryValidationWorkarounds
10+
using PactBroker::HashRefinements
11+
12+
SCHEMA = Dry::Validation.Schema do
13+
optional(:providerVersionTags).maybe(:array?)
14+
optional(:consumerVersionSelectors).each do
15+
schema do
16+
required(:tag).filled(:str?)
17+
optional(:latest).filled(included_in?: [true, false])
18+
end
19+
end
20+
end
21+
22+
def self.call(params)
23+
select_first_message(flatten_indexed_messages(SCHEMA.call(params&.symbolize_keys).messages(full: true)))
24+
end
25+
end
26+
end
27+
end
28+
end

lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb

+5-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
require 'dry-validation'
2+
require 'pact_broker/api/contracts/dry_validation_workarounds'
23

34
module PactBroker
45
module Api
56
module Contracts
67
class VerifiablePactsQuerySchema
8+
extend DryValidationWorkarounds
9+
using PactBroker::HashRefinements
10+
711
SCHEMA = Dry::Validation.Schema do
812
optional(:provider_version_tags).maybe(:array?)
9-
# optional(:exclude_other_pending).filled(included_in?: ["true", "false"])
1013
optional(:consumer_version_selectors).each do
1114
schema do
1215
required(:tag).filled(:str?)
@@ -16,24 +19,7 @@ class VerifiablePactsQuerySchema
1619
end
1720

1821
def self.call(params)
19-
select_first_message(flatten_index_messages(SCHEMA.call(params).messages(full: true)))
20-
end
21-
22-
def self.select_first_message(messages)
23-
messages.each_with_object({}) do | (key, value), new_messages |
24-
new_messages[key] = [value.first]
25-
end
26-
end
27-
28-
def self.flatten_index_messages(messages)
29-
if messages[:consumer_version_selectors]
30-
new_messages = messages[:consumer_version_selectors].collect do | index, value |
31-
value.values.flatten.collect { | text | "#{text} at index #{index}"}
32-
end.flatten
33-
messages.merge(consumer_version_selectors: new_messages)
34-
else
35-
messages
36-
end
22+
select_first_message(flatten_indexed_messages(SCHEMA.call(params&.symbolize_keys).messages(full: true)))
3723
end
3824
end
3925
end

lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
require_relative 'base_decorator'
22
require_relative 'verifiable_pact_decorator'
33
require 'pact_broker/api/pact_broker_urls'
4+
require 'pact_broker/hash_refinements'
45

56
module PactBroker
67
module Api
78
module Decorators
89
class VerifiablePactsQueryDecorator < BaseDecorator
9-
collection :provider_version_tags
10+
using PactBroker::HashRefinements
1011

11-
collection :consumer_version_selectors, class: OpenStruct do
12+
collection :provider_version_tags, default: []
13+
14+
collection :consumer_version_selectors, default: [], class: OpenStruct do
1215
property :tag
13-
property :latest, setter: ->(fragment:, represented:, **) { represented.latest = (fragment == 'true') }
16+
property :latest,
17+
setter: ->(fragment:, represented:, **) {
18+
represented.latest = (fragment == 'true' || fragment == true)
19+
}
1420
end
1521

16-
17-
def from_hash(*args)
18-
# Should remember how to do this via Representable...
19-
result = super
20-
result.consumer_version_selectors = [] if result.consumer_version_selectors.nil?
21-
result.provider_version_tags = [] if result.provider_version_tags.nil?
22-
result
22+
def from_hash(hash)
23+
# This handles both the snakecase keys from the GET query and the camelcase JSON POST body
24+
super(hash&.snakecase_keys)
2325
end
2426
end
2527
end

lib/pact_broker/api/resources/base_resource.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def params
8888
end
8989

9090
def params_with_string_keys
91-
JSON.parse(request.body.to_s, {symbolize_names: false}.merge(PACT_PARSING_OPTIONS))
91+
@params_with_string_keys ||= JSON.parse(request.body.to_s, {symbolize_names: false}.merge(PACT_PARSING_OPTIONS))
9292
end
9393

9494
def pact_params

lib/pact_broker/api/resources/provider_pacts_for_verification.rb

+27-6
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
require 'pact_broker/api/decorators/verifiable_pacts_decorator'
33
require 'pact_broker/api/contracts/verifiable_pacts_query_schema'
44
require 'pact_broker/api/decorators/verifiable_pacts_query_decorator'
5+
require 'pact_broker/api/contracts/verifiable_pacts_json_query_schema'
56

67
module PactBroker
78
module Api
89
module Resources
910
class ProviderPactsForVerification < ProviderPacts
10-
def initialize
11-
super
12-
@query = Rack::Utils.parse_nested_query(request.uri.query)
11+
def allowed_methods
12+
["GET", "POST", "OPTIONS"]
13+
end
14+
15+
def content_types_accepted
16+
[["application/json"]]
1317
end
1418

1519
def malformed_request?
@@ -21,9 +25,12 @@ def malformed_request?
2125
end
2226
end
2327

24-
private
28+
def process_post
29+
response.body = to_json
30+
true
31+
end
2532

26-
attr_reader :query
33+
private
2734

2835
def pacts
2936
pact_service.find_for_verification(
@@ -43,12 +50,26 @@ def to_json
4350

4451

4552
def query_schema
46-
PactBroker::Api::Contracts::VerifiablePactsQuerySchema
53+
if request.get?
54+
PactBroker::Api::Contracts::VerifiablePactsQuerySchema
55+
else
56+
PactBroker::Api::Contracts::VerifiablePactsJSONQuerySchema
57+
end
4758
end
4859

4960
def parsed_query_params
5061
@parsed_query_params ||= PactBroker::Api::Decorators::VerifiablePactsQueryDecorator.new(OpenStruct.new).from_hash(query)
5162
end
63+
64+
def query
65+
@query ||= begin
66+
if request.get?
67+
Rack::Utils.parse_nested_query(request.uri.query)
68+
elsif request.post?
69+
params_with_string_keys
70+
end
71+
end
72+
end
5273
end
5374
end
5475
end

lib/pact_broker/hash_refinements.rb

+48
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,61 @@
1+
require 'pact_broker/string_refinements'
2+
13
module PactBroker
24
module HashRefinements
5+
36
refine Hash do
7+
using PactBroker::StringRefinements
8+
49
def deep_merge(other_hash, &block)
510
block_actual = Proc.new {|key, oldval, newval|
611
newval = block.call(key, oldval, newval) if block_given?
712
[oldval, newval].all? {|v| v.is_a?(Hash)} ? oldval.merge(newval, &block_actual) : newval
813
}
914
merge(other_hash, &block_actual)
1015
end
16+
17+
def symbolize_keys
18+
symbolize_keys_private(self)
19+
end
20+
21+
def snakecase_keys
22+
snakecase_keys_private(self)
23+
end
24+
25+
private
26+
27+
def snakecase_keys_private(params)
28+
case params
29+
when Hash
30+
params.inject({}) do |result, (key, value)|
31+
snake_key = case key
32+
when String then key.snakecase
33+
when Symbol then key.to_s.snakecase.to_sym
34+
else
35+
key
36+
end
37+
result.merge(snake_key => snakecase_keys_private(value))
38+
end
39+
when Array
40+
params.collect { |value| snakecase_keys_private(value) }
41+
else
42+
params
43+
end
44+
45+
end
46+
47+
def symbolize_keys_private(params)
48+
case params
49+
when Hash
50+
params.inject({}) do |result, (key, value)|
51+
result.merge(key.to_sym => symbolize_keys_private(value))
52+
end
53+
when Array
54+
params.collect { |value| symbolize_keys_private(value) }
55+
else
56+
params
57+
end
58+
end
1159
end
1260
end
1361
end

lib/pact_broker/string_refinements.rb

+36-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,41 @@ def not_blank?
88
def blank?
99
self.strip.size == 0
1010
end
11+
12+
# ripped from rubyworks/facets, thank you
13+
def snakecase
14+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
15+
.gsub(/([a-z\d])([A-Z])/,'\1_\2')
16+
.tr('-', '_')
17+
.gsub(/\s/, '_')
18+
.gsub(/__+/, '_')
19+
.downcase
20+
end
21+
22+
# ripped from rubyworks/facets, thank you
23+
def camelcase(*separators)
24+
case separators.first
25+
when Symbol, TrueClass, FalseClass, NilClass
26+
first_letter = separators.shift
27+
end
28+
29+
separators = ['_', '\s'] if separators.empty?
30+
31+
str = self.dup
32+
33+
separators.each do |s|
34+
str = str.gsub(/(?:#{s}+)([a-z])/){ $1.upcase }
35+
end
36+
37+
case first_letter
38+
when :upper, true
39+
str = str.gsub(/(\A|\s)([a-z])/){ $1 + $2.upcase }
40+
when :lower, false
41+
str = str.gsub(/(\A|\s)([A-Z])/){ $1 + $2.downcase }
42+
end
43+
44+
str
45+
end
1146
end
1247
end
13-
end
48+
end

0 commit comments

Comments
 (0)