Don't try to load roles which no longer exist
[catagits/CatalystX-Routes.git] / lib / CatalystX / Routes.pm
index add044b..0e4acc3 100644 (file)
@@ -3,20 +3,15 @@ package CatalystX::Routes;
 use strict;
 use warnings;
 
-use CatalystX::Routes::Role::Class;
-use CatalystX::Routes::Role::Controller;
 use Moose::Util qw( apply_all_roles );
-use Params::Util qw( _STRING _CODELIKE );
+use Params::Util qw( _CODELIKE _REGEX _STRING );
 use Scalar::Util qw( blessed );
 
 use Moose::Exporter;
 
 Moose::Exporter->setup_import_methods(
-    with_meta => [qw( get get_html post put del )],
-    as_is     => [qw( chained args capture_args path_part action )],
-    class_metaroles => {
-        class => ['CatalystX::Routes::Role::Class'],
-    },
+    with_meta => [qw( get get_html post put del chain_point )],
+    as_is => [qw( chained args capture_args path_part action_class_name )],
 );
 
 sub get {
@@ -42,7 +37,21 @@ sub del {
 sub _add_route {
     my $rest = shift;
     my $meta = shift;
-    my ( $name, $attrs, $sub ) = _process_args( $meta, @_ );
+    my ( $attrs, $sub ) = _process_args( $meta, @_ );
+
+    unless ( exists $attrs->{Chained} ) {
+        $attrs->{Chained} = q{/};
+    }
+
+    my $name = $_[0];
+    $name =~ s{^/}{};
+
+    # We need to turn the full chain name into a path, since two end points
+    # from two different chains could have the same end point name.
+    $name = ( $attrs->{Chained} eq '/' ? q{} : $attrs->{Chained} ) . q{/}
+        . $name;
+
+    $name =~ s{/}{|}g;
 
     my $meth_base = '__route__' . $name;
 
@@ -55,24 +64,22 @@ sub _add_route {
     return;
 }
 
-sub chained ($) {
-    return ( Chained => [ $_[0] ] );
+sub chain_point {
+    my $meta = shift;
+    my $name = shift;
+    _add_chain_point( $meta, $name, chain_point => 1, @_ );
 }
 
-sub args ($) {
-    return ( Args => [ $_[0] ] );
-}
+sub _add_chain_point {
+    my $meta = shift;
+    my ( $attrs, $sub ) = _process_args( $meta, @_ );
 
-sub capture_args ($) {
-    return ( CaptureArgs => [ $_[0] ] );
-}
+    my $name = $_[0];
+    $name =~ s{/}{|}g;
 
-sub path_part ($) {
-    return ( PathPart => [ $_[0] ] );
-}
+    $meta->add_method( $name => $sub );
 
-sub action ($) {
-    return ( ActionClass => [ $_[0] ] );
+    $meta->name()->config()->{actions}{$name} = $attrs;
 }
 
 sub _process_args {
@@ -91,28 +98,25 @@ sub _process_args {
 
     my %p = @_;
 
-    $p{ActionClass} ||= 'REST::ForBrowsers';
+    unless ( delete $p{chain_point} ) {
+        $p{ActionClass} ||= 'REST::ForBrowsers';
+    }
 
-    unless ( exists $p{Chained} ) {
-        $p{Chained} = q{/};
+    unless ( $p{PathPart} ) {
+        my $part = $path;
 
-        unless ( $p{PathPart} ) {
-            my $part = $path;
+        unless ( exists $p{Chained} ) {
             unless ( $part =~ s{^/}{} ) {
-                $part = $meta->name()->action_namespace('FakeConfig') . q{/} . $part;
+                $part = join q{/},
+                    $meta->name()->action_namespace('FakeConfig'), $part;
+                $part =~ s{^/}{};
             }
-
-            $p{PathPart} = [$part];
         }
-    }
 
-    unless ( $p{Args} ) {
-        $p{Args} = [0];
+        $p{PathPart} = [$part];
     }
 
-    ( my $name = $path ) =~ s/(\W)/'X' . sprintf( '%x', ord($1) )/eg;
-
-    return $name, \%p, $sub;
+    return \%p, $sub;
 }
 
 sub _maybe_add_rest_route {
@@ -122,23 +126,33 @@ sub _maybe_add_rest_route {
 
     return if $meta->has_method($name);
 
-    # This could be done by Moose::Exporter, but that would require that the
-    # module has already inherited from Cat::Controller when it calls "use
-    # CatalystX::Routes".
-    unless ( $meta->does_role('CatalystX::Routes::Role::Controller') ) {
-        apply_all_roles(
-            $meta->name(),
-            'CatalystX::Routes::Role::Controller'
-        );
-    }
-
     $meta->add_method( $name => sub { } );
 
-    $meta->add_route( $name => [ $attrs, $meta->get_method($name) ] );
+    $meta->name()->config()->{actions}{$name} = $attrs;
 
     return;
 }
 
+sub chained ($) {
+    return ( Chained => $_[0] );
+}
+
+sub args ($) {
+    return ( Args => [ $_[0] ] );
+}
+
+sub capture_args ($) {
+    return ( CaptureArgs => [ $_[0] ] );
+}
+
+sub path_part ($) {
+    return ( PathPart => [ $_[0] ] );
+}
+
+sub action_class_name ($) {
+    return ( ActionClass => [ $_[0] ] );
+}
+
 # XXX - this should be added to Params::Util
 sub _STRINGLIKE0 ($) {
     return _STRING( $_[0] )
@@ -150,6 +164,7 @@ sub _STRINGLIKE0 ($) {
 }
 
 {
+
     # This is a nasty hack around some weird back compat code in
     # Catalyst::Controller->action_namespace
     package FakeConfig;
@@ -160,3 +175,172 @@ sub _STRINGLIKE0 ($) {
 }
 
 1;
+
+# ABSTRACT: Sugar for declaring RESTful chained actions in Catalyst
+
+__END__
+
+=head1 SYNOPSIS
+
+  package MyApp::Controller::User;
+
+  use Moose;
+  use CatalystX::Routes;
+
+  BEGIN { extends 'Catalyst::Controller'; }
+
+  # /user/:user_id
+
+  chain_point '_set_user'
+      => chained '/'
+      => path_part 'user'
+      => capture_args 1
+      => sub {
+          my $self = shift;
+          my $c    = shift;
+          my $user_id = shift;
+
+          $c->stash()->{user} = ...;
+      };
+
+  # GET /user/:user_Id
+  get ''
+     => chained('_set_user')
+     => args 0
+     => sub { ... };
+
+  # GET /user/foo
+  get 'foo' => sub { ... };
+
+  sub _post { ... }
+
+  # POST /user/foo
+  post 'foo' => \&_post;
+
+  # PUT /root
+  put '/root' => sub { ... };
+
+  # /user/plain_old_catalyst
+  sub plain_old_catalyst : Local { ... }
+
+=head1 DESCRIPTION
+
+B<WARNING>: This module is still experimental. It works well, but the APIs may
+change without warning.
+
+This module provides a sugar layer that allows controllers to declare chained
+RESTful actions.
+
+Under the hood, all the sugar declarations are turned into Chained subs. All
+chain end points are declared using one of C<get>, C<get_html>, C<post>,
+C<put>, or C<del>. These will declare actions using the
+L<Catalyst::Action::REST::ForBrowsers> action class from the
+L<Catalyst::Action::REST> distribution.
+
+=head1 PUTTING IT ALL TOGETHER
+
+This module is merely sugar over Catalyst's built-in L<Chained
+dispatching|Catalyst::DispatchType::Chained> and L<Catalyst::Action::REST>. It
+helps to know how those two things work.
+
+=head1 SUGAR FUNCTIONS
+
+All of these functions will be exported into your controller class when you
+use C<CatalystX::Routes>.
+
+=head2 get ...
+
+This declares a C<GET> handler.
+
+=head2 get_html
+
+This declares a C<GET> handler for browsers. Use this to generate a standard
+HTML page for browsers while still being able to generate some sort of RESTful
+data response for other clients.
+
+If a browser makes a C<GET> request and no C<get_html> action has been
+declared, a C<get> action is used as a fallback. See
+C<Catalyst::TraitFor::Request::REST::ForBrowsers> for details on how
+"browser-ness" is determined.
+
+=head2 post ...
+
+This declares a C<POST> handler.
+
+=head2 put
+
+This declares a C<PUT> handler.
+
+=head2 del
+
+This declares a C<DELETE> handler.
+
+=head2 chain_point
+
+This declares an intermediate chain point that should not be exposed as a
+public URI.
+
+=head2 chained $path
+
+This function takes a single argument, the previous chain point from which the
+action is chained.
+
+=head2 args $number
+
+This declares the number of arguments that this action expects. This should
+only be used for the end of a chain.
+
+=head2 capture_args $number
+
+The number of arguments to capture at this point in the chain. This should
+only be used for the beginning or middle parts of a chain.
+
+=head2 path_part $path
+
+The path part for this part of the chain. If you are declaring a chain end
+point with C<get>, etc., then this isn't necessary. By default, the name
+passed to the initial sugar function will be converted to a path part. See
+below for details.
+
+=head2 action_class_name $class
+
+Use this to declare an action class. By default, this will be
+L<Catalyst::Action::REST::ForBrowsers> for end points. For other parts of a
+chain, it simply won't be set.
+
+=head1 PATH GENERATION
+
+All of the end point function (C<get>, C<post>, etc.) take a path as the first
+argument. By default, this will be used as the C<path_part> for the chain. You
+can override this by explicitly calling C<path_part>, in which case the name
+is essentially ignored (but still required).
+
+Note that it is legitimate to pass the empty string as the name for a chain's
+end point.
+
+If the end point's name does not start with a slash, it will be prefixed with
+the controller's namespace.
+
+If you don't specify a C<chained> value for an end point, then it will use the
+root URI, C</>, as the root of the chain.
+
+By default, no arguments are specified for a chain's end point, meaning it
+will accept any number of arguments.
+
+=head1 CAVEATS
+
+When adding subroutines for end points to your controller, a name is generated
+for each subroutine based on the chained path to the subroutine. Some
+template-based views will automatically pick a template based on the
+subroutine's name if you don't specify one explicitly. This won't work very
+well with the bizarro names that this module generates, so you are strongly
+encouraged to specify a template name explicitly.
+
+=head1 BUGS
+
+Please report any bugs or feature requests to
+C<bug-catalystx-routes@rt.cpan.org>, or through the web interface at
+L<http://rt.cpan.org>.  I will be notified, and then you'll automatically be
+notified of progress on your bug as I make changes.
+
+=cut