Skip to content

Commit

Permalink
refactor: performance improvements in core/matchers module (Matchers,…
Browse files Browse the repository at this point in the history
… Matching, CollectionUtils)
  • Loading branch information
Tomasz Linkowski committed Dec 12, 2022
1 parent 21af9f5 commit c9a62cc
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>): 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<PathToken>, path: List<String>): 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<String>): 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<PathToken>, path: List<String>): Int {
return path
.zip(pathTokens) { pathElement, pathToken -> matchesToken(pathElement, pathToken) }
.reduce { acc, i -> acc * i }
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,13 +26,9 @@ data class MatchingContext @JvmOverloads constructor(
) {
@JvmOverloads
fun matcherDefined(path: List<String>, pathComparator: Comparator<String> = 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<String>, pathComparator: Comparator<String>): MatchingRuleCategory {
Expand All @@ -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<BestMatcherResult> { 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<String>, val pathExp: String, val ruleGroup: MatchingRuleGroup) {
val pathTokens: List<PathToken> = 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<String>): Boolean {
val resolvedMatchers = resolveMatchers(path, Comparator.naturalOrder())
return resolvedMatchers.allMatchingRules().any { it is TypeMatcher }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
package au.com.dius.pact.core.matchers.util

fun tails(col: List<String>): List<List<String>> {
val result = mutableListOf<List<String>>()
var acc = col
while (acc.isNotEmpty()) {
result.add(acc)
acc = acc.drop(1)
}
result.add(acc)
return result
}

fun <A, B> corresponds(l1: List<A>, l2: List<B>, 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 <E> List<E>.padTo(size: Int, item: E): List<E> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.<Integer, String>corresponds([1, 2, 3], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) })
!CollectionUtilsKt.<Integer, String>corresponds([1, 2, 4], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) })
!CollectionUtilsKt.<Integer, String>corresponds([1, 2, 3, 4], ['1', '2', '3'], { a, b -> a == Integer.parseInt(b) })
}

@Unroll
def 'padTo test'() {
expect:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,6 @@ data class MatchingRuleCategory @JvmOverloads constructor(
fun filter2(predicate: Predicate<Pair<String,MatchingRuleGroup>>) =
copy(matchingRules = matchingRules.filter { predicate.test(it.key to it.value) }.toMutableMap())

fun maxBy(comparator: Comparator<String>): Pair<String, MatchingRuleGroup>? {
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
*/
Expand Down

0 comments on commit c9a62cc

Please sign in to comment.