NAME

   CatalystX::Eta are composed of Moose::Roles for consistent
   CRUD/Validation/Testing between apps.

   "Eta" is just a cool Greek letter. I'm using it for not polluting CPAN
   CatalystX namespace with this module.

WTH CatalystX::Eta is and why did you do that

   I started (although not with this namespace) as set of Catalyst
   Controller Roles to extend and reduce repeatable tasks that I had to do
   to make REST/CRUD stuff.

   Later, I had to start more Catalyst projects. After a while, others
   collaborators were using it on their projects too, but copying the code
   in each app.

   After a while, they made modifications on those files as well, and now
   we have lot of versions of *almost* same thing, and this is hell! So,
   I'm using this namespace to group and keep those changes together.

   This module may not fit for you, but it's a very simple way to make
   CRUD schemas on REST, without prohibit or complicate use of catalyst
   power, like chains or anything else.

How it works

   CatalystX::Eta do not create any path on you application. This is your
   job.

   Almost all CatalystX::Eta roles need DBIx::Class to work good.

   CatalystX::Eta have those packages:

       CatalystX::Eta::Controller::REST
       CatalystX::Eta::Controller::AutoBase
       CatalystX::Eta::Controller::AutoList
       CatalystX::Eta::Controller::AutoObject
       CatalystX::Eta::Controller::AutoResult
       CatalystX::Eta::Controller::CheckRoleForPOST
       CatalystX::Eta::Controller::CheckRoleForPUT
       CatalystX::Eta::Controller::ListAutocomplete
       CatalystX::Eta::Controller::Search
       CatalystX::Eta::Controller::TypesValidation
       CatalystX::Eta::Controller::ParamsAsArray
       CatalystX::Eta::Controller::SimpleCRUD
       CatalystX::Eta::Controller::AssignCollection
       CatalystX::Eta::Test::REST

   And now, with a little description:

       CatalystX::Eta::Controller::REST
           - NOT a Moose::ROLE.
           - extends Catalyst::Controller::REST
           - overwrite /end to catch die.

       CatalystX::Eta::Controller::AutoBase
           - requires 'base';
           - load $c->stash->{collection} a $c->model( $self->config->{result} )

       CatalystX::Eta::Controller::AutoList
           - requires 'list_GET';
           - requires 'list_POST';
           - list_GET read lines on $c->stash->{collection} then $self->status_ok
           - list_POST $c->stash->{collection}->execute(...) then $self->status_created

       CatalystX::Eta::Controller::AutoObject
           - May $c->detach('/error_404'), so better you implement this Private Path.
           - requires 'object';
           - $c->stash->{object} = $c->stash->{collection}->search( { "me.id" => $id } )

       CatalystX::Eta::Controller::AutoResult
           - requires 'result_GET';
           - requires 'result_PUT';
           - requires 'result_DELETE';
           - result_GET $self->status_ok a $c->stash->{object}
           - result_PUT $c->stash->{object}->execute(...) and $self->status_accepted
           - result_DELETE $c->stash->{object}->delete and $self->status_no_content

       CatalystX::Eta::Controller::CheckRoleForPOST
           - requires 'list_POST';
           - basically:
               if ( !$c->check_any_user_role( @{ $config->{create_roles} } ) ) {
                   $self->status_forbidden( $c, message => "insufficient privileges" );
                   $c->detach;
               }

       CatalystX::Eta::Controller::CheckRoleForPUT
           - requires 'result_PUT';
           - that's not so simple as CheckRoleForPOST, because it
             depends on what you have the user_id field on $c->stash->{object}
             and sometimes it is true.

       CatalystX::Eta::Controller::ListAutocomplete
           - requires list_GET
           - return { suggestions => [ value => $row->name, data => $row->id ] } instead of
             the normal response, if $c->req->params->{list_autocompleate} is true.

       CatalystX::Eta::Controller::Search
           - requires 'list_GET';
           - read $self->config->{search_ok} and
             $c->stash->{collection}->search( ... ) if the $c->req->params->{$search_keys} are valid.

       CatalystX::Eta::Controller::TypesValidation
           - add validate_request_params method.
           - validate_request_params uses Moose::Util::TypeConstraints::find_or_parse_type_constraint
             to validate $c->req->params->{...}

       CatalystX::Eta::Controller::ParamsAsArray
           - add params_as_array
           - params_as_array is a litle crazy, see it bellow.

       CatalystX::Eta::Controller::SimpleCRUD
           - just a group of with's.

           with 'CatalystX::Eta::Controller::AutoBase';      # 1
           with 'CatalystX::Eta::Controller::AutoObject';    # 2
           with 'CatalystX::Eta::Controller::AutoResult';    # 3

           with 'CatalystX::Eta::Controller::CheckRoleForPUT';
           with 'CatalystX::Eta::Controller::CheckRoleForPOST';

           with 'CatalystX::Eta::Controller::AutoList';      # 1
           with 'CatalystX::Eta::Controller::Search';        # 2

       CatalystX::Eta::Controller::AssignCollection
           - another group of with's

           with 'CatalystX::Eta::Controller::Search';
           with 'CatalystX::Eta::Controller::AutoBase';
           with 'CatalystX::Eta::Controller::AutoObject';
           with 'CatalystX::Eta::Controller::CheckRoleForPUT';
           with 'CatalystX::Eta::Controller::CheckRoleForPOST';

       CatalystX::Eta::Test::REST
           - extends Stash::REST and use Test::More
           - add a trigger process_response to Stash::REST
           this add a test for each request made with Stash::REST
           is(
               $opt->{res}->code,
               $opt->{conf}->{code},
               $desc . ( exists $opt->{conf}->{name} ? ' - ' . $opt->{conf}->{name} : '' )
           );

