Skip to content

CatalystX::Eta are composed of Moose::Roles for consistent CRUD/Validation/Testing API

License

Notifications You must be signed in to change notification settings

eokoe/CatalystX-Eta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

SEE ALSO

CatalystX::CRUD

About

CatalystX::Eta are composed of Moose::Roles for consistent CRUD/Validation/Testing API

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages