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 <rentocron@cpan.org>
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.