]> git.andy128k.dev Git - routeexpression.git/commitdiff
initial
authorAndrey Kutejko <andy128k@gmail.com>
Sat, 22 Jun 2013 07:54:38 +0000 (10:54 +0300)
committerAndrey Kutejko <andy128k@gmail.com>
Sat, 22 Jun 2013 07:54:38 +0000 (10:54 +0300)
.gitignore [new file with mode: 0644]
README.org [new file with mode: 0644]
composer.json [new file with mode: 0644]
composer.lock [new file with mode: 0644]
src/routeexpression.php [new file with mode: 0644]
t/ExpressionTest.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..fcac4a7
--- /dev/null
@@ -0,0 +1,2 @@
+/vendor
+
diff --git a/README.org b/README.org
new file mode 100644 (file)
index 0000000..48a55dd
--- /dev/null
@@ -0,0 +1,49 @@
+* Route Expression
+
+** Build expression
+
+   There are two ways to build expression: using fluent api and compiling.
+
+*** Fluent API
+
+    Create instance of RouteExpression class and compose expression by chaining parts.
+
+    Example: ~$expr = (new RouteExpression)->optional('/ru')->just('/story/');~
+
+    Available parts:
+
+    1. ~just($text)~ -- literal text;
+    2. ~oneOf($choice1, $choice2, ...)~ -- one of given choices;
+    3. ~oneOfv($choices)~ -- same as above, except it takes array of choices;
+    4. ~optional($option)~ -- optional text, same as ~oneOf($option, '')~;
+    5. ~id()~ -- nonempty sequence of digits;
+    6. ~slug()~ -- nonempty sequence of letters, digits, underscores and dashes.
+
+*** Compilation
+
+    Route expression can be compiled from text representation.
+
+    Example: ~$expr = RouteExpression::compile('[/ru]/story/');~
+
+    Text enclosed in brackets ~[]~ is treated as ~optional~. 
+    Text enclosed in braces ~{}~ is treated as ~oneOf~. Choices are separated by ~|~ pipe character.
+    Id and slug are written as ~:id~ and ~:slug~.
+    
+** Matching
+
+   #+BEGIN_SRC php
+   bool match($url, &$matches)
+   #+END_SRC
+   
+   This method returns ~true~ on successfull match of ~$url~ and matched parts in ~$matches~ array.
+
+   Example: ~RouteExpression::compile('[/ru]/story/')->match('/story/', $matches);~ returns ~true~ and ~$matches~ contains empty string which corresponds to optional part of expression.
+
+** Reversing
+    
+   #+BEGIN_SRC php
+   string reverse($matches)
+   #+END_SRC
+
+   This method substitutes values from ~$matches~ array to the expression and returns url.
+
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..48e3119
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "name": "andy128k/routeexpression",
+  "description": "Route expression",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "Andrey Kutejko",
+      "email": "andy128k@gmail.com"
+    }
+  ],
+  "autoload": {
+    "classmap" : ["src"]
+  },
+  "require-dev": {
+    "phpunit/phpunit": "3.7.*"
+  }
+}
+
diff --git a/composer.lock b/composer.lock
new file mode 100644 (file)
index 0000000..a7b1197
--- /dev/null
@@ -0,0 +1,429 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
+    ],
+    "hash": "e93568fdb3ba2b300af261eed15a4b56",
+    "packages": [
+
+    ],
+    "packages-dev": [
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "1.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+                "reference": "1.2.11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.11",
+                "reference": "1.2.11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "phpunit/php-file-iterator": ">=1.3.0@stable",
+                "phpunit/php-text-template": ">=1.1.1@stable",
+                "phpunit/php-token-stream": ">=1.1.3@stable"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "3.7.*"
+            },
+            "suggest": {
+                "ext-dom": "*",
+                "ext-xdebug": ">=2.0.5"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "PHP/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "time": "2013-05-23 18:23:24"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "1.3.3",
+            "source": {
+                "type": "git",
+                "url": "git://github.com/sebastianbergmann/php-file-iterator.git",
+                "reference": "1.3.3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3",
+                "reference": "1.3.3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "File/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+            "homepage": "http://www.phpunit.de/",
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "time": "2012-10-11 04:44:38"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "git://github.com/sebastianbergmann/php-text-template.git",
+                "reference": "1.1.4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4",
+                "reference": "1.1.4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "Text/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+            "keywords": [
+                "template"
+            ],
+            "time": "2012-10-31 11:15:28"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "1.0.4",
+            "source": {
+                "type": "git",
+                "url": "git://github.com/sebastianbergmann/php-timer.git",
+                "reference": "1.0.4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://github.com/sebastianbergmann/php-timer/zipball/1.0.4",
+                "reference": "1.0.4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "PHP/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "http://www.phpunit.de/",
+            "keywords": [
+                "timer"
+            ],
+            "time": "2012-10-11 04:45:58"
+        },
+        {
+            "name": "phpunit/php-token-stream",
+            "version": "1.1.5",
+            "source": {
+                "type": "git",
+                "url": "git://github.com/sebastianbergmann/php-token-stream.git",
+                "reference": "1.1.5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://github.com/sebastianbergmann/php-token-stream/zipball/1.1.5",
+                "reference": "1.1.5",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "PHP/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Wrapper around PHP's tokenizer extension.",
+            "homepage": "http://www.phpunit.de/",
+            "keywords": [
+                "tokenizer"
+            ],
+            "time": "2012-10-11 04:47:14"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "3.7.21",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git",
+                "reference": "3.7.21"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.21",
+                "reference": "3.7.21",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-pcre": "*",
+                "ext-reflection": "*",
+                "ext-spl": "*",
+                "php": ">=5.3.3",
+                "phpunit/php-code-coverage": ">=1.2.1,<1.3.0",
+                "phpunit/php-file-iterator": ">=1.3.1",
+                "phpunit/php-text-template": ">=1.1.1",
+                "phpunit/php-timer": ">=1.0.2,<1.1.0",
+                "phpunit/phpunit-mock-objects": ">=1.2.0,<1.3.0",
+                "symfony/yaml": ">=2.0,<3.0"
+            },
+            "require-dev": {
+                "pear-pear/pear": "1.9.4"
+            },
+            "suggest": {
+                "ext-json": "*",
+                "ext-simplexml": "*",
+                "ext-tokenizer": "*",
+                "phpunit/php-invoker": ">=1.1.0,<1.2.0"
+            },
+            "bin": [
+                "composer/bin/phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.7.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "PHPUnit/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                "",
+                "../../symfony/yaml/"
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "http://www.phpunit.de/",
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "time": "2013-05-23 18:54:29"
+        },
+        {
+            "name": "phpunit/phpunit-mock-objects",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git",
+                "reference": "1.2.3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip",
+                "reference": "1.2.3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "phpunit/php-text-template": ">=1.1.1@stable"
+            },
+            "suggest": {
+                "ext-soap": "*"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "PHPUnit/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "include-path": [
+                ""
+            ],
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Mock Object library for PHPUnit",
+            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+            "keywords": [
+                "mock",
+                "xunit"
+            ],
+            "time": "2013-01-13 10:24:48"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v2.3.1",
+            "target-dir": "Symfony/Component/Yaml",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/Yaml.git",
+                "reference": "v2.3.1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/Yaml/zipball/v2.3.1",
+                "reference": "v2.3.1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Symfony\\Component\\Yaml\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "http://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Yaml Component",
+            "homepage": "http://symfony.com",
+            "time": "2013-05-10 18:12:13"
+        }
+    ],
+    "aliases": [
+
+    ],
+    "minimum-stability": "stable",
+    "stability-flags": [
+
+    ],
+    "platform": [
+
+    ],
+    "platform-dev": [
+
+    ]
+}
diff --git a/src/routeexpression.php b/src/routeexpression.php
new file mode 100644 (file)
index 0000000..2f6a4ad
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+
+final class RouteExpression
+{
+    private $expression, $regex;
+
+    public function __construct()
+    {
+        $this->expression = array();
+        $this->regex = '';
+    }
+
+    public function just($text)
+    {
+        if ($text) {
+            $this->expression[] = array('text', $text);
+            $this->regex .= preg_quote($text, '#');
+        }
+        return $this;
+    }
+
+    public function oneOf()
+    {
+        return $this->oneOfv(func_get_args());
+    }
+
+    public function oneOfv($options)
+    {
+        $quoted = array();
+        foreach ($options as $option) {
+            $quoted[] = preg_quote($option, '#');
+        }
+        $this->regex .= '(' . implode('|', $quoted) . ')';
+
+        array_unshift($options, 'oneOf');
+        $this->expression[] = $options;
+        return $this;
+    }
+
+    public function optional($option)
+    {
+        $this->expression[] = array('optional', $option);
+        $this->regex .= '((?:' . preg_quote($option, '#') . ')?)';
+        return $this;
+    }
+
+    public function id()
+    {
+        $this->expression[] = array('id');
+        $this->regex .= '(\d+)';
+        return $this;
+    }
+
+    public function slug()
+    {
+        $this->expression[] = array('slug');
+        $this->regex .= '([\w\_\-]+)';
+        return $this;
+    }
+
+    public function match($url, &$matches)
+    {
+        if (1 == preg_match('#^'.$this->regex.'$#', $url, $matches)) {
+            array_shift($matches);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public function reverse($matches)
+    {
+        $matchesIndex = 0;
+        $url = '';
+        foreach ($this->expression as $part) {
+            switch ($part[0]) {
+                case 'text':
+                    $url .= $part[1];
+                    break;
+                case 'oneOf':
+                    $m = $matches[$matchesIndex++];
+                    $url .= $m;
+                    break;
+                case 'optional':
+                    $m = $matches[$matchesIndex++];
+                    if ($m && $m !== $part[1]) {
+                        throw new Exception('Wrong optional parameter "'.$m.'". "'.$part[1].'" is expected.');
+                    }
+                    $url .= $m;
+                    break;
+                case 'id':
+                    $m = $matches[$matchesIndex++];
+                    $url .= $m;
+                    break;
+                case 'slug':
+                    $m = $matches[$matchesIndex++];
+                    $url .= $m;
+                    break;
+            }
+        }
+        if ($matchesIndex !== count($matches)) {
+            throw new Exception('Parameters doesn\'t match to url expression.');
+        }
+        return $url;
+    }
+
+    public function __toString()
+    {
+        $str = '';
+        foreach ($this->expression as $part) {
+            switch ($part[0]) {
+                case 'text':
+                    $str .= $part[1];
+                    break;
+                case 'oneOf':
+                    $str .= '{' . implode('|', array_slice($part, 1)) . '}';
+                    break;
+                case 'optional':
+                    $str .= '[' . $part[1] . ']';
+                    break;
+                case 'id':
+                    $str .= ':id';
+                    break;
+                case 'slug':
+                    $str .= ':slug';
+                    break;
+            }
+        }
+        return $str;
+    }
+
+    public static function compile($expression)
+    {
+        $state = 0;
+        $text = '';
+        $result = new RouteExpression();
+        for ($i = 0; $i < strlen($expression); ++$i) {
+            $ch = $expression[$i];
+            switch ($state) {
+                case 0:
+                    switch ($ch) {
+                        case '{':
+                            $result->just($text);
+                            $state = 1;
+                            $text = '';
+                            break;
+                        case '[':
+                            $result->just($text);
+                            $state = 2;
+                            $text = '';
+                            break;
+                        case ':':
+                            $result->just($text);
+                            $state = 3;
+                            $text = '';
+                            break;
+                        default:
+                            $text .= $ch;
+                            break;
+                    }
+                    break;
+                case 1:
+                    if ($ch === '}') {
+                        $result->oneOfv(explode('|', $text));
+                        $state = 0;
+                        $text = '';
+                    } else {
+                        $text .= $ch;
+                    }
+                    break;
+                case 2:
+                    if ($ch === ']') {
+                        $result->optional($text);
+                        $state = 0;
+                        $text = '';
+                    } else {
+                        $text .= $ch;
+                    }
+                    break;
+                case 3:
+                    if (ctype_alpha($ch)) {
+                        $text .= $ch;
+                    } else {
+                        if ($text === 'id') {
+                            $result->id();
+                        } elseif ($text === 'slug') {
+                            $result->slug();
+                        } else {
+                            throw new Exception('Unsupported route part "'.$text.'".');
+                        }
+                        $state = 0;
+                        $text = $ch;
+                    }
+                    break;
+            }
+        }
+        if ($state !== 0)
+            throw new Exception('Unexpected end of route expression.');
+        $result->just($text);
+        return $result;
+    }
+}
+
diff --git a/t/ExpressionTest.php b/t/ExpressionTest.php
new file mode 100644 (file)
index 0000000..c634708
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+class RouteExpressionTest extends PHPUnit_Framework_TestCase
+{
+    private function assertMatch($expression, $url, $params=null)
+    {
+        $m = array();
+        $r = $expression->match($url, $m);
+        $this->assertTrue($r);
+        if ($params !== null)
+            $this->assertEquals($m, $params);
+    }
+
+    private function assertNotMatch($expression, $url)
+    {
+        $m = array();
+        $r = $expression->match($url, $m);
+        $this->assertFalse($r);
+    }
+
+    public function testOptional()
+    {
+        $e = (new RouteExpression)->optional('/ru')->just('/story/');
+        $this->assertNotMatch($e, '/story');
+        $this->assertMatch($e, '/story/', array(''));
+        $this->assertNotMatch($e, '/en/story/');
+        $this->assertMatch($e, '/ru/story/', array('/ru'));
+        $this->assertNotMatch($e, '/ru/story');
+    }
+
+    public function testOptionalAndSlug()
+    {
+        $e = (new RouteExpression)->just('/')->optional('ru/')->just('story/')->slug()->just('/');
+        $this->assertNotMatch($e, '/story/wrghsh');
+        $this->assertMatch($e, '/story/srghwrghew/', array('', 'srghwrghew'));
+        $this->assertNotMatch($e, '/en/story/wgwrgewrghwrgh/');
+        $this->assertMatch($e, '/ru/story/wrhwrhr4j4hee/', array('ru/', 'wrhwrhr4j4hee'));
+        $this->assertNotMatch($e, '/ru/story/rewewrhewrhe');
+    }
+
+    public function testOptionalAndId()
+    {
+        $e = (new RouteExpression)->just('/')->optional('ru/')->just('story/')->id()->just('/');
+        $this->assertNotMatch($e, '/story/23');
+        $this->assertMatch($e, '/story/354/', array('', 354));
+        $this->assertMatch($e, '/ru/story/44/', array('ru/', 44));
+        $this->assertNotMatch($e, '/ru/story/-34/');
+    }
+
+    public function testOneOf()
+    {
+        $e = (new RouteExpression)->oneOf('/ru', '/en')->just('/story/');
+        $this->assertNotMatch($e, '/story/');
+        $this->assertMatch($e, '/ru/story/', array('/ru'));
+        $this->assertMatch($e, '/en/story/', array('/en'));
+        $this->assertNotMatch($e, '/fr/story/');
+    }
+
+    public function testOneOfWithEmptyCase()
+    {
+        $e = (new RouteExpression)->oneOf('/ru', '/en', '')->just('/story/');
+        $this->assertMatch($e, '/story/', array(''));
+        $this->assertMatch($e, '/ru/story/', array('/ru'));
+        $this->assertMatch($e, '/en/story/', array('/en'));
+        $this->assertNotMatch($e, '/fr/story/');
+    }
+
+    public function testCompiler()
+    {
+        $this->assertInstanceOf('RouteExpression', RouteExpression::compile('[/ru]/story/'));
+        $this->assertInstanceOf('RouteExpression', RouteExpression::compile('{/ru|/en}/story/'));
+        $this->assertInstanceOf('RouteExpression', RouteExpression::compile('{/ru|/en}/story/:slug/'));
+        $this->assertInstanceOf('RouteExpression', RouteExpression::compile('{/ru|/en}/story/:id/'));
+    }
+
+    public function testReverseOptional()
+    {
+        $e = RouteExpression::compile('[/ru]/story/');
+        $this->assertEquals('/ru/story/', $e->reverse(array('/ru')));
+        $this->assertEquals('/story/', $e->reverse(array('')));
+    }
+
+    public function testReverseOneOfAndId()
+    {
+        $e = RouteExpression::compile('{/ru|/en}/story/:id/');
+        $this->assertEquals('/ru/story/46536/', $e->reverse(array('/ru', 46536)));
+        $this->assertEquals('/en/story/15/', $e->reverse(array('/en', 0xF)));
+    }
+}
+