The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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