Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,72 @@ sealed class DeepLinkEntry(
}
}

/**
* Count of placeholder parameters in the URI template
*/
private val placeholderCount: Int by lazy {
placeholderRegex.findAll(uriTemplate).count()
}

/**
* Count of configurable path segments in the URI template
*/
private val configurablePathSegmentCount: Int by lazy {
uriTemplate.count { it == CONFIGURABLE_PATH_SEGMENT_PREFIX_CHAR }
}

/**
* Total number of non-concrete elements (placeholders + configurable path segments)
*/
private val totalNonConcreteElements: Int by lazy {
placeholderCount + configurablePathSegmentCount
}

/**
* List of all non-concrete element indices and their types, in order
*/
private val nonConcreteElementIndicesAndTypes: List<Pair<Int, Char>> by lazy {
val result = mutableListOf<Pair<Int, Char>>()
var index = 0
while (index < uriTemplate.length) {
val char = uriTemplate[index]
if (char == CONFIGURABLE_PATH_SEGMENT_PREFIX_CHAR || char == COMPONENT_PARAM_PREFIX_CHAR) {
result.add(Pair(index, char))
}
index++
}
result
}

fun templatesMatchesSameUrls(other: DeepLinkEntry) = uriTemplateWithoutPlaceholders == other.uriTemplateWithoutPlaceholders

