View test reports →

Coverage Summary for Class: RuleMatcher (dev.skymansandy.wiretap.domain.usecase)

Class Class, % Method, % Branch, % Line, % Instruction, %
RuleMatcher 100% (1/1) 100% (16/16) 82.2% (97/118) 91.9% (57/62) 93.5% (560/599)


 /*
  * Copyright (c) 2026 skymansandy. All rights reserved.
  */
 
 package dev.skymansandy.wiretap.domain.usecase
 
 import dev.skymansandy.wiretap.domain.model.MatcherType
 import dev.skymansandy.wiretap.domain.model.WiretapRule
 import dev.skymansandy.wiretap.domain.model.matchers.BodyMatcher
 import dev.skymansandy.wiretap.domain.model.matchers.HeaderMatcher
 import dev.skymansandy.wiretap.domain.model.matchers.UrlMatcher
 
 internal object RuleMatcher {
 
     fun matchesMethod(requestMethod: String, ruleMethod: String): Boolean =
         ruleMethod == "*" || ruleMethod.equals(requestMethod, ignoreCase = true)
 
     fun matchesAllCriteria(
         rule: WiretapRule,
         url: String,
         headers: Map<String, String>,
         body: String?,
     ): Boolean {
         if (rule.urlMatcher == null && rule.headerMatchers.isEmpty() && rule.bodyMatcher == null) return false
 
         rule.urlMatcher?.let { if (!matchesUrl(it, url)) return false }
         if (rule.headerMatchers.any { !matchesHeader(it, headers) }) return false
         rule.bodyMatcher?.let { if (!matchesBody(it, body)) return false }
 
         return true
     }
 
     fun matchesUrl(matcher: UrlMatcher, url: String): Boolean = when (matcher) {
         is UrlMatcher.Exact -> url.equals(matcher.pattern, ignoreCase = true)
         is UrlMatcher.Contains -> url.contains(matcher.pattern, ignoreCase = true)
         is UrlMatcher.Regex -> matchesRegex(matcher.pattern, url)
     }
 
     fun matchesHeader(matcher: HeaderMatcher, headers: Map<String, String>): Boolean {
         val value by lazy { headers.headerValue(matcher.key) }
         return when (matcher) {
             is HeaderMatcher.KeyExists -> headers.keys.any { it.equals(matcher.key, ignoreCase = true) }
             is HeaderMatcher.ValueExact -> value?.equals(matcher.value, ignoreCase = true) == true
             is HeaderMatcher.ValueContains -> value?.contains(matcher.value, ignoreCase = true) == true
             is HeaderMatcher.ValueRegex -> value?.let { matchesRegex(matcher.pattern, it) } == true
         }
     }
 
     fun matchesBody(matcher: BodyMatcher, body: String?): Boolean = when (matcher) {
         is BodyMatcher.Exact -> body?.equals(matcher.pattern, ignoreCase = true) == true
         is BodyMatcher.Contains -> body?.contains(matcher.pattern, ignoreCase = true) == true
         is BodyMatcher.Regex -> body?.let { matchesRegex(matcher.pattern, it) } == true
     }
 
     fun methodsOverlap(a: String, b: String): Boolean =
         a == "*" || b == "*" || a.equals(b, ignoreCase = true)
 
     fun urlMatchersOverlap(a: UrlMatcher?, b: UrlMatcher?): Boolean {
         if (a == null || b == null) return true
         return patternsOverlap(a.type, a.pattern, b.type, b.pattern)
     }
 
     fun headerMatchersOverlap(a: List<HeaderMatcher>, b: List<HeaderMatcher>): Boolean {
         if (a.isEmpty() || b.isEmpty()) return true
 
         val aByKey = a.groupBy { it.key.lowercase() }
         val bByKey = b.groupBy { it.key.lowercase() }
 
         return aByKey.keys.intersect(bByKey.keys).all { key ->
             val aMatchers = aByKey.getValue(key)
             val bMatchers = bByKey.getValue(key)
             aMatchers.all { am -> bMatchers.all { bm -> singleHeaderMatchersOverlap(am, bm) } }
         }
     }
 
     fun bodyMatchersOverlap(a: BodyMatcher?, b: BodyMatcher?): Boolean {
         if (a == null || b == null) return true
         return patternsOverlap(a.type, a.pattern, b.type, b.pattern)
     }
 
     fun rulesOverlap(a: WiretapRule, b: WiretapRule): Boolean =
         methodsOverlap(a.method, b.method) &&
             urlMatchersOverlap(a.urlMatcher, b.urlMatcher) &&
             headerMatchersOverlap(a.headerMatchers, b.headerMatchers) &&
             bodyMatchersOverlap(a.bodyMatcher, b.bodyMatcher)
 
     // -- Private helpers --
 
     private fun matchesRegex(pattern: String, input: String): Boolean = runCatching {
         pattern.toRegex(RegexOption.IGNORE_CASE).containsMatchIn(input)
     }.getOrDefault(false)
 
     private fun patternsOverlap(
         aType: MatcherType,
         aPattern: String,
         bType: MatcherType,
         bPattern: String,
     ): Boolean = when {
         aType == MatcherType.Regex || bType == MatcherType.Regex -> true
 
         aType == MatcherType.Exact && bType == MatcherType.Exact ->
             aPattern.equals(bPattern, ignoreCase = true)
 
         aType == MatcherType.Contains && bType == MatcherType.Contains -> true
 
         aType == MatcherType.Exact && bType == MatcherType.Contains ->
             aPattern.contains(bPattern, ignoreCase = true)
 
         aType == MatcherType.Contains && bType == MatcherType.Exact ->
             bPattern.contains(aPattern, ignoreCase = true)
 
         else -> true
     }
 
     private fun singleHeaderMatchersOverlap(a: HeaderMatcher, b: HeaderMatcher): Boolean {
         if (a is HeaderMatcher.KeyExists || b is HeaderMatcher.KeyExists) return true
         return patternsOverlap(a.matcherType(), a.valuePattern(), b.matcherType(), b.valuePattern())
     }
 
     private fun HeaderMatcher.matcherType(): MatcherType = when (this) {
         is HeaderMatcher.KeyExists -> MatcherType.Exact
         is HeaderMatcher.ValueExact -> MatcherType.Exact
         is HeaderMatcher.ValueContains -> MatcherType.Contains
         is HeaderMatcher.ValueRegex -> MatcherType.Regex
     }
 
     private fun HeaderMatcher.valuePattern(): String = when (this) {
         is HeaderMatcher.KeyExists -> key
         is HeaderMatcher.ValueExact -> value
         is HeaderMatcher.ValueContains -> value
         is HeaderMatcher.ValueRegex -> pattern
     }
 
     private fun Map<String, String>.headerValue(key: String): String? =
         entries.firstOrNull { it.key.equals(key, ignoreCase = true) }?.value
 }