Skip to content

Commit

Permalink
feat: add v3/v4 generators (thanks @slt)
Browse files Browse the repository at this point in the history
  • Loading branch information
YOU54F committed Aug 13, 2024
1 parent 7a09875 commit ec1be05
Show file tree
Hide file tree
Showing 29 changed files with 911 additions and 0 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,48 @@
![Build status](https://github.com/pact-foundation/pact-support/workflows/Test/badge.svg)

Provides shared code for the Pact gems

## Supported matching rules

| matcher | Spec Version | Implemented | Usage|
|---------------|--------------|-------------|-------------|
| Equality | V1 | | |
| Regex | V2 || `Pact.term(generate, matcher)` |
| Type | V2 || `Pact.like(generate)` |
| MinType | V2 || `Pact.each_like(generate, min: <val>)` |
| MaxType | V2 | | |
| MinMaxType | V2 | | |
| Include | V3 | | |
| Integer | V3 | | |
| Decimal | V3 | | |
| Number | V3 | | |
| Timestamp | V3 | | |
| Time | V3 | | |
| Date | V3 | | |
| Null | V3 | | |
| Boolean | V3 | | |
| ContentType | V3 | | |
| Values | V3 | | |
| ArrayContains | V4 | | |
| StatusCode | V4 | | |
| NotEmpty | V4 | | |
| Semver | V4 | | |
| EachKey | V4 | | |
| EachValue | V4 | | |

## Supported generators

| matcher | Spec Version | Implemented |
|------------------------|--------------|----|
| RandomInt | V3 ||
| RandomDecimal | V3 ||
| RandomHexadecimal | V3 ||
| RandomString | V3 ||
| Regex | V3 ||
| Uuid | V3/V4 ||
| Date | V3 ||
| Time | V3 ||
| DateTime | V3 ||
| RandomBoolean | V3 ||
| ProviderState | V4 ||
| MockServerURL | V4 | 🚧 |
133 changes: 133 additions & 0 deletions lib/pact/from_provider_state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
require 'pact/shared/active_support_support'

module Pact
class FromProviderState
include Pact::ActiveSupportSupport

attr_reader :expression, :default_string, :params

def self.json_create(obj)
new(obj['data']['expression'], obj['data']['params'])
end

def initialize(expression, arg2)
@expression = expression
if arg2.is_a? String
@default_string = arg2
@params = find_default_values
else
@params = stringify_params(arg2)
@default_string = default_string_from_params @params
end
end

def replace_params(params)
@params = stringify_params(params)
@default_string = default_string_from_params @params
end

def to_hash
{ json_class: self.class.name, data: { expression: @expression, params: @params } }
end

def as_json
to_hash
end

def to_json(options = {})
as_json.to_json(options)
end

def ==(other)
return false if !other.respond_to?(:expression) || other.expression != @expression
return false if !other.respond_to?(:params) || other.params != @params

true
end

def to_s
"Pact::FromProviderState: #{@expression} #{@params}"
end

def empty?
false
end

private

def stringify_params(params)
stringified_params = {}
params.each { |k, v| stringified_params[k.to_s] = v }
stringified_params
end

def param_name_regex
/\${[a-zA-Z0-9_-]+}/
end

def parse_expression
matches = @expression.scan(param_name_regex)
matches.map do |match|
match[2..(match.length - 2)]
end
end

def find_strings_between_variables(_var_names)
in_between_strings = []
previous_string_end = 0
matches = @expression.scan(param_name_regex)

matches.size.times do |index|
# get the locations of the string in between the matched variable names
variable_name_start = @expression.index(matches[index])
variable_name_end = variable_name_start + matches[index].length
string_text = @expression[previous_string_end...variable_name_start]
previous_string_end = variable_name_end
in_between_strings << string_text unless string_text.empty?
end
last_part = @expression[previous_string_end...@expression.length]
in_between_strings << last_part unless last_part.empty?

in_between_strings
end

def find_variable_values_in_default_string(in_between_strings)
previous_value_end = 0
values = []

in_between_strings.each do |string|
string_start = @default_string.index(string)
value = @default_string[previous_value_end...string_start]
values << value unless string_start == 0
previous_value_end = string_start + string.length
end

last_string = @default_string[previous_value_end..@default_string.length - 1]
values << last_string unless last_string.empty?

values
end

def find_default_values
var_names = parse_expression
in_between_strings = find_strings_between_variables(var_names)

values = find_variable_values_in_default_string(in_between_strings)

param_hash = {}
new_params_arr = var_names.zip(values)
new_params_arr.each do |key, value|
param_hash[key] = value
end
param_hash
end

def default_string_from_params(params)
default_string = @expression
params.each do |key, value|
default_string = default_string.gsub('${' + key + '}', value)
end
default_string
end
end
end
62 changes: 62 additions & 0 deletions lib/pact/generator/date.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'date'

module Pact
module Generator
# Date provides the time generator which will give the current date in the defined format
class Date
def can_generate?(hash)
hash.key?('type') && hash['type'] == type
end

def call(hash, _params = nil, _example_value = nil)
format = hash['format'] || default_format
::Time.now.strftime(convert_from_java_simple_date_format(format))
end

def type
'Date'
end

def default_format
'yyyy-MM-dd'
end

# Format for the pact specficiation should be the Java DateTimeFormmater
# This tries to convert to something Ruby can format.
def convert_from_java_simple_date_format(format)
# Year
format.sub!(/(?<!%)y{4,}/, '%Y')
format.sub!(/(?<!%)y{1,}/, '%y')

# Month
format.sub!(/(?<!%)M{4,}/, '%B')
format.sub!(/(?<!%)M{3}/, '%b')
format.sub!(/(?<!%)M{1,2}/, '%m')

# Week
format.sub!(/(?<!%)M{1,}/, '%W')

# Day
format.sub!(/(?<!%)D{1,}/, '%j')
format.sub!(/(?<!%)d{1,}/, '%d')
format.sub!(/(?<!%)E{4,}/, '%A')
format.sub!(/(?<!%)D{1,}/, '%a')
format.sub!(/(?<!%)u{1,}/, '%u')

# Time
format.sub!(/(?<!%)a{1,}/, '%p')
format.sub!(/(?<!%)k{1,}/, '%H')
format.sub!(/(?<!%)n{1,}/, '%M')
format.sub!(/(?<!%)s{1,}/, '%S')
format.sub!(/(?<!%)S{1,}/, '%L')

# Timezone
format.sub!(/(?<!%)z{1,}/, '%z')
format.sub!(/(?<!%)Z{1,}/, '%z')
format.sub!(/(?<!%)X{1,}/, '%Z')

format
end
end
end
end
16 changes: 16 additions & 0 deletions lib/pact/generator/datetime.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'date'

module Pact
module Generator
# DateTime provides the time generator which will give the current date time in the defined format
class DateTime < Date
def type
'DateTime'
end

def default_format
'yyyy-MM-dd HH:mm'
end
end
end
end
53 changes: 53 additions & 0 deletions lib/pact/generator/provider_state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'pact/logging'

module Pact
module Generator
# ProviderState provides the provider state generator which will inject
# values provided by the provider state setup url.
class ProviderState
include Pact::Logging

# 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
VALUES_SEPARATOR = ','
START_EXPRESSION = "\${"
END_EXPRESSION = '}'

def can_generate?(hash)
hash.key?('type') && hash['type'] == 'ProviderState'
end

def call(hash, params = nil, _example_value = nil)
params ||= {}
parse_expression hash['expression'], params
end

def parse_expression(expression, params)
return_string = []
buffer = expression
# initial value
position = buffer.index(START_EXPRESSION)

while position && position >= 0
if position.positive?
# add string
return_string.push(buffer[0...position])
end
end_position = buffer.index(END_EXPRESSION, position)
raise 'Missing closing brace in expression string' if !end_position || end_position.negative?

variable = buffer[position + 2...end_position]

logger.info "Could not subsitute provider state key #{variable}, have #{params}" unless params[variable]

expression = params[variable] || ''
return_string.push(expression)

buffer = buffer[end_position + 1...-1]
position = buffer.index(START_EXPRESSION)
end

return_string.join('')
end
end
end
end
14 changes: 14 additions & 0 deletions lib/pact/generator/random_boolean.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Pact
module Generator
# Boolean provides the boolean generator which will give a true or false value
class RandomBoolean
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'RandomBoolean'
end

def call(_hash, _params = nil, _example_value = nil)
[true, false].sample
end
end
end
end
37 changes: 37 additions & 0 deletions lib/pact/generator/random_decimal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'bigdecimal'

module Pact
module Generator
# RandomDecimal provides the random decimal generator which will generate a decimal value of digits length
class RandomDecimal
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'RandomDecimal'
end

def call(hash, _params = nil, _example_value = nil)
digits = hash['digits'] || 6

raise 'RandomDecimalGenerator digits must be > 0, got $digits' if digits < 1

return rand(0..9) if digits == 1

return rand(0..9) + rand(1..9) / 10 if digits == 2

pos = rand(1..digits - 1)
precision = digits - pos
integers = ''
decimals = ''
while pos.positive?
integers += String(rand(1..9))
pos -= 1
end
while precision.positive?
decimals += String(rand(1..9))
precision -= 1
end

Float("#{integers}.#{decimals}")
end
end
end
end
19 changes: 19 additions & 0 deletions lib/pact/generator/random_hexadecimal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'securerandom'

module Pact
module Generator
# RandomHexadecimal provides the random hexadecimal generator which will generate a hexadecimal
class RandomHexadecimal
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'RandomHexadecimal'
end

def call(hash, _params = nil, _example_value = nil)
digits = hash['digits'] || 8
bytes = (digits / 2).ceil
string = SecureRandom.hex(bytes)
string[0, digits]
end
end
end
end
Loading

0 comments on commit ec1be05

Please sign in to comment.