complete overhaul, now with documentation
Robert 'phaylon' Sedlacek [Thu, 30 Jul 2009 17:53:14 +0000 (19:53 +0200)]
15 files changed:
.gitignore [new file with mode: 0644]
Changes [new file with mode: 0644]
MANIFEST.SKIP [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
lib/CatalystX/Declare.pm
lib/CatalystX/Declare/Keyword/Action.pm
lib/CatalystX/Declare/Keyword/Application.pm [new file with mode: 0644]
lib/CatalystX/Declare/Keyword/Controller.pm
lib/CatalystX/Declare/Keyword/Role.pm [new file with mode: 0644]
t/001_basic.t
t/lib/TestApp.pm
t/lib/TestApp/Controller/Foo.pm
t/lib/TestApp/TestRole.pm [new file with mode: 0644]
xt/kwalitee.t [new file with mode: 0644]
xt/pod.t [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..820dc71
--- /dev/null
@@ -0,0 +1,31 @@
+# VIM swap files
+*.swp
+*.swo
+
+# VIM project files
+*.vim
+
+# root directory files
+/*.txt
+/*.pl
+/Makefile*
+/*.sx
+/*.sxc
+
+# except
+!Makefile.PL
+
+# M:I and builds
+inc/
+blib/
+cover_db/
+pm_to_blib
+test.out
+META.yml
+
+# hidden
+.*
+!.gitignore
+
+# misc
+Debian_CPANTS.txt
diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..de4ddac
--- /dev/null
+++ b/Changes
@@ -0,0 +1,2 @@
+[0.001] ...
+    - Initial release
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..ae2283c
--- /dev/null
@@ -0,0 +1,42 @@
+#
+#   This file is a modified version of the MANIFEST.SKIP file
+#   from DBIx-Class.
+#
+
+# Avoid version control files.
+\bRCS\b
+\bCVS\b
+,v$
+\B\.svn\b
+\B\.git\b
+
+# Avoid Makemaker generated and utility files.
+\bMakefile$
+\bblib
+\bMakeMaker-\d
+\bpm_to_blib$
+\bblibdirs$
+^MANIFEST\.SKIP$
+
+# for developers only :)
+^TODO$
+
+# Avoid temp and backup files.
+~$
+\.tmp$
+\.old$
+\.bak$
+\.swp$
+\.swo$
+\#$
+\b\.#
+
+# avoid OS X finder files
+\.DS_Store$
+
+# Don't ship the last dist we built :)
+\.tar\.gz$
+
+# Skip maint stuff
+^maint/
+^Debian_CPANTS\.txt$
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..59796f5
--- /dev/null
@@ -0,0 +1,34 @@
+use inc::Module::Install;
+
+name            'CatalystX-Declare';
+author          'Robert Sedlacek <rs@474.at>';
+license         'perl';
+
+all_from        'lib/CatalystX/Declare.pm';
+readme_from     'lib/CatalystX/Declare.pm', 'clean';
+
+author_tests    'xt';
+
+requires        'Carp',                     '1.08';
+requires        'Class::Inspector',         '1.24';
+requires        'Perl6::Junction',          '1.40000';
+requires        'aliased',                  '0.22';
+requires        'Data::Dump',               '1.14';
+requires        'Catalyst',                 '5.80007';
+requires        'Moose',                    '0.88';
+requires        'MooseX::AttributeHelpers', '0.16';
+requires        'MooseX::Declare',          '0.23';
+requires        'MooseX::MethodAttributes', '0.15';
+requires        'MooseX::Types',            '0.16';
+
+test_requires   'Catalyst::Test';
+test_requires   'FindBin';
+test_requires   'Test::More',               '0.86';
+
+repository      'http://github.com/phaylon/catalystx-declarative/tree/master';
+bugtracker      'http://github.com/phaylon/catalystx-declarative/issues';
+
+auto_manifest;
+auto_provides_class;
+
+WriteAll;
index 0aa8cf2..770b7f8 100644 (file)
 use MooseX::Declare;
 
-class CatalystX::Declare extends MooseX::Declare {
+class CatalystX::Declare extends MooseX::Declare is dirty {
 
-    use aliased 'CatalystX::Declare::Keyword::Controller', 'ControllerKeyword';
+    use aliased 'CatalystX::Declare::Keyword::Controller',  'ControllerKeyword';
+    use aliased 'CatalystX::Declare::Keyword::Role',        'RoleKeyword';
+    use aliased 'CatalystX::Declare::Keyword::Application', 'ApplicationKeyword';
+
+    clean;
+
+    our $VERSION = '0.001';
 
     around keywords {
         $self->$orig,
         ControllerKeyword->new(identifier => 'controller'),
+        RoleKeyword->new(identifier => 'controller_role'),
+        ApplicationKeyword->new(identifier => 'application'),
     }
 }
 
+__END__
+
+=head1 NAME
+
+CatalystX::Declare - EXPERIMENTAL Declarative Syntax for Catalyst Applications
+
+=head1 SYNOPSIS
+
+=head2 Application
+
+    use CatalystX::Declare;
+    
+    application MyApp::Web with Static::Simple {
+    
+        $CLASS->config(name => 'My Declarative Web Application');
+    }
+
+=head2 Controllers
+
+    use CatalystX::Declare;
+
+    controller MyApp::Web::Controller::Foo
+          with MyApp::Web::ControllerRole::Bar {
+        
+        use MooseX::Types::Moose qw( Str );
+        
+        
+        has welcome_message => (
+            is          => 'rw',
+            isa         => Str,
+            required    => 1,
+            lazy_build  => 1,
+        );
+        
+        method _build_welcome_message { 'Welcome' }
+        
+        
+        action base as '';
+        
+        under base {
+            
+            final action welcome {
+                $ctx->response->body( $self->welcome_message );
+            }
+        }
+    }
+
+=head2 Roles
+
+    use CatalystX::Declare;
+
+    controller_role MyApp::Web::ControllerRole::Bar {
+
+        use MyApp::Types qw( Username );
+
+
+        around _build_welcome_message { $self->$orig . '!' }
+
+        after welcome (Object $ctx) {
+
+            $ctx->response->body(join "\n",
+                $ctx->response->body,
+                time(),
+            );
+        }
+
+
+        final action special_welcome (Username $name) under base {
+
+            $ctx->response->body('Hugs to ' . $name);
+        }
+    }
+
+=head1 DESCRIPTION
+
+This module provides a declarative syntax for L<Catalyst|Catalyst::Runtime> 
+applications. Its main focus is currently on common and repetitious parts of
+the application, such as the application class itself, controllers, and
+controller roles.
+
+=head2 Not a Source Filter
+
+The used syntax elements are not parsed via source filter mechanism, but 
+through L<Devel::Declare>, which is a much less fragile deal to handle and
+allows extensions to mix without problems. For example, all keywords added
+by this module are separete handlers.
+
+=head2 Syntax Documentation
+
+The documentation about syntax is in the respective parts of the distribution
+below the C<CatalystX::Declare::Keyword::> namespace. Here are the manual
+pages you will be interested in to familiarize yourself with this module's
+syntax extensions:
+
+=over
+
+=item L<CatalystX::Declare::Keyword::Application>
+
+=item L<CatalystX::Declare::Keyword::Controller>
+
+=item L<CatalystX::Declare::Keyword::Action>
+
+=item L<CatalystX::Declare::Keyword::Role>
+
+=back
+
+Things like models, views, roles for request or response objects, can be built
+declaratively with L<MooseX::Declare>, which is used to additionally provide
+keywords for C<class>, C<role>, C<method> and the available method modifier
+declarations. This allows for constructs such as:
+
+    use CatalystX::Declare;
+
+    class Foo {
+
+        method bar { 23 }
+    }
+
+    controller MyApp::Web::Controller::Baz {
+
+        final action qux { 
+            $ctx->response->body(Foo->new->bar) 
+        }
+    }
+
+=head1 SEE ALSO
+
+=head2 For Usage Information
+
+These links are intended for the common user of this module.
+
+=over
+
+=item L<Catalyst::Runtime>
+
+=item L<Catalyst::Devel>
+
+=item L<Catalyst::Manual>
+
+Although you probably already know Catalyst, since you otherwise probably
+wouldn't be here, I include these links for completeness sake.
+
+=item L<Moose>
+
+The powerful modern Perl object orientation implementation that is used
+as basis for Catalyst. L<MooseX::Declare>, on which L<CatalystX::Declare>
+is based, provides a declarative syntax for L<Moose>.
+
+=item L<MooseX::Declare>
+
+We inherit almost all functionality from L<MooseX::Declare> to allow the
+declaration of traditional classes, roles, methods, modifiers, etc. Refer
+to this documentation first for syntax elements that aren't part of
+L<CatalystX::Declare>.
+
+=item L<MooseX::Method::Signatures>
+
+This isn't directly used, but L<MooseX::Declare> utilises this to provide
+us with method and modifier declarations. For extended information on the
+usage of methods, especially signatures, refer to this module after 
+looking for an answer in the L<MooseX::Declare> documentation.
+
+=back
+
+=head2 For Developer Information
+
+This section contains links relevant to the implementation of this module.
+
+=over
+
+=item L<Devel::Declare>
+
+You could call this is the basic machine room that runs the interaction with 
+perl. It provides a way to hook into perl's source code parsing and change 
+small parts on a per-statement basis.
+
+=item L<MooseX::MethodAttributes>
+
+We use this module to easily communicate the action attributes to 
+L<Catalyst|Catalyst::Runtime>. Currently, this is the easiest solution for
+now but may be subject to change in the future.
+
+=back
+
+
+=head1 AUTHOR
+
+=over
+
+=item Robert 'phaylon' Sedlacek, L<E<lt>rs@474.atE<gt>>
+
+=back
+
+With contributions from, and many thanks to:
+
+=over
+
+=item Florian Ragwitz
+
+=item John Napiorkowski
+
+=back
+
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it under 
+the same terms as perl itself.
+
+=cut
index d4c5543..c4d6f07 100644 (file)
@@ -8,6 +8,7 @@ class CatalystX::Declare::Keyword::Action
     use Perl6::Junction     qw( any );
     use Data::Dump          qw( pp );
     use MooseX::Types::Util qw( has_available_type_export );
+    use Moose::Util         qw( add_method_modifier );
     use Class::Inspector;
     use Class::MOP;
 
@@ -19,12 +20,11 @@ class CatalystX::Declare::Keyword::Action
     use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
 
 
-    method parse (Object $ctx) {
+    method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
 
         # somewhere to put the attributes
         my %attributes;
         my @populators;
-        my $skipped_declarator;
 
         # parse declarations
         until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
@@ -75,6 +75,7 @@ class CatalystX::Declare::Keyword::Action
             unless exists $attributes{Signature};
 
         my $name   = $attributes{Subname};
+
         my $method = Method->wrap(
             signature       => qq{($attributes{Signature})},
             package_name    => $ctx->get_curstash_name,
@@ -126,12 +127,20 @@ class CatalystX::Declare::Keyword::Action
 
         return $ctx->shadow(sub (&) {
             my $class = caller;
+            my $body  = shift;
 
-            $method->_set_actual_body(shift);
+            $method->_set_actual_body($body);
             $method->{attributes} = \@attributes;
-    
-            $class->meta->add_method($name, $method);
-            $class->meta->register_method_attributes($class->can($method->name), \@attributes);
+
+            if ($modifier) {
+
+                add_method_modifier $class, $modifier, [$name, $method];
+            }
+            else {
+
+                $class->meta->add_method($name, $method);
+                $class->meta->register_method_attributes($class->can($method->name), \@attributes);
+            }
         });
     }
 
@@ -317,5 +326,298 @@ class CatalystX::Declare::Keyword::Action
     }
 }
 
+__END__
+
+=head1 NAME
+
+CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
+
+=head1 SYNOPSIS
+
+    use CatalystX::Declare;
+
+    controller MyApp::Web::Controller::Example {
+
+        # chain base action with path part setting of ''
+        # body-less actions don't do anything by themselves
+        action base as '';
+
+        # simple end-point action
+        action controller_class is final under base {
+            $ctx->response->body( 'controller: ' . ref $self );
+        }
+
+        # chain part actions can have arguments
+        action str (Str $string) under base {
+
+            $ctx->stash(chars => [split //, $string]);
+        }
+
+        # and end point actions too, of course
+        action uc_chars (Int $count) under str is final {
+    
+            my $chars = $ctx->stash->{chars};
+            ...
+        }
+
+
+        # you can use a shortcut for multiple actions with
+        # a common base
+        under base {
+
+            # this is an endpoint after base
+            action normal is final;
+
+            # the final keyword can be used to be more 
+            # visually explicit about end-points
+            final action some_action { ... }
+        }
+
+        # of course you can also chain to external actions
+        final action some_end under '/some/controller/some/action';
+    }
+
+=head1 DESCRIPTION
+
+This handler class provides the user with C<action>, C<final> and C<under> 
+keywords. There are multiple ways to define actions to allow for greater
+freedom of expression. While the parts of the action declaration itself do
+not care about their order, their syntax is rather strict.
+
+You can choose to separate syntax elements via C<,> if you think it is more
+readable. The action declaration
+
+    action foo is final under base;
+
+is parsed in exactly the same way if you write it as
+
+    action foo, is final, under base;
+
+=head2 Basic Action Declaration
+
+The simplest possible declaration is
+
+    action foo;
+
+This would define a chain-part action chained to C</> with the name C<foo>
+and no arguments. Since it isn't followed by a block, the body of the action
+will be empty.
+
+You will automatically be provided with two variables: C<$self> is, as you
+might expect, your controller instance. C<$ctx> will be the Catalyst context
+object. Thus, the following code would stash the value returned by the 
+C<get_item> method:
+
+    action foo {
+        $ctx->stash(item => $self->get_item);
+    }
+
+=head2 Setting a Path Part
+
+As usual with Catalyst actions, the path part (the public name of this part of
+the URI, if you're not familiar with the term yet) will default to the name of
+the action itself (or more correctly: to whatever Catalyst defaults).
+
+To change that, use the C<as> option:
+
+    action base      as '';             # <empty>
+    action something as 'foo/bar';      # foo/bar
+    action barely    as bareword;       # bareword
+
+=head2 Chaining Actions
+
+Currently, L<CatalystX::Declare> is completely based on the concept of
+L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
+chained to something. No base specification means you chain to the root. You 
+can specify the action you want to chain to with the C<under> option:
+
+    action foo;                     # chained to /
+    action foo under '/';           # also chained to /
+    action foo under bar;           # chained to the local bar action
+    action foo under '/bar/baz';    # chained to baz in /bar
+
+C<under> is also provided as a grouping keyword. Every action inside the block
+will be chained to the specified action:
+
+    under base {
+        action foo { ... }
+        action bar { ... }
+    }
+
+You can also use the C<under> keyword for a single action. This is useful if
+you want to highlight a single action with a significant diversion from what
+is to be expected:
+
+    action base;
+
+    under '/the/sink' is final action foo;
+
+    final action bar under base;
+
+    final action baz under base;
+
+Instead of the C<under> option declaration, you can also use a more english
+variant named C<chains to>. While C<under> might be nice and concise, some
+people might prefer this if they confuse C<under> with the specification of
+a public path part. The argument to C<chains to> is the same as to C<under>:
+
+    action foo chains to bar;
+    action foo under bar;
+
+By default all actions are chain-parts, not end-points. If you want an action 
+to be picked up as end-point and available via a public path, you have to say
+so explicitely by  using the C<is final> option:
+
+    action base;
+    action foo under base is final;   # /base/foo
+
+You can also drop the C<is> part of the C<is final> option if you want:
+
+    under base, final action foo { ... }
+
+You can make end-points more visually distinct by using the C<final> keyword
+instead of the option:
+
+    action base;
+    final action foo under base;      # /base/foo
+
+And of course, the C<final>, C<under> and C<action> keywords can be used in
+combination whenever needed:
+
+    action base as '';
+
+    under base {
+
+        final action list;          # /list
+
+        action load;
+
+        under load {
+
+            final action view;      # /list/load/view
+            final action edit;      # /list/load/edit
+        }
+    }
+
+There is also one shorthand alternative for declaring chain targets. You can
+specify an action after a C<E<lt>-> following the action name:
+
+    action base;
+    final action foo <- base;       # /base/foo
+
+=head2 Arguments
+
+You can use signatures like you are use to from L<MooseX::Method::Signatures>
+to declare action parameters. The number of arguments will be used during 
+dispatching. Dispatching by type constraint is planned but not yet implemented.
+
+The signature follows the action name:
+
+    # /foo/*/*/*
+    final action foo (Int $year, Int $month, Int $day);
+
+If you are using the shorthand definition, the signature follows the chain 
+target:
+
+    # /foo/*
+    final action foo <- base ($x) { ... }
+
+Parameters may be specified on chain-parts and end-points:
+
+    # /base/*/foo/*
+    action base (Str $lang);
+    final action page (Int $page_num) under base;
+
+Named parameters will be populated with the values in the query parameters:
+
+    # /view/17/?page=3
+    final action view (Int $id, Int :$page = 1);
+
+Your end-points can also take an unspecified amount of arguments by specifying
+an array as a variable:
+
+    # /find/some/deep/path/spec
+    final action find (@path);
+
+=head2 Actions and Method Modifiers
+
+Method modifiers can not only be applied to methods, but also to actions. There
+is no way yet to override the attributes of an already established action via
+modifiers. However, you can modify the method underlying the action.
+
+The following code is an example role modifying the consuming controller's
+C<base> action:
+
+    use CatalystX::Declare;
+
+    controller_role MyApp::Web::ControllerRole::RichBase {
+
+        before base (Object $ctx) {
+            $ctx->stash(something => $ctx->model('Item'));
+        }
+    }
+
+Note that you have to specify the C<$ctx> argument yourself, since you are 
+modifying a method, not an action.
+
+Any controller having a C<base> action (or method, for this purpose), can now
+consume the C<RichBase> role declared above:
+
+    use CatalystX::Declare;
+
+    controller MyApp::Web::Controller::Foo
+        with   MyApp::Web::Controller::RichBase {
+
+        action base as '';
+
+        action show, final under base { 
+            $ctx->response->body(
+                $ctx->stash->{something}->render,
+            );
+        }
+    }
+
+=head1 ROLES
+
+=over
+
+=item L<MooseX::Declare::Syntax::KeywordHandling>
+
+=back
+
+=head1 METHODS
+
+These methods are implementation details. Unless you are extending or 
+developing L<CatalystX::Declare>, you should not be concerned with them.
+
+=head2 parse
+
+    Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
+
+A hook that will be invoked by L<MooseX::Declare> when this instance is called 
+to handle syntax. It will parse the action declaration, prepare attributes and 
+add the actions to the controller.
+
+=head1 SEE ALSO
+
+=over
+
+=item L<CatalystX::Declare>
+
+=item L<CatalystX::Declare::Keyword::Controller>
+
+=item L<MooseX::Method::Signatures>
+
+=back
+
+=head1 AUTHOR
+
+See L<CatalystX::Declare/AUTHOR> for author information.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it under 
+the same terms as perl itself.
 
+=cut
 
diff --git a/lib/CatalystX/Declare/Keyword/Application.pm b/lib/CatalystX/Declare/Keyword/Application.pm
new file mode 100644 (file)
index 0000000..698fd95
--- /dev/null
@@ -0,0 +1,102 @@
+use MooseX::Declare;
+
+class CatalystX::Declare::Keyword::Application
+    extends MooseX::Declare::Syntax::Keyword::Class {
+
+    
+    override auto_make_immutable { 0 }
+
+    override add_with_option_customizations (Object $ctx, Str $package, ArrayRef $plugins, HashRef $options) {
+
+        $ctx->add_cleanup_code_parts(
+            sprintf(
+                '%s->setup(qw( %s ))',
+                $package,
+                join(' ', @$plugins),
+            ),
+            '1;',
+        );
+    }
+
+    before add_namespace_customizations (Object $ctx, Str $package) {
+
+        $ctx->add_preamble_code_parts(
+            'use CLASS',
+            'use parent q{Catalyst}',
+        );
+    }
+}
+
+=head1 NAME
+
+CatalystX::Declare::Keyword::Application - Declare Catalyst Application Classes
+
+=head1 SYNOPSIS
+
+    use CatalystX::Declare;
+
+    application MyApp::Web
+           with Static::Simple
+           with ConfigLoader {
+
+        $CLASS->config(name => 'My App');
+
+        method debug_timestamp {
+            $self->log->debug('Timestamp: ' . time)
+                if $self->debug;
+        }
+    }
+
+=head1 DESCRIPTION
+
+This module provides a keyword handler for the C<application> keyword. It is an
+extension of L<MooseX::Declare/class>. The role application mechanism behind 
+the C<with> specification is hijacked and the arguments are passed to 
+Catalyst's C<setup> method. This hijacking is proably going away someday since
+in the future plugins will be actual roles.
+
+You don't have to call the C<setup> method yourself, this will be done by the
+handler after the body has been run. If you need to run code specifically before
+or after the C<setup> method has been run, you can always use method modifiers.
+
+=head1 SUPERCLASSES
+
+=over
+
+=item L<MooseX::Declare::Syntax::Keyword::Class>
+
+=back
+
+=head1 METHODS
+
+=head2 auto_make_immutable
+
+    Bool Object->auto_make_immutable ()
+
+A modified method that returns C<0> to signal to L<MooseX::Declare> that it 
+should not make this class immutable. Currently, making application classes
+immutable isn't supported yet, therefore C<is mutable> is currently a no-op.
+This will likely change as soon as application classes can be made immutable,
+
+=head1 SEE ALSO
+
+=over
+
+=item L<CatalystX::Declare>
+
+=item L<MooseX::Declare/class>
+
+=item L<MooseX::Declare::Syntax::Keyword::Class>
+
+=back
+
+=head1 AUTHOR
+
+See L<CatalystX::Declare/AUTHOR> for author information.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it under 
+the same terms as perl itself.
+
+=cut
index 046b246..56c8b6c 100644 (file)
@@ -10,6 +10,8 @@ class CatalystX::Declare::Keyword::Controller
     use aliased 'CatalystX::Declare::Controller::RegisterActionRoles';
     use aliased 'CatalystX::Declare::Controller::DetermineActionClass';
 
+    use Data::Dump qw( pp );
+
 
     before add_namespace_customizations (Object $ctx, Str $package) {
 
@@ -25,10 +27,28 @@ class CatalystX::Declare::Keyword::Controller
 
     method default_superclasses { 'Catalyst::Controller' }
 
+    method auto_make_immutable { 0 }
+
+    method add_with_option_customizations (Object $ctx, $package, ArrayRef $roles, HashRef $options) {
+
+        $ctx->add_cleanup_code_parts(
+            map {
+                sprintf('Class::MOP::load_class(%s)', pp "$_"),
+                sprintf('%s->meta->apply(%s->meta)', $_, $package),
+            } @$roles
+        );
+
+        $ctx->add_cleanup_code_parts(
+            sprintf '%s->meta->make_immutable', $package
+        ) unless $options->{is}{mutable};
+    }
+
     around default_inner () {
 
-        return [ 
-            @{ $self->$orig() || [] },
+        my @modifiers = qw( ); 
+
+        return [
+            ( grep { my $id = $_->identifier; not grep { $id eq $_ } @modifiers } @{ $self->$orig() || [] } ),
             ActionKeyword->new(identifier => 'action'),
             ActionKeyword->new(identifier => 'under'),
             ActionKeyword->new(identifier => 'final'),
@@ -36,3 +56,140 @@ class CatalystX::Declare::Keyword::Controller
     }
 }
 
+__END__
+
+=head1 NAME
+
+CatalystX::Declare::Keyword::Controller - Declare Catalyst Controllers
+
+=head1 SYNOPSIS
+
+    controller MyApp::Web::Controller::Example
+       extends MyApp::Web::ControllerBase::CRUD
+       with    MyApp::Web::ControllerRole::Caching {
+    
+
+        $CLASS->config(option_name => 'value');
+
+
+        has attr => (is => 'rw', lazy_build => 1);
+
+        method _build_attr { 'Hello World' }
+
+
+        action base as '';
+
+        final action site, under base {
+            $ctx->response->body( $self->attr );
+        }
+    }
+
+=head1 DESCRIPTION
+
+This handler module allows the declaration of Catalyst controllers. The
+C<controller> keyword is an extension of L<MooseX::Declare/class> with all the
+bells and whistles, including C<extends>, C<with>, C<method> and modifier
+declarations.
+
+In addition to the keywords and features provided by L<MooseX::Declare>, you
+can also specify your controller's actions declaratively. For the whole truth
+about the syntax refer to L<CatalystX::Declare::Keyword::Action>.
+
+For controller roles, please see L<CatalystX::Declare::Keyword::Role>. You can
+extend controllers with the C<extends> keyword and consume roles via C<with> as
+usual.
+
+=head1 SUPERCLASSES
+
+=over
+
+=item L<MooseX::Declare::Syntax::Keyword::Class>
+
+=back
+
+=head1 ROLES
+
+=over
+
+=item L<CatalystX::Declare::DefaultSuperclassing>
+
+=back
+
+=head1 METHODS
+
+These methods are implementation details. Unless you are extending or 
+developing L<CatalystX::Declare>, you should not be concerned with them.
+
+=head2 add_namespace_customizations
+
+    Object->add_namespace_customizations (Object $ctx, Str $package)
+
+This method modifier will initialise the controller with 
+L<MooseX::MethodAttributes>, import L<CLASS> and add the 
+L<CatalystX::Declare::Controller::RegisterActionRoles> and
+L<CatalystX::Declare::Controller::DetermineActionClass> controller roles
+before calling the original.
+
+=head2 default_superclasses
+
+    Str Object->default_superclasses ()
+
+Returns L<Catalyst::Controller> as the default superclass for all declared
+controllers.
+
+=head2 auto_make_immutable
+
+    Bool Object->auto_make_immutable ()
+
+Returns C<0>, indicating that L<MooseX::Declare> should not make this class
+immutable by itself. We will do that in the L</add_with_option_customizations>
+method ourselves.
+
+=head2 add_with_option_customizations
+
+    Object->add_with_option_customizations (
+        Object   $ctx,
+        Str      $package,
+        ArrayRef $roles,
+        HashRef  $options,
+    )
+
+This hook method will be called by L<MooseX::Declare> when C<with> options were
+encountered. It will load the specified class and apply them to the controller
+one at a time. This will change in the future, and they will be all applied 
+together.
+
+This method will also add a callback to make the controller immutable to the
+cleanup code parts unless C<is mutable> was specified.
+
+=head2 default_inner
+
+    ArrayRef[Object] Object->default_inner ()
+
+A method modifier around the original. The inner syntax handlers inherited by
+L<MooseX::Declare::Syntax::Keyword::Class> are extended with instances of the
+L<CatalystX::Declare::Keyword::Action> handler class for the C<action>, 
+C<under> and C<final> identifiers.
+
+=head1 SEE ALSO
+
+=over
+
+=item L<CatalystX::Declare>
+
+=item L<CatalystX::Declare::Keyword::Action>
+
+=item L<MooseX::Declare/class>
+
+=back
+
+=head1 AUTHOR
+
+See L<CatalystX::Declare/AUTHOR> for author information.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it under 
+the same terms as perl itself.
+
+=cut
diff --git a/lib/CatalystX/Declare/Keyword/Role.pm b/lib/CatalystX/Declare/Keyword/Role.pm
new file mode 100644 (file)
index 0000000..f53e23a
--- /dev/null
@@ -0,0 +1,106 @@
+use MooseX::Declare;
+
+class CatalystX::Declare::Keyword::Role
+    extends MooseX::Declare::Syntax::Keyword::Role {
+
+
+    use aliased 'MooseX::MethodAttributes::Role::Meta::Role';
+    use aliased 'CatalystX::Declare::Keyword::Action', 'ActionKeyword';
+
+
+    before add_namespace_customizations (Object $ctx, Str $package) {
+
+        $ctx->add_preamble_code_parts(
+            'use CLASS',
+            'use Moose::Role -traits => q(MethodAttributes)',
+        );
+    }
+
+    around default_inner () {
+
+        my @modifiers = qw( );
+
+        return [ 
+            ( grep { my $id = $_->identifier; not grep { $id eq $_ } @modifiers } @{ $self->$orig() || [] } ),
+            ActionKeyword->new(identifier => 'action'),
+            ActionKeyword->new(identifier => 'under'),
+            ActionKeyword->new(identifier => 'final'),
+        ];
+    }
+}
+
+__END__
+
+=head1 NAME
+
+CatalystX::Declare::Keyword::Role - Declare Catalyst Controller Roles
+
+=head1 SYNOPSIS
+
+    use CatalystX::Declare;
+
+    controller_role MyApp::Web::ControllerRole::Foo {
+
+        method provided_method { ... }
+
+        action foo, under base, is final { ... }
+
+        around bar_action (Object $ctx) { ... }
+    }
+
+=head1 DESCRIPTION
+
+This handler provides the C<controller_role> keyword. It is an extension of the
+L<MooseX::Declare::Syntax::Keyword::Role> handler. Like with declared 
+controllers, the C<method> keyword and the modifiers are provided. For details
+on the syntax for action declarations have a look at
+L<CatalystX::Declare::Keyword::Action>, which also documents the effects of
+method modifiers on actions.
+
+=head1 SUPERCLASSES
+
+=over
+
+=item L<MooseX::Declare::Syntax::Keyword::Role>
+
+=back
+
+=head1 METHODS
+
+=head2 add_namespace_customizations
+
+    Object->add_namespace_customizations (Object $ctx, Str $package)
+
+This hook is called by L<MooseX::Declare> and will set the package up as a role
+and apply L<MooseX::MethodAttributes>.
+
+=head2 default_inner
+
+    ArrayRef[Object] Object->default_inner ()
+
+Same as L<CatalystX::Declare::Keyword::Class/default_inner>.
+
+=head1 SEE ALSO
+
+=over
+
+=item L<CatalystX::Declare>
+
+=item L<MooseX::Declare/role>
+
+=item L<CatalystX::Declare::Keyword::Action>
+
+=item L<CatalystX::Declare::Keyword::Controller>
+
+=back
+
+=head1 AUTHOR
+
+See L<CatalystX::Declare/AUTHOR> for author information.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it under 
+the same terms as perl itself.
+
+=cut
index b6e112a..8c611d1 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 use FindBin;
 use lib "$FindBin::Bin/lib";
 
-use Test::More tests => 17;
+use Test::More tests => 21;
 use Catalyst::Test 'TestApp';
 
 # simple stuff
@@ -50,3 +50,13 @@ is get('/foo/expose_not_really_here'), 23, 'private action works';
 
 # specify chain target directly via action
 is get('/foo/pointed/beaver'), 'Your beaver is pointed!', 'chain target specified via action';
+
+# an action from a role
+is get('/foo/action_from_ctrl_role'), 'foo/action_from_ctrl_role', 'action from controller role';
+
+# an action body that was modified
+is get('/foo/modifier_target'), 'foo/modifier_target modified', 'action was modified by role';
+is get('/foo/surrounded_target'), 'foo/surrounded_target surrounded', 'action was modified with around by role';
+
+# inline classes
+is get('/foo/inline_class'), 'HELLO', 'inline classes work as expected';
index fe552c2..d2942ab 100644 (file)
@@ -1,11 +1,7 @@
-package TestApp;
-use strict;
-use warnings;
+use CatalystX::Declare;
 
-use parent 'Catalyst';
-use Catalyst qw( Static::Simple );
+application TestApp with Static::Simple {
 
-__PACKAGE__->config(name => 'CatalystX::Declare TestApp');
-__PACKAGE__->setup;
+    $CLASS->config(name => 'CatalystX::Declare TestApp');
+}
 
-1;
index b8715ae..1b0313d 100644 (file)
@@ -1,24 +1,32 @@
 use CatalystX::Declare;
 
-role MyActionYes {
-    around match (@args) { $ENV{TESTAPP_ACTIONROLE} ? $self->$orig(@args) : undef }
-}
 
-role TestApp::Try::Aliasing::MyActionNo {
-    around match (@args) { $ENV{TESTAPP_ACTIONROLE} ? undef : $self->$orig(@args) }
-}
+controller TestApp::Controller::Foo with TestApp::TestRole {
+
+    use constant MyActionNo => 'TestApp::Try::Aliasing::MyActionNo';
 
-class TestApp::Action::Page extends Catalyst::Action {
+    class ::Messenger {
 
-    around execute ($controller, $ctx, @args) {
-        my $page = $ctx->request->params->{page} || 1;
-        return $self->$orig($controller, $ctx, @args, page => $page);
+        has message => (is => 'rw');
+
+        method format { uc $self->message }
     }
-}
 
-controller TestApp::Controller::Foo {
+    role MyActionYes {
+        around match (@args) { $ENV{TESTAPP_ACTIONROLE} ? $self->$orig(@args) : undef }
+    }
 
-    use constant MyActionNo => 'TestApp::Try::Aliasing::MyActionNo';
+    role TestApp::Try::Aliasing::MyActionNo {
+        around match (@args) { $ENV{TESTAPP_ACTIONROLE} ? undef : $self->$orig(@args) }
+    }
+
+    class TestApp::Action::Page extends Catalyst::Action {
+
+        around execute ($controller, $ctx, @args) {
+            my $page = $ctx->request->params->{page} || 1;
+            return $self->$orig($controller, $ctx, @args, page => $page);
+        }
+    }
 
     #
     #   look, a Moose!
@@ -200,5 +208,25 @@ controller TestApp::Controller::Foo {
     #
 
     action pointed <- base ($what) is final { $ctx->response->body("Your $what is pointed!") }
+
+
+    #
+    #   targets for action modifiers
+    #
+
+    action modifier_target under base is final { $ctx->response->body($ctx->action->reverse) }
+
+    action surrounded_target under base is final { 
+        $ctx->response->body(join ' ', $ctx->action->reverse, $ctx->response->body || ());
+    }
+
+
+    #
+    #   inline classes
+    #
+
+    final action inline_class under base {
+        $ctx->response->body( TestApp::Controller::Foo::Messenger->new(message => 'Hello')->format );
+    }
 }
 
diff --git a/t/lib/TestApp/TestRole.pm b/t/lib/TestApp/TestRole.pm
new file mode 100644 (file)
index 0000000..dd2d0c5
--- /dev/null
@@ -0,0 +1,19 @@
+use CatalystX::Declare;
+
+controller_role TestApp::TestRole {
+
+    method something_from_the_role { 23 }
+
+    final action action_from_ctrl_role under base {
+        $ctx->response->body($ctx->action->reverse);
+    }
+
+    after modifier_target (Object $ctx) {
+        $ctx->response->body(join ' ', $ctx->response->body, 'modified');
+    }
+
+    around surrounded_target (Object $ctx) {
+        $ctx->response->body('surrounded');
+        $self->$orig($ctx);
+    }
+}
diff --git a/xt/kwalitee.t b/xt/kwalitee.t
new file mode 100644 (file)
index 0000000..77e5bc0
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Test::More;
+
+eval { 
+    # autogenerated files are excluded
+    # use strict is implied by Moose & Co.
+    require Test::Kwalitee; Test::Kwalitee->import(
+        tests => [qw( 
+            -use_strict
+            -has_manifest
+            -has_test_pod_coverage
+        )],
+    );
+};
+
+plan skip_all => 'Test::Kwalitee not installed; skipping' if $@;
diff --git a/xt/pod.t b/xt/pod.t
new file mode 100644 (file)
index 0000000..79e6ce3
--- /dev/null
+++ b/xt/pod.t
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Test::More;
+
+eval "use Test::Pod 1.00";
+
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+
+all_pod_files_ok();