Group
Extension

Mojolicious-Plugin-REST/lib/Mojolicious/Plugin/REST.pm

package Mojolicious::Plugin::REST;

# ABSTRACT: Mojolicious Plugin for RESTful operations
our $VERSION = '0.006'; # VERSION
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Exception;
use Lingua::EN::Inflect 1.895 qw/PL/;

my $http2crud = {
    collection => {
        get  => 'list',
        post => 'create',
    },
    resource => {
        get    => 'read',
        put    => 'update',
        delete => 'delete'
    },
};

has install_hook => 1;

sub register {
    my $self    = shift;
    my $app     = shift;
    my $options = { @_ ? ( ref $_[0] ? %{ $_[0] } : @_ ) : () };

    # prefix, version, stuff...
    my $url_prefix = '';
    foreach my $modifier (qw(prefix version)) {
        if ( defined $options->{$modifier} && $options->{$modifier} ne '' ) {
            $url_prefix .= "/" . $options->{$modifier};
        }
    }

    # method name for bridged actions...
    my $method_chained = $options->{method_chained} // 'chained';

    # override default http2crud mapping from options...
    if ( exists( $options->{http2crud} ) ) {
        foreach my $method_type ( keys( %{$http2crud} ) ) {
            next unless exists $options->{http2crud}->{$method_type};
            foreach my $method ( keys( %{ $http2crud->{$method_type} } ) ) {
                next unless exists $options->{http2crud}->{$method_type}->{$method};
                $http2crud->{$method_type}->{$method} = $options->{http2crud}->{$method_type}->{$method};
            }
        }
    }

    # install app hook if not disabled...
    $self->install_hook(0) if ( defined( $options->{hook} ) and $options->{hook} == 0 );
    if ( $self->install_hook ) {
        $app->hook(
            before_render => sub {
                my $c = shift;
                my $path_substr = substr "" . $c->req->url->path, 0, length $url_prefix;
                if ( $path_substr eq $url_prefix ) {
                    my $json = $c->stash('json');
                    unless ( defined $json->{data} ) {
                        $json->{data} = {};
                        $c->stash( 'json' => $json );
                    }
                }
            }
        );
    }

    $app->routes->add_shortcut(
        rest_routes => sub {

            my $routes = shift;

            my $params = { @_ ? ( ref $_[0] ? %{ $_[0] } : @_ ) : () };

            Mojo::Exception->throw('Route name is required in rest_routes') unless defined $params->{name};

            # name setting
            my $route_name = $params->{name};
            my ( $route_name_lower, $route_name_plural, $route_id );
            $route_name_lower  = lc $route_name;
            $route_name_plural = PL( $route_name_lower, 10 );
            $route_id          = ":" . $route_name_lower . "Id";

            # under setting
            my $under_name = $params->{under};
            my ( $under_name_lower, $under_name_plural, $under_id );
            if ( defined($under_name) and $under_name ne '' ) {
                $under_name_lower  = lc $under_name;
                $under_name_plural = PL( $under_name_lower, 10 );
                $under_id          = ":" . $under_name_lower . "Id";
            }

            # controller
            my $controller = $params->{controller} // ucfirst($route_name_lower);

            foreach my $collection_method ( sort keys( %{ $http2crud->{collection} } ) ) {
                next
                    if ( defined $params->{methods}
                    && index( $params->{methods}, substr( $http2crud->{collection}->{$collection_method}, 0, 1 ) )
                    == -1 );

                my $url           = "/$route_name_plural";
                my $action_suffix = "_" . $route_name_lower;
                if ( defined($under_name) ) {
                    $url           = "/$under_name_plural/$under_id" . $url;
                    $action_suffix = "_" . $under_name_lower . $action_suffix;
                }

                $url = $url_prefix . $url;
                my $action = $http2crud->{collection}->{$collection_method} . $action_suffix;

                if ( defined($under_name) ) {
                    my $under_controller = ucfirst($under_name_lower);
                    my $under
                        = $routes->under($url)->to( controller => $under_controller, action => $method_chained )
                        ->name("${under_controller}::${method_chained}()")
                        ->route->via($collection_method)->to( controller => $controller, action => $action )
                        ->name("${controller}::${action}()");
                }
                else {
                    $routes->route($url)->via($collection_method)->to( controller => $controller, action => $action )
                        ->name("${controller}::${action}()");

                }

            }
            foreach my $resource_method ( sort keys( %{ $http2crud->{resource} } ) ) {
                next
                    if ( defined $params->{methods}
                    && index( $params->{methods}, substr( $http2crud->{resource}->{$resource_method}, 0, 1 ) ) == -1 );

                my $ids = [];

                if ( defined( $params->{types} ) ) {
                    $ids = $params->{types};
                }
                else {
                    push @$ids, $route_id;
                }

                foreach my $id (@$ids) {
                    if ( defined( $params->{types} ) ) {
                        $controller = $params->{controller} // ucfirst($route_name_lower);
                        $controller .= '::' . ucfirst($id);
                    }

                    my $url           = "/$route_name_plural/$id";
                    my $action_suffix = "_" . $route_name_lower;
                    if ( defined($under_name) ) {
                        $url           = "/$under_name_plural/$under_id" . $url;
                        $action_suffix = "_" . $under_name_lower . $action_suffix;
                    }
                    $url = $url_prefix . $url;
                    my $action = $http2crud->{resource}->{$resource_method} . $action_suffix;

                    if ( defined($under_name) ) {
                        my $under_controller = ucfirst($under_name_lower);
                        my $under
                            = $routes->under($url)->to( controller => $under_controller, action => $method_chained )
                            ->name("${under_controller}::${method_chained}()")
                            ->route->via($resource_method)->to( controller => $controller, action => $action )
                            ->name("${controller}::${action}()");

                    }
                    else {
                        $routes->route($url)->via($resource_method)->to( controller => $controller, action => $action )
                            ->name("${controller}::${action}()");
                    }

                }

            }

        }
    );

}

