diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt index 6c858d4538..6b38cc8bcd 100755 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt @@ -2,10 +2,8 @@ package au.com.dius.pact.core.matchers import au.com.dius.pact.core.matchers.util.IndicesCombination import au.com.dius.pact.core.matchers.util.LargestKeyValue -import au.com.dius.pact.core.matchers.util.corresponds import au.com.dius.pact.core.matchers.util.memoizeFixed import au.com.dius.pact.core.matchers.util.padTo -import au.com.dius.pact.core.matchers.util.tails import au.com.dius.pact.core.model.PathToken import au.com.dius.pact.core.model.constructPath import au.com.dius.pact.core.model.matchingrules.ArrayContainsMatcher @@ -16,53 +14,47 @@ import au.com.dius.pact.core.model.matchingrules.EqualsMatcher import au.com.dius.pact.core.model.matchingrules.MatchingRule import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup -import au.com.dius.pact.core.model.matchingrules.MatchingRules import au.com.dius.pact.core.model.matchingrules.MaxEqualsIgnoreOrderMatcher import au.com.dius.pact.core.model.matchingrules.MinEqualsIgnoreOrderMatcher import au.com.dius.pact.core.model.matchingrules.MinMaxEqualsIgnoreOrderMatcher -import au.com.dius.pact.core.model.matchingrules.TypeMatcher import au.com.dius.pact.core.model.matchingrules.ValuesMatcher import au.com.dius.pact.core.model.parsePath import mu.KLogging import java.math.BigInteger import java.util.Comparator -import java.util.function.Predicate @Suppress("TooManyFunctions") object Matchers : KLogging() { - private val intRegex = Regex("\\d+") - private fun matchesToken(pathElement: String, token: PathToken): Int { return when (token) { is PathToken.Root -> if (pathElement == "$") 2 else 0 is PathToken.Field -> if (pathElement == token.name) 2 else 0 - is PathToken.Index -> if (pathElement.matches(intRegex) && token.index == pathElement.toInt()) 2 else 0 - is PathToken.StarIndex -> if (pathElement.matches(intRegex)) 1 else 0 + is PathToken.Index -> if (pathElement.toIntOrNull() == token.index) 2 else 0 + is PathToken.StarIndex -> if (pathElement.toIntOrNull() != null) 1 else 0 is PathToken.Star -> 1 else -> 0 } } fun matchesPath(pathExp: String, path: List): Int { - val parseResult = parsePath(pathExp) - val filter = tails(path.reversed()).filter { l -> - corresponds(l.reversed(), parseResult) { pathElement, pathToken -> - matchesToken(pathElement, pathToken) != 0 - } - } - return if (filter.isNotEmpty()) { - filter.maxByOrNull { seq -> seq.size }?.size ?: 0 - } else { - 0 - } + return matchesPath(parsePath(pathExp), path) + } + + private fun matchesPath(pathTokens: List, path: List): Int { + val matchesPath = pathTokens.size <= path.size && pathTokens.indices + .none { index -> matchesToken(path[index], pathTokens[index]) == 0 } + return if (matchesPath) pathTokens.size else 0 } fun calculatePathWeight(pathExp: String, path: List): Int { - val parseResult = parsePath(pathExp) - return path.zip(parseResult).asSequence().map { - matchesToken(it.first, it.second) - }.reduce { acc, i -> acc * i } + return calculatePathWeight(parsePath(pathExp), path) + } + + fun calculatePathWeight(pathTokens: List, path: List): Int { + return path + .zip(pathTokens) { pathElement, pathToken -> matchesToken(pathElement, pathToken) } + .reduce { acc, i -> acc * i } } @JvmStatic diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt index 2ed1a90ed7..7f27329b8f 100644 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matching.kt @@ -2,6 +2,7 @@ package au.com.dius.pact.core.matchers import au.com.dius.pact.core.model.HttpPart import au.com.dius.pact.core.model.IRequest +import au.com.dius.pact.core.model.PathToken import au.com.dius.pact.core.model.constructPath import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher import au.com.dius.pact.core.model.matchingrules.EachValueMatcher @@ -25,13 +26,9 @@ data class MatchingContext @JvmOverloads constructor( ) { @JvmOverloads fun matcherDefined(path: List, pathComparator: Comparator = Comparator.naturalOrder()): Boolean { - return resolveMatchers(path, pathComparator).filter2 { (p, rule) -> - if (rule.rules.any { it is ValuesMatcher }) { - parsePath(p).size == path.size - } else { - true - } - }.isNotEmpty() + return resolveMatchers(path, pathComparator) + .filter2 { (p, ruleGroup) -> ruleGroup.rules.none { it is ValuesMatcher } || parsePath(p).size == path.size } + .isNotEmpty() } private fun resolveMatchers(path: List, pathComparator: Comparator): MatchingRuleCategory { @@ -49,31 +46,23 @@ data class MatchingContext @JvmOverloads constructor( ): MatchingRuleGroup { val matcherCategory = resolveMatchers(path, pathComparator) return if (matchers.name == "body") { - val result = matcherCategory.filter2 { (p, rule) -> - if (rule.rules.any { it is ValuesMatcher }) { - parsePath(p).size == path.size - } else { - true - } - }.maxBy { a, b -> - val weightA = Matchers.calculatePathWeight(a, path) - val weightB = Matchers.calculatePathWeight(b, path) - when { - weightA == weightB -> when { - a.length > b.length -> 1 - a.length < b.length -> -1 - else -> 0 - } - weightA > weightB -> 1 - else -> -1 - } - } - result?.second?.copy(cascaded = parsePath(result.first).size != path.size) ?: MatchingRuleGroup() + val result = matcherCategory.matchingRules + .map { BestMatcherResult(path = path, pathExp = it.key, ruleGroup = it.value) } + .filter { it.pathWeight > 0 } + .maxWithOrNull(compareBy { it.pathWeight }.thenBy { it.pathExp.length }) + result?.ruleGroup?.copy(cascaded = result.pathTokens.size < path.size) ?: MatchingRuleGroup() } else { matcherCategory.matchingRules.values.first() } } + private class BestMatcherResult(path: List, val pathExp: String, val ruleGroup: MatchingRuleGroup) { + val pathTokens: List = parsePath(pathExp) + val pathWeight: Int = if (ruleGroup.rules.none { it is ValuesMatcher } || pathTokens.size == path.size) + Matchers.calculatePathWeight(pathTokens, path) + else 0 + } + fun typeMatcherDefined(path: List): Boolean { val resolvedMatchers = resolveMatchers(path, Comparator.naturalOrder()) return resolvedMatchers.allMatchingRules().any { it is TypeMatcher } diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/util/CollectionUtils.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/util/CollectionUtils.kt index 9ff1cedf5c..a464e33dcf 100644 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/util/CollectionUtils.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/util/CollectionUtils.kt @@ -1,32 +1,11 @@ package au.com.dius.pact.core.matchers.util -fun tails(col: List): List> { - val result = mutableListOf>() - var acc = col - while (acc.isNotEmpty()) { - result.add(acc) - acc = acc.drop(1) - } - result.add(acc) - return result -} - -fun corresponds(l1: List, l2: List, fn: (a: A, b: B) -> Boolean): Boolean { - return if (l1.size == l2.size) { - l1.zip(l2).all { fn(it.first, it.second) } - } else { - false - } -} +import java.util.Collections.nCopies fun List.padTo(size: Int, item: E): List { - return if (size < this.size) { - this.dropLast(this.size - size) - } else { - val list = this.toMutableList() - for (i in this.size.until(size)) { - list.add(item) - } - return list + return when { + size < this.size -> subList(fromIndex = 0, toIndex = size) + size > this.size -> this + nCopies(size - this.size, item) + else -> this } } diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatchingContextSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatchingContextSpec.groovy index 538e3c3b3f..78f8d2018f 100644 --- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatchingContextSpec.groovy +++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/MatchingContextSpec.groovy @@ -14,6 +14,7 @@ import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher import au.com.dius.pact.core.model.matchingrules.NullMatcher import au.com.dius.pact.core.model.matchingrules.RegexMatcher import au.com.dius.pact.core.model.matchingrules.TypeMatcher +import au.com.dius.pact.core.model.matchingrules.ValuesMatcher import kotlin.Triple import spock.lang.Issue import spock.lang.Specification @@ -291,6 +292,20 @@ class MatchingContextSpec extends Specification { category << [ 'header', 'query', 'metadata' ] } + @Issue('#1347') + def 'values matcher must not cascade'() { + given: + def matchingRules = new MatchingRuleCategory('body') + matchingRules.addRule('$', ValuesMatcher.INSTANCE) + def context = new MatchingContext(matchingRules, true) + + when: + def rules = context.selectBestMatcher(['$', 'id']) + + then: + rules.rules == [] + } + @Issue('#1367') def 'array contains matcher with simple values'() { given: diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/util/CollectionUtilsSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/util/CollectionUtilsSpec.groovy index f5b3125d2c..6288cf3a4b 100644 --- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/util/CollectionUtilsSpec.groovy +++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/util/CollectionUtilsSpec.groovy @@ -6,19 +6,6 @@ import spock.lang.Unroll @SuppressWarnings('ClosureAsLastMethodParameter') class CollectionUtilsSpec extends Specification { - def 'tails test'() { - expect: - CollectionUtilsKt.tails(['a', 'b', 'c', 'd']) == [['a', 'b', 'c', 'd'], ['b', 'c', 'd'], ['c', 'd'], ['d'], []] - CollectionUtilsKt.tails(['something', '$']) == [['something', '$'], ['$'], []] - } - - def 'corresponds test'() { - expect: - CollectionUtilsKt.corresponds([1, 2, 3], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) }) - !CollectionUtilsKt.corresponds([1, 2, 4], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) }) - !CollectionUtilsKt.corresponds([1, 2, 3, 4], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) }) - } - @Unroll def 'padTo test'() { expect: diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRuleCategory.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRuleCategory.kt index c95b23fbce..00733793aa 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRuleCategory.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/matchingrules/MatchingRuleCategory.kt @@ -96,17 +96,6 @@ data class MatchingRuleCategory @JvmOverloads constructor( fun filter2(predicate: Predicate>) = copy(matchingRules = matchingRules.filter { predicate.test(it.key to it.value) }.toMutableMap()) - fun maxBy(comparator: Comparator): Pair? { - val max = matchingRules.entries.fold(matchingRules.entries.firstOrNull()) { acc, entry -> - if (acc != null && comparator.compare(acc.key, entry.key) >= 0) { - acc - } else { - entry - } - } - return max?.toPair() - } - /** * Returns all the matching rules */