From e4ef1a8cb3a1a2aff6511b109433c625d43c5ee8 Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Thu, 2 Apr 2015 20:53:54 -0400 Subject: [PATCH] Add TemplateRoute --- src/Routing/Route/TemplateRoute.php | 94 ++++++++++ .../unit/Routing/Route/TemplateRouteTest.php | 166 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 src/Routing/Route/TemplateRoute.php create mode 100644 test/tests/unit/Routing/Route/TemplateRouteTest.php diff --git a/src/Routing/Route/TemplateRoute.php b/src/Routing/Route/TemplateRoute.php new file mode 100644 index 0000000..ef6cf71 --- /dev/null +++ b/src/Routing/Route/TemplateRoute.php @@ -0,0 +1,94 @@ +buildPattern($template, $defaultPattern, $variablePatterns); + parent::__construct($pattern, $middleware); + } + + /** + * Translate the URI template into a regular expression. + * + * @param string $template URI template the path must match + * @param string $defaultPattern Regular expression for variables + * @param array $variablePatterns Map of variable names and regular expression + * @return string + */ + private function buildPattern($template, $defaultPattern, $variablePatterns) + { + // Ensure $variablePatterns is an array. + if (is_null($variablePatterns)) { + $variablePatterns = array(); + } elseif (is_object($variablePatterns)) { + $variablePatterns = (array) $variablePatterns; + } + + // Ensure a default is set. + if (!$defaultPattern) { + $defaultPattern = self::RE_SLUG; + } + + // Convert the template into the pattern + $pattern = $template; + + // Escape allowable characters with regex meaning. + $pattern = str_replace( + array("-", "."), + array("\\-", "\\."), + $pattern); + + // Replace * with .* AFTER escaping to avoid escaping .* + $pattern = str_replace("*", ".*", $pattern); + + // Surround the pattern with delimiters. + $pattern = "~^{$pattern}$~"; + + // Replace all template variables with matching subpatterns. + $callback = function ($matches) use ($variablePatterns, $defaultPattern) { + $key = $matches[1]; + if (isset($variablePatterns[$key])) { + $pattern = $variablePatterns[$key]; + } else { + $pattern = $defaultPattern; + } + return "(?<{$key}>{$pattern})"; + }; + $pattern = preg_replace_callback(self::URI_TEMPLATE_EXPRESSION_RE, $callback, $pattern); + + return $pattern; + } +} diff --git a/test/tests/unit/Routing/Route/TemplateRouteTest.php b/test/tests/unit/Routing/Route/TemplateRouteTest.php new file mode 100644 index 0000000..57a34ff --- /dev/null +++ b/test/tests/unit/Routing/Route/TemplateRouteTest.php @@ -0,0 +1,166 @@ +request = $this->prophesize("\\Psr\\Http\\Message\\ServerRequestInterface"); + $this->response = $this->prophesize("\\Psr\\Http\\Message\\ResponseInterface"); + $this->middleware = $this->prophesize("\\WellRESTed\\Routing\\MiddlewareInterface"); + } + + /** + * @dataProvider matchingTemplateProvider + */ + public function testMatchesTemplate($template, $default, $vars, $path) + { + $route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars); + $this->assertTrue($route->matchesRequestTarget($path)); + } + + /** + * @dataProvider matchingTemplateProvider + */ + public function testExtractsCaptures($template, $default, $vars, $path, $expectedCaptures) + { + $route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars); + $route->matchesRequestTarget($path, $captures); + $this->assertEquals(0, count(array_diff_assoc($expectedCaptures, $captures))); + } + + public function matchingTemplateProvider() + { + return [ + ["/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/12", ["id" => "12"]], + [ + "/cat/{catId}/{dogId}", + TemplateRoute::RE_SLUG, + null, + "/cat/molly/bear", + [ + "catId" => "molly", + "dogId" => "bear" + ] + ], + [ + "/cat/{catId}/{dogId}", + TemplateRoute::RE_NUM, + [ + "catId" => TemplateRoute::RE_SLUG, + "dogId" => TemplateRoute::RE_SLUG + ], + "/cat/molly/bear", + [ + "catId" => "molly", + "dogId" => "bear" + ] + ], + [ + "/cat/{catId}/{dogId}", + TemplateRoute::RE_NUM, + (object) [ + "catId" => TemplateRoute::RE_SLUG, + "dogId" => TemplateRoute::RE_SLUG + ], + "/cat/molly/bear", + [ + "catId" => "molly", + "dogId" => "bear" + ] + ], + ["/cat/{id}/*", null, null, "/cat/12/molly", ["id" => "12"]], + [ + "/cat/{id}-{width}x{height}.jpg", + TemplateRoute::RE_NUM, + null, + "/cat/17-200x100.jpg", + [ + "id" => "17", + "width" => "200", + "height" => "100" + ] + ], + ["/cat/{path}", ".*", null, "/cat/this/section/has/slashes", ["path" => "this/section/has/slashes"]] + ]; + } + + /** + * @dataProvider allowedVariableNamesProvider + */ + public function testMatchesAllowedVariablesNames($template, $path, $expectedCaptures) + { + $route = new TemplateRoute($template, $this->middleware->reveal()); + $route->matchesRequestTarget($path, $captures); + $this->assertEquals(0, count(array_diff_assoc($expectedCaptures, $captures))); + } + + public function allowedVariableNamesProvider() + { + return [ + ["/{n}", "/lower", ["n" => "lower"]], + ["/{N}", "/UPPER", ["N" => "UPPER"]], + ["/{var1024}", "/digits", ["var1024" => "digits"]], + ["/{variable_name}", "/underscore", ["variable_name" => "underscore"]], + ]; + } + + /** + * @dataProvider illegalVariableNamesProvider + */ + public function testFailsToMatchIllegalVariablesNames($template, $path) + { + $route = new TemplateRoute($template, $this->middleware->reveal()); + $this->assertFalse($route->matchesRequestTarget($path, $captures)); + } + + public function illegalVariableNamesProvider() + { + return [ + ["/{not-legal}", "/hyphen"], + ["/{1digitfirst}", "/digitfirst"], + ["/{%2f}", "/percent-encoded"], + ["/{}", "/empty"], + ["/{{nested}}", "/nested"] + ]; + } + + /** + * @dataProvider nonmatchingTemplateProvider + */ + public function testFailsToMatchNonmatchingTemplate($template, $default, $vars, $path) + { + $route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars); + $this->assertFalse($route->matchesRequestTarget($path, $captures)); + } + + public function nonmatchingTemplateProvider() + { + return array( + array("/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/molly"), + array("/cat/{catId}/{dogId}", TemplateRoute::RE_ALPHA, null, "/cat/12/13"), + array( + "/cat/{catId}/{dogId}", + TemplateRoute::RE_NUM, + array( + "catId" => TemplateRoute::RE_ALPHA, + "dogId" => TemplateRoute::RE_ALPHA + ), + "/cat/12/13" + ) + ); + } +}