Dancer2-Plugin-RPC-RESTISH

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN


2.01_01 2022-09-27T11:36:19+02:00 (beb80dd => Abe Timmerman)
 - (Abe Timmerman, Tue, 27 Sep 2022 11:36:19 +0200) Autocommit for
   distribution Dancer2::Plugin::RPC::RESTISH 2.01_01 (test)

 - (Abe Timmerman, Mon, 3 Oct 2022 11:18:05 +0200) Move the 'allow_origin'
   to an attribute
 -     The plugin-object is instantiated only once and not for every use of
   the
 -     keyword, so we need to do extra bookkeeping for things related to
 -     different endpoints. The new allow_origin attribute is a HashRef
   that
 -     will keep the 'cors_allow_origin' per $endpoint, so we can have
   diffent
 -     allowed origins for different endpoints.

 - (Abe Timmerman, Mon, 3 Oct 2022 19:31:59 +0200) Try to get the logging
   the same as the other RPC plugins
 -     Log the duration of handling the call.
 -     Adjust the test suite so it isn't loud.

0.00_00 2019-05-14T14:57:36+02:00 (9d6d295 => Abe Timmerman)
 - (Abe Timmerman, Tue, 14 May 2019 14:57:36 +0200) Initial commit for
   Dancer::Plugin::RPC::RESTISH
 -     A new plugin for the Dancer::Plugin::RPC framework that enables one

README  view on Meta::CPAN

NAME
    Dancer::Plugin::RPC::RESTISH - Simple plugin to implement a restish
    interface.

SYNOPSIS
    In the Controler-bit:

        use Dancer::Plugin::RPC::RESTISH;
        restish '/endpoint' => {
            publish   => 'pod',
            arguments => ['MyProject::Admin'],
        };

    and in the Model-bit (MyProject::Admin):

        package MyProject::Admin;

        =for restish GET@ability/:id rpc_get_ability_details

example/lib/Example.pm  view on Meta::CPAN

{
    my $system_config = Example::EndpointConfig->new(
        publish          => 'pod',
        bread_board      => $system_api,
        plugin_arguments => {
            arguments => ['Example::API::System'],
        },
    );
    my @plugins = grep { /^RPC::/ } keys %{ config->{plugins} };
    for my $plugin (@plugins) {
        $system_config->register_endpoint($plugin, '/system');
    }
}

{
    my $db_config = Example::EndpointConfig->new(
        publish          => 'config',
        bread_board      => $db_api,
        plugin_arguments => {
            plugin_args => { cors_allow_origin => '*' },
        }
    );
    my @plugins = grep { /^RPC::/ } keys %{ config->{plugins} };
    for my $plugin (@plugins) {
        for my $path (keys %{ config->{plugins}{$plugin} }) {
            $db_config->register_endpoint($plugin, $path);
        }
    }
}

1;

example/lib/Example/EndpointConfig.pm  view on Meta::CPAN

                    service 'Client::MetaCpan' => as (
                        class => 'Client::MetaCpan',
                        dependencies => {
                            base_uri => literal config->{base_uri},
                    ),
                };
            };
        ),
    );

    $config->register_endpoint('RPC::JSONRPC' => '/metacpan');
    $config->register_endpoint('RPC::XMLRPC'  => '/metacpan');

=head1 ATTRIBUTES

=head2 publish  [required]

This attribute can have the value of B<config> or B<pod>, it will be bassed to
L<Dancer::Plugin::RPC>

=head2 callback [optional]

example/lib/Example/EndpointConfig.pm  view on Meta::CPAN

        return $instance->$code(@arguments);
    };
}

sub _registrar_for_plugin {
    my $self = shift;
    my ($plugin) = @_;
    return $_plugin_info{$plugin}{registrar} // die "Cannot find plugin '$plugin'";
}

=head2 endpoint_config($path)

Returns a config-hash for the C<Dancer::Plugin::RPC::*> plugins.

=cut

sub endpoint_config {
    my $self = shift;
    my ($path) = @_;

    return {
        publish      => $self->publish,
        code_wrapper => $self->code_wrapper,
        (defined $self->callback
            ? (callback => $self->callback)
            : ()
        ),
        (defined $self->plugin_arguments
            ? (%{ $self->plugin_arguments })
            : ()
        ),
    };
}

