diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatcherDefinition.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatcherDefinition.kt index 1817f78fe..c07b69a6b 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatcherDefinition.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatcherDefinition.kt @@ -1,6 +1,7 @@ package au.com.dius.pact.core.model.matchingrules.expressions import au.com.dius.pact.core.model.generators.Generator +import au.com.dius.pact.core.model.generators.ProviderStateGenerator import au.com.dius.pact.core.model.matchingrules.BooleanMatcher import au.com.dius.pact.core.model.matchingrules.ContentTypeMatcher import au.com.dius.pact.core.model.matchingrules.DateMatcher @@ -9,8 +10,8 @@ import au.com.dius.pact.core.model.matchingrules.EachValueMatcher import au.com.dius.pact.core.model.matchingrules.EqualsMatcher import au.com.dius.pact.core.model.matchingrules.IncludeMatcher import au.com.dius.pact.core.model.matchingrules.MatchingRule -import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher +import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher import au.com.dius.pact.core.model.matchingrules.NotEmptyMatcher import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher import au.com.dius.pact.core.model.matchingrules.RegexMatcher @@ -149,11 +150,16 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { } lexer.matchString("notEmpty") -> { if (matchChar('(')) { - when (val primitiveValueResult = primitiveValue()) { + when (val primitiveValueResult = primitiveValue(false)) { is Result.Ok -> { if (matchChar(')')) { - Result.Ok(MatchingRuleDefinition(primitiveValueResult.value.first, NotEmptyMatcher, null) - .withType(primitiveValueResult.value.second)) + Result.Ok( + MatchingRuleDefinition( + primitiveValueResult.value.first, + NotEmptyMatcher, + primitiveValueResult.value.third + ).withType(primitiveValueResult.value.second) + ) } else { Result.Err(parseError("Was expecting a ')' at index ${lexer.index}")) } @@ -310,24 +316,42 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { when (val formatResult = string()) { is Result.Ok -> { val matcher = when (type) { - "date" -> if (formatResult.value != null) DateMatcher(formatResult.value!!) else DateMatcher() - "time" -> if (formatResult.value != null) TimeMatcher(formatResult.value!!) else TimeMatcher() - else -> if (formatResult.value != null) TimestampMatcher(formatResult.value!!) else TimestampMatcher() + "date" -> DateMatcher(formatResult.value) + "time" -> TimeMatcher(formatResult.value) + else -> TimestampMatcher(formatResult.value) } + if (matchChar(',')) { - when (val stringResult = string()) { - is Result.Ok -> Result.Ok(MatchingRuleResult(stringResult.value, ValueType.String, matcher)) - is Result.Err -> stringResult + lexer.skipWhitespace() + if (lexer.matchString("fromProviderState")) { + when (val providerStateResult = fromProviderState()) { + is Result.Ok -> { + Result.Ok( + MatchingRuleResult( + providerStateResult.value.first, + providerStateResult.value.second, + matcher, + providerStateResult.value.third + ) + ) + } + is Result.Err -> providerStateResult + } + } else { + when (val stringResult = string()) { + is Result.Ok -> Result.Ok(MatchingRuleResult(stringResult.value, ValueType.String, matcher)) + is Result.Err -> stringResult + } } } else { - Result.Err("Was expecting a ',' at index ${lexer.index}") + Result.Err(parseError("Was expecting a ',' at index ${lexer.index}")) } } is Result.Err -> formatResult } } else { - Result.Err("Was expecting a ',' at index ${lexer.index}") + Result.Err(parseError("Was expecting a ',' at index ${lexer.index}")) } } @@ -341,6 +365,22 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { ) ) + lexer.matchString("fromProviderState") -> { + when (val providerStateResult = fromProviderState()) { + is Result.Ok -> { + Result.Ok( + MatchingRuleResult( + providerStateResult.value.first, + providerStateResult.value.second, + NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL), + providerStateResult.value.third + ) + ) + } + is Result.Err -> providerStateResult + } + } + else -> Result.Err("Was expecting a decimal number at index ${lexer.index}") } } else { @@ -357,6 +397,22 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { ) ) + lexer.matchString("fromProviderState") -> { + when (val providerStateResult = fromProviderState()) { + is Result.Ok -> { + Result.Ok( + MatchingRuleResult( + providerStateResult.value.first, + providerStateResult.value.second, + NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER), + providerStateResult.value.third + ) + ) + } + is Result.Err -> providerStateResult + } + } + else -> Result.Err("Was expecting an integer at index ${lexer.index}") } } else { @@ -380,10 +436,26 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { ) ) - else -> Result.Err("Was expecting a number at index ${lexer.index}") + lexer.matchString("fromProviderState") -> { + when (val providerStateResult = fromProviderState()) { + is Result.Ok -> { + Result.Ok( + MatchingRuleResult( + providerStateResult.value.first, + providerStateResult.value.second, + NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER), + providerStateResult.value.third + ) + ) + } + is Result.Err -> providerStateResult + } + } + + else -> Result.Err(parseError("Was expecting a number at index ${lexer.index}")) } } else { - Result.Err("Was expecting a ',' at index ${lexer.index}") + Result.Err(parseError("Was expecting a ',' at index ${lexer.index}")) } private fun unsignedNumber(): Result { @@ -396,12 +468,14 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { } private fun matchEqualOrType(equalTo: Boolean) = if (matchChar(',')) { - when (val primitiveValueResult = primitiveValue()) { + when (val primitiveValueResult = primitiveValue(false)) { is Result.Ok -> { Result.Ok( MatchingRuleResult( - primitiveValueResult.value.first, primitiveValueResult.value.second, - if (equalTo) EqualsMatcher else TypeMatcher + primitiveValueResult.value.first, + primitiveValueResult.value.second, + if (equalTo) EqualsMatcher else TypeMatcher, + primitiveValueResult.value.third ) ) } @@ -409,13 +483,13 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { is Result.Err -> primitiveValueResult } } else { - Result.Err("Was expecting a ',' at index ${lexer.index}") + Result.Err(parseError("Was expecting a ',' at index ${lexer.index}")) } // 'include' COMMA s=string { $rule = new IncludeMatcher($s.contents); $value = $s.contents; $type = ValueType.String; } private fun matchInclude() = if (matchChar(',')) { when (val stringResult = string()) { - is Result.Ok -> Result.Ok(MatchingRuleResult(stringResult.value, ValueType.String, IncludeMatcher(stringResult.value.toString()))) + is Result.Ok -> Result.Ok(MatchingRuleResult(stringResult.value, ValueType.String, IncludeMatcher(stringResult.value))) is Result.Err -> stringResult } } else { @@ -488,21 +562,24 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { // | v=DECIMAL_LITERAL // | v=INTEGER_LITERAL // | v=BOOLEAN_LITERAL + // | 'null' + // | 'fromProviderState' fromProviderState // ; - fun primitiveValue(): Result, String> { + fun primitiveValue(alreadyCalled: Boolean): Result, String> { lexer.skipWhitespace() return when { lexer.peekNextChar() == '\'' -> { when (val stringResult = string()) { - is Result.Ok -> Result.Ok(stringResult.value to ValueType.String) + is Result.Ok -> Result.Ok(Triple(stringResult.value, ValueType.String, null)) is Result.Err -> stringResult } } - lexer.matchString("null") -> Result.Ok(null to ValueType.String) - lexer.matchDecimal() -> Result.Ok(lexer.lastMatch to ValueType.Decimal) - lexer.matchInteger() -> Result.Ok(lexer.lastMatch to ValueType.Decimal) - lexer.matchBoolean() -> Result.Ok(lexer.lastMatch to ValueType.Boolean) - else -> Result.Err("Was expecting a primitive value at index ${lexer.index}") + lexer.matchString("null") -> Result.Ok(Triple(null, ValueType.String, null)) + lexer.matchDecimal() -> Result.Ok(Triple(lexer.lastMatch, ValueType.Decimal, null)) + lexer.matchInteger() -> Result.Ok(Triple(lexer.lastMatch, ValueType.Integer, null)) + lexer.matchBoolean() -> Result.Ok(Triple(lexer.lastMatch, ValueType.Boolean, null)) + !alreadyCalled && lexer.matchString("fromProviderState") -> fromProviderState() + else -> Result.Err(parseError("Was expecting a primitive value at index ${lexer.index}")) } } @@ -511,9 +588,8 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { // String contents = $STRING_LITERAL.getText(); // $contents = contents.substring(1, contents.length() - 1); // } - // | 'null' // ; - fun string(): Result { + fun string(): Result { lexer.skipWhitespace() return if (lexer.matchChar('\'')) { var ch = lexer.nextChar() @@ -532,10 +608,10 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { if (ch == '\'') { processRawString(stringResult) } else { - Result.Err("Unterminated string found at index ${lexer.index}") + Result.Err(parseError("Unterminated string found at index ${lexer.index}")) } } else { - Result.Err("Was expecting a string at index ${lexer.index}") + Result.Err(parseError("Was expecting a string at index ${lexer.index}")) } } @@ -611,4 +687,39 @@ class MatcherDefinitionParser(private val lexer: MatcherDefinitionLexer) { } return Result.Ok(buffer.toString()) } + + // '(' exp=STRING_LITERAL COMMA v=primitiveValue ')' + private fun fromProviderState(): Result, String> { + return if (matchChar('(')) { + when (val expressionResult = string()) { + is Result.Ok -> { + lexer.skipWhitespace() + if (matchChar(',')) { + when (val primitiveResult = primitiveValue(true)) { + is Result.Ok -> { + lexer.skipWhitespace() + if (matchChar(')')) { + Result.Ok( + Triple( + primitiveResult.value.first, + primitiveResult.value.second, + ProviderStateGenerator(expressionResult.value, primitiveResult.value.second.toDataType()) + ) + ) + } else { + Result.Err(parseError("Was expecting a ')' at index ${lexer.index}")) + } + } + is Result.Err -> primitiveResult + } + } else { + Result.Err(parseError("Was expecting a ',' at index ${lexer.index}")) + } + } + is Result.Err -> return expressionResult + } + } else { + Result.Err(parseError("Was expecting a '(' at index ${lexer.index}")) + } + } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatchingRuleDefinition.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatchingRuleDefinition.kt index 7a0d044c6..c4f5b2b27 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatchingRuleDefinition.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/expressions/MatchingRuleDefinition.kt @@ -4,6 +4,7 @@ import au.com.dius.pact.core.model.generators.Generator import au.com.dius.pact.core.model.matchingrules.MatchingRule import au.com.dius.pact.core.support.Either import au.com.dius.pact.core.support.Result +import au.com.dius.pact.core.support.expressions.DataType import au.com.dius.pact.core.support.isNotEmpty import io.github.oshai.kotlinlogging.KLogging @@ -50,6 +51,17 @@ enum class ValueType { Unknown -> valueType } } + + fun toDataType(): DataType { + return when (this) { + Unknown -> DataType.RAW + String -> DataType.STRING + Number -> DataType.DECIMAL + Integer -> DataType.INTEGER + Decimal -> DataType.DECIMAL + Boolean -> DataType.BOOLEAN + } + } } data class MatchingRuleDefinition( diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/matchingrules/expressions/MatchingDefinitionParserSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/matchingrules/expressions/MatchingDefinitionParserSpec.groovy index 0a604326d..ad20b18e2 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/matchingrules/expressions/MatchingDefinitionParserSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/matchingrules/expressions/MatchingDefinitionParserSpec.groovy @@ -1,9 +1,11 @@ package au.com.dius.pact.core.model.matchingrules.expressions +import au.com.dius.pact.core.model.generators.ProviderStateGenerator import au.com.dius.pact.core.model.matchingrules.BooleanMatcher import au.com.dius.pact.core.model.matchingrules.DateMatcher import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher import au.com.dius.pact.core.model.matchingrules.EachValueMatcher +import au.com.dius.pact.core.model.matchingrules.EqualsMatcher import au.com.dius.pact.core.model.matchingrules.IncludeMatcher import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher @@ -15,6 +17,7 @@ import au.com.dius.pact.core.model.matchingrules.TimestampMatcher import au.com.dius.pact.core.model.matchingrules.TypeMatcher import au.com.dius.pact.core.support.Either import au.com.dius.pact.core.support.Result +import au.com.dius.pact.core.support.expressions.DataType import spock.lang.Specification @SuppressWarnings(['LineLength', 'UnnecessaryGString']) @@ -34,29 +37,45 @@ class MatchingDefinitionParserSpec extends Specification { def 'parse type matcher'() { expect: MatchingRuleDefinition.parseMatchingRuleDefinition(expression).value == - new MatchingRuleDefinition('Name', TypeMatcher.INSTANCE, null) + new MatchingRuleDefinition('Name', TypeMatcher.INSTANCE, generator) where: - expression << [ - "matching(type,'Name')", - "matching( type , 'Name' ) " - ] + expression | generator + "matching(type,'Name')" | null + "matching( type , 'Name' ) " | null + "matching(type, fromProviderState('exp', 'Name'))" | new ProviderStateGenerator('exp', DataType.STRING) + } + + def 'parse equal to matcher'() { + expect: + MatchingRuleDefinition.parseMatchingRuleDefinition(expression).value == + new MatchingRuleDefinition(value, EqualsMatcher.INSTANCE, generator) + + where: + + expression | value | generator + "matching(equalTo,'Name')" | 'Name' | null + "matching( equalTo , 123.4 ) " | '123.4' | null + "matching(equalTo, fromProviderState('exp', 3))" | '3' | new ProviderStateGenerator('exp', DataType.INTEGER) } def 'parse number matcher'() { expect: MatchingRuleDefinition.parseMatchingRuleDefinition(expression).value == - new MatchingRuleDefinition(value, new NumberTypeMatcher(matcher), null) + new MatchingRuleDefinition(value, new NumberTypeMatcher(matcher), generator) where: - expression | value | matcher - 'matching(number,100)' | '100' | NumberTypeMatcher.NumberType.NUMBER - 'matching( number , 100 )' | '100' | NumberTypeMatcher.NumberType.NUMBER - 'matching(number, -100.101)' | '-100.101' | NumberTypeMatcher.NumberType.NUMBER - 'matching(integer,100)' | '100' | NumberTypeMatcher.NumberType.INTEGER - 'matching(decimal,100.101)' | '100.101' | NumberTypeMatcher.NumberType.DECIMAL + expression | value | matcher | generator + 'matching(number,100)' | '100' | NumberTypeMatcher.NumberType.NUMBER | null + 'matching( number , 100 )' | '100' | NumberTypeMatcher.NumberType.NUMBER | null + 'matching(number, -100.101)' | '-100.101' | NumberTypeMatcher.NumberType.NUMBER | null + 'matching(integer,100)' | '100' | NumberTypeMatcher.NumberType.INTEGER | null + 'matching(decimal,100.101)' | '100.101' | NumberTypeMatcher.NumberType.DECIMAL | null + "matching(number, fromProviderState('exp', 3))" | '3' | NumberTypeMatcher.NumberType.NUMBER | new ProviderStateGenerator('exp', DataType.INTEGER) + "matching(integer, fromProviderState('exp', 3))" | '3' | NumberTypeMatcher.NumberType.INTEGER | new ProviderStateGenerator('exp', DataType.INTEGER) + "matching(decimal, fromProviderState('exp', 3))" | '3' | NumberTypeMatcher.NumberType.DECIMAL | new ProviderStateGenerator('exp', DataType.INTEGER) } def 'invalid number matcher'() { @@ -72,15 +91,18 @@ class MatchingDefinitionParserSpec extends Specification { def 'parse datetime matcher'() { expect: MatchingRuleDefinition.parseMatchingRuleDefinition(expression).value == - new MatchingRuleDefinition(value, matcherClass.newInstance(format), null) + new MatchingRuleDefinition(value, matcherClass.newInstance(format), generator) where: - expression | format | value | matcherClass - "matching(datetime, 'yyyy-MM-dd HH:mm:ss','2000-01-01 12:00:00')" | 'yyyy-MM-dd HH:mm:ss' | '2000-01-01 12:00:00' | TimestampMatcher - "matching(date, 'yyyy-MM-dd','2000-01-01')" | 'yyyy-MM-dd' | '2000-01-01' | DateMatcher - "matching(time, 'HH:mm:ss','12:00:00')" | 'HH:mm:ss' | '12:00:00' | TimeMatcher - "matching( time , 'HH:mm:ss' , '12:00:00' )" | 'HH:mm:ss' | '12:00:00' | TimeMatcher + expression | format | value | matcherClass | generator + "matching(datetime, 'yyyy-MM-dd HH:mm:ss','2000-01-01 12:00:00')" | 'yyyy-MM-dd HH:mm:ss' | '2000-01-01 12:00:00' | TimestampMatcher | null + "matching(date, 'yyyy-MM-dd','2000-01-01')" | 'yyyy-MM-dd' | '2000-01-01' | DateMatcher | null + "matching(time, 'HH:mm:ss','12:00:00')" | 'HH:mm:ss' | '12:00:00' | TimeMatcher | null + "matching( time , 'HH:mm:ss' , '12:00:00' )" | 'HH:mm:ss' | '12:00:00' | TimeMatcher | null + "matching(datetime, 'yyyy-MM-dd HH:mm:ss', fromProviderState('exp', '2000-01-01 12:00:00'))" | 'yyyy-MM-dd HH:mm:ss' | '2000-01-01 12:00:00' | TimestampMatcher | new ProviderStateGenerator('exp', DataType.STRING) + "matching(date, 'yyyy-MM-dd', fromProviderState('exp', '2000-01-01'))" | 'yyyy-MM-dd' | '2000-01-01' | DateMatcher | new ProviderStateGenerator('exp', DataType.STRING) + "matching(time, 'HH:mm:ss', fromProviderState('exp', '12:00:00'))" | 'HH:mm:ss' | '12:00:00' | TimeMatcher | new ProviderStateGenerator('exp', DataType.STRING) } def 'parse regex matcher'() { @@ -156,14 +178,15 @@ class MatchingDefinitionParserSpec extends Specification { def 'parse notEmpty matcher'() { expect: MatchingRuleDefinition.parseMatchingRuleDefinition(expression).value == - new MatchingRuleDefinition(value, type, [ Either.a(NotEmptyMatcher.INSTANCE) ], null) + new MatchingRuleDefinition(value, type, [ Either.a(NotEmptyMatcher.INSTANCE) ], generator) where: - expression | value | type - "notEmpty('true')" | 'true' | ValueType.String - "notEmpty( 'true' )" | 'true' | ValueType.String - 'notEmpty(true)' | 'true' | ValueType.Boolean + expression | value | type | generator + "notEmpty('true')" | 'true' | ValueType.String | null + "notEmpty( 'true' )" | 'true' | ValueType.String | null + 'notEmpty(true)' | 'true' | ValueType.Boolean | null + "notEmpty(fromProviderState('exp', 3))" | '3' | ValueType.Integer | new ProviderStateGenerator('exp', DataType.INTEGER) } def 'parsing string values'() {