From d4f2ba2263028ba379ade2dfda64ea221e682e60 Mon Sep 17 00:00:00 2001 From: Martin Joiner Date: Tue, 31 Jan 2023 16:46:56 +0000 Subject: [PATCH] PathFinder: Adding filtering of placeholder routes scored by matching parts count --- src/PSR7/OperationAddress.php | 20 ++++++++++++- src/PSR7/PathFinder.php | 53 +++++++++++++++++++++++++++++++++++ tests/PSR7/PathFinderTest.php | 53 +++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/PSR7/OperationAddress.php b/src/PSR7/OperationAddress.php index 0ec96e37..c71ac29a 100644 --- a/src/PSR7/OperationAddress.php +++ b/src/PSR7/OperationAddress.php @@ -62,7 +62,25 @@ public function path(): string public function hasPlaceholders(): bool { - return preg_match(self::PATH_PLACEHOLDER, $this->path()) === 1; + return (bool) $this->countPlaceholders(); + } + + public function countPlaceholders(): int + { + return preg_match_all(self::PATH_PLACEHOLDER, $this->path()) ?? 0; + } + + public function countExactMatchParts(string $comparisonPath): int + { + $comparisonPathParts = explode('/', trim($comparisonPath, '/')); + $pathParts = explode('/', trim($this->path(), '/')); + $exactMatchCount = 0; + foreach($comparisonPathParts as $key => $comparisonPathPart) { + if ($comparisonPathPart === $pathParts[$key]) { + $exactMatchCount++; + } + } + return $exactMatchCount; } /** diff --git a/src/PSR7/PathFinder.php b/src/PSR7/PathFinder.php index 00ab1929..2ea4c1fe 100644 --- a/src/PSR7/PathFinder.php +++ b/src/PSR7/PathFinder.php @@ -206,6 +206,59 @@ private function prioritizeStaticPaths(array $paths): array return 0; }); + return $this->attemptNarrowDown($paths); + } + + /** + * Some paths are more static than others. + * + * @param OperationAddress[] $paths + * + * @return OperationAddress[] + */ + private function attemptNarrowDown(array $paths): array + { + if (count($paths) === 1) { + return $paths; + } + + $partCounts = []; + $placeholderCounts = []; + foreach ($paths as $path) { + $partCounts[] = $this->countParts($path->path()); + $placeholderCounts[] = $path->countPlaceholders(); + } + $partCounts[] = $this->countParts($this->path); + if (count(array_unique($partCounts)) === 1 && count(array_unique($placeholderCounts)) > 1) { + // All paths have the same number of parts but there are differing placeholder counts. We can narrow down! + return $this->filterToHighestExactMatchingParts($paths); + } + return $paths; } + + /** + * Scores all paths by how many parts match exactly with $this->path and returns only the highest scoring group + * + * @param OperationAddress[] $paths + * + * @return OperationAddress[] + */ + private function filterToHighestExactMatchingParts(array $paths): array + { + $scoredCandidates = []; + foreach ($paths as $candidate) { + $score = $candidate->countExactMatchParts($this->path); + $scoredCandidates[$score][] = $candidate; + } + + $highestScoreKey = max(array_keys($scoredCandidates)); + + return $scoredCandidates[$highestScoreKey]; + } + + private function countParts(string $path): int + { + return preg_match_all('#/#', trim($path, '/')) + 1; + } } diff --git a/tests/PSR7/PathFinderTest.php b/tests/PSR7/PathFinderTest.php index 36d95765..8f42cd2c 100644 --- a/tests/PSR7/PathFinderTest.php +++ b/tests/PSR7/PathFinderTest.php @@ -116,4 +116,57 @@ public function testItFindsMatchingOperationForMultipleServersWithSamePath(): vo $this->assertCount(1, $opAddrs); $this->assertEquals('/products/{id}', $opAddrs[0]->path()); } + + public function testItPrioritisesOperatorsThatAreMoreStatic(): void + { + $spec = <<search(); + + $this->assertCount(1, $opAddrs); + $this->assertEquals('/products/{product}/images/thumbnails', $opAddrs[0]->path()); + } + + public function testItPrioritises2EquallyDynamicPaths(): void + { + $spec = <<search(); + + $this->assertCount(2, $opAddrs); + $this->assertEquals('/products/{product}/images/thumbnails/{size}', $opAddrs[0]->path()); + $this->assertEquals('/products/{product}/images/{image}/primary', $opAddrs[1]->path()); + } + }