1;

__END__

=pod

=head1 NAME

Mojolicious::Plugin::REST - Mojolicious Plugin for RESTful operations

=head1 VERSION

version 0.006

=head1 SYNOPSIS

    # In Mojolicious Application
    $self->plugin( 'REST' => { prefix => 'api', version => 'v1' } );

    $routes->rest_routes( name => 'Account' );

    # Installs following routes:

    # /api/v1/accounts             ....  GET     "Account::list_account()"    ^/api/v1/accounts(?:\.([^/]+)$)?
    # /api/v1/accounts             ....  POST    "Account::create_account()"  ^/api/v1/accounts(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId  ....  DELETE  "Account::delete_account()"  ^/api/v1/accounts/([^\/\.]+)(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId  ....  GET     "Account::read_account()"    ^/api/v1/accounts/([^\/\.]+)(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId  ....  PUT     "Account::update_account()"  ^/api/v1/accounts/([^\/\.]+)(?:\.([^/]+)$)?


    $routes->rest_routes( name => 'Feature', under => 'Account' );

    # Installs following routes:

    # /api/v1/accounts/:accountId/features             B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features
    #   +/                                             ....  GET     "Feature::list_account_feature()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features             B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features
    #   +/                                             ....  POST    "Feature::create_account_feature()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  DELETE  "Feature::delete_account_feature()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  GET     "Feature::read_account_feature()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  PUT     "Feature::update_account_feature()"  ^(?:\.([^/]+)$)?


    $routes->rest_routes( name => 'Product', under => 'Account', types => [qw(ftp ssh)] );

    # Installs following routes:

    # /api/v1/accounts/:accountId/products      B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products
    #   +/                                      ....  GET     "Product::list_account_product()"         ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products      B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products
    #   +/                                      ....  POST    "Product::create_account_product()"       ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ftp  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ftp
    #   +/                                      ....  DELETE  "Product::Ftp::delete_account_product()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ssh  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ssh
    #   +/                                      ....  DELETE  "Product::Ssh::delete_account_product()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ftp  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ftp
    #   +/                                      ....  GET     "Product::Ftp::read_account_product()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ssh  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ssh
    #   +/                                      ....  GET     "Product::Ssh::read_account_product()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ftp  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ftp
    #   +/                                      ....  PUT     "Product::Ftp::update_account_product()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/products/ssh  B...  *       "Account::chained()"                      ^/api/v1/accounts/([^\/\.]+)/products/ssh
    #   +/                                      ....  PUT     "Product::Ssh::update_account_product()"  ^(?:\.([^/]+)$)?

=head1 DESCRIPTION

L<Mojolicious::Plugin::REST> adds various helpers for L<REST|http://en.wikipedia.org/wiki/Representational_state_transfer>ful
L<CRUD|http://en.wikipedia.org/wiki/Create,_read,_update_and_delete> operations via
L<HTTP|http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol> to your mojolicious application.

As much as possible, it tries to follow L<RESTful API Design|https://blog.apigee.com/detail/restful_api_design> principles from Apigee.

Used in conjuction with L<Mojolicious::Controller::REST>, this module makes building RESTful application a breeze.

This module is inspired from L<Mojolicious::Plugin::RESTRoutes>.

=head1 WARNING

This module is still under development, and it's possible that things may change between releases without warning or deprecations.

=head1 MOJOLICIOUS HELPERS

=head2 rest_routes

A routes shortcut to easily add RESTful routes for a resource and associations.

=head1 MOJOLICIOUS HOOKS

This module installs an before_render application hook, which gurantees JSON output.

Refer L<Mojolicious::Controller::REST> documentation for output format.

