Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit cb83d40

Browse files
azjezzfredemmott
authored andcommitted
Add MethodNotAllowedException::getAllowedMethods() (#13)
1 parent d02bd48 commit cb83d40

File tree

3 files changed

+103
-66
lines changed

3 files changed

+103
-66
lines changed

src/http-exceptions/MethodNotAllowedException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,16 @@
1111
namespace Facebook\HackRouter;
1212

1313
class MethodNotAllowedException extends HttpException {
14+
public function __construct(
15+
protected keyset<HttpMethod> $allowed,
16+
string $message = '',
17+
int $code = 0,
18+
?\Exception $previous = null,
19+
) {
20+
parent::__construct($message, $code, $previous);
21+
}
22+
23+
public function getAllowedMethods(): keyset<HttpMethod> {
24+
return $this->allowed;
25+
}
1426
}

src/router/BaseRouter.php

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace Facebook\HackRouter;
1212

13-
use namespace HH\Lib\Dict;
13+
use namespace HH\Lib\{C, Dict};
1414
use function Facebook\AutoloadMap\Generated\is_dev;
1515

1616
abstract class BaseRouter<+TResponder> {
@@ -27,22 +27,20 @@ final public function routeMethodAndPath(
2727
$data = Dict\map($data, $value ==> \urldecode($value));
2828
return tuple($responder, new ImmMap($data));
2929
} catch (NotFoundException $e) {
30-
foreach (HttpMethod::getValues() as $next) {
31-
if ($next === $method) {
32-
continue;
33-
}
34-
try {
35-
list($responder, $data) = $resolver->resolve($next, $path);
36-
if ($method === HttpMethod::HEAD && $next === HttpMethod::GET) {
37-
$data = Dict\map($data, $value ==> \urldecode($value));
38-
return tuple($responder, new ImmMap($data));
39-
}
40-
throw new MethodNotAllowedException();
41-
} catch (NotFoundException $_) {
42-
continue;
43-
}
30+
$allowed = $this->getAllowedMethods($path);
31+
if (C\is_empty($allowed)) {
32+
throw $e;
4433
}
45-
throw $e;
34+
35+
if (
36+
$method === HttpMethod::HEAD && $allowed === keyset[HttpMethod::GET]
37+
) {
38+
list($responder, $data) = $resolver->resolve(HttpMethod::GET, $path);
39+
$data = Dict\map($data, $value ==> \urldecode($value));
40+
return tuple($responder, new ImmMap($data));
41+
}
42+
43+
throw new MethodNotAllowedException($allowed);
4644
}
4745
}
4846

@@ -51,11 +49,29 @@ final public function routeRequest(
5149
): (TResponder, ImmMap<string, string>) {
5250
$method = HttpMethod::coerce($request->getMethod());
5351
if ($method === null) {
54-
throw new MethodNotAllowedException();
52+
throw new MethodNotAllowedException(
53+
$this->getAllowedMethods($request->getUri()->getPath()),
54+
);
5555
}
56+
5657
return $this->routeMethodAndPath($method, $request->getUri()->getPath());
5758
}
5859

60+
private function getAllowedMethods(string $path): keyset<HttpMethod> {
61+
$resolver = $this->getResolver();
62+
$allowed = keyset[];
63+
foreach (HttpMethod::getValues() as $method) {
64+
try {
65+
list($_responder, $_data) = $resolver->resolve($method, $path);
66+
$allowed[] = $method;
67+
} catch (NotFoundException $_) {
68+
continue;
69+
}
70+
}
71+
72+
return $allowed;
73+
}
74+
5975
private ?IResolver<TResponder> $resolver = null;
6076

6177
protected function getResolver(): IResolver<TResponder> {
@@ -76,9 +92,8 @@ protected function getResolver(): IResolver<TResponder> {
7692
if ($routes === null) {
7793
$routes = Dict\map(
7894
$this->getRoutes(),
79-
$method_routes ==> PrefixMatching\PrefixMap::fromFlatMap(
80-
dict($method_routes),
81-
),
95+
$method_routes ==>
96+
PrefixMatching\PrefixMap::fromFlatMap(dict($method_routes)),
8297
);
8398

8499
if (!is_dev()) {

tests/RouterTest.php

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,23 @@
1818
use type Facebook\Experimental\Http\Message\HTTPMethod;
1919

2020
final class RouterTest extends \Facebook\HackTest\HackTest {
21-
const keyset<string>
22-
MAP = keyset[
23-
'/foo',
24-
'/foo/',
25-
'/foo/bar',
26-
'/foo/bar/{baz}',
27-
'/foo/{bar}',
28-
'/foo/{bar}/baz',
29-
'/foo/{bar}{baz:.+}',
30-
'/food/{noms}',
31-
'/bar/{herp:\\d+}',
32-
'/bar/{herp}',
33-
'/unique/{foo}/bar',
34-
'/optional_suffix_[foo]',
35-
'/optional_suffix[/]',
36-
'/optional_suffixes/[herp[/derp]]',
37-
'/manual/en/{LegacyID}.php',
38-
];
21+
const keyset<string> MAP = keyset[
22+
'/foo',
23+
'/foo/',
24+
'/foo/bar',
25+
'/foo/bar/{baz}',
26+
'/foo/{bar}',
27+
'/foo/{bar}/baz',
28+
'/foo/{bar}{baz:.+}',
29+
'/food/{noms}',
30+
'/bar/{herp:\\d+}',
31+
'/bar/{herp}',
32+
'/unique/{foo}/bar',
33+
'/optional_suffix_[foo]',
34+
'/optional_suffix[/]',
35+
'/optional_suffixes/[herp[/derp]]',
36+
'/manual/en/{LegacyID}.php',
37+
];
3938

4039
public function expectedMatches(
4140
): varray<(string, string, dict<string, string>)> {
@@ -124,35 +123,45 @@ public function expectedMatchesWithResolvers(
124123
<<DataProvider('getAllResolvers')>>
125124
public function testMethodNotAllowedResponses(
126125
string $_name,
127-
(function(dict<HttpMethod, dict<string, string>>): IResolver<string>)
128-
$factory,
126+
(function(
127+
dict<HttpMethod, dict<string, string>>,
128+
): IResolver<string>) $factory,
129129
): void {
130130
$map = dict[
131131
HttpMethod::GET => dict[
132-
'getonly' => 'getonly',
132+
'/get' => 'get',
133133
],
134134
HttpMethod::HEAD => dict[
135-
'headonly' => 'headonly',
135+
'/head' => 'head',
136136
],
137137
HttpMethod::POST => dict[
138-
'postonly' => 'postonly',
138+
'/post' => 'post',
139139
],
140140
];
141141

142142
$router = $this->getRouter()->setResolver($factory($map));
143143

144-
list($responder, $_data) =
145-
$router->routeMethodAndPath(HttpMethod::HEAD, 'getonly');
146-
expect($responder)->toBeSame('getonly');
147-
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, 'headonly'))->toThrow(
148-
MethodNotAllowedException::class,
149-
);
150-
expect(() ==> $router->routeMethodAndPath(HttpMethod::HEAD, 'postonly'))->toThrow(
151-
MethodNotAllowedException::class,
152-
);
153-
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, 'postonly'))->toThrow(
154-
MethodNotAllowedException::class,
144+
// HEAD -> GET ( re-routing )
145+
list($responder, $_data) = $router->routeMethodAndPath(
146+
HttpMethod::HEAD,
147+
'/get',
155148
);
149+
expect($responder)->toBeSame('get');
150+
151+
// GET -> HEAD
152+
$e = expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/head'))
153+
->toThrow(MethodNotAllowedException::class);
154+
expect($e->getAllowedMethods())->toBeSame(keyset[HttpMethod::HEAD]);
155+
156+
// HEAD -> POST
157+
$e = expect(() ==> $router->routeMethodAndPath(HttpMethod::HEAD, '/post'))
158+
->toThrow(MethodNotAllowedException::class);
159+
expect($e->getAllowedMethods())->toBeSame(keyset[HttpMethod::POST]);
160+
161+
// GET -> POST
162+
$e = expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/post'))
163+
->toThrow(MethodNotAllowedException::class);
164+
expect($e->getAllowedMethods())->toEqual(keyset[HttpMethod::POST]);
156165
}
157166

158167
<<DataProvider('expectedMatches')>>
@@ -161,8 +170,8 @@ public function testMatchesPattern(
161170
string $expected_responder,
162171
dict<string, string> $expected_data,
163172
): void {
164-
list($actual_responder, $actual_data) =
165-
$this->getRouter()->routeMethodAndPath(HttpMethod::GET, $in);
173+
list($actual_responder, $actual_data) = $this->getRouter()
174+
->routeMethodAndPath(HttpMethod::GET, $in);
166175
expect($actual_responder)->toBeSame($expected_responder);
167176
expect(dict($actual_data))->toBeSame($expected_data);
168177
}
@@ -199,8 +208,10 @@ public function testRequestResponseInterfacesSupport(
199208
dict<string, string> $_expected_data,
200209
): void {
201210
$router = $this->getRouter();
202-
list($direct_responder, $direct_data) =
203-
$router->routeMethodAndPath(HttpMethod::GET, $path);
211+
list($direct_responder, $direct_data) = $router->routeMethodAndPath(
212+
HttpMethod::GET,
213+
$path,
214+
);
204215

205216
expect($path[0])->toBeSame('/');
206217

@@ -217,21 +228,20 @@ public function testRequestResponseInterfacesSupport(
217228
<<DataProvider('getAllResolvers')>>
218229
public function testNotFound(
219230
string $_resolver_name,
220-
(function(dict<HttpMethod, dict<string, string>>): IResolver<string>)
221-
$factory,
231+
(function(
232+
dict<HttpMethod, dict<string, string>>,
233+
): IResolver<string>) $factory,
222234
): void {
223235
$router = $this->getRouter()->setResolver($factory(dict[]));
224-
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/__404'))->toThrow(
225-
NotFoundException::class,
226-
);
236+
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/__404'))
237+
->toThrow(NotFoundException::class);
227238

228239
$router = $this->getRouter()
229240
->setResolver($factory(dict[
230241
HttpMethod::GET => dict['/foo' => '/foo'],
231242
]));
232-
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/__404'))->toThrow(
233-
NotFoundException::class,
234-
);
243+
expect(() ==> $router->routeMethodAndPath(HttpMethod::GET, '/__404'))
244+
->toThrow(NotFoundException::class);
235245
}
236246

237247
public function testMethodNotAllowed(): void {

0 commit comments

Comments
 (0)