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