File Coverage

lib/HTTP/Router/Declare.pm
Criterion Covered Total %
statement 163 164 99.3
branch 43 46 93.4
condition 6 7 85.7
subroutine 41 42 97.6
pod 0 5 0.0
total 253 264 95.8


line stmt bran cond sub pod time code
1             package HTTP::Router::Declare;
2              
3 10     10   9292 use strict;
  10         22  
  10         536  
4 10     10   54 use warnings;
  10         17  
  10         365  
5 10     10   53 use Carp 'croak';
  10         20  
  10         504  
6 10     10   12575 use Storable 'dclone';
  10         40253  
  10         751  
7 10     10   12455 use Devel::Caller::Perl 'called_args';
  10         61103  
  10         225  
8 10     10   9582 use String::CamelCase 'decamelize';
  10         154  
  10         736  
9 10     10   9300 use Lingua::EN::Inflect::Number 'to_S';
  10         268264  
  10         87  
10 10     10   8450 use HTTP::Router;
  10         34  
  10         125  
11 10     10   296 use HTTP::Router::Route;
  10         18  
  10         54  
12              
13             sub import {
14 10     10   106 my $caller = caller;
15              
16 10     10   369 no strict 'refs';
  10         17  
  10         267  
17 10     10   47 no warnings 'redefine';
  10         19  
  10         3532  
18              
19 10         26 *{ $caller . '::router' } = \&routing;
  10         64  
20 10         24 *{ $caller . '::routes' } = \&routing; # alias router
  10         46  
21              
22             # lexical bindings
23 10     16   35 *{ $caller . '::match' } = sub { goto &match };
  10         43  
  16         31  
24 10     1   34 *{ $caller . '::with' } = sub { goto &with };
  10         44  
  1         4  
25 10     13   120 *{ $caller . '::to' } = sub ($) { goto &to };
  10         38  
  13         65  
26 10     6   32 *{ $caller . '::then' } = sub (&) { goto &then };
  10         42  
  6         69  
27             # resource(s)
28 10     7   27 *{ $caller . '::resource' } = sub { goto &resource };
  10     3   42  
  7         61  
29 10     7   25 *{ $caller . '::resources' } = sub { goto &resources };
  10         19656  
  7         86  
30             }
31              
32             sub _stub {
33 60     60   77 my $name = shift;
34 60     0   306 return sub { croak "Can't call $name() outside routing block" };
  0         0  
35             }
36              
37             {
38             my @Declarations = qw(match with to then resource resources);
39             for my $keyword (@Declarations) {
40 10     10   64 no strict 'refs';
  10         20  
  10         1024  
41             *$keyword = _stub $keyword;
42             }
43             }
44              
45             sub routing (&) {
46 9     9 0 118 my $block = shift;
47 9         115 my $router = HTTP::Router->new;
48              
49 9 50       161 if ($block) {
50 10     10   59 no warnings 'redefine';
  10         32  
  10         16913  
51              
52 9         45 local *match = create_match($router);
53 9         55 local *with = create_with($router);
54 9     13   55 local *to = sub { params => $_[0] };
  13         51  
55 9     6   64 local *then = sub { $_[0] };
  6         27  
56              
57 9         46 local *resource = create_resource($router);
58 9         40 local *resources = create_resources($router);
59              
60 9         113 my $root = HTTP::Router::Route->new;
61 9         42 $block->($root);
62             }
63              
64 9         70 return $router;
65             }
66              
67             sub _map {
68 17     17   40 my ($router, $block, %args) = @_;
69              
70 17         57 my $route = dclone called_args(1)->[0];
71 17 100       1469 $route->append_path($args{path}) if exists $args{path};
72 17 100       39 $route->add_conditions(%{ $args{conditions} }) if exists $args{conditions};
  3         16  
73 17 100       66 $route->add_params(%{ $args{params} }) if exists $args{params};
  14         62  
74              
75 17 100       159 return defined $block ? $block->($route) : $router->add_route($route);
76             }
77              
78             sub create_match {
79 9     9 0 23 my $router = shift;
80             return sub {
81 16 100   16   45 my $block = ref $_[-1] eq 'CODE' ? pop : undef;
82 16         27 my %args = ();
83 16 100       47 $args{path} = shift unless ref $_[0];
84 16 100       46 $args{conditions} = shift if ref $_[0] eq 'HASH';
85 16         68 _map $router, $block, %args, @_;
86 9         239 };
87             }
88              
89             sub create_with {
90 9     9 0 27 my $router = shift;
91             return sub {
92 1 50   1   6 my $block = ref $_[-1] eq 'CODE' ? pop : undef;
93 1         6 _map $router, $block, params => @_;
94 9         55 };
95             }
96              
97             {
98             my $Resource = {
99             collection => {},
100             member => {
101             create => { method => 'POST', suffix => '', action => 'create' },
102             show => { method => 'GET', suffix => '', action => 'show' },
103             update => { method => 'PUT', suffix => '', action => 'update' },
104             destroy => { method => 'DELETE', suffix => '', action => 'destroy' },
105             new => { method => 'GET', suffix => '/new', action => 'post' },
106             edit => { method => 'GET', suffix => '/edit', action => 'edit' },
107             delete => { method => 'GET', suffix => '/delete', action => 'delete' },
108             },
109             };
110 7     7   27 sub _resource_collection { $Resource->{collection} }
111 7     7   18 sub _resource_member { $Resource->{member} }
112              
113             my $Resources = {
114             collection => {
115             index => { method => 'GET', suffix => '', action => 'index' },
116             create => { method => 'POST', suffix => '', action => 'create' },
117             new => { method => 'GET', suffix => '/new', action => 'post' },
118             },
119             member => {
120             show => { method => 'GET', suffix => '', action => 'show' },
121             update => { method => 'PUT', suffix => '', action => 'update' },
122             destroy => { method => 'DELETE', suffix => '', action => 'destroy' },
123             edit => { method => 'GET', suffix => '/edit', action => 'edit' },
124             delete => { method => 'GET', suffix => '/delete', action => 'delete' },
125             },
126             };
127 7     7   23 sub _resources_collection { $Resources->{collection} }
128 7     7   28 sub _resources_member { $Resources->{member} }
129             }
130              
131             sub _map_resources {
132 14     14   30 my ($router, $args) = @_;
133              
134 14         38 for my $symbol (qw'collection member') {
135 28         38 while (my ($key, $config) = each %{ $args->{$symbol} }) {
  112         462  
136 84 100       296 $config = { method => $config } unless ref $config;
137              
138 84 100       1063 my $action = exists $config->{action} ? $config->{action} : $key;
139 84 100       197 my $suffix = exists $config->{suffix} ? $config->{suffix} : "/$action";
140 84         155 my $prefix = $args->{"${symbol}_prefix"};
141              
142 84         134 my $path = $prefix . $suffix;
143 84         227 my $conditions = { method => $config->{method} };
144 84         227 my $params = { controller => $args->{controller}, action => $action };
145              
146 84         1677 my $formatted_route = HTTP::Router::Route->new(
147             path => "${path}.{format}",
148             conditions => $conditions,
149             params => $params,
150             );
151 84         323 $router->add_route($formatted_route);
152              
153 84         270 my $route = HTTP::Router::Route->new(
154             path => $path,
155             conditions => $conditions,
156             params => $params,
157             );
158 84         274 $router->add_route($route);
159             }
160             }
161             }
162              
163             sub _create_resources {
164 14     14   37 my ($router, $name, $block, $args) = @_;
165              
166 14 100       26 my %only = map { $_ => 1 } @{ $args->{only} || [] };
  8         29  
  14         94  
167 14 100       32 my %except = map { $_ => 1 } @{ $args->{except} || [] };
  5         17  
  14         103  
168              
169 14         38 for my $symbol (qw'collection member') {
170 28         62 my $extra = delete $args->{$symbol}; # save extra maps
171              
172 10     10   65 no strict 'refs';
  10         23  
  10         7869  
173 28 100       78 my $default = exists $args->{singleton} ? &{"_resource_$symbol"}() : &{"_resources_$symbol"}();
  14         54  
  14         77  
174              
175 28 100       102 if (exists $args->{only}) {
    100          
176 8         27 $args->{$symbol} = {
177 8         23 map { $_ => $default->{$_} } grep { $only{$_} } keys %$default
  30         51  
178             };
179             }
180             elsif (exists $args->{except}) {
181 10         25 $args->{$symbol} = {
182 4         13 map { $_ => $default->{$_} } grep { !$except{$_} } keys %$default
  15         27  
183             };
184             }
185             else {
186 16         37 $args->{$symbol} = $default;
187             }
188              
189 28 100       110 $args->{$symbol} = { %{ $args->{$symbol} }, %$extra } if defined $extra;
  6         68  
190             }
191              
192 14         75 my $decamelized = decamelize $name;
193 14         497 my $singular = to_S $decamelized;
194              
195 14 50       104138 $args->{collection_prefix} = called_args(1)->[0]->path .
196             (exists $args->{path_prefix} ? $args->{path_prefix} : "/$decamelized");
197 14 100       641 $args->{member_prefix} = $args->{collection_prefix} .
198             (exists $args->{singleton} ? '' : "/{${singular}_id}");
199              
200 14   66     93 $args->{controller} ||= $name;
201              
202 14         47 _map_resources($router, $args);
203              
204 14 100       296 if (defined $block) {
205 2         13 my $route = HTTP::Router::Route->new(path => $args->{member_prefix});
206 2         11 $block->($route);
207             }
208             }
209              
210             sub create_resource {
211 9     9 0 26 my $router = shift;
212             return sub {
213 7 100   7   25 my $block = ref $_[-1] eq 'CODE' ? pop : undef;
214 7         19 my $name = shift;
215 7   100     30 my $args = shift || {};
216 7         19 $args->{singleton} = 1;
217 7         27 _create_resources $router, $name, $block, $args;
218 9         68 };
219             }
220              
221             sub create_resources {
222 9     9 0 23 my $router = shift;
223             return sub {
224 7 100   7   461 my $block = ref $_[-1] eq 'CODE' ? pop : undef;
225 7         17 my $name = shift;
226 7   100     36 my $args = shift || {};
227 7         33 _create_resources $router, $name, $block, $args;
228 9         51 };
229             }
230              
231             1;
232              
233             =head1 NAME
234              
235             HTTP::Router::Declare
236              
237             =head1 SYNOPSIS
238              
239             use HTTP::Router::Declare;
240              
241             my $router = router {
242             # path and params
243             match '/' => to { controller => 'Root', action => 'index' };
244              
245             # path, conditions, and params
246             match '/home', { method => 'GET' }
247             => to { controller => 'Home', action => 'show' };
248             match '/date/{year}', { year => qr/^\d{4}$/ }
249             => to { controller => 'Date', action => 'by_year' };
250              
251             # path, params, and nesting
252             match '/account' => to { controller => 'Account' } => then {
253             match '/login' => to { action => 'login' };
254             match '/logout' => to { action => 'logout' };
255             };
256              
257             # path nesting
258             match '/account' => then {
259             match '/signup' => to { controller => 'Users', action => 'register' };
260             match '/logout' => to { controller => 'Account', action => 'logout' };
261             };
262              
263             # conditions nesting
264             match { method => 'GET' } => then {
265             match '/search' => to { controller => 'Items', action => 'search' };
266             match '/tags' => to { controller => 'Tags', action => 'index' };
267             };
268              
269             # params nesting
270             with { controller => 'Account' } => then {
271             match '/login' => to { action => 'login' };
272             match '/logout' => to { action => 'logout' };
273             match '/signup' => to { action => 'signup' };
274             };
275              
276             # match only
277             match '/{controller}/{action}/{id}.{format}';
278             match '/{controller}/{action}/{id}';
279             };
280              
281             =head1 METHODS
282              
283             =head2 router $block
284              
285             =head2 match $path?, $conditions?
286              
287             =head2 to $params
288              
289             =head2 with $params
290              
291             =head2 then $block
292              
293             =head2 resources $name
294              
295             =head2 resource $name
296              
297             =head1 AUTHOR
298              
299             NAKAGAWA Masaki Emasaki@cpan.orgE
300              
301             =head1 LICENSE
302              
303             This library is free software; you can redistribute it and/or modify
304             it under the same terms as Perl itself.
305              
306             =head1 SEE ALSO
307              
308             L, L
309              
310             =cut