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 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