Skip to content

Commit

Permalink
Merge pull request #107 from pact-foundation/feat/v3_generators
Browse files Browse the repository at this point in the history
feat: add v3/v4 generators (thanks @slt)
  • Loading branch information
YOU54F authored Nov 29, 2024
2 parents 855d495 + d92cf3b commit 1206f04
Show file tree
Hide file tree
Showing 26 changed files with 736 additions and 0 deletions.
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,69 @@
[![Join the chat at https://pact-foundation.slack.com/](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack)](https://slack.pact.io)

Provides shared code for the Pact gems

## Compatibility

<details><summary>Specification Compatibility</summary>

| Version | Stable | [Spec] Compatibility |
| ------- | ------ | -------------------- |
| 1.x.x | Yes | 2, 3\* |

_\*_ v3 support is limited to the subset of functionality required to enable language inter-operable [Message support].

- See V3 tracking [issue](https://github.com/pact-foundation/pact-ruby/issues/318).
- See V4 tracking [issue](https://github.com/pact-foundation/pact-ruby/issues/319).

Want V3/V4 support now? See the new standalone [pact-verifier](https://github.com/pact-foundation/pact-reference/tree/master/rust/pact_verifier_cli#standalone-pact-verifier)

[message support]: https://github.com/pact-foundation/pact-specification/tree/version-3#introduces-messages-for-services-that-communicate-via-event-streams-and-message-queues

</details>

### 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

Currently limited to provider verification only. No current way to set in consumer tests.

| Generator | 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 | 🚧 |
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
16 changes: 16 additions & 0 deletions lib/pact/generator/random_int.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Pact
module Generator
# RandomInt provides the random int generator which generate a random integer, with a min/max
class RandomInt
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'RandomInt'
end

def call(hash, _params = nil, _example_value = nil)
min = hash['min'] || 0
max = hash['max'] || 2_147_483_647
rand(min..max)
end
end
end
end
16 changes: 16 additions & 0 deletions lib/pact/generator/random_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Pact
module Generator
# RandomString provides the random string generator which generate a random string of size length
class RandomString
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'RandomString'
end

def call(hash, _params = nil, _example_value = nil)
size = hash['size'] || 20
string = rand(36**(size + 2)).to_s(36)
string[0, size]
end
end
end
end
17 changes: 17 additions & 0 deletions lib/pact/generator/regex.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'string_pattern'

module Pact
module Generator
# Regex provides the regex generator which will generate a value based on the regex pattern provided
class Regex
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'Regex'
end

def call(hash, _params = nil, _example_value = nil)
pattern = hash['pattern'] || ''
StringPattern.generate(Regexp.new(pattern))
end
end
end
end
16 changes: 16 additions & 0 deletions lib/pact/generator/time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'date'

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

def default_format
'HH:mm'
end
end
end
end
19 changes: 19 additions & 0 deletions lib/pact/generator/uuid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'securerandom'

module Pact
module Generator
# Uuid provides the uuid generator
class Uuid
def can_generate?(hash)
hash.key?('type') && hash['type'] == 'Uuid'
end

# If we had the example value, we could determine what type of uuid
# to send, this is what pact-jvm does
# See https://github.com/pact-foundation/pact-jvm/blob/master/core/model/src/main/kotlin/au/com/dius/pact/core/model/generators/Generator.kt
def call(_hash, _params = nil, _example_value = nil)
SecureRandom.uuid
end
end
end
end
Loading

0 comments on commit 1206f04

Please sign in to comment.