view release on metacpan or search on metacpan
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
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 {