Add docs and fix various bugs
[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
51 # We need to turn the full chain name into a path, since two end points
52 # from two different chains could have the same end point name.
53 ( my $name = ( $attrs->{Chained} eq '/' ? q{} : $attrs->{Chained} ) . q{/}
54 . $_[0] ) =~ s/(\W)/'X' . sprintf( '%x', ord($1) )/eg;
c4057ce2 55
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
77 ( my $name = $_[0] ) =~ s/(\W)/'X' . sprintf('%x', ord(1) )/eg;
77d62699 78
73bef299 79 $meta->add_chain_point( $name => [ $attrs, $sub ] );
c4057ce2 80}
81
82sub _process_args {
77d62699 83 my $meta = shift;
c4057ce2 84 my $path = shift;
85 my $sub = pop;
86
87 my $caller = ( caller(2) )[3];
88
89 die
90 "The $caller keyword expects a path string or regex as its first argument"
91 unless _STRINGLIKE0($path) || _REGEX($path);
92
93 die "The $caller keyword expects a sub ref as its final argument"
94 unless _CODELIKE($sub);
95
96 my %p = @_;
97
73bef299 98 unless ( delete $p{chain_point} ) {
99 $p{ActionClass} ||= 'REST::ForBrowsers';
cffed5b1 100 }
69d9fc4e 101
cffed5b1 102 unless ( $p{PathPart} ) {
103 my $part = $path;
73bef299 104
105 unless ( exists $p{Chained} ) {
106 unless ( $part =~ s{^/}{} ) {
107 $part = join q{/},
108 $meta->name()->action_namespace('FakeConfig'), $part;
109 }
69d9fc4e 110 }
cffed5b1 111
112 $p{PathPart} = [$part];
69d9fc4e 113 }
114
07583481 115 return \%p, $sub;
c4057ce2 116}
117
118sub _maybe_add_rest_route {
119 my $meta = shift;
120 my $name = shift;
121 my $attrs = shift;
122
123 return if $meta->has_method($name);
124
125 # This could be done by Moose::Exporter, but that would require that the
126 # module has already inherited from Cat::Controller when it calls "use
127 # CatalystX::Routes".
128 unless ( $meta->does_role('CatalystX::Routes::Role::Controller') ) {
129 apply_all_roles(
130 $meta->name(),
131 'CatalystX::Routes::Role::Controller'
132 );
133 }
134
135 $meta->add_method( $name => sub { } );
136
137 $meta->add_route( $name => [ $attrs, $meta->get_method($name) ] );
138
139 return;
140}
141
73bef299 142sub chained ($) {
143 return ( Chained => $_[0] );
144}
145
146sub args ($) {
147 return ( Args => [ $_[0] ] );
148}
149
150sub capture_args ($) {
151 return ( CaptureArgs => [ $_[0] ] );
152}
153
154sub path_part ($) {
155 return ( PathPart => [ $_[0] ] );
156}
157
07583481 158sub action_class_name ($) {
73bef299 159 return ( ActionClass => [ $_[0] ] );
160}
161
c4057ce2 162# XXX - this should be added to Params::Util
163sub _STRINGLIKE0 ($) {
164 return _STRING( $_[0] )
165 || ( defined $_[0]
166 && $_[0] eq q{} )
167 || ( blessed $_[0]
168 && overload::Method( $_[0], q{""} )
169 && length "$_[0]" );
170}
171
77d62699 172{
05ac8ec7 173
77d62699 174 # This is a nasty hack around some weird back compat code in
175 # Catalyst::Controller->action_namespace
176 package FakeConfig;
177
178 sub config {
179 return { case_sensitive => 0 };
180 }
181}
182
c4057ce2 1831;
07583481 184
185# ABSTRACT: Sugar for declaring RESTful chained action in Catalyst
186
187__END__
188
189=head1 SYNOPSIS
190
191 package MyApp::Controller::User;
192
193 use Moose;
194 use CatalystX::Routes;
195
196 BEGIN { extends 'Catalyst::Controller'; }
197
198 # /user/:user_id
199
200 chain_point '_set_user'
201 => chained '/'
202 => path_part 'user'
203 => capture_args 1
204 => sub {
205 my $self = shift;
206 my $c = shift;
207 my $user_id = shift;
208
209 $c->stash()->{user} = ...;
210 };
211
212 # GET /user/:user_Id
213 get ''
214 => chained('_set_user')
215 => args 0
216 => sub { ... };
217
218 # GET /user/foo
219 get 'foo' => sub { ... }
220
221 sub _post { ... }
222
223 # POST /user/foo
224 post 'foo' => \&_post;
225
226 # PUT /root
227 put '/root' => sub { ... };
228
229 # /user/plain_old_catalyst
230 sub plain_old_catalyst : Local { ... }
231
232=head1 DESCRIPTION
233
234This module provides a sugar layer that allows controllers to declare chained
235RESTful actions.
236
237Under the hood, all the sugar declarations are turned into Chained subs. All
238chain end points are declared using one of C<get>, C<get_html>, C<post>,
239C<put>, or C<del>. These will declare actions using the
240L<Catalyst::Action::REST::ForBrowsers> action class from the
241L<Catalyst::Action::REST> distribution.
242
243=head1 PUTTING IT ALL TOGETHER
244
245This module is merely sugar over Catalyst's built-in L<Chained
246dispatching|Catalyst::DispatchType::Chained> and L<Catalyst::Action::REST>. It
247helps to know how those two things work.
248
249=head1 SUGAR FUNCTIONS
250
251All of these functions will be exported into your controller class when you
252use C<CatalystX::Routes>.
253
254=head2 get ...
255
256This declares a C<GET> handler.
257
258=head2 get_html
259
260This declares a C<GET> handler for browsers. Use this to generate a standard
261HTML page for browsers while still being able to generate some sort of RESTful
262data response for other clients.
263
264If a browser makes a C<GET> request and no C<get_html> action has been
265declared, a C<get> action is used as a fallback. See
266C<Catalyst::TraitFor::Request::REST::ForBrowsers> for details on how
267"browser-ness" is determined.
268
269=head2 post ...
270
271This declares a C<POST> handler.
272
273=head2 put
274
275This declares a C<PUT> handler.
276
277=head2 del
278
279This declares a C<DELETE> handler.
280
281=head2 chain_point
282
283This declares an intermediate chain point that should not be exposed as a
284public URI.
285
286=head2 chained $path
287
288This function takes a single argument, the previous chain point from which the
289action is chained.
290
291=head2 args $number
292
293This declares the number of arguments that this action expects. This should
294only be used for the end of a chain.
295
296=head2 capture_args $number
297
298The number of arguments to capture at this point in the chain. This should
299only be used for the beginning or middle parts of a chain.
300
301=head2 path_part $path
302
303The path part for this part of the chain. If you are declaring a chain end
304point with C<get>, etc., then this isn't necessary. By default, the name
305passed to the initial sugar function will be converted to a path part. See
306below for details.
307
308=head2 action_class_name $class
309
310Use this to declare an action class. By default, this will be
311L<Catalyst::Action::REST::ForBrowsers> for end points. For other parts of a
312chain, it simply won't be set.
313
314=head1 Path Generation
315
316All of the end point function (C<get>, C<post>, etc.) take a path as the first
317argument. By default, this will be used as the C<path_part> for the chain. You
318can override this by explicitly calling C<path_part>, in which case the name
319is essentially ignored (but still required).
320
321Note that it is legitimate to pass the empty string as the name for a chain's
322end point.
323
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
330=head1 BUGS
331
332Please report any bugs or feature requests to
333C<bug-catalystx-routes@rt.cpan.org>, or through the web interface at
334L<http://rt.cpan.org>. I will be notified, and then you'll automatically be
335notified of progress on your bug as I make changes.
336
337=cut