Skip to content

Commit f55adf1

Browse files
committed
feat(generators): Add more generators as per spec
1 parent 7a1cf3b commit f55adf1

29 files changed

+765
-92
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Pact
2+
module Provider
3+
module Generator
4+
# Boolean provides the boolean generator which will give a true or false value
5+
class Boolean
6+
def can_generate?(hash)
7+
hash.key?('type') && hash['type'] == 'Boolean'
8+
end
9+
10+
def call(_hash, _params = nil, _example_value = nil)
11+
[true, false].sample
12+
end
13+
end
14+
end
15+
end
16+
end

lib/pact/provider/generator/date.rb

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
require 'date'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# Date provides the time generator which will give the current date in the defined format
7+
class Date
8+
def can_generate?(hash)
9+
hash.key?('type') && hash['type'] == type
10+
end
11+
12+
def call(hash, _params = nil, _example_value = nil)
13+
format = hash['format'] || default_format
14+
::Time.now.strftime(convert_from_java_simple_date_format(format))
15+
end
16+
17+
def type
18+
'Date'
19+
end
20+
21+
def default_format
22+
'yyyy-MM-dd'
23+
end
24+
25+
# Format for the pact specficiation should be the Java DateTimeFormmater
26+
# This tries to convert to something Ruby can format.
27+
def convert_from_java_simple_date_format(format)
28+
# Year
29+
format.sub!(/(?<!%)y{4,}/, '%Y')
30+
format.sub!(/(?<!%)y{1,}/, '%y')
31+
32+
# Month
33+
format.sub!(/(?<!%)M{4,}/, '%B')
34+
format.sub!(/(?<!%)M{3}/, '%b')
35+
format.sub!(/(?<!%)M{1,2}/, '%m')
36+
37+
# Week
38+
format.sub!(/(?<!%)M{1,}/, '%W')
39+
40+
# Day
41+
format.sub!(/(?<!%)D{1,}/, '%j')
42+
format.sub!(/(?<!%)d{1,}/, '%d')
43+
format.sub!(/(?<!%)E{4,}/, '%A')
44+
format.sub!(/(?<!%)D{1,}/, '%a')
45+
format.sub!(/(?<!%)u{1,}/, '%u')
46+
47+
# Time
48+
format.sub!(/(?<!%)a{1,}/, '%p')
49+
format.sub!(/(?<!%)k{1,}/, '%H')
50+
format.sub!(/(?<!%)n{1,}/, '%M')
51+
format.sub!(/(?<!%)s{1,}/, '%S')
52+
format.sub!(/(?<!%)S{1,}/, '%L')
53+
54+
# Timezone
55+
format.sub!(/(?<!%)z{1,}/, '%z')
56+
format.sub!(/(?<!%)Z{1,}/, '%z')
57+
format.sub!(/(?<!%)X{1,}/, '%Z')
58+
59+
format
60+
end
61+
end
62+
end
63+
end
64+
end
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'date'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# DateTime provides the time generator which will give the current date time in the defined format
7+
class DateTime < Date
8+
def type
9+
'DateTime'
10+
end
11+
12+
def default_format
13+
'yyyy-MM-dd HH:mm'
14+
end
15+
end
16+
end
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
require 'pact/logging'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# ProviderState provides the provider state generator which will inject
7+
# values provided by the provider state setup url.
8+
class ProviderState
9+
include Pact::Logging
10+
11+
# rewrite of https://github.com/DiUS/pact-jvm/blob/master/core/support/src/main/kotlin/au/com/dius/pact/core/support/expressions/ExpressionParser.kt#L27
12+
VALUES_SEPARATOR = ','
13+
START_EXPRESSION = "\${"
14+
END_EXPRESSION = '}'
15+
16+
def can_generate?(hash)
17+
hash.key?('type') && hash['type'] == 'ProviderState'
18+
end
19+
20+
def call(hash, params = nil, _example_value = nil)
21+
params ||= {}
22+
parse_expression hash['expression'], params
23+
end
24+
25+
def parse_expression(expression, params)
26+
return_string = []
27+
buffer = expression
28+
# initial value
29+
position = buffer.index(START_EXPRESSION)
30+
31+
while position && position >= 0
32+
if position.positive?
33+
# add string
34+
return_string.push(buffer[0...position])
35+
end
36+
end_position = buffer.index(END_EXPRESSION, position)
37+
raise 'Missing closing brace in expression string' if !end_position || end_position.negative?
38+
39+
variable = buffer[position + 2...end_position]
40+
41+
if !params[variable]
42+
logger.info "Could not subsitute provider state key #{variable}, have #{params}"
43+
end
44+
45+
expression = params[variable] || ''
46+
return_string.push(expression)
47+
48+
buffer = buffer[end_position + 1...-1]
49+
position = buffer.index(START_EXPRESSION)
50+
end
51+
52+
return_string.join('')
53+
end
54+
end
55+
end
56+
end
57+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require 'bigdecimal'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# RandomDecimal provides the random decimal generator which will generate a decimal value of digits length
7+
class RandomDecimal
8+
def can_generate?(hash)
9+
hash.key?('type') && hash['type'] == 'RandomDecimal'
10+
end
11+
12+
def call(hash, _params = nil, _example_value = nil)
13+
digits = hash['digits'] || 6
14+
15+
raise 'RandomDecimalGenerator digits must be > 0, got $digits' if digits < 1
16+
17+
return rand(0..9) if digits == 1
18+
19+
return rand(0..9) + rand(1..9) / 10 if digits == 2
20+
21+
pos = rand(1..digits - 1)
22+
precision = digits - pos
23+
integers = ''
24+
decimals = ''
25+
while pos.positive?
26+
integers += String(rand(1..9))
27+
pos -= 1
28+
end
29+
while precision.positive?
30+
decimals += String(rand(1..9))
31+
precision -= 1
32+
end
33+
34+
Float("#{integers}.#{decimals}")
35+
end
36+
end
37+
end
38+
end
39+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'securerandom'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# RandomHexadecimal provides the random hexadecimal generator which will generate a hexadecimal
7+
class RandomHexadecimal
8+
def can_generate?(hash)
9+
hash.key?('type') && hash['type'] == 'RandomHexadecimal'
10+
end
11+
12+
def call(hash, _params = nil, _example_value = nil)
13+
digits = hash['digits'] || 8
14+
bytes = (digits / 2).ceil
15+
string = SecureRandom.hex(bytes)
16+
string[0, digits]
17+
end
18+
end
19+
end
20+
end
21+
end
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Pact
2+
module Provider
3+
module Generator
4+
# RandomInt provides the random int generator which generate a random integer, with a min/max
5+
class RandomInt
6+
def can_generate?(hash)
7+
hash.key?('type') && hash['type'] == 'RandomInt'
8+
end
9+
10+
def call(hash, _params = nil, _example_value = nil)
11+
min = hash['min'] || 0
12+
max = hash['max'] || 2_147_483_647
13+
rand(min..max)
14+
end
15+
end
16+
end
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Pact
2+
module Provider
3+
module Generator
4+
# RandomString provides the random string generator which generate a random string of size length
5+
class RandomString
6+
def can_generate?(hash)
7+
hash.key?('type') && hash['type'] == 'RandomString'
8+
end
9+
10+
def call(hash, _params = nil, _example_value = nil)
11+
size = hash['size'] || 20
12+
string = rand(36**(size + 2)).to_s(36)
13+
string[0, size]
14+
end
15+
end
16+
end
17+
end
18+
end