A Controller using CatalystX::Eta::Controller::SimpleCRUD

       package MyApp::Controller::API::User;

       use Moose;

       BEGIN { extends 'CatalystX::Eta::Controller::REST' }

       __PACKAGE__->config(

           # what resultset will be on $c->stash->{collection}
           # used by AutoBase
           result      => 'DB::User',

           # WARNING: you should never change it during "requests",
           # or behavior may be wrong, because Controllers are Singleton objects
           result_cond => { active => 1 },
           result_attr => { order_by => ['me.id'] },

           # where on stash the $c->stash->{collection}->next should be put
           # used by AutoObject and others.
           object_key => 'user',
           # what list_GET key should put collection results.
           list_key   => 'users',

           # check_only_roles => 0 # default.

           # used by CheckRoleForPUT
           update_roles => [qw/superadmin/],

           # used by CheckRoleForPOST
           create_roles => [qw/superadmin/],

           # used by AutoResult
           delete_roles => [qw/superadmin/],

           # if the user requesting delete or update have any of listed roles,
           # the action will be executed.
           # if the role was denied and config->{check_only_roles} is not true,
           # the code test if the object have the column (user_id | created_by ) and
           # if is equals $c->user->id, the action is executed even without the role.

           # used by AutoList and AutoResult
           # to generate the row.
           build_row => sub {
               my ( $r, $self, $c ) = @_;

               return {
                   (
                       map { $_ => $r->$_ }
                       qw(
                       id name email type
                       )
                   ),

               };
           },

           # change delete behavior to a update.
           before_delete => sub {
               my ( $self, $c, $item ) = @_;

               $item->update({ active => 0 });

               return 0;
           },

           # let the user search for a name using query-parameters
           search_ok => {
               'name' => 'Str',
           }
       );
       with 'CatalystX::Eta::Controller::SimpleCRUD';

       sub base : Chained('/api/base') : PathPart('users') : CaptureArgs(0) { }

       # here we implement read permissons
       after 'base' => sub {
           my ( $self, $c ) = @_;

           # if you are not a superadmin, (or, if you are a user)
           # you can only see youself on GET /users for example.
           $c->stash->{collection} = $c->stash->{collection}->search(
               {
                   'me.id' => $c->user->id
               }
           ) if $c->check_any_user_role('user');

       };

       sub object : Chained('base') : PathPart('') : CaptureArgs(1) { }

       sub result : Chained('object') : PathPart('') : Args(0) : ActionClass('REST') { }

       sub result_GET { }

       sub result_PUT { }

       sub result_DELETE { }

       sub list : Chained('base') : PathPart('') : Args(0) : ActionClass('REST') { }

       sub list_GET { }

       sub list_POST { }

       1;

CatalystX::Eta::Controller::AutoObject

   In order to use CatalystX::Eta::Controller::AutoObject you need need
   '/error_404' Catalyst Private action defined.

CatalystX::Eta::Controller::AutoResult

   In order to use CatalystX::Eta::Controller::AutoResult->result_PUT you
   need that your DBIx::Class::Result have a sub execute defined.

   The routine will be executed as:

       $result->execute(
           $c,
           for => 'update',
           with => $c->req->params,
       );

   You should not use $c for things differ than detach to an form_error.

