Dancer-Plugin-RPC-RESTISH

 view release on metacpan or  search on metacpan

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,
    );
    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/Dancer/Plugin/RPC/RESTISH.pm  view on Meta::CPAN


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

my %dispatch_builder_map = (
        pod    => \&build_dispatcher_from_pod,
        config => \&build_dispatcher_from_config,
        );

register PLUGIN_NAME ,=> sub {
    my ($self, $endpoint, $arguments) = plugin_args(@_);
    my $allow_origin = $arguments->{cors_allow_origin} || '';
    my @allowed_origins = split(' ', $allow_origin);

    my $publisher;
    given ($arguments->{publish} // 'config') {
        when (exists $dispatch_builder_map{$_}) {
            $publisher = $dispatch_builder_map{$_};
            $arguments->{arguments} = plugin_setting() if $_ eq 'config';
        }
        default {
            $publisher = $_;
        }
    }
    my $dispatcher = $publisher->($arguments->{arguments}, $endpoint);

    my $lister = Dancer::RPCPlugin::DispatchMethodList->new();
    $lister->set_partial(
            protocol => PLUGIN_NAME,
            endpoint => $endpoint,
            methods  => [ sort keys %{ $dispatcher } ],
            );

    my $code_wrapper = $arguments->{code_wrapper}
        ? $arguments->{code_wrapper}
        : sub {
            my $code = shift;
            my $pkg  = shift;
            $code->(@_);
        };

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

        if ($allow_origin && $has_origin && !$allowed_origin) {
            debug("[RESTISH-CORS] '$has_origin' not allowed ($allow_origin)");
            status(403);
            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/Dancer/Plugin/RPC/RESTISH.pm  view on Meta::CPAN

            %$query_args,
        };
        debug("[handling_restish_request('$request_path' via '$found_match')] ", $method_args);

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

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

        if (config->{encoding} && 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;
    };

    debug("setting routes (restish): $endpoint ", $lister);
    # split the keys in $dispatcher so we can register 'any' methods for all
    # the handler will know what to do...
    for my $dispatch_route (keys %$dispatcher) {
        my ($hm, $route) = split(/$_HM_POSTFIX/, $dispatch_route, 2);
        my $dancer_route = "$endpoint/$route";
        debug("[restish] registering `any $dancer_route` ($hm)");
        any $dancer_route, $handle_call;
    }

};

sub build_dispatcher_from_pod {
    my ($pkgs, $endpoint) = @_;
    debug("[build_dispatcher_from_pod]");
    return dispatch_table_from_pod(
        plugin   => 'restish',
        packages => $pkgs,
        endpoint => $endpoint,
    );
}

sub build_dispatcher_from_config {
    my ($config, $endpoint) = @_;
    debug("[build_dispatcher_from_config]");

    return dispatch_table_from_config(
        plugin   => 'restish',
        config   => $config,
        endpoint => $endpoint,
    );
}

register_plugin();
true;

=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'],
        cors_allow_origin => '*',
    };

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

    package MyProject::Admin;
    
    =for restish GET@ability/:id rpc_get_ability_details

lib/Dancer/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<$Dancer::RPCPlugin::ROUTE_INFO>:

        local $Dancer::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 Dancer::RPCPlugin::CallbackResult;
use Dancer::RPCPlugin::DispatchItem;
use Dancer::RPCPlugin::ErrorResponse;

use Dancer::Test;

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

    route_exists([GET => '/endpoint/ping'],    "GET /endpoint/ping registered");
    route_exists([GET => '/endpoint/version'], "GET /endpoint/version registered");

    my $response = dancer_response(
        GET => '/endpoint/ping',
    );

    my $ping = from_json('{"response": true}');
    if (JSON->VERSION >= 2.90) {
        my $t = 1;
        $ping->{response} = bless \$t, 'JSON::PP::Boolean';
    }

    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' => dispatch_item(
                    code    => TestProject::SystemCalls->can('do_version'),
                    package => 'TestProject::SystemCalls',
                ),
            };
        },
        callback => sub { return callback_success(); },
    };

    route_exists([GET => '/endpoint2/version'], "GET /endpoint2/version registered");

    my $response = dancer_response(
        GET => '/endpoint2/version',
    );

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

{
    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.661 second using v1.01-cache-2.11-cpan-b61123c0432 )