lib/pact/provider/generator/regex.rb

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'string_pattern'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# Regex provides the regex generator which will generate a value based on the regex pattern provided
7+
class Regex
8+
def can_generate?(hash)
9+
hash.key?('type') && hash['type'] == 'Regex'
10+
end
11+
12+
def call(hash, _params = nil, _example_value = nil)
13+
pattern = hash['pattern'] || ''
14+
StringPattern.generate(Regexp.new(pattern))
15+
end
16+
end
17+
end
18+
end
19+
end

lib/pact/provider/generator/time.rb

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'date'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# Time provides the time generator which will give the current time in the defined format
7+
class Time < Date
8+
def type
9+
'Time'
10+
end
11+
12+
def default_format
13+
'HH:mm'
14+
end
15+
end
16+
end
17+
end
18+
end

lib/pact/provider/generator/uuid.rb

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'securerandom'
2+
3+
module Pact
4+
module Provider
5+
module Generator
6+
# Uuid provides the uuid generator
7+
class Uuid
8+
def can_generate?(hash)
9+
hash.key?('type') && hash['type'] == 'Uuid'
10+
end
11+
12+
# If we had the example value, we could determine what type of uuid
13+
# to send, this is what pact-jvm does
14+
# See https://github.com/pact-foundation/pact-jvm/blob/master/core/model/src/main/kotlin/au/com/dius/pact/core/model/generators/Generator.kt
15+
def call(_hash, _params = nil, _example_value = nil)
16+
SecureRandom.uuid
17+
end
18+
end
19+
end
20+
end
21+
end