CatalystX::Eta::Controller::AutoList

   In order to use CatalystX::Eta::Controller::AutoList->list_POST you
   need that your DBIx::Class::ResultSet have a sub execute defined.

   The routine will be executed as:

       $result->execute(
           $c,
           for => 'create',
           with => $c->req->params,
       );

   You should not use $c for things differ than detach to an form_error.

CatalystX::Eta::Controller::REST

   CatalystX::Eta::Controller::REST extends `Catalyst::Controller::REST`.

   All your controllers should extends `CatalystX::Eta::Controller::REST`.

   All exceptions will be more "api friendly" than HTML with '(en) Please
   come back later\n...' Response code are set to 500, and rest response
   to { error => 'Internal Server Error' }

   You can also do

       die \['foobar', 'something']

   anywhere (where the die goes freely until reach /end) and it will be
   transformed in a 400 reponse code with { error => 'form_error',
   form_error => { 'foobar' => 'something' } }

MyApp::TraitFor::Controller::TypesValidation

   This role add a sub validate_request_params;

   validate_request_params uses
   Moose::Util::TypeConstraints::find_or_parse_type_constraint to valid
   content, so you can do things like:

       $self->validate_request_params(
           $c,
           extra_days => {
               type     => 'Int',
               required => 1,
           },
           credit_card_id => {
               type     => 'Int',
               required => 0,
           },
       );

   On your controllers, and it do the $c->status_bad_request and
   $c->detach on invalid/missing params.

CatalystX::Eta::Controller::ParamsAsArray

   This role add a sub params_as_array;

   it transform keys of a hash to array of hashes:

       $self->params_as_array( 'foo', {
           'foo:1' => 'a',
           'bar:1' => 'b',
           'zoo:1' => 1,
           'zoo:2' => 2,
       })

       Returns:

       [
           { foo => 'a', zoo => 1},
           { foo => 'b', zoo => 2}
       ]

Tests Coverage

   This is the first version, and need a lot of progress on tests.

       @ version 0.01
       ---------------------------- ------ ------ ------ ------ ------ ------ ------
       File                           stmt   bran   cond    sub    pod   time  total
       ---------------------------- ------ ------ ------ ------ ------ ------ ------
       ...ta/Controller/AutoBase.pm  100.0   50.0   33.3  100.0    n/a   29.5   83.3
       ...ta/Controller/AutoList.pm  100.0   50.0   33.3  100.0    n/a    1.5   87.8
       .../Controller/AutoObject.pm  100.0   75.0    n/a  100.0    n/a    0.7   94.4
       .../Controller/AutoResult.pm   93.3   50.0   33.3  100.0    n/a    0.6   71.4
       ...oller/CheckRoleForPOST.pm   84.6   50.0    n/a  100.0    n/a    0.0   82.3
       ...roller/CheckRoleForPUT.pm  100.0   64.2   44.4  100.0    n/a    0.0   72.7
       ...tX/Eta/Controller/REST.pm   57.7   16.6   30.4  100.0   50.0   62.1   49.4
       .../Eta/Controller/Search.pm   32.7   10.0   11.1  100.0    n/a    0.3   25.7
       .../Controller/SimpleCRUD.pm  100.0    n/a    n/a  100.0    n/a    0.1  100.0
       ...atalystX/Eta/Test/REST.pm   93.3   83.3    n/a  100.0    0.0    4.8   88.4
       Total                          74.4   39.0   32.3  100.0   33.3  100.0   61.5
       ---------------------------- ------ ------ ------ ------ ------ ------ ------

TODO

   - The documentation of all modules need to be created, and this
   updated.

AUTHOR

   Renato CRON <[email protected]>

COPYRIGHT

   Copyright 2015- Renato CRON

   Thanks to http://eokoe.com

Disclaimer

   I'm using the word "REST" application but it really depends on you
   implement the truly REST. Catalyst::Controller::REST and
   CatalystX::Eta::Controller::REST only implement a JSON/YAML response,
   but lot of people would call those applications REST.

   Please do not use XML response with Catalyst::Controller::REST, because
   it use Simple::XML transform your data into something potentially
   unstable! If you want XML responses, use create it with a DTD.

LICENSE

   This library is free software; you can redistribute it and/or modify it
   under the same terms as Perl itself.

SEE ALSO

   CatalystX::CRUD