Skip to content

Commit

Permalink
PathFinder: Adding filtering of placeholder routes scored by matching…
Browse files Browse the repository at this point in the history
… parts count
  • Loading branch information
Martin Joiner authored and scaytrase committed Mar 23, 2023
1 parent 6772c57 commit d4f2ba2
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 1 deletion.
20 changes: 19 additions & 1 deletion src/PSR7/OperationAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
53 changes: 53 additions & 0 deletions src/PSR7/PathFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
53 changes: 53 additions & 0 deletions tests/PSR7/PathFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,57 @@ public function testItFindsMatchingOperationForMultipleServersWithSamePath(): vo
$this->assertCount(1, $opAddrs);
$this->assertEquals('/products/{id}', $opAddrs[0]->path());
}

public function testItPrioritisesOperatorsThatAreMoreStatic(): void
{
$spec = <<<SPEC
openapi: "3.0.0"
info:
title: Uber API
description: Move your app forward with the Uber API
version: "1.0.0"
paths:
/products/{product}/images/{image}:
get:
summary: A specific image for a specific product
/products/{product}/images/thumbnails:
get:
summary: All thumbnail images for a specific product
SPEC;

$pathFinder = new PathFinder(Reader::readFromYaml($spec), '/products/10/images/thumbnails', 'get');
$opAddrs = $pathFinder->search();

$this->assertCount(1, $opAddrs);
$this->assertEquals('/products/{product}/images/thumbnails', $opAddrs[0]->path());
}

public function testItPrioritises2EquallyDynamicPaths(): void
{
$spec = <<<SPEC
openapi: "3.0.0"
info:
title: Uber API
description: Move your app forward with the Uber API
version: "1.0.0"
paths:
/products/{product}/images/{image}/{size}:
get:
summary: A specific image for a specific product
/products/{product}/images/thumbnails/{size}:
get:
summary: All thumbnail images for a specific product
/products/{product}/images/{image}/primary:
get:
summary: All thumbnail images for a specific product
SPEC;

$pathFinder = new PathFinder(Reader::readFromYaml($spec), '/products/10/images/thumbnails/primary', 'get');
$opAddrs = $pathFinder->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());
}

}

0 comments on commit d4f2ba2

Please sign in to comment.