Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

fixed format inheritance and added tests

  • Loading branch information...
commit b588e73455402d1379edab332fe950f5264f69e0 1 parent 8e06b58
forwardever authored
View
89 lib/Forward/Routes.pm
@@ -1,5 +1,4 @@
package Forward::Routes;
-
use strict;
use warnings;
@@ -301,26 +300,58 @@ sub via {
sub _match {
- my ($self, $method, $path) = @_;
+ my $self = shift;
+ my ($method, $path, $format_extracted_from_path, $last_path_part, $last_pattern) = @_;
+
+ # re-evaluate last path part if format changes from undef to def or vice versa
+ # and last path part has already been checked (empty path)
+ my $re_eval_pattern;
+ if (!(length $path) && defined($self->format) ne defined($self->parent->format)) {
+ $path = $last_path_part;
+ $re_eval_pattern = 1;
+ }
- # Format
- my $request_format;
- if (!@{$self->children} && $self->{format}) {
+
+ # change from def to undef format -> add format extension back to path
+ # (reverse format extraction)
+ if (!(defined $self->format) && $self->parent && defined $self->parent->format) {
+ $path .= '.' . $format_extracted_from_path if $format_extracted_from_path ne '';
+ $format_extracted_from_path = undef;
+ }
+
+
+ # use pattern of current route, or if it does not exist and path has to be
+ # re-evaluated because of format change, use last pattern
+ my $pattern;
+ if (defined $self->pattern->pattern) {
+ $pattern = $self->pattern;
+ }
+ elsif ($re_eval_pattern) {
+ $pattern = $last_pattern;
+ }
+ else {
+ $pattern = undef;
+ }
+
+
+ # extract format from path if not already done and format option is activated
+ if ($self->format && !(defined $format_extracted_from_path)) {
$path =~m/\.([\a-zA-Z0-9]{1,4})$/;
- $request_format = defined $1 ? $1 : '';
+ $format_extracted_from_path = defined $1 ? $1 : '';
- # format extension is only replaced if format constraint exists
- $path =~s/\.[\a-zA-Z0-9]{1,4}$// if $request_format;
+ $path =~s/\.[\a-zA-Z0-9]{1,4}$// if $format_extracted_from_path ne '';
}
- # Current pattern match
+
+ # match current pattern or return
my $captures = [];
- if (defined $self->pattern->pattern) {
- $captures = $self->_match_current_pattern(\$path) || return;
+ if ($pattern) {
+ ($captures, $last_path_part, $last_pattern) = $self->_match_current_pattern(\$path, $pattern);
+ $captures || return;
}
- # No Match, as path not empty, but further children
- return if length($path) && !@{$self->children};
+ # no match, as path not empty and no further children exist
+ return if length $path && !@{$self->children};
# Children match
my $matches = [];
@@ -330,7 +361,7 @@ sub _match {
foreach my $child (@{$self->children}) {
# Match?
- $matches = $child->_match($method => $path);
+ $matches = $child->_match($method => $path, $format_extracted_from_path, $last_path_part, $last_pattern);
last if $matches;
}
@@ -341,7 +372,7 @@ sub _match {
# Format and Method
unless (@{$self->children}) {
$self->_match_method($method) || return;
- $self->_match_format($request_format) || return;
+ $self->_match_format($format_extracted_from_path) || return;
}
# Match object
@@ -372,14 +403,17 @@ sub _match {
$match = $matches->[0];
}
- my $captures_hash = $self->_captures_to_hash(@$captures);
+ my $captures_hash = {};
+ if ($pattern) {
+ $captures_hash = $self->_captures_to_hash($pattern, @$captures);
+ }
# Merge defaults and captures, Copy! of $self->defaults
$match->_add_params({%{$self->defaults}, %$captures_hash});
# Format
unless (@{$self->children}) {
- $match->_add_params({format => $request_format}) if $self->{format};
+ $match->_add_params({format => $format_extracted_from_path}) if $self->{format};
}
# Captures
@@ -390,20 +424,25 @@ sub _match {
sub _match_current_pattern {
- my ($self, $path_ref) = @_;
+ my ($self, $path_ref, $pattern) = @_;
+
+ my $last_path_part = $$path_ref;
# Pattern
- my $regex = $self->pattern->compile->pattern;
+ my $regex = $pattern->compile->pattern;
+
my @captures = ($$path_ref =~ m/$regex/);
return unless @captures;
# Remove 1 at the end of array if no real captures present
- splice @captures, @{$self->pattern->captures};
+ splice @captures, @{$pattern->captures};
# Replace matching part
$$path_ref =~ s/$regex//;
-
+ if (length($last_path_part) && !(length $$path_ref)) {
+ return (\@captures, $last_path_part, $pattern);
+ }
return \@captures;
}
@@ -411,13 +450,13 @@ sub _match_current_pattern {
sub _captures_to_hash {
my $self = shift;
- my (@captures) = @_;
+ my ($pattern, @captures) = @_;
my $captures = {};
my $defaults = $self->{defaults};
- foreach my $name (@{$self->pattern->captures}) {
+ foreach my $name (@{$pattern->captures}) {
my $capture = shift @captures;
if (defined $capture) {
@@ -723,11 +762,11 @@ sub format {
sub _match_format {
- my ($self, $request_format) = @_;
+ my ($self, $format) = @_;
return 1 unless defined $self->format;
- my @success = grep { $_ eq $request_format } @{$self->format};
+ my @success = grep { $_ eq $format } @{$self->format};
return unless @success;
View
35 t/format_constraints.t
@@ -22,10 +22,10 @@ is_deeply $m->[0]->params => {foo => 'hello', bar => 'there.html'};
-### one format constraint
-$r = Forward::Routes->new->format('html');
-$r->add_route('foo')->name('one');
-$r->add_route(':foo/:bar')->name('two');
+### format constraint
+$r = Forward::Routes->new;
+$r->add_route('foo')->name('one')->format('html');
+$r->add_route(':foo/:bar')->name('two')->format('html');
$m = $r->match(get => 'foo.html');
is_deeply $m->[0]->params => {format => 'html'};
@@ -58,10 +58,11 @@ is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2.html';
is $r->build_path('two', foo => 0, bar => 2)->{path}, '0/2.html';
+
### pass empty format explicitly
-$r = Forward::Routes->new->format('');
-$r->add_route('foo')->name('one');
-$r->add_route(':foo/:bar')->name('two');
+$r = Forward::Routes->new;
+$r->add_route('foo')->name('one')->format('');
+$r->add_route(':foo/:bar')->name('two')->format('');
$m = $r->match(get => 'foo');
is_deeply $m->[0]->params => {format => ''};
@@ -89,9 +90,9 @@ is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2';
### pass undef format (no contraint validation)
-$r = Forward::Routes->new->format(undef);
-$r->add_route('foo')->name('one');
-$r->add_route(':foo/:bar')->name('two');
+$r = Forward::Routes->new;
+$r->add_route('foo')->name('one')->format(undef);
+$r->add_route(':foo/:bar')->name('two')->format(undef);
$m = $r->match(get => 'foo');
is_deeply $m->[0]->params => {};
@@ -114,9 +115,9 @@ is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2';
### multiple format constraints
-$r = Forward::Routes->new->format('html','xml');
-$r->add_route('foo')->name('one');
-$r->add_route(':foo/:bar')->name('two');
+$r = Forward::Routes->new;
+$r->add_route('foo')->name('one')->format('html','xml');
+$r->add_route(':foo/:bar')->name('two')->format('html','xml');
$m = $r->match(get => 'foo.html');
is_deeply $m->[0]->params => {format => 'html'};
@@ -157,9 +158,9 @@ is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2.html';
### multiple format constraints, with empty format allowed
-$r = Forward::Routes->new->format('html','');
-$r->add_route('foo')->name('one');
-$r->add_route(':foo/:bar')->name('two');
+$r = Forward::Routes->new;
+$r->add_route('foo')->name('one')->format('html','');
+$r->add_route(':foo/:bar')->name('two')->format('html','');
$m = $r->match(get => 'foo.html');
is_deeply $m->[0]->params => {format => 'html'};
@@ -187,4 +188,4 @@ is $m, undef;
# build path
is $r->build_path('one')->{path}, 'foo.html';
-is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2.html';
+is $r->build_path('two', foo => 1, bar => 2)->{path}, '1/2.html';
View
327 t/nested_routes_with_format_inheritance.t
@@ -1,137 +1,342 @@
use strict;
use warnings;
-use Test::More tests => 28;
+use Test::More tests => 93;
use lib 'lib';
use Forward::Routes;
+#############################################################################
+# base format and empty route
+
+my $r = Forward::Routes->new;
+ my $html = $r->add_route->format('html'); # base format
+ my $nested = $html->add_route('hello/:id');
+ $nested->add_route->name('one'); # emtpy route
+ $nested->add_route('foo')->name('two');
+
+my $m = $r->match(get => 'hello/1.html');
+is_deeply $m->[0]->params => {id => 1, format => 'html'};
+is $m->[0]->name, 'one';
+
+$m = $r->match(get => 'hello/1.xml');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/foo.html');
+is_deeply $m->[0]->params => {id => 1, format => 'html'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1/foo.xml');
+is $m, undef;
+
+# build path
+is $r->build_path('one', id => 1)->{path}, 'hello/1.html';
+is $r->build_path('two', id => 1)->{path}, 'hello/1/foo.html';
+
#############################################################################
-### nested routes with format inheritance
+# set format after pattern match
-my $root = Forward::Routes->new->format('html');
-my $nested = $root->add_route('foo')->format('xml');
-$nested->add_route('bar')->name('one');
-$root->add_route('baz')->name('two');
-$root->add_route('buz')->name('three')->format('jpeg');
+$r = Forward::Routes->new;
+$nested = $r->add_route('hello/:id');
+ $nested->add_route->format('html')->name('one');
+ $nested->add_route->format('xml')->name('two');
+ my $name = $nested->add_route(':name');
+ $name->add_route->format('html')->name('three');
+ $name->add_route->format('xml')->name('four');
+$m = $r->match(get => 'hello/1.html');
+is_deeply $m->[0]->params => {id => 1, format => 'html'};
+is $m->[0]->name, 'one';
-my $m = $root->match(get => 'foo/bar');
+$m = $r->match(get => 'hello/1.xml');
+is_deeply $m->[0]->params => {id => 1, format => 'xml'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/foo.html');
+is_deeply $m->[0]->params => {id => 1, name => 'foo', format => 'html'};
+is $m->[0]->name, 'three';
+
+$m = $r->match(get => 'hello/1/foo.xml');
+is_deeply $m->[0]->params => {id => 1, name => 'foo', format => 'xml'};
+is $m->[0]->name, 'four';
+
+$m = $r->match(get => 'hello/1/foo');
is $m, undef;
-$m = $root->match(get => 'foo/bar.html');
+$m = $r->match(get => 'hello/1/foo.jpeg');
is $m, undef;
-$m = $root->match(get => 'foo/bar.xml');
-is_deeply $m->[0]->params => {format => 'xml'};
-$m = $root->match(get => '/baz');
+#############################################################################
+# redefine format after pattern match
+
+$r = Forward::Routes->new;
+ $html = $r->format('html');
+ $nested = $html->add_route('hello/:id'); # pattern
+ $nested->add_route->format('jpeg')->name('one');
+ $nested->add_route->format('xml')->name('two');
+ my $foo = $nested->add_route('/my/:name');
+ $foo->add_route->name('three');
+ $foo->add_route->format('xml')->name('four');
+
+$m = $r->match(get => 'hello/1.jpeg');
+is_deeply $m->[0]->params => {id => '1', format => 'jpeg'};
+is $m->[0]->name, 'one';
+
+$m = $r->match(get => 'hello/1');
is $m, undef;
-$m = $root->match(get => '/baz.xml');
+$m = $r->match(get => 'hello/1.xml');
+is_deeply $m->[0]->params => {id => '1', format => 'xml'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1/my/foo.xml');
+is_deeply $m->[0]->params => {id => '1', name => 'foo', format => 'xml'};
+is $m->[0]->name, 'four';
+
+$m = $r->match(get => 'hello/1/my/foo.html');
+is_deeply $m->[0]->params => {id => '1', name => 'foo', format => 'html'};
+is $m->[0]->name, 'three';
+
+$m = $r->match(get => 'hello/1/my/foo.jpeg');
is $m, undef;
-$m = $root->match(get => '/baz.html');
-is_deeply $m->[0]->params => {format => 'html'};
+#############################################################################
+# redefine format after pattern match to undef
-$m = $root->match(get => '/buz');
+$r = Forward::Routes->new;
+ $html = $r->format('html');
+ $nested = $html->add_route('hello/:id');
+ $nested->add_route->format('xml')->name('one');
+ $nested->add_route->format(undef)->name('two');
+ $foo = $nested->add_route('/my/:name');
+ $foo->add_route->name('three');
+ $foo->add_route->format(undef)->name('four');
+
+$m = $r->match(get => 'hello/1.html');
+is_deeply $m->[0]->params => {id => '1.html'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1.jpeg');
+is_deeply $m->[0]->params => {id => '1.jpeg'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1');
+is_deeply $m->[0]->params => {id => '1'};
+is $m->[0]->name, 'two';
+
+$m = $r->match(get => 'hello/1.xml');
+is_deeply $m->[0]->params => {id => '1', format => 'xml'};
+is $m->[0]->name, 'one';
+
+
+$m = $r->match(get => 'hello/1/my/foo.html');
+is_deeply $m->[0]->params => {id => '1', name => 'foo', format => 'html'};
+is $m->[0]->name, 'three';
+
+$m = $r->match(get => 'hello/1/my/foo.xml');
+is_deeply $m->[0]->params => {id => '1', name => 'foo.xml'};
+is $m->[0]->name, 'four';
+
+
+#############################################################################
+# redefine format before pattern match
+
+$r = Forward::Routes->new;
+ $html = $r->format('html');
+ $nested = $html->add_route('hello/:id');
+ my $xml = $nested->add_route->format('xml');
+ $xml->add_route('one/:first')->name('one');
+ my $jpeg = $nested->add_route->format('jpeg');
+ $jpeg->add_route('two/:second')->name('two');
+ $nested->add_route('three/:third');
+
+$m = $r->match(get => 'hello/1.html');
is $m, undef;
-$m = $root->match(get => '/buz.html');
+$m = $r->match(get => 'hello/1.xml');
is $m, undef;
-$m = $root->match(get => '/buz.jpeg');
-is_deeply $m->[0]->params => {format => 'jpeg'};
+$m = $r->match(get => 'hello/1.jpeg');
+is $m, undef;
+$m = $r->match(get => 'hello/1');
+is $m, undef;
+
+
+
+$m = $r->match(get => 'hello/1/one/2.html');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/one/2');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/one/2.xml');
+is_deeply $m->[0]->params => {id => '1', first => '2', format => 'xml'};
+is $m->[0]->name, 'one';
-# build path
-is $root->build_path('one')->{path}, 'foo/bar.xml';
-is $root->build_path('two')->{path}, 'baz.html';
-is $root->build_path('three')->{path}, 'buz.jpeg';
+$m = $r->match(get => 'hello/1/two/2');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/two/2.xml');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/two/2.jpeg');
+is_deeply $m->[0]->params => {id => '1', second => '2', format => 'jpeg'};
+is $m->[0]->name, 'two';
+
+
+
+$m = $r->match(get => 'hello/1/three/2');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/three/2.xml');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/three/2.jpeg');
+is $m, undef;
+
+$m = $r->match(get => 'hello/1/three/2.html');
+is_deeply $m->[0]->params => {id => '1', third => '2', format => 'html'};
+
#############################################################################
-### now with undef format
-### ==> no format constraint validation
+# redefine format before pattern match -> to undef (TO DO)
-$root = Forward::Routes->new->format('html');
-$nested = $root->add_route('foo')->format(undef);
-$nested->add_route('bar')->name('one');
-$root->add_route('baz')->name('two');
-$root->add_route('buz')->name('three')->format(undef);
-$m = $root->match(get => 'foo/bar');
-is_deeply $m->[0]->params => {};
+#############################################################################
+### (re)define pattern and format at same time
+
+$r = Forward::Routes->new;
+$html = $r->format('html'); # base format
+ $xml = $html->add_route('foo')->format('xml'); # add to pattern and override base format
+ $xml->add_route(':name')->name('one');
+$html->add_route('baz')->name('two');
+$html->add_route('buz')->name('three')->format('jpeg'); # add to pattern and override base format
-$m = $root->match(get => 'foo/bar.html');
+
+$m = $r->match(get => 'foo/bar');
+is $m, undef;
+
+$m = $r->match(get => 'foo/bar.html');
is $m, undef;
+$m = $r->match(get => 'foo/bar.xml');
+is_deeply $m->[0]->params => {name => 'bar', format => 'xml'};
+is $m->[0]->name, 'one';
-$m = $root->match(get => '/baz');
+
+$m = $r->match(get => '/baz');
is $m, undef;
-$m = $root->match(get => '/baz.xml');
+$m = $r->match(get => '/baz.xml');
is $m, undef;
-$m = $root->match(get => '/baz.html');
+$m = $r->match(get => '/baz.html');
is_deeply $m->[0]->params => {format => 'html'};
+is $m->[0]->name, 'two';
-$m = $root->match(get => '/buz');
-is_deeply $m->[0]->params => {};
+$m = $r->match(get => '/buz');
+is $m, undef;
-$m = $root->match(get => '/buz.html');
+$m = $r->match(get => '/buz.html');
is $m, undef;
+$m = $r->match(get => '/buz.jpeg');
+is_deeply $m->[0]->params => {format => 'jpeg'};
+is $m->[0]->name, 'three';
+
+
+# build path
+is $r->build_path('one', name => 'bar')->{path}, 'foo/bar.xml';
+is $r->build_path('two')->{path}, 'baz.html';
+is $r->build_path('three')->{path}, 'buz.jpeg';
+
+
+
+#############################################################################
+### (re)define pattern and format at same time -> to undef
+
+$r = Forward::Routes->new;
+$html = $r->format('html'); # base format
+ my $undef = $html->add_route('foo')->format(undef); # add to pattern and override base format
+ $undef->add_route(':name')->name('one');
+$r->add_route('baz')->name('two');
+$r->add_route('buz')->name('three')->format(undef); # add to pattern and override base format
+
-### with placeholder
-$root = Forward::Routes->new->format('html');
-$nested = $root->add_route('foo')->format(undef);
-$nested->add_route(':bar')->name('one');
+$m = $r->match(get => 'foo/bar');
+is_deeply $m->[0]->params => {name => 'bar'};
+is $m->[0]->name, 'one';
-$m = $root->match(get => 'foo/bar');
-is_deeply $m->[0]->params => {bar => 'bar'};
+$m = $r->match(get => 'foo/bar.html');
+is_deeply $m->[0]->params => {name => 'bar.html'};
+is $m->[0]->name, 'one';
-$m = $root->match(get => 'foo/bar.html');
-is_deeply $m->[0]->params => {bar => 'bar.html'};
+$m = $r->match(get => '/baz');
+is $m, undef;
+
+$m = $r->match(get => '/baz.xml');
+is $m, undef;
+
+$m = $r->match(get => '/baz.html');
+is_deeply $m->[0]->params => {format => 'html'};
+is $m->[0]->name, 'two';
+
+
+$m = $r->match(get => '/buz');
+is_deeply $m->[0]->params => {};
+is $m->[0]->name, 'three';
+
+$m = $r->match(get => '/buz.html');
+is $m, undef;
#############################################################################
-### now with empty format
+### (re)define pattern and format at same time -> to empty
-$root = Forward::Routes->new->format('html');
-$nested = $root->add_route('foo')->format('');
-$nested->add_route(':bar')->name('one');
-$root->add_route('baz')->name('two');
-$root->add_route('buz')->name('three')->format('');
+$r = Forward::Routes->new;
+$html = $r->format('html'); # base format
+ my $empty = $html->add_route('foo')->format(''); # add to pattern and override base format
+ $empty->add_route(':name')->name('one');
+$html->add_route('baz')->name('two');
+$html->add_route('buz')->name('three')->format(''); # add to pattern and override base format
-$m = $root->match(get => 'foo/bar');
-is_deeply $m->[0]->params => {bar => 'bar', format => ''};
+$m = $r->match(get => 'foo/bar');
+is_deeply $m->[0]->params => {name => 'bar', format => ''};
+is $m->[0]->name, 'one';
-$m = $root->match(get => 'foo/bar.html');
+$m = $r->match(get => 'foo/bar.html');
is $m, undef;
-$m = $root->match(get => '/baz');
+$m = $r->match(get => '/baz');
is $m, undef;
-$m = $root->match(get => '/baz.xml');
+$m = $r->match(get => '/baz.xml');
is $m, undef;
-$m = $root->match(get => '/baz.html');
+$m = $r->match(get => '/baz.html');
is_deeply $m->[0]->params => {format => 'html'};
+is $m->[0]->name, 'two';
-$m = $root->match(get => '/buz');
+$m = $r->match(get => '/buz');
is_deeply $m->[0]->params => {format => ''};
+is $m->[0]->name, 'three';
-$m = $root->match(get => '/buz.html');
+$m = $r->match(get => '/buz.html');
is $m, undef;
View
3  t/resources_with_format_constraint.t
@@ -39,8 +39,6 @@ is $m, undef;
$m = $r->match(delete => 'photos/1.html');
is $m, undef;
-
-
### format constraint
$r = Forward::Routes->new->format('html');
@@ -109,7 +107,6 @@ is $r->build_path('photos_delete', id => 654)->{method} => 'delete';
is $r->build_path('photos_delete_form', id => 222)->{method} => 'get';
-
### empty format (explicitly)
$r = Forward::Routes->new->format('');
Please sign in to comment.
Something went wrong with that request. Please try again.