diff --git a/README.md b/README.md index a45d3dd..9967980 100644 --- a/README.md +++ b/README.md @@ -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: )` | +| 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 | 🚧 | diff --git a/lib/pact/generator/date.rb b/lib/pact/generator/date.rb new file mode 100644 index 0000000..d444241 --- /dev/null +++ b/lib/pact/generator/date.rb @@ -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!(/(?= 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 diff --git a/lib/pact/generator/random_boolean.rb b/lib/pact/generator/random_boolean.rb new file mode 100644 index 0000000..beb1224 --- /dev/null +++ b/lib/pact/generator/random_boolean.rb @@ -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 diff --git a/lib/pact/generator/random_decimal.rb b/lib/pact/generator/random_decimal.rb new file mode 100644 index 0000000..f255989 --- /dev/null +++ b/lib/pact/generator/random_decimal.rb @@ -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 diff --git a/lib/pact/generator/random_hexadecimal.rb b/lib/pact/generator/random_hexadecimal.rb new file mode 100644 index 0000000..b90a3de --- /dev/null +++ b/lib/pact/generator/random_hexadecimal.rb @@ -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 diff --git a/lib/pact/generator/random_int.rb b/lib/pact/generator/random_int.rb new file mode 100644 index 0000000..fc157be --- /dev/null +++ b/lib/pact/generator/random_int.rb @@ -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 diff --git a/lib/pact/generator/random_string.rb b/lib/pact/generator/random_string.rb new file mode 100644 index 0000000..e61c57a --- /dev/null +++ b/lib/pact/generator/random_string.rb @@ -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 diff --git a/lib/pact/generator/regex.rb b/lib/pact/generator/regex.rb new file mode 100644 index 0000000..eb587b2 --- /dev/null +++ b/lib/pact/generator/regex.rb @@ -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 diff --git a/lib/pact/generator/time.rb b/lib/pact/generator/time.rb new file mode 100644 index 0000000..bbd7651 --- /dev/null +++ b/lib/pact/generator/time.rb @@ -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 diff --git a/lib/pact/generator/uuid.rb b/lib/pact/generator/uuid.rb new file mode 100644 index 0000000..457dcce --- /dev/null +++ b/lib/pact/generator/uuid.rb @@ -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 diff --git a/lib/pact/generators.rb b/lib/pact/generators.rb new file mode 100644 index 0000000..4d41b02 --- /dev/null +++ b/lib/pact/generators.rb @@ -0,0 +1,64 @@ +require 'pact/generator/random_boolean' +require 'pact/generator/date' +require 'pact/generator/datetime' +require 'pact/generator/provider_state' +require 'pact/generator/random_decimal' +require 'pact/generator/random_hexadecimal' +require 'pact/generator/random_int' +require 'pact/generator/random_string' +require 'pact/generator/regex' +require 'pact/generator/time' +require 'pact/generator/uuid' +require 'pact/matching_rules/jsonpath' +require 'pact/matching_rules/v3/extract' +require 'jsonpath' + +module Pact + class Generators + def self.add_generator(generator) + generators.unshift(generator) + end + + def self.generators + @generators ||= [] + end + + def self.execute_generators(object, state_params = nil, example_value = nil) + generators.each do |parser| + return parser.call(object, state_params, example_value) if parser.can_generate?(object) + end + + raise Pact::UnrecognizePactFormatError, "This document does not use a recognised Pact generator: #{object}" + end + + def self.apply_generators(expected_request, component, example_value, state_params) + # Latest pact-support is required to have generators exposed + if expected_request.methods.include?(:generators) && expected_request.generators[component] + # Some component will have single generator without selectors, i.e. path + generators = expected_request.generators[component] + if generators.is_a?(Hash) && generators.key?('type') + return execute_generators(generators, state_params, example_value) + end + + generators.each do |selector, generator| + val = JsonPath.new(selector).on(example_value) + replace = execute_generators(generator, state_params, val) + example_value = JsonPath.for(example_value).gsub(selector) { |_v| replace }.to_hash + end + end + example_value + end + + add_generator(Generator::RandomBoolean.new) + add_generator(Generator::Date.new) + add_generator(Generator::DateTime.new) + add_generator(Generator::ProviderState.new) + add_generator(Generator::RandomDecimal.new) + add_generator(Generator::RandomHexadecimal.new) + add_generator(Generator::RandomInt.new) + add_generator(Generator::RandomString.new) + add_generator(Generator::Regex.new) + add_generator(Generator::Time.new) + add_generator(Generator::Uuid.new) + end +end diff --git a/pact-support.gemspec b/pact-support.gemspec index a984465..b240d6f 100644 --- a/pact-support.gemspec +++ b/pact-support.gemspec @@ -23,6 +23,8 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "awesome_print", "~> 1.9" spec.add_runtime_dependency "diff-lcs", "~> 1.5" spec.add_runtime_dependency "expgen", "~> 0.1" + spec.add_runtime_dependency 'string_pattern', '~> 2.0' + spec.add_runtime_dependency 'jsonpath', '~> 1.0' spec.add_development_dependency "rspec", ">= 2.14", "< 4.0" spec.add_development_dependency "rake", "~> 13.0" diff --git a/spec/lib/pact/generator/date_spec.rb b/spec/lib/pact/generator/date_spec.rb new file mode 100644 index 0000000..4319d9e --- /dev/null +++ b/spec/lib/pact/generator/date_spec.rb @@ -0,0 +1,20 @@ +require 'pact/generator/date' + +describe Pact::Generator::Date do + generator = Pact::Generator::Date.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Date' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Date' } + expect(generator.call(hash).length).to eq(10) + end +end diff --git a/spec/lib/pact/generator/datetime_spec.rb b/spec/lib/pact/generator/datetime_spec.rb new file mode 100644 index 0000000..345a858 --- /dev/null +++ b/spec/lib/pact/generator/datetime_spec.rb @@ -0,0 +1,21 @@ +require 'pact/generator/datetime' + +describe Pact::Generator::DateTime do + generator = Pact::Generator::DateTime.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'DateTime' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'DateTime' } + p generator.call(hash) + expect(generator.call(hash).length).to eq(16) + end +end diff --git a/spec/lib/pact/generator/provider_state_spec.rb b/spec/lib/pact/generator/provider_state_spec.rb new file mode 100644 index 0000000..d371685 --- /dev/null +++ b/spec/lib/pact/generator/provider_state_spec.rb @@ -0,0 +1,37 @@ +require 'pact/generator/provider_state' + +describe Pact::Generator::ProviderState do + generator = Pact::Generator::ProviderState.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'ProviderState' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call without params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + expect(generator.call(hash)).to eq 'Bearer ' + end + + it 'call with correct params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'access_token' => 'ABC' } + expect(generator.call(hash, params)).to eq 'Bearer ABC' + end + + it 'call with wrong params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'refresh_token' => 'ABC' } + expect(generator.call(hash, params)).to eq 'Bearer ' + end + + it 'call with incomplete expression' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token' } + expect { generator.call(hash) }.to raise_error('Missing closing brace in expression string') + end +end diff --git a/spec/lib/pact/generator/random_boolean_spec.rb b/spec/lib/pact/generator/random_boolean_spec.rb new file mode 100644 index 0000000..8834861 --- /dev/null +++ b/spec/lib/pact/generator/random_boolean_spec.rb @@ -0,0 +1,20 @@ +require 'pact/generator/random_boolean' + +describe Pact::Generator::RandomBoolean do + generator = Pact::Generator::RandomBoolean.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomBoolean' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomBoolean' } + expect(generator.call(hash)).to eq(true).or eq(false) + end +end diff --git a/spec/lib/pact/generator/random_decimal_spec.rb b/spec/lib/pact/generator/random_decimal_spec.rb new file mode 100644 index 0000000..4c1689f --- /dev/null +++ b/spec/lib/pact/generator/random_decimal_spec.rb @@ -0,0 +1,27 @@ +require 'pact/generator/random_decimal' + +describe Pact::Generator::RandomDecimal do + generator = Pact::Generator::RandomDecimal.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomDecimal' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomDecimal' } + value = generator.call(hash) + expect(String(value).length).to eq 7 + end + + it 'call with digits' do + hash = { 'type' => 'RandomDecimal', 'digits' => 10 } + value = generator.call(hash) + expect(String(value).length).to eq 11 + end +end diff --git a/spec/lib/pact/generator/random_hexadecimal_spec.rb b/spec/lib/pact/generator/random_hexadecimal_spec.rb new file mode 100644 index 0000000..8b124ba --- /dev/null +++ b/spec/lib/pact/generator/random_hexadecimal_spec.rb @@ -0,0 +1,25 @@ +require 'pact/generator/random_hexadecimal' + +describe Pact::Generator::RandomHexadecimal do + generator = Pact::Generator::RandomHexadecimal.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomHexadecimal' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomHexadecimal' } + expect(generator.call(hash).length).to eq(8) + end + + it 'call with size' do + hash = { 'type' => 'RandomHexadecimal', 'digits' => 2 } + expect(generator.call(hash).length).to eq(2) + end +end diff --git a/spec/lib/pact/generator/random_int_spec.rb b/spec/lib/pact/generator/random_int_spec.rb new file mode 100644 index 0000000..a2290fd --- /dev/null +++ b/spec/lib/pact/generator/random_int_spec.rb @@ -0,0 +1,25 @@ +require 'pact/generator/random_int' + +describe Pact::Generator::RandomInt do + generator = Pact::Generator::RandomInt.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomInt' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomInt' } + expect(generator.call(hash).instance_of?(Integer)).to be true + end + + it 'call with min/max' do + hash = { 'type' => 'RandomInt', 'min' => 5, 'max' => 5 } + expect(generator.call(hash)).to eq 5 + end +end diff --git a/spec/lib/pact/generator/random_string_spec.rb b/spec/lib/pact/generator/random_string_spec.rb new file mode 100644 index 0000000..8675058 --- /dev/null +++ b/spec/lib/pact/generator/random_string_spec.rb @@ -0,0 +1,25 @@ +require 'pact/generator/random_string' + +describe Pact::Generator::RandomString do + generator = Pact::Generator::RandomString.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomString' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomString' } + expect(generator.call(hash).length).to eq(20) + end + + it 'call with size' do + hash = { 'type' => 'RandomString', 'size' => 30 } + expect(generator.call(hash).length).to eq(30) + end +end diff --git a/spec/lib/pact/generator/regex_spec.rb b/spec/lib/pact/generator/regex_spec.rb new file mode 100644 index 0000000..723383d --- /dev/null +++ b/spec/lib/pact/generator/regex_spec.rb @@ -0,0 +1,20 @@ +require 'pact/generator/regex' + +describe Pact::Generator::Regex do + generator = Pact::Generator::Regex.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Regex' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Regex', 'pattern' => '(one|two)' } + expect(generator.call(hash)).to eq('one').or eq('two') + end +end diff --git a/spec/lib/pact/generator/time_spec.rb b/spec/lib/pact/generator/time_spec.rb new file mode 100644 index 0000000..21cd9a5 --- /dev/null +++ b/spec/lib/pact/generator/time_spec.rb @@ -0,0 +1,20 @@ +require 'pact/generator/time' + +describe Pact::Generator::Time do + generator = Pact::Generator::Time.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Time' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Time' } + expect(generator.call(hash).length).to eq(5) + end +end diff --git a/spec/lib/pact/generator/uuid_spec.rb b/spec/lib/pact/generator/uuid_spec.rb new file mode 100644 index 0000000..aa9a94d --- /dev/null +++ b/spec/lib/pact/generator/uuid_spec.rb @@ -0,0 +1,20 @@ +require 'pact/generator/uuid' + +describe Pact::Generator::Uuid do + generator = Pact::Generator::Uuid.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Uuid' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Uuid' } + expect(generator.call(hash).length).to eq(36) + end +end diff --git a/spec/lib/pact/generators_spec.rb b/spec/lib/pact/generators_spec.rb new file mode 100644 index 0000000..5a190a5 --- /dev/null +++ b/spec/lib/pact/generators_spec.rb @@ -0,0 +1,59 @@ +require 'pact/generators' + +describe Pact::Generators do + it 'execute_generators with RandomBoolean' do + hash = { 'type' => 'RandomBoolean' } + expect(Pact::Generators.execute_generators(hash)).to eq(true).or eq(false) + end + + it 'execute_generators with Date' do + hash = { 'type' => 'Date' } + expect(Pact::Generators.execute_generators(hash).length).to eq(10) + end + + it 'execute_generators with DateTime' do + hash = { 'type' => 'DateTime' } + expect(Pact::Generators.execute_generators(hash).length).to eq(16) + end + + it 'execute_generators with ProviderState' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'access_token' => 'ABC' } + expect(Pact::Generators.execute_generators(hash, params)).to eq('Bearer ABC') + end + + it 'execute_generators with RandomDecimal' do + hash = { 'type' => 'RandomDecimal' } + expect(String(Pact::Generators.execute_generators(hash)).length).to eq(7) + end + + it 'execute_generators with RandomHexadecimal' do + hash = { 'type' => 'RandomHexadecimal' } + expect(Pact::Generators.execute_generators(hash).length).to eq(8) + end + + it 'execute_generators with RandomInt' do + hash = { 'type' => 'RandomInt' } + expect(Pact::Generators.execute_generators(hash).instance_of?(Integer)).to be true + end + + it 'execute_generators with RandomString' do + hash = { 'type' => 'RandomString' } + expect(Pact::Generators.execute_generators(hash).length).to eq(20) + end + + it 'execute_generators with Regex' do + hash = { 'type' => 'Regex', 'pattern' => '(one|two)' } + expect(Pact::Generators.execute_generators(hash)).to eq('one').or eq('two') + end + + it 'execute_generators with Time' do + hash = { 'type' => 'Time' } + expect(Pact::Generators.execute_generators(hash).length).to eq(5) + end + + it 'execute_generators with Uuid' do + hash = { 'type' => 'Uuid' } + expect(Pact::Generators.execute_generators(hash).length).to eq(36) + end +end