Don't try to load roles which no longer exist
[catagits/CatalystX-Routes.git] / lib / CatalystX / Routes.pm
CommitLineData
c4057ce2 1package CatalystX::Routes;
2
3use strict;
4use warnings;
5
c4057ce2 6use Moose::Util qw( apply_all_roles );
73bef299 7use Params::Util qw( _CODELIKE _REGEX _STRING );
c4057ce2 8use Scalar::Util qw( blessed );
9
10use Moose::Exporter;
11
12Moose::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
17sub get {
18 _add_route( 'GET', @_ );
19}
20
21sub get_html {
22 _add_route( 'GET_html', @_ );
23}
24
25sub post {
26 _add_route( 'POST', @_ );
27}
28
29sub put {
30 _add_route( 'PUT', @_ );
31}
32
33sub del {
34 _add_route( 'DELETE', @_ );
35}
36
37sub _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 67sub chain_point {
68 my $meta = shift;
69 my $name = shift;
70 _add_chain_point( $meta, $name, chain_point => 1, @_ );
c4057ce2 71}
72
73bef299 73sub _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
85sub _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
122sub _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 136sub chained ($) {
137 return ( Chained => $_[0] );
138}
139
140sub args ($) {
141 return ( Args => [ $_[0] ] );
142}
143
144sub capture_args ($) {
145 return ( CaptureArgs => [ $_[0] ] );
146}
147
148sub path_part ($) {
149 return ( PathPart => [ $_[0] ] );
150}
151
07583481 152sub action_class_name ($) {
73bef299 153 return ( ActionClass => [ $_[0] ] );
154}
155
c4057ce2 156# XXX - this should be added to Params::Util
157sub _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 1771;
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 228B<WARNING>: This module is still experimental. It works well, but the APIs may
229change without warning.
230
07583481 231This module provides a sugar layer that allows controllers to declare chained
232RESTful actions.
233
234Under the hood, all the sugar declarations are turned into Chained subs. All
235chain end points are declared using one of C<get>, C<get_html>, C<post>,
236C<put>, or C<del>. These will declare actions using the
237L<Catalyst::Action::REST::ForBrowsers> action class from the
238L<Catalyst::Action::REST> distribution.
239
240=head1 PUTTING IT ALL TOGETHER
241
242This module is merely sugar over Catalyst's built-in L<Chained
243dispatching|Catalyst::DispatchType::Chained> and L<Catalyst::Action::REST>. It
244helps to know how those two things work.
245
246=head1 SUGAR FUNCTIONS
247
248All of these functions will be exported into your controller class when you
249use C<CatalystX::Routes>.
250
251=head2 get ...
252
253This declares a C<GET> handler.
254
255=head2 get_html
256
257This declares a C<GET> handler for browsers. Use this to generate a standard
258HTML page for browsers while still being able to generate some sort of RESTful
259data response for other clients.
260
261If a browser makes a C<GET> request and no C<get_html> action has been
262declared, a C<get> action is used as a fallback. See
263C<Catalyst::TraitFor::Request::REST::ForBrowsers> for details on how
264"browser-ness" is determined.
265
266=head2 post ...
267
268This declares a C<POST> handler.
269
270=head2 put
271
272This declares a C<PUT> handler.
273
274=head2 del
275
276This declares a C<DELETE> handler.
277
278=head2 chain_point
279
280This declares an intermediate chain point that should not be exposed as a
281public URI.
282
283=head2 chained $path
284
285This function takes a single argument, the previous chain point from which the
286action is chained.
287
288=head2 args $number
289
290This declares the number of arguments that this action expects. This should
291only be used for the end of a chain.
292
293=head2 capture_args $number
294
295The number of arguments to capture at this point in the chain. This should
296only be used for the beginning or middle parts of a chain.
297
298=head2 path_part $path
299
300The path part for this part of the chain. If you are declaring a chain end
301point with C<get>, etc., then this isn't necessary. By default, the name
302passed to the initial sugar function will be converted to a path part. See
303below for details.
304
305=head2 action_class_name $class
306
307Use this to declare an action class. By default, this will be
308L<Catalyst::Action::REST::ForBrowsers> for end points. For other parts of a
309chain, it simply won't be set.
310
d16275f4 311=head1 PATH GENERATION
07583481 312
313All of the end point function (C<get>, C<post>, etc.) take a path as the first
314argument. By default, this will be used as the C<path_part> for the chain. You
315can override this by explicitly calling C<path_part>, in which case the name
316is essentially ignored (but still required).
317
318Note that it is legitimate to pass the empty string as the name for a chain's
319end point.
320
cc9a3314 321If the end point's name does not start with a slash, it will be prefixed with
322the controller's namespace.
323
07583481 324If you don't specify a C<chained> value for an end point, then it will use the
325root URI, C</>, as the root of the chain.
326
327By default, no arguments are specified for a chain's end point, meaning it
328will accept any number of arguments.
329
d16275f4 330=head1 CAVEATS
331
332When adding subroutines for end points to your controller, a name is generated
333for each subroutine based on the chained path to the subroutine. Some
334template-based views will automatically pick a template based on the
335subroutine's name if you don't specify one explicitly. This won't work very
336well with the bizarro names that this module generates, so you are strongly
337encouraged to specify a template name explicitly.
338
07583481 339=head1 BUGS
340
341Please report any bugs or feature requests to
342C<bug-catalystx-routes@rt.cpan.org>, or through the web interface at
343L<http://rt.cpan.org>. I will be notified, and then you'll automatically be
344notified of progress on your bug as I make changes.
345
346=cut