lib/pact/provider/generators.rb

+51-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,66 @@
1-
2-
require 'pact/provider/generators/provider_state';
1+
require 'pact/provider/generator/boolean'
2+
require 'pact/provider/generator/date'
3+
require 'pact/provider/generator/datetime'
4+
require 'pact/provider/generator/provider_state'
5+
require 'pact/provider/generator/random_decimal'
6+
require 'pact/provider/generator/random_hexadecimal'
7+
require 'pact/provider/generator/random_int'
8+
require 'pact/provider/generator/random_string'
9+
require 'pact/provider/generator/regex'
10+
require 'pact/provider/generator/time'
11+
require 'pact/provider/generator/uuid'
12+
require 'pact/matching_rules/jsonpath'
13+
require 'pact/matching_rules/v3/extract'
14+
require 'jsonpath'
315

416
module Pact
5-
module Provider
17+
module Provider
618
class Generators
7-
def self.add_generator generator
19+
def self.add_generator(generator)
820
generators.unshift(generator)
921
end
1022

11-
def self.generators
23+
def self.generators
1224
@generators ||= []
1325
end
1426

15-
def self.execute_generators object, interaction_context = nil
16-
generators.each do | parser |
17-
return parser.call(object, interaction_context) if parser.can_generate?(object)
27+
def self.execute_generators(object, state_params = nil, example_value = nil)
28+
generators.each do |parser|
29+
return parser.call(object, state_params, example_value) if parser.can_generate?(object)
30+
end
31+
32+
raise Pact::UnrecognizePactFormatError, "This document does not use a recognised Pact generator: #{object}"
33+
end
34+
35+
def self.apply_generators(expected_request, component, example_value, state_params)
36+
# Latest pact-support is required to have generators exposed
37+
if expected_request.methods.include?(:generators) && expected_request.generators[component]
38+
# Some component will have single generator without selectors, i.e. path
39+
generators = expected_request.generators[component]
40+
if generators.is_a?(Hash) && generators.key?('type')
41+
return execute_generators(generators, state_params, example_value)
42+
end
43+
44+
generators.each do |selector, generator|
45+
val = JsonPath.new(selector).on(example_value)
46+
replace = execute_generators(generator, state_params, val)
47+
example_value = JsonPath.for(example_value).gsub(selector) { |_v| replace }.to_hash
48+
end
1849
end
19-
20-
raise Pact::UnrecognizePactFormatError.new("This document does not use a recognised Pact generator: #{object}")
50+
example_value
2151
end
2252

23-
add_generator(ProviderStateGenerator.new)
53+
add_generator(Generator::Boolean.new)
54+
add_generator(Generator::Date.new)
55+
add_generator(Generator::DateTime.new)
56+
add_generator(Generator::ProviderState.new)
57+
add_generator(Generator::RandomDecimal.new)
58+
add_generator(Generator::RandomHexadecimal.new)
59+
add_generator(Generator::RandomInt.new)
60+
add_generator(Generator::RandomString.new)
61+
add_generator(Generator::Regex.new)
62+
add_generator(Generator::Time.new)
63+
add_generator(Generator::Uuid.new)
2464
end
2565
end
2666
end

0 commit comments

Comments
 (0)