Commit | Line | Data |
c4057ce2 |
1 | package CatalystX::Routes; |
2 | |
3 | use strict; |
4 | use warnings; |
5 | |
c4057ce2 |
6 | use Moose::Util qw( apply_all_roles ); |
73bef299 |
7 | use Params::Util qw( _CODELIKE _REGEX _STRING ); |
c4057ce2 |
8 | use Scalar::Util qw( blessed ); |
9 | |
10 | use Moose::Exporter; |
11 | |
12 | Moose::Exporter->setup_import_methods( |
bd470bcc |
13 | with_meta => [qw( get get_html post put del chain_point )], |
14 | as_is => [qw( chained args capture_args path_part action_class_name )], |
c4057ce2 |
15 | ); |
16 | |
17 | sub get { |
18 | _add_route( 'GET', @_ ); |
19 | } |
20 | |
21 | sub get_html { |
22 | _add_route( 'GET_html', @_ ); |
23 | } |
24 | |
25 | sub post { |
26 | _add_route( 'POST', @_ ); |
27 | } |
28 | |
29 | sub put { |
30 | _add_route( 'PUT', @_ ); |
31 | } |
32 | |
33 | sub del { |
34 | _add_route( 'DELETE', @_ ); |
35 | } |
36 | |
37 | sub _add_route { |
38 | my $rest = shift; |
39 | my $meta = shift; |
07583481 |
40 | my ( $attrs, $sub ) = _process_args( $meta, @_ ); |
41 | |
42 | unless ( exists $attrs->{Chained} ) { |
43 | $attrs->{Chained} = q{/}; |
44 | } |
45 | |
058ab36b |
46 | my $name = $_[0]; |
47 | $name =~ s{^/}{}; |
48 | |
07583481 |
49 | # We need to turn the full chain name into a path, since two end points |
50 | # from two different chains could have the same end point name. |
058ab36b |
51 | $name = ( $attrs->{Chained} eq '/' ? q{} : $attrs->{Chained} ) . q{/} |
52 | . $name; |
c4057ce2 |
53 | |
0f72bf19 |
54 | $name =~ s{/}{|}g; |
55 | |
c4057ce2 |
56 | my $meth_base = '__route__' . $name; |
57 | |
58 | _maybe_add_rest_route( $meta, $meth_base, $attrs ); |
59 | |
60 | my $meth_name = $meth_base . q{_} . $rest; |
61 | |
62 | $meta->add_method( $meth_name => sub { goto &$sub } ); |
63 | |
64 | return; |
65 | } |
66 | |
73bef299 |
67 | sub chain_point { |
68 | my $meta = shift; |
69 | my $name = shift; |
70 | _add_chain_point( $meta, $name, chain_point => 1, @_ ); |
c4057ce2 |
71 | } |
72 | |
73bef299 |
73 | sub _add_chain_point { |
74 | my $meta = shift; |
07583481 |
75 | my ( $attrs, $sub ) = _process_args( $meta, @_ ); |
76 | |
24eee4ae |
77 | my $name = $_[0]; |
0f72bf19 |
78 | $name =~ s{/}{|}g; |
77d62699 |
79 | |
bd470bcc |
80 | $meta->add_method( $name => $sub ); |
81 | |
82 | $meta->name()->config()->{actions}{$name} = $attrs; |
c4057ce2 |
83 | } |
84 | |
85 | sub _process_args { |
77d62699 |
86 | my $meta = shift; |
c4057ce2 |
87 | my $path = shift; |
88 | my $sub = pop; |
89 | |
90 | my $caller = ( caller(2) )[3]; |
91 | |
92 | die |
93 | "The $caller keyword expects a path string or regex as its first argument" |
94 | unless _STRINGLIKE0($path) || _REGEX($path); |
95 | |
96 | die "The $caller keyword expects a sub ref as its final argument" |
97 | unless _CODELIKE($sub); |
98 | |
99 | my %p = @_; |
100 | |
73bef299 |
101 | unless ( delete $p{chain_point} ) { |
102 | $p{ActionClass} ||= 'REST::ForBrowsers'; |
cffed5b1 |
103 | } |
69d9fc4e |
104 | |
cffed5b1 |
105 | unless ( $p{PathPart} ) { |
106 | my $part = $path; |
73bef299 |
107 | |
108 | unless ( exists $p{Chained} ) { |
109 | unless ( $part =~ s{^/}{} ) { |
110 | $part = join q{/}, |
111 | $meta->name()->action_namespace('FakeConfig'), $part; |
7bdb05c2 |
112 | $part =~ s{^/}{}; |
73bef299 |
113 | } |
69d9fc4e |
114 | } |
cffed5b1 |
115 | |
116 | $p{PathPart} = [$part]; |
69d9fc4e |
117 | } |
118 | |
07583481 |
119 | return \%p, $sub; |
c4057ce2 |
120 | } |
121 | |
122 | sub _maybe_add_rest_route { |
123 | my $meta = shift; |
124 | my $name = shift; |
125 | my $attrs = shift; |
126 | |
127 | return if $meta->has_method($name); |
128 | |
c4057ce2 |
129 | $meta->add_method( $name => sub { } ); |
130 | |
bd470bcc |
131 | $meta->name()->config()->{actions}{$name} = $attrs; |
c4057ce2 |
132 | |
133 | return; |
134 | } |
135 | |
73bef299 |
136 | sub chained ($) { |
137 | return ( Chained => $_[0] ); |
138 | } |
139 | |
140 | sub args ($) { |
141 | return ( Args => [ $_[0] ] ); |
142 | } |
143 | |
144 | sub capture_args ($) { |
145 | return ( CaptureArgs => [ $_[0] ] ); |
146 | } |
147 | |
148 | sub path_part ($) { |
149 | return ( PathPart => [ $_[0] ] ); |
150 | } |
151 | |
07583481 |
152 | sub action_class_name ($) { |
73bef299 |
153 | return ( ActionClass => [ $_[0] ] ); |
154 | } |
155 | |
c4057ce2 |
156 | # XXX - this should be added to Params::Util |
157 | sub _STRINGLIKE0 ($) { |
158 | return _STRING( $_[0] ) |
159 | || ( defined $_[0] |
160 | && $_[0] eq q{} ) |
161 | || ( blessed $_[0] |
162 | && overload::Method( $_[0], q{""} ) |
163 | && length "$_[0]" ); |
164 | } |
165 | |
77d62699 |
166 | { |
05ac8ec7 |
167 | |
77d62699 |
168 | # This is a nasty hack around some weird back compat code in |
169 | # Catalyst::Controller->action_namespace |
170 | package FakeConfig; |
171 | |
172 | sub config { |
173 | return { case_sensitive => 0 }; |
174 | } |
175 | } |
176 | |
c4057ce2 |
177 | 1; |
07583481 |
178 | |
2cfd1691 |
179 | # ABSTRACT: Sugar for declaring RESTful chained actions in Catalyst |
07583481 |
180 | |
181 | __END__ |
182 | |
183 | =head1 SYNOPSIS |
184 | |
185 | package MyApp::Controller::User; |
186 | |
187 | use Moose; |
188 | use CatalystX::Routes; |
189 | |
190 | BEGIN { extends 'Catalyst::Controller'; } |
191 | |
192 | # /user/:user_id |
193 | |
194 | chain_point '_set_user' |
195 | => chained '/' |
196 | => path_part 'user' |
197 | => capture_args 1 |
198 | => sub { |
199 | my $self = shift; |
200 | my $c = shift; |
201 | my $user_id = shift; |
202 | |
203 | $c->stash()->{user} = ...; |
204 | }; |
205 | |
206 | # GET /user/:user_Id |
207 | get '' |
208 | => chained('_set_user') |
209 | => args 0 |
210 | => sub { ... }; |
211 | |
212 | # GET /user/foo |
f57cfc35 |
213 | get 'foo' => sub { ... }; |
07583481 |
214 | |
215 | sub _post { ... } |
216 | |
217 | # POST /user/foo |
218 | post 'foo' => \&_post; |
219 | |
220 | # PUT /root |
221 | put '/root' => sub { ... }; |
222 | |
223 | # /user/plain_old_catalyst |
224 | sub plain_old_catalyst : Local { ... } |
225 | |
226 | =head1 DESCRIPTION |
227 | |
2bde5b9e |
228 | B<WARNING>: This module is still experimental. It works well, but the APIs may |
229 | change without warning. |
230 | |
07583481 |
231 | This module provides a sugar layer that allows controllers to declare chained |
232 | RESTful actions. |
233 | |
234 | Under the hood, all the sugar declarations are turned into Chained subs. All |
235 | chain end points are declared using one of C<get>, C<get_html>, C<post>, |
236 | C<put>, or C<del>. These will declare actions using the |
237 | L<Catalyst::Action::REST::ForBrowsers> action class from the |
238 | L<Catalyst::Action::REST> distribution. |
239 | |
240 | =head1 PUTTING IT ALL TOGETHER |
241 | |
242 | This module is merely sugar over Catalyst's built-in L<Chained |
243 | dispatching|Catalyst::DispatchType::Chained> and L<Catalyst::Action::REST>. It |
244 | helps to know how those two things work. |
245 | |
246 | =head1 SUGAR FUNCTIONS |
247 | |
248 | All of these functions will be exported into your controller class when you |
249 | use C<CatalystX::Routes>. |
250 | |
251 | =head2 get ... |
252 | |
253 | This declares a C<GET> handler. |
254 | |
255 | =head2 get_html |
256 | |
257 | This declares a C<GET> handler for browsers. Use this to generate a standard |
258 | HTML page for browsers while still being able to generate some sort of RESTful |
259 | data response for other clients. |
260 | |
261 | If a browser makes a C<GET> request and no C<get_html> action has been |
262 | declared, a C<get> action is used as a fallback. See |
263 | C<Catalyst::TraitFor::Request::REST::ForBrowsers> for details on how |
264 | "browser-ness" is determined. |
265 | |
266 | =head2 post ... |
267 | |
268 | This declares a C<POST> handler. |
269 | |
270 | =head2 put |
271 | |
272 | This declares a C<PUT> handler. |
273 | |
274 | =head2 del |
275 | |
276 | This declares a C<DELETE> handler. |
277 | |
278 | =head2 chain_point |
279 | |
280 | This declares an intermediate chain point that should not be exposed as a |
281 | public URI. |
282 | |
283 | =head2 chained $path |
284 | |
285 | This function takes a single argument, the previous chain point from which the |
286 | action is chained. |
287 | |
288 | =head2 args $number |
289 | |
290 | This declares the number of arguments that this action expects. This should |
291 | only be used for the end of a chain. |
292 | |
293 | =head2 capture_args $number |
294 | |
295 | The number of arguments to capture at this point in the chain. This should |
296 | only be used for the beginning or middle parts of a chain. |
297 | |
298 | =head2 path_part $path |
299 | |
300 | The path part for this part of the chain. If you are declaring a chain end |
301 | point with C<get>, etc., then this isn't necessary. By default, the name |
302 | passed to the initial sugar function will be converted to a path part. See |
303 | below for details. |
304 | |
305 | =head2 action_class_name $class |
306 | |
307 | Use this to declare an action class. By default, this will be |
308 | L<Catalyst::Action::REST::ForBrowsers> for end points. For other parts of a |
309 | chain, it simply won't be set. |
310 | |
d16275f4 |
311 | =head1 PATH GENERATION |
07583481 |
312 | |
313 | All of the end point function (C<get>, C<post>, etc.) take a path as the first |
314 | argument. By default, this will be used as the C<path_part> for the chain. You |
315 | can override this by explicitly calling C<path_part>, in which case the name |
316 | is essentially ignored (but still required). |
317 | |
318 | Note that it is legitimate to pass the empty string as the name for a chain's |
319 | end point. |
320 | |
cc9a3314 |
321 | If the end point's name does not start with a slash, it will be prefixed with |
322 | the controller's namespace. |
323 | |
07583481 |
324 | If you don't specify a C<chained> value for an end point, then it will use the |
325 | root URI, C</>, as the root of the chain. |
326 | |
327 | By default, no arguments are specified for a chain's end point, meaning it |
328 | will accept any number of arguments. |
329 | |
d16275f4 |
330 | =head1 CAVEATS |
331 | |
332 | When adding subroutines for end points to your controller, a name is generated |
333 | for each subroutine based on the chained path to the subroutine. Some |
334 | template-based views will automatically pick a template based on the |
335 | subroutine's name if you don't specify one explicitly. This won't work very |
336 | well with the bizarro names that this module generates, so you are strongly |
337 | encouraged to specify a template name explicitly. |
338 | |
07583481 |
339 | =head1 BUGS |
340 | |
341 | Please report any bugs or feature requests to |
342 | C<bug-catalystx-routes@rt.cpan.org>, or through the web interface at |
343 | L<http://rt.cpan.org>. I will be notified, and then you'll automatically be |
344 | notified of progress on your bug as I make changes. |
345 | |
346 | =cut |