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