/**
* Whatever template has the first placeholder (and then configurable path segment) is the less
* concrete one.
* Because if they would have been all in the same index those elements would have been on the
* same level and in the same "list" of elements we compare in order.
* In this case the one with the more concete element would have won and the same is true here.
* Compares two DeepLinkEntry instances by their concreteness (specificity).
* More concrete (specific) entries are considered "less than" and will be matched first.
*
* Comparison logic:
* 1. Fully concrete URLs (no placeholders/configurable segments) are most concrete
* 2. URLs with earlier first non-concrete element position are more concrete
* 3. When firstNonConcreteIndex is equal, we compare by:
* a. Total count of non-concrete elements (fewer = more concrete)
* b. Pairwise type and index comparison at each non-concrete position:
* - Iterate through all non-concrete element positions in order
* - At the first position where types differ, configurable path segment (<)
* is more concrete than placeholder ({})
* - If types are the same but indices differ, later index is more concrete
* c. Length of concrete parts (longer = more concrete)
*
* Examples of concreteness ordering (most to least concrete):
* - scheme://host/path1/path2/path3 (fully concrete)
* - scheme://host/path1/{param}/path3 (1 placeholder, later position)
* - scheme://host/{param}/path2/path3 (1 placeholder, earlier position)
* - scheme://host/{p1}/path2/path3/{p2} (2 placeholders, 2nd at later position)
* - scheme://host/{p1}/path2/{p2}/path4 (2 placeholders, 2nd at earlier position)
* - scheme://host/{param1}/path2/<config> (2 non-concrete, configurable at 2nd position)
* - scheme://host/{param1}/path2/{param2} (2 non-concrete, placeholder at 2nd position)
*/
override fun compareTo(other: DeepLinkEntry): Int =
when {
override fun compareTo(other: DeepLinkEntry): Int {
return when {
/**
* Specific conditions added for fully concrete links.
* Concrete link will always return -1 for firstNonConcreteIndex,
Expand All @@ -131,14 +186,53 @@ sealed class DeepLinkEntry(
other.firstNonConcreteIndex < 0 && other.firstNonConcreteIndex != this.firstNonConcreteIndex -> 1
this.firstNonConcreteIndex < other.firstNonConcreteIndex -> 1
this.firstNonConcreteIndex == other.firstNonConcreteIndex -> {
if (this.firstNonConcreteIndex == -1 || uriTemplate[firstNonConcreteIndex] == other.uriTemplate[firstNonConcreteIndex]) {
0
} else if (uriTemplate[firstNonConcreteIndex] == CONFIGURABLE_PATH_SEGMENT_PREFIX_CHAR) {
-1
} else {
1
when {
// Both are fully concrete
this.firstNonConcreteIndex == -1 -> 0
// Compare by total number of non-concrete elements (fewer is more concrete)
this.totalNonConcreteElements != other.totalNonConcreteElements ->
this.totalNonConcreteElements.compareTo(other.totalNonConcreteElements)
// Same number of non-concrete elements, compare types and positions pairwise
else -> {
val thisElements = this.nonConcreteElementIndicesAndTypes
val otherElements = other.nonConcreteElementIndicesAndTypes

// Compare types and indices at each non-concrete position
for (i in 0 until min(thisElements.size, otherElements.size)) {
val thisIndex = thisElements[i].first
val otherIndex = otherElements[i].first
val thisType = thisElements[i].second
val otherType = otherElements[i].second

// If types differ, configurable path segment is more concrete than placeholder
if (thisType != otherType) {
return if (thisType == CONFIGURABLE_PATH_SEGMENT_PREFIX_CHAR) {
-1
} else {
1
}
}

// Same type but different indices - later index is more concrete
if (thisIndex != otherIndex) {
return if (thisIndex > otherIndex) {
-1
} else {
1
}
}
}

// All types and positions match, compare by length of concrete parts (longer is more concrete)
if (this.uriTemplateWithoutPlaceholders.length != other.uriTemplateWithoutPlaceholders.length) {
-this.uriTemplateWithoutPlaceholders.length.compareTo(other.uriTemplateWithoutPlaceholders.length)
} else {
0
}
}
}
}
else -> -1
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,147 @@ class DeepLinkEntryTest {
assertThat(matchEnHost!!.parameterMap[url]).isEqualTo(parameterMap)
}

@Test
fun testCompareToFullyConcreteVsPlaceholder() {
// Fully concrete should be "less than" (more specific) than one with placeholder
val concreteEntry = activityDeepLinkEntry("airbnb://host/path1/path2")
val placeholderEntry = activityDeepLinkEntry("airbnb://host/{param}/path2")

assertThat(concreteEntry.compareTo(placeholderEntry)).isLessThan(0)
assertThat(placeholderEntry.compareTo(concreteEntry)).isGreaterThan(0)
}

@Test
fun testCompareToSameFirstPlaceholderDifferentTotalConcreteness() {
// Both have placeholder at same position, but first has more concrete segments after
val moreConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/path3")
val lessConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/{param2}/path3")

// More concrete entry should be "less than" (higher priority)
assertThat(moreConcreteEntry.compareTo(lessConcreteEntry)).isLessThan(0)
assertThat(lessConcreteEntry.compareTo(moreConcreteEntry)).isGreaterThan(0)
}

@Test
fun testCompareToSameFirstPlaceholderDifferentLaterPositions() {
// The first placeholder is at same position for both, but one of them does not have a second placeholder
val moreConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/path3/path4")
val lessConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/path4")

// More concrete entry should be "less than" (higher priority)
assertThat(moreConcreteEntry.compareTo(lessConcreteEntry)).isLessThan(0)
assertThat(lessConcreteEntry.compareTo(moreConcreteEntry)).isGreaterThan(0)
}

@Test
fun testCompareToBothHaveTwoPlaceholdersSecondAtDifferentPositions() {
// Both have two placeholders with first at same position, but second placeholder is at different positions
val secondPlaceholderLaterEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/path3/{param2}")
val secondPlaceholderEarlierEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/path4")

// Entry with second placeholder later is more concrete (less than)
assertThat(secondPlaceholderLaterEntry.compareTo(secondPlaceholderEarlierEntry)).isLessThan(0)
assertThat(secondPlaceholderEarlierEntry.compareTo(secondPlaceholderLaterEntry)).isGreaterThan(0)
}

@Test
fun testCompareToBothHaveThreePlaceholdersDifferentThirdPosition() {
// Both have three placeholders with first two at same positions, third at different positions
val thirdPlaceholderLaterEntry = activityDeepLinkEntry("airbnb://host/{p1}/path2/{p2}/path4/path5/{p3}")
val thirdPlaceholderEarlierEntry = activityDeepLinkEntry("airbnb://host/{p1}/path2/{p2}/path4/{p3}/path6")

// Entry with third placeholder later is more concrete
assertThat(thirdPlaceholderLaterEntry.compareTo(thirdPlaceholderEarlierEntry)).isLessThan(0)
assertThat(thirdPlaceholderEarlierEntry.compareTo(thirdPlaceholderLaterEntry)).isGreaterThan(0)
}

@Test
fun testCompareToBothHaveTwoConfigurablesDifferentSecondPosition() {
// Both have two configurables with first at same position, second at different positions
val secondConfigLaterEntry = activityDeepLinkEntry("airbnb://host/<config1>/path2/path3/<config2>")
val secondConfigEarlierEntry = activityDeepLinkEntry("airbnb://host/<config1>/path2/<config2>/path4")

// Entry with second configurable later is more concrete
assertThat(secondConfigLaterEntry.compareTo(secondConfigEarlierEntry)).isLessThan(0)
assertThat(secondConfigEarlierEntry.compareTo(secondConfigLaterEntry)).isGreaterThan(0)
}

@Test
fun testCompareToBothHaveMixedNonConcretesDifferentSecondPosition() {
// Both have placeholder first, then one has configurable and other has placeholder at different positions
val secondNonConcreteLaterEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/path3/<config>")
val secondNonConcreteEarlierEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/path4")

// Entry with second non-concrete later is more concrete
assertThat(secondNonConcreteLaterEntry.compareTo(secondNonConcreteEarlierEntry)).isLessThan(0)
assertThat(secondNonConcreteEarlierEntry.compareTo(secondNonConcreteLaterEntry)).isGreaterThan(0)
}

@Test
fun testCompareToBothHaveSamePositionsSameTypesButDifferentLengths() {
// Both have placeholders at exact same positions and types, but different concrete part lengths
val longerConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/longerpath/{param2}")
val shorterConcreteEntry = activityDeepLinkEntry("airbnb://host/{param1}/path/{param2}")

// Entry with longer concrete parts is more concrete
assertThat(longerConcreteEntry.compareTo(shorterConcreteEntry)).isLessThan(0)
assertThat(shorterConcreteEntry.compareTo(longerConcreteEntry)).isGreaterThan(0)
}

@Test
fun testCompareToConfigurablePathSegmentVsPlaceholder() {
// Configurable path segment should be more concrete than placeholder
val placeholderEntry = activityDeepLinkEntry("airbnb://host/{param}/path2")
val configurableEntry = activityDeepLinkEntry("airbnb://host/<config>/path2")

// Configurable entry should be "less than" (higher priority)
assertThat(configurableEntry.compareTo(placeholderEntry)).isLessThan(0)
assertThat(placeholderEntry.compareTo(configurableEntry)).isGreaterThan(0)
}

@Test
fun testCompareToMultiplePlaceholdersVsFullyConcrete() {
val concreteEntry = activityDeepLinkEntry("airbnb://host/path1/path2/path3")
val twoPlaceholdersEntry = activityDeepLinkEntry("airbnb://host/{param1}/{param2}/path3")

assertThat(concreteEntry.compareTo(twoPlaceholdersEntry)).isLessThan(0)
assertThat(twoPlaceholdersEntry.compareTo(concreteEntry)).isGreaterThan(0)
}

@Test
fun testCompareToSameFirstDifferentSecondNonConcreteType() {
// Both have placeholder at same first position, but second non-concrete element differs in type
// Configurable path segment is more concrete than placeholder
val configurableSecondEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/<config>/path4")
val placeholderSecondEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/path4")

// Configurable at second position should be more concrete (less than)
assertThat(configurableSecondEntry.compareTo(placeholderSecondEntry)).isLessThan(0)
assertThat(placeholderSecondEntry.compareTo(configurableSecondEntry)).isGreaterThan(0)
}

@Test
fun testCompareToSameFirstTwoNonConcretesDifferentThirdType() {
// Both have same types for first two non-concrete elements, but differ at third
val configurableThirdEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/<config>/path5")
val placeholderThirdEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/{param2}/{param3}/path5")

// Configurable at third position should be more concrete (less than)
assertThat(configurableThirdEntry.compareTo(placeholderThirdEntry)).isLessThan(0)
assertThat(placeholderThirdEntry.compareTo(configurableThirdEntry)).isGreaterThan(0)
}

@Test
fun testCompareToMultipleConfigurableVsPlaceholder() {
// First has configurable, second has placeholder at first position
val configurableFirstEntry = activityDeepLinkEntry("airbnb://host/<config>/path2/{param2}/path4")
val placeholderFirstEntry = activityDeepLinkEntry("airbnb://host/{param1}/path2/<config2>/path4")

// Configurable at first position should be more concrete (less than)
assertThat(configurableFirstEntry.compareTo(placeholderFirstEntry)).isLessThan(0)
assertThat(placeholderFirstEntry.compareTo(configurableFirstEntry)).isGreaterThan(0)
}

companion object {
private fun activityDeepLinkEntry(
uriTemplate: String,
Expand Down