File Coverage

blib/lib/Trickster/Router.pm
Criterion Covered Total %
statement 51 54 94.4
branch 13 24 54.1
condition 4 8 50.0
subroutine 8 9 88.8
pod 0 5 0.0
total 76 100 76.0


line stmt bran cond sub pod time code
1             package Trickster::Router;
2              
3 6     6   245855 use strict;
  6         12  
  6         205  
4 6     6   25 use warnings;
  6         10  
  6         287  
5 6     6   104 use v5.14;
  6         19  
6              
7             sub new {
8 8     8 0 528 my ($class) = @_;
9            
10 8         107 return bless {
11             routes => {},
12             named_routes => {},
13             }, $class;
14             }
15              
16             sub add_route {
17 23     23 0 1088 my ($self, $method, $path, $handler, %opts) = @_;
18            
19 23 50       70 die "Route handler must be a code reference" unless ref($handler) eq 'CODE';
20            
21 23         55 my $pattern = $self->_compile_route($path);
22            
23             my $route = {
24             path => $path,
25             pattern => $pattern,
26             handler => $handler,
27             params => $pattern->{params} || [],
28             name => $opts{name},
29             constraints => $opts{constraints} || {},
30 23   50     257 };
      100        
31            
32 23         39 push @{$self->{routes}{$method}}, $route;
  23         80  
33            
34 23 100       54 if ($opts{name}) {
35 2         7 $self->{named_routes}{$opts{name}} = $route;
36             }
37            
38 23         70 return $route;
39             }
40              
41             sub _compile_route {
42 23     23   43 my ($self, $path) = @_;
43            
44 23         33 my @params;
45 23         32 my $pattern = $path;
46            
47             # Escape special regex characters except : and *
48 23         63 $pattern =~ s{([.+?^\${}()\[\]|\\])}{\\$1}g;
49            
50             # Convert :param to named captures
51 23         63 $pattern =~ s{:(\w+)}{
52 5         17 push @params, $1;
53 5         21 "(?<$1>[^/]+)"
54             }ge;
55            
56             # Convert * to wildcard
57 23         94 $pattern =~ s{\\\*}{.*}g;
58            
59             return {
60 23         503 regex => qr{^$pattern$},
61             params => \@params,
62             };
63             }
64              
65             sub match {
66 26     26 0 1208 my ($self, $method, $path) = @_;
67            
68 26   50     102 my $routes = $self->{routes}{$method} || [];
69            
70 26         86 for my $route (@$routes) {
71 39 100       301 if ($path =~ $route->{pattern}{regex}) {
72 25         182 my %captures = %+;
73            
74             # Validate constraints
75 25 50       75 if ($route->{constraints}) {
76 25         39 my $valid = 1;
77 25         36 for my $param (keys %{$route->{constraints}}) {
  25         104  
78 2 50       3 if (exists $captures{$param}) {
79 2         3 my $constraint = $route->{constraints}{$param};
80 2 50       8 if (ref($constraint) eq 'Regexp') {
    0          
81 2 100       9 $valid = 0 unless $captures{$param} =~ $constraint;
82             } elsif (ref($constraint) eq 'CODE') {
83 0 0       0 $valid = 0 unless $constraint->($captures{$param});
84             }
85             }
86             }
87 25 100       60 next unless $valid;
88             }
89            
90             return {
91 24         116 route => $route,
92             params => \%captures,
93             };
94             }
95             }
96            
97 2         7 return undef;
98             }
99              
100             sub url_for {
101 2     2 0 8 my ($self, $name, %params) = @_;
102            
103 2         10 my $route = $self->{named_routes}{$name};
104 2 50       14 return undef unless $route;
105            
106 2         5 my $path = $route->{path};
107            
108 2         7 for my $param (keys %params) {
109 2         53 $path =~ s{:$param\b}{$params{$param}};
110             }
111            
112 2         10 return $path;
113             }
114              
115             sub routes {
116 0     0 0   my ($self, $method) = @_;
117            
118 0 0 0       return $method ? ($self->{routes}{$method} || []) : $self->{routes};
119             }
120              
121             1;
122              
123             __END__