Skip to content

Commit 8918d78

Browse files
committed
Add SortPaths processor to prioritize and sort OpenAPI paths
1 parent 721240b commit 8918d78

3 files changed

Lines changed: 232 additions & 157 deletions

File tree

src/OpenApi/OpenApiGeneratorFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use OpenSolid\Api\OpenApi\Processor\GenerateOperationsFromApiRoutes;
2121
use OpenSolid\Api\OpenApi\Processor\MergeMethodAnnotationsIntoOperations;
2222
use OpenSolid\Api\OpenApi\Processor\RemoveScannedQueryParameters;
23+
use OpenSolid\Api\OpenApi\Processor\SortPaths;
2324
use Symfony\Component\Validator\Constraint;
2425
use Psr\Log\LoggerInterface;
2526
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
@@ -44,6 +45,7 @@ public function __invoke(): OpenApiGenerator
4445
$pl->insert(new MergeMethodAnnotationsIntoOperations(), BuildPaths::class);
4546
$pl->insert(new AugmentOperations($this->config['media_type'], $this->typeResolver), BuildPaths::class);
4647
$pl->insert(new AugmentRequestBodies($this->config['media_type']), BuildPaths::class);
48+
$pl->insert(new SortPaths(), AugmentParameters::class);
4749
$pl->insert(new AugmentQueryParameterSets(), AugmentParameters::class);
4850
$pl->insert(new RemoveScannedQueryParameters(), MergeIntoComponents::class);
4951
$pl->insert(new AugmentQueryParameters($this->pathParameterSchemaResolvers), AugmentRefs::class);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenSolid\Api\OpenApi\Processor;
6+
7+
use OpenApi\Analysis;
8+
use OpenApi\Annotations as OA;
9+
use OpenApi\Generator;
10+
11+
/**
12+
* Sorts OpenAPI paths by HTTP method priority and path structure.
13+
*
14+
* Order: POST (create), GET collection, GET item/sub-resources, DELETE, PATCH, PUT, then the rest.
15+
* Within the same method priority, paths are sorted alphabetically.
16+
*/
17+
final readonly class SortPaths
18+
{
19+
private const METHOD_PRIORITY = [
20+
'post' => 0,
21+
'get' => 1,
22+
'delete' => 2,
23+
'patch' => 3,
24+
'put' => 4,
25+
'head' => 5,
26+
'options' => 6,
27+
'trace' => 7,
28+
];
29+
30+
public function __invoke(Analysis $analysis): void
31+
{
32+
if (Generator::isDefault($analysis->openapi->paths)) {
33+
return;
34+
}
35+
36+
$paths = $analysis->openapi->paths;
37+
38+
usort($paths, function (OA\PathItem $a, OA\PathItem $b): int {
39+
$methodA = $this->getPrimaryMethod($a);
40+
$methodB = $this->getPrimaryMethod($b);
41+
42+
$priorityA = self::METHOD_PRIORITY[$methodA] ?? 8;
43+
$priorityB = self::METHOD_PRIORITY[$methodB] ?? 8;
44+
45+
// Different methods: sort by method priority
46+
if ($priorityA !== $priorityB) {
47+
return $priorityA <=> $priorityB;
48+
}
49+
50+
// Same method: sort by path segments count (fewer segments first), then alphabetically
51+
$segmentsA = substr_count($a->path, '/');
52+
$segmentsB = substr_count($b->path, '/');
53+
54+
return $segmentsA <=> $segmentsB ?: $a->path <=> $b->path;
55+
});
56+
57+
$analysis->openapi->paths = $paths;
58+
}
59+
60+
/**
61+
* Returns the highest-priority HTTP method defined on the path item.
62+
*/
63+
private function getPrimaryMethod(OA\PathItem $pathItem): string
64+
{
65+
foreach (self::METHOD_PRIORITY as $method => $priority) {
66+
if (!Generator::isDefault($pathItem->$method)) {
67+
return $method;
68+
}
69+
}
70+
71+
return 'unknown';
72+
}
73+
}

0 commit comments

Comments
 (0)