Hook installation can be disabled by passing hook => 0 in plugin options. For Example:

    $self->plugin( 'REST', prefix => 'api', version => 'v1', hook => 0 );

=head1 OPTIONS

Following options can be used to control route creation:

=over

=item methods

This option can be used to control which methods are created for declared rest_route. Each character in the value of this option,
determines if corresponding route will be created or ommited. For Example:

    $routes->rest_routes( name => 'Account', methods => 'crudl' );

This will install all the rest routes, value C<crudl> signifies:

    c - create
    r - read
    u - update
    d - delete
    l - list

Only methods whose first character is mentioned in the value for this option will be created. For Example:

    $routes->rest_routes( name => 'Account', methods => 'crd' );

This will install only create, read and delete routes as below:

    # /api/v1/accounts             ....  POST    "Account::create_account()"  ^/api/v1/accounts(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId  ....  DELETE  "Account::delete_account()"  ^/api/v1/accounts/([^\/\.]+)(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId  ....  GET     "Account::read_account()"    ^/api/v1/accounts/([^\/\.]+)(?:\.([^/]+)$)?

Option value C<crd> signifies:

    c - create,
    r - read,
    d - delete

Old B<readonly> behaviour can thus be achieved using:

    $routes->rest_routes( name => 'Account', methods => 'cl' );

This will install only create and list routes as below:

    # /api/v1/accounts  ....  GET   "Account::list_account()"    ^/api/v1/accounts(?:\.([^/]+)$)?
    # /api/v1/accounts  ....  POST  "Account::create_account()"  ^/api/v1/accounts(?:\.([^/]+)$)?

=item name

The name of the resource, e.g. 'User'. This name will be used to build the route url as well as the controller name.

=item controller

By default, resource name will be converted to CamelCase controller name. You can change it by providing controller name.

If customized, this options needs a full namespace of the controller class.

=item under

This option can be used for associations. If present, url's for named resource will be created under given under resource. The actions created,
will be bridged under C<method_chained> method of given under resouce. For Example:

    $routes->rest_routes( name => 'Feature', under => 'Account' );

This will create following routes, where routes for feature are bridged under C<Account::chained()>

    # /api/v1/accounts/:accountId/features             B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features
    #   +/                                             ....  GET     "Feature::list_account_feature()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features             B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features
    #   +/                                             ....  POST    "Feature::create_account_feature()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  DELETE  "Feature::delete_account_feature()"  ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  GET     "Feature::read_account_feature()"    ^(?:\.([^/]+)$)?
    # /api/v1/accounts/:accountId/features/:featureId  B...  *       "Account::chained()"                 ^/api/v1/accounts/([^\/\.]+)/features/([^\/\.]+)
    #   +/                                             ....  PUT     "Feature::update_account_feature()"  ^(?:\.([^/]+)$)?

In Mojolicious version 5.X, the actual bridge code needs to return a true value or the dispatch chain will be broken. Please refer
L<Mojolicious Bridges Documentation|https://metacpan.org/pod/release/JHTHORSEN/Mojolicious-5.17/lib/Mojolicious/Guides/Routing.pod#Bridges> for more information on bridges in Mojolicious.
Since Mojolicious 6.0 removed the method C<bridge>, you should use C<under> prefered - indeed, the C<bridge> method has been marked B<deprecated> since
L<Mojolicious 5.73|https://metacpan.org/source/SRI/Mojolicious-5.73/lib/Mojolicious/Routes/Route.pm>.

    Mojo::Util::deprecated 'Mojolicious::Routes::Route::bridge is DEPRECATED in'
        . ' favor of Mojolicious::Routes::Route::under';

Please refer to the latest guides for C<under> method at L<Mojolicious Under Documentation|https://metacpan.org/pod/Mojolicious::Guides::Routing#Under>.

=item types

This option can be used to specify types of resources available in application.

=back

=head1 PLUGIN OPTIONS

=over

=item method_chained

If present, this value will be used as a method name for chained methods in route bridges.

=item prefix

If present, this value will be added as prefix to all routes created.

=item version

If present, this value will be added as prefix to all routes created but after prefix.

=item http2crud

If present, given HTTP to CRUD mapping will be used to determine method names. Default mapping:

    {
        collection => {
            get  => 'list',
            post => 'create',
        },

        resource => {
            get    => 'read',
            put    => 'update',
            delete => 'delete'
        }
    }

=back

=head1 AUTHOR

Abhishek Shende <abhishekisnot@gmail.com>

=head1 CONTRIBUTORS

=for stopwords Denis Ibaev Huo Linhe Vincent HETRU

=over 4

=item *

Denis Ibaev <dionys@gmail.com>

=item *

Huo Linhe <huolinhe@berrygenomics.com>

=item *

Vincent HETRU <vincent.hetru@13pass.com>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Abhishek Shende.

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

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.