=head2 register_endpoint($plugin, $path)

=cut

sub register_endpoint {
    my $self = shift;
    my ($plugin, $path) = @_;

    my $registrar = $self->_registrar_for_plugin($plugin);
    $registrar->($path, $self->endpoint_config($path));
}

use namespace::autoclean;
1;

=head1 COPYRIGHT

(c) MMXIX - Abe Timmerman <abeltje@cpan.org>

=cut

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

use JSON;
use Scalar::Util 'blessed';
use Time::HiRes 'time';

# A char between the HTTP-Method and the REST-route
our $_HM_POSTFIX = '@';

plugin_keywords PLUGIN_NAME;

sub restish {
    my ($plugin, $endpoint, $arguments) = @_;
    my $restish_args = $arguments->{plugin_args} || {};

    $plugin->allow_origin->{$endpoint} = $restish_args->{cors_allow_origin} || '';

    my $dispatcher = $plugin->dispatch_builder(
        $endpoint,
        $arguments->{publish},
        $arguments->{arguments},
        plugin_setting(),
    )->();

    my $lister = $plugin->partial_method_lister(
        protocol => __PACKAGE__->rpcplugin_tag,
        endpoint => $endpoint,
        methods  => [ sort keys %{ $dispatcher } ],
    );

    my $code_wrapper = $plugin->code_wrapper($arguments);
    my $callback = $arguments->{callback};

    $plugin->app->log(debug => "Starting restish-handler build: ", $lister);
    my $handle_call = sub {
        my ($dsl) = @_;
        my ($pi) = grep { ref($_) eq __PACKAGE__ } @{ $dsl->plugins };

        my $allow_origin = $pi->allow_origin->{$endpoint};
        my @allowed_origins = split(" ", $allow_origin);

        # we'll only handle requests that have either a JSON body or no body
        my $http_request = $dsl->app->request;
        my ($ct) = split(/;\s*/, $http_request->content_type // "", 2);
        $ct //= "";

        if ($http_request->body && ($ct ne 'application/json')) {
            $dsl->pass();
        }

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

                debug => "[RESTISH-CORS] '$has_origin' not allowed ($allow_origin)"
            );
            $dsl->response->status(403);
            $dsl->response->content_type('text/plain');
            return "[CORS] $has_origin not allowed";
        }

        # method_name should exist...
        # we need to turn 'GET@some_resource/:id' into a regex that we can use
        # to match this request so we know what thing to call...
        (my $method_name = $request_path) =~ s{^$endpoint/}{};
        my ($found_match, $found_method);
        my @sorted_dispatch_keys = sort {
            # reverse length of the regex we use to match
            my ($am, $ar) = split(/\b$_HM_POSTFIX/, $a);
            $ar =~ s{/:\w+}{/[^/]+};
            my ($bm, $br) = split(/\b$_HM_POSTFIX/, $b);
            $br =~ s{/:\w+}{/[^/]+};
            length($br) <=> length($ar)
        } keys %$dispatcher;

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

                   , $method_args
        );

        my $start_request = time();
        my $continue = eval {
            (my $match_re = $found_match) =~ s{:\w+}{[^/]+}g;
            local $Dancer2::RPCPlugin::ROUTE_INFO = {
                plugin        => PLUGIN_NAME,
                route_matched => $found_match,
                matched_re    => $match_re,
                endpoint      => $endpoint,
                rpc_method    => $method_name,
                full_path     => $http_request->path,
                http_method   => $http_method,
            };
            $callback
                ? $callback->($http_request, $method_name, $method_args)
                : callback_success();
        };
        my $error = $@;
        my $response;

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

        if ($dsl->config->{encoding} && $dsl->config->{encoding} =~ m{^utf-?8$}i) {
            $jsonise_options->{utf8} = 1;
        }

        # non-refs will be send as-is
        return ref($response)
            ? to_json($response, $jsonise_options)
            : $response;
    };

    $plugin->app->log(debug => "Setting routes (restish): $endpoint ", $lister);
    # split the keys in $dispatcher so we can register methods for all
    for my $dispatch_route (keys %$dispatcher) {
        my ($hm, $route) = split(/$_HM_POSTFIX/, $dispatch_route, 2);
        my $dancer_route = "$endpoint/$route";
        $plugin->app->log(debug => "[restish] registering `$hm $dancer_route`");
        $plugin->app->add_route(
            method => lc($hm),
            regexp => $dancer_route,
            code   => $handle_call,
        );
        $plugin->app->add_route(
            method => 'options',
            regexp => $dancer_route,
            code   => $handle_call

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN


=head1 NAME

Dancer::Plugin::RPC::RESTISH - Simple plugin to implement a restish interface.

=head1 SYNOPSIS

In the Controler-bit:

    use Dancer::Plugin::RPC::RESTISH;
    restish '/endpoint' => {
        publish     => 'pod',
        arguments   => ['MyProject::Admin'],
        plugin_args => {
            cors_allow_origin => '*',
        },
    };

and in the Model-bit (B<MyProject::Admin>):

    package MyProject::Admin;

lib/Dancer2/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

    =for restish DELETE@resource/:id delete_resource   /rest

The third argument (the base_path) is optional.

=back

The plugin for RESTISH also adds 2 fields to C<$Dancer2::RPCPlugin::ROUTE_INFO>:

        local $Dancer2::RPCPlugin::ROUTE_INFO = {
            plugin        => PLUGIN_NAME,
            endpoint      => $endpoint,
            rpc_method    => $method_name,
            full_path     => request->path,
            http_method   => $http_method,
            # These two are added
            route_matched => $found_match,      # PATCH@resource/:id
            matched_re    => $match_re,         # PATCH@resource/[^/]+
        };

=head2 CORS (Cross-Origin Resource Sharing)

t/225-register-restish.t  view on Meta::CPAN

use Dancer2::RPCPlugin::ErrorResponse;

use HTTP::Request;
use Plack::Test;

{
    note("default publish == 'config'");
    set(
        plugins => {
            'RPC::RESTISH' => {
                '/endpoint' => {
                    'TestProject::SystemCalls' => {
                        'GET@ping'    => 'do_ping',
                        'GET@version' => 'do_version',
                    },
                },
            }
        },
        log      => ($ENV{TEST_DEBUG} ? 'debug' : 'error'),
        encoding => 'utf-8',
       );

    restish '/endpoint' => { };

    my $tester = Plack::Test->create(main->to_app());
    my $response = $tester->request(
        HTTP::Request->new(GET => '/endpoint/ping')
    );

    my $ping = from_json('{"response": true}');

    is_deeply(
        from_json($response->content),
        $ping,
        "GET /endpoint/ping"
    ) or diag(explain($response));
}

{
    note("publish is code that returns the dispatch-table");
    restish '/endpoint2' => {
        publish => sub {
            eval { require TestProject::SystemCalls; };
            error("Cannot load: $@") if $@;
            return {
                'GET@version' => Dancer2::RPCPlugin::DispatchItem->new(
                    code    => TestProject::SystemCalls->can('do_version'),
                    package => 'TestProject::SystemCalls',
                ),
            };
        },
        callback => sub { return callback_success(); },
    };

    my $tester = Plack::Test->create(main->to_app());
    my $response = $tester->request(
        HTTP::Request->new(GET => '/endpoint2/version')
    );

    is_deeply(
        from_json($response->content),
        { software_version => $TestProject::SystemCalls::VERSION },
        "GET /endpoint2/version"
    ) or diag(explain($response->content));
}

{
    note("callback fails");
    restish '/fail1' => {
        publish => sub {
            eval { require TestProject::SystemCalls; };
            error("Cannot load: $@") if $@;
            return {

t/225-register-restish.t  view on Meta::CPAN

    my $result = $response->header('content-type') eq 'application/json'
        ? from_json($response->content)
        : $response->content;
    is_deeply(
        $result,
        {
            error_code    => -32500,
            error_message => "terrible death\n",
            error_data    => { },
        },
        "GET /endpoint_fail2/version (callback dies)"
    ) or diag(explain($result));
}

{
    note("callback returns unknown object");
    restish '/fail3' => {
        publish => sub {
            eval { require TestProject::SystemCalls; };
            error("Cannot load: $@") if $@;
            return {



( run in 0.259 second using v1.01-cache-2.11-cpan-b61123c0432 )