view release on metacpan or search on metacpan
- (Dancer2::Plugin::RPC::RESTISH).
2.01 2022-07-11T09:55:40+02:00 (43848fb => Abe Timmerman)
- (Abe Timmerman, Mon, 11 Jul 2022 09:55:40 +0200) Autocommit for
distribution Dancer2::Plugin::RPC 2.01 (minor)
- (Abe Timmerman, Mon, 26 Sep 2022 16:03:34 +0200) Fix documentation
- (Abe Timmerman, Mon, 26 Sep 2022 16:03:42 +0200) Fix validation
templates
- - Also allow '-' (dash) in endpoints
- - Add 'plugin_args' to the 'plugin_config' dict to have an optional
- HashRef to pass to other Dancer2::Plugin::RPC plugins (for now
- Dancer2::Plugin::RPC::RESTISH)
1.99_06 2022-07-01T13:56:39+02:00 (d60d234 => Abe Timmerman)
- (Abe Timmerman, Fri, 1 Jul 2022 13:56:39 +0200) Autocommit for
distribution Dancer2::Plugin::RPC 1.99_06 (same)
- (Abe Timmerman, Mon, 11 Jul 2022 09:51:05 +0200) I screwed up the
versioning
versions (Dancer1.3 and Dancer2)
- (abeltje, Thu, 10 Aug 2017 12:00:17 +0200) More bookkeeping...
- (abeltje, Wed, 16 Aug 2017 20:04:46 +0200) Fix regex for argument
validation
- (abeltje, Wed, 6 Sep 2017 18:54:12 +0200) Documentation fixes, complete
RESTRPC stuff
- (abeltje, Wed, 6 Sep 2017 20:12:50 +0200) Introduce explicit endpoints
for POD-publishing
- - The Dancer2::RPCPlugin::DispatchFromPod class has a new attribute
- 'endpoint', this will be used with the new optional item for
endpoint
- in the POD directive
- - Renamed the 'label' attribute to plugin (sync with the Dancer
- version) in the Dancer2::RPCPlugin::DispatchFrom* classes
- - Had to rename the 'plugin' attribute to 'plugin_object' in those
- classes
- (abeltje, Wed, 6 Sep 2017 20:21:54 +0200) Fix agent-identifier in
http-client.
- (abeltje, Wed, 6 Sep 2017 21:50:01 +0200) Use Params::ValidationCompiler
Dancer2::Plugin::RPC - Namespace for XMLRPC, JSONRPC2 and RESTRPC plugins
# DESCRIPTION
This module contains plugins for [Dancer2](https://metacpan.org/pod/Dancer2): [Dancer2::Plugin::RPC::XMLRPC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ARPC%3A%3AXMLRPC),
[Dancer2::Plugin::RPC::JSONRPC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ARPC%3A%3AJSONRPC) and [Dancer2::Plugin::RPC::RESTRPC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ARPC%3A%3ARESTRPC).
## Dancer2::Plugin::RPC::XMLRPC
This plugin exposes the new keyword `xmlrpc` that is followed by 2 arguments:
the endpoint and the arguments to configure the xmlrpc-calls at this endpoint.
## Dancer2::Plugin::RPC::JSONRPC
This plugin exposes the new keyword `jsonrpc` that is followed by 2 arguments:
the endpoint and the arguments to configure the jsonrpc-calls at this endpoint.
## Dancer2::Plugin::RPC::RESTRPC
This plugin exposes the new keyword `restrpc` that is followed by 2 arguments:
the endpoint and the arguments to configure the restrpc-calls at this endpoint.
## General arguments to xmlrpc/jsonrpc/restrpc
The dispatch table is build by endpoint.
### publish => <config|pod|$coderef>
- publish => **config**
The dispatch table is build from the YAML-config:
```yaml
plugins:
'RPC::XMLRPC':
'/endpoint1':
'Module::Name1':
method1: sub1
method2: sub2
'Module::Name2':
method3: sub3
'/endpoint2':
'Module::Name3':
method4: sub4
```
The **arguments** argument should be empty for this publishing type.
- publish => **pod**
The dispatch table is build by parsing the POD for `=for xmlrpc`,
`=for jsonrpc` or `=for restrpc`.
```perl
=for xmlrpc <method_name> <sub_name> [<endpoint>]
```
The **arguments** argument must be an Arrayref with module names. The
POD-directive must be in the same file as the code!
- publish => **$coderef**
With this publishing type, you will need to build your own dispatch table and return it.
```perl
use Dancer2::RPCPlugin::DispatchItem;
return {
ex/MixedEndpoints.pm view on Meta::CPAN
package MixedEndpoints;
use warnings;
use strict;
=head1 DESCRIPTION
Package with calls for diffent endpoints.
=head2 call_for_system
=for xmlrpc system.call call_for_system /system
=for jsonrpc system_call call_for_system /system
=for restrpc call call_for_system /system
=cut
sub call_for_system { return {endpoint => '/system'} }
=head2 call_for_testing
=for xmlrpc testing.call call_for_testing /testing
=for jsonrpc testing_call call_for_testing /testing
=for restrpc call call_for_testing /testing
=cut
sub call_for_testing { return {endpoint => '/testing'} }
=head2 call_for_all_endpoints
=for xmlrpc any.call call_for_all_endpoints
=for jsonrpc any_call call_for_all_endpoints
=for restrpc any-call call_for_all_endpoints
=cut
sub call_for_all_endpoints { return {endpoint => '*'} }
1;
ex/MyApp.pm view on Meta::CPAN
use Dancer2::RPCPlugin::CallbackResult;
BEGIN {
set(log => 'debug');
}
my $callback = sub {
return callback_succes();
};
jsonrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
callback => $callback,
};
restrpc '/rest' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
callback => $callback,
};
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
callback => $callback,
};
1;
example/bin/do-rpc view on Meta::CPAN
$option{call} //= '';
$class->$new(%option, arguments => \%arguments);
};
sub run {
my $self = shift;
my ($client, @method);
given ($self->type) {
when ('jsonrpc') {
$client = JSONRPCClient->new(endpoint => $self->url);
}
when ('xmlrpc') {
$client = XMLRPCClient->new(endpoint => $self->url);
}
when ('restrpc') {
$client = RESTRPCClient->new(endpoint => $self->url);
@method = uc($self->method);
}
};
my $answer = $client->call($self->call, @method, $self->arguments);
if ($self->asjson) {
my $jsonise_options = {
utf8 => 1,
allow_nonref => 1,
pretty => 1,
example/bin/do-rpc view on Meta::CPAN
package HTTPClient {
use Moo::Role;
use URI;
use HTTP::Tiny;
use Scalar::Util 'blessed';
with 'MooseX::Log::Log4perl::Easy';
our $VERSION = '0.90';
has endpoint => (is => 'ro', isa => sub { blessed($_[0]) eq 'URI' });
has client => (is => 'lazy', isa => sub { blessed($_[0]) eq 'HTTP::Tiny' });
has ssl_opts => (is => 'ro', default => undef);
has timeout => (is => 'ro', default => 300);
requires 'call';
around BUILDARGS => sub {
my $method = shift;
my $class = shift;
my %args = @_;
if (ref($args{endpoint}) ne 'URI') {
$args{endpoint} = URI->new($args{endpoint});
}
$class->$method(%args);
};
sub _build_client {
my $self = shift;
return HTTP::Tiny->new(
agent => "Dancer2-RPCPlugin-do-rpc/$VERSION",
verify_SSL => 0,
timeout => $self->timeout,
example/bin/do-rpc view on Meta::CPAN
my ($method_name, $data) = @_;
my $request = $self->jsonrpc_request($method_name => $data);
my $headers = {
'Content-Type' => 'application/json',
'Content-Length' => length($request),
};
$self->log_debug(Dumper($headers));
$self->log_debug($request);
my $response = $self->client->request(
POST => $self->endpoint,
{
headers => $headers,
content => $request,
}
);
local $Data::Dumper::Indent = 1;
$self->log_trace("jsonrpc($method_name)". Dumper($response));
my $result;
if ($response->{success}) {
my $p_response = decode_json($response->{content});
example/bin/do-rpc view on Meta::CPAN
return RPC::XML::ParserFactory->new();
}
sub call {
my $self = shift;
my ($method_name, $data) = @_;
my $request = RPC::XML::request->new($method_name => $data)->as_string();
$self->log_debug($request);
my $response = $self->client->request(
POST => $self->endpoint,
{
headers => {
'Content-Type' => 'text/xml',
'Content-Length' => length($request),
},
content => $request,
}
);
$self->log_trace(Dumper($response));
example/bin/do-rpc view on Meta::CPAN
with 'HTTPClient';
use JSON;
use URI;
use Data::Dumper;
sub call {
my $self = shift;
my $call = shift;
my $http_method = shift || 'GET';
(my $endpoint = $self->endpoint->as_string) =~ s{/+$}{};
$endpoint .= "/$call" if $call;
my $request = @_ ? encode_json(shift) : '';
$self->log_debug("$http_method: $endpoint => $request");
my $response = $self->client->request(
$http_method => $endpoint,
{
headers => {
'Content-Type' => 'application/json',
'Content-Length' => length($request),
},
content => $request,
}
);
local $Data::Dumper::Indent = 1;
$self->log_trace(Dumper($response));
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'],
},
);
for my $plugin (qw{ RPC::JSONRPC RPC::RESTRPC RPC::XMLRPC}) {
$system_config->register_endpoint($plugin, '/system');
}
}
{
my $example_config = Example::EndpointConfig->new(
publish => 'config',
bread_board => $example_api,
);
my $plugins = config->{plugins};
for my $plugin (keys %$plugins) {
for my $path (keys %{$plugins->{$plugin}}) {
$example_config->register_endpoint($plugin, $path);
}
}
}
setup_default_route();
1;
=head1 NAME
Example - An example RPC-application for L<Dancer2::Plugin::RPC>
example/lib/Example/API/System.pm view on Meta::CPAN
{software_version => 'X.YZ'}
=head2 rpc_list_methods()
=for jsonrpc list_methods rpc_list_methods /system
=for restrpc list_methods rpc_list_methods /system
=for xmlrpc list_methods rpc_list_methods /system
Returns a struct for all protocols with all endpoints and functions for that endpoint.
=head1 COPYRIGHT
(c) MMXVII - Abe Timmerman <abeltje@cpan.org>
=cut
example/lib/Example/Client/MetaCpan.pm view on Meta::CPAN
with 'Client::HTTP';
sub call {
my $self = shift;
my ($query) = @_;
$query =~ s{::}{-}g;
my $params = $self->client->www_form_urlencode({q => $query});
(my $endpoint = $self->base_uri->as_string) =~ s{/+$}{};
my $response = $self->client->get("$endpoint/?$params");
my $content = eval { decode_json($response->{content}) };
return $content if !$@;
return { error => $@, data => $response->{content} };
}
use namespace::autoclean;
1;
=head1 COPYRIGHT
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.pm view on Meta::CPAN
Dancer2::Plugin::RPC - Namespace for XMLRPC, JSONRPC2 and RESTRPC plugins
=head1 DESCRIPTION
This module contains plugins for L<Dancer2>: L<Dancer2::Plugin::RPC::XMLRPC>,
L<Dancer2::Plugin::RPC::JSONRPC> and L<Dancer2::Plugin::RPC::RESTRPC>.
=head2 Dancer2::Plugin::RPC::XMLRPC
This plugin exposes the new keyword C<xmlrpc> that is followed by 2 arguments:
the endpoint and the arguments to configure the xmlrpc-calls at this endpoint.
=head2 Dancer2::Plugin::RPC::JSONRPC
This plugin exposes the new keyword C<jsonrpc> that is followed by 2 arguments:
the endpoint and the arguments to configure the jsonrpc-calls at this endpoint.
=head2 Dancer2::Plugin::RPC::RESTRPC
This plugin exposes the new keyword C<restrpc> that is followed by 2 arguments:
the endpoint and the arguments to configure the restrpc-calls at this endpoint.
=head2 General arguments to xmlrpc/jsonrpc/restrpc
The dispatch table is build by endpoint.
=head3 publish => <config|pod|$coderef>
=over
=item publish => B<config>
The dispatch table is build from the YAML-config:
plugins:
'RPC::XMLRPC':
'/endpoint1':
'Module::Name1':
method1: sub1
method2: sub2
'Module::Name2':
method3: sub3
'/endpoint2':
'Module::Name3':
method4: sub4
The B<arguments> argument should be empty for this publishing type.
=item publish => B<pod>
The dispatch table is build by parsing the POD for C<=for xmlrpc>,
C<=for jsonrpc> or C<=for restrpc>.
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
use Dancer2::RPCPlugin::ErrorResponse;
use Dancer2::RPCPlugin::FlattenData;
use JSON;
use Scalar::Util 'blessed';
use Time::HiRes 'time';
plugin_keywords PLUGIN_NAME;
sub jsonrpc {
my ($plugin, $endpoint, $config) = @_;
my $dispatcher = $plugin->dispatch_builder(
$endpoint,
$config->{publish},
$config->{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($config);
my $callback = $config->{callback};
$plugin->app->log(debug => "Starting jsonrpc-handler build: ", $lister);
my $jsonrpc_handler = sub {
my ($dsl) = @_;
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
$dsl->app->log(
debug => "[handle_jsonrpc_request] Processing: ", $http_request->body
);
$dsl->app->response->content_type('application/json');
my @responses;
for my $request (@requests) {
my $method_name = $request->{method};
if (!exists $dispatcher->{$method_name}) {
$dsl->app->log(warning => "$endpoint/#$method_name not found.");
# # single request; might be another handler
# $dsl->app->pass() if @requests == 1;
push @responses, jsonrpc_response(
$request->{id},
error => {
code => -32601,
message => "Method '$method_name' not found at '$endpoint' (skipped)",
},
);
next;
}
my $method_args = $request->{params};
my $start_request = time();
$dsl->app->log(debug => "[handle_jsonrpc_call($method_name)] ", $method_args);
my $continue = eval {
local $Dancer2::RPCPlugin::ROUTE_INFO = {
plugin => PLUGIN_NAME,
endpoint => $endpoint,
rpc_method => $method_name,
full_path => $http_request->path,
http_method => uc($http_request->method),
};
$callback
? $callback->($plugin->app->request(), $method_name, $method_args)
: callback_success();
};
if (my $error = $@) {
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
else {
$response = to_json([grep {defined($_->{id})} @responses], $jsonise_options);
}
$dsl->app->log(debug => "[jsonrpc_response] ", $response);
return $response;
};
$plugin->app->add_route(
method => 'post',
regexp => $endpoint,
code => $jsonrpc_handler,
);
}
sub unjson {
my ($body) = @_;
return if !$body;
my @requests;
my $unjson = decode_json($body);
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
=head1 NAME
Dancer2::Plugin::RPC::JSON - Dancer Plugin to register jsonrpc2 methods.
=head1 SYNOPSIS
In the Controler-bit:
use Dancer2::Plugin::RPC::JSON;
jsonrpc '/endpoint' => {
publish => 'pod',
arguments => ['MyProject::Admin']
};
and in the Model-bit (B<MyProject::Admin>):
package MyProject::Admin;
=for jsonrpc rpc.abilities rpc_show_abilities
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
sub rpc_show_abilities {
return {
# datastructure
};
}
1;
=head1 DESCRIPTION
This plugin lets one bind an endpoint to a set of modules with the new B<jsonrpc> keyword.
=head2 jsonrpc '/endpoint' => \%publisher_arguments;
=head3 C<\%publisher_arguments>
=over
=item callback => $coderef [optional]
The callback will be called just before the actual rpc-code is called from the
dispatch table. The arguments are positional: (full_request, method_name).
lib/Dancer2/Plugin/RPC/JSONRPC.pm view on Meta::CPAN
The publiser key determines the way one connects the rpc-method name with the actual code.
=over
=item publisher => 'config'
This way of publishing requires you to create a dispatch-table in the app's config YAML:
plugins:
"RPC::JSONRPC":
'/endpoint':
'MyProject::Admin':
admin.someFunction: rpc_admin_some_function_name
'MyProject::User':
user.otherFunction: rpc_user_other_function_name
The Config-publisher doesn't use the C<arguments> value of the C<%publisher_arguments> hash.
=item publisher => 'pod'
This way of publishing enables one to use a special POD directive C<=for jsonrpc>
lib/Dancer2/Plugin/RPC/RESTRPC.pm view on Meta::CPAN
my $dispatcher = $plugin->dispatch_builder(
$base_url,
$config->{publish},
$config->{arguments},
plugin_setting(),
)->();
my $lister = $plugin->partial_method_lister(
protocol => __PACKAGE__->rpcplugin_tag,
endpoint => $base_url,
methods => [ sort keys %{ $dispatcher } ],
);
my $code_wrapper = $plugin->code_wrapper($config);
my $callback = $config->{callback};
$plugin->app->log(debug => "Starting handler build: ", $lister);
my $restrpc_handler = sub {
my $dsl = shift;
lib/Dancer2/Plugin/RPC/RESTRPC.pm view on Meta::CPAN
$dsl->response->content_type('application/json');
my $response;
my $method_args = $http_request->body
? from_json($http_request->body)
: undef;
$dsl->app->log(debug => "[handle_restrpc_call($method_name)] ", $method_args);
my $start_request = time();
my Dancer2::RPCPlugin::CallbackResult $continue = eval {
local $Dancer2::RPCPlugin::ROUTE_INFO = {
plugin => PLUGIN_NAME,
endpoint => $base_url,
rpc_method => $method_name,
full_path => $http_request->path,
http_method => uc($http_request->method),
};
$callback
? $callback->($http_request, $method_name, $method_args)
: callback_success();
};
if (my $error = $@) {
lib/Dancer2/Plugin/RPC/RESTRPC.pm view on Meta::CPAN
}
elsif (blessed($response)) {
$response = flatten_data($response);
}
}
return restrpc_response($dsl, $response);
};
for my $call (keys %{ $dispatcher }) {
my $endpoint = "$base_url/$call";
$plugin->app->log(debug => "setting route (restrpc): $endpoint ", $lister);
$plugin->app->add_route(
method => 'post',
regexp => $endpoint,
code => $restrpc_handler,
);
}
}
sub restrpc_response {
my ($dsl, $data) = @_;
my $jsonise_options = {canonical => 1};
if ($dsl->config->{encoding} && $dsl->config->{encoding} =~ m{^utf-?8$}i) {
lib/Dancer2/Plugin/RPC/RESTRPC.pm view on Meta::CPAN
sub rpc_show_abilities {
return {
# datastructure
};
}
1;
=head1 DESCRIPTION
RESTRPC is a simple protocol that uses HTTP-POST to post a JSON-string (with
C<Content-Type: application/json> to an endpoint. This endpoint is the
C<base_url> concatenated with the rpc-method name.
This plugin lets one bind a base_url to a set of modules with the new B<restrpc> keyword.
=head2 restrpc '/base_url' => \%publisher_arguments;
=head3 C<\%publisher_arguments>
=over
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
use Dancer2::RPCPlugin::FlattenData;
use RPC::XML;
use RPC::XML::ParserFactory;
use Scalar::Util 'blessed';
use Time::HiRes 'time';
plugin_keywords PLUGIN_NAME;
sub xmlrpc {
my ($plugin, $endpoint, $config) = @_;
my $dispatcher = $plugin->dispatch_builder(
$endpoint,
$config->{publish},
$config->{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($config);
my $callback = $config->{callback};
$plugin->app->log(debug => "Starting xmlrpc-handler build: ", $lister);
my $xmlrpc_handler = sub {
my $dsl = shift;
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
$dsl->app->log(
debug => "[handle_xmlrpc_request] Processing: ", $http_request->body
);
local $RPC::XML::ENCODING = $RPC::XML::ENCODING ='UTF-8';
my $p = RPC::XML::ParserFactory->new();
my $request = $p->parse($http_request->body);
my $method_name = $request->name;
if (! exists $dispatcher->{$method_name}) {
$dsl->app->log(warning => "$endpoint/#$method_name not found, pass()");
$dsl->pass();
}
$dsl->response->content_type('text/xml');
my $response;
$dsl->app->log(debug => "[handle_xmlrpc_call($method_name)] ", $request->args);
my @method_args = map $_->value, @{$request->args};
my $start_request = time();
my Dancer2::RPCPlugin::CallbackResult $continue = eval {
local $Dancer2::RPCPlugin::ROUTE_INFO = {
plugin => PLUGIN_NAME,
endpoint => $endpoint,
rpc_method => $method_name,
full_path => $http_request->path,
http_method => uc($http_request->method),
};
$callback
? $callback->($dsl->app->request, $method_name, @method_args)
: callback_success();
};
if (my $error = $@) {
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
if (blessed($response) && $response->can('as_xmlrpc_fault')) {
$response = $response->as_xmlrpc_fault;
}
elsif (blessed($response)) {
$response = flatten_data($response);
}
}
return xmlrpc_response($dsl, $response);
};
$plugin->app->log(debug => "setting route (xmlrpc): $endpoint ", $lister);
$plugin->app->add_route(
method => 'post',
regexp => $endpoint,
code => $xmlrpc_handler,
);
}
sub xmlrpc_response {
my $dsl = shift;
my ($data) = @_;
local $RPC::XML::ENCODING = 'UTF-8';
my $response;
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
=head1 NAME
Dancer2::Plugin::RPC::XML - XMLRPC Plugin for Dancer2
=head2 SYNOPSIS
In the Controler-bit:
use Dancer2::Plugin::RPC::XMLRPC;
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => ['MyProject::Admin']
};
and in the Model-bit (B<MyProject::Admin>):
package MyProject::Admin;
=for xmlrpc rpc.abilities rpc_show_abilities
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
sub rpc_show_abilities {
return {
# datastructure
};
}
1;
=head1 DESCRIPTION
This plugin lets one bind an endpoint to a set of modules with the new B<xmlrpc> keyword.
=head2 xmlrpc '/endpoint' => \%publisher_arguments;
=head3 C<\%publisher_arguments>
=over
=item callback => $coderef [optional]
The callback will be called just before the actual rpc-code is called from the
dispatch table. The arguments are positional: (full_request, method_name).
lib/Dancer2/Plugin/RPC/XMLRPC.pm view on Meta::CPAN
The publiser key determines the way one connects the rpc-method name with the actual code.
=over
=item publisher => 'config'
This way of publishing requires you to create a dispatch-table in the app's config YAML:
plugins:
"RPC::XMLRPC":
'/endpoint':
'MyProject::Admin':
admin.someFunction: rpc_admin_some_function_name
'MyProject::User':
user.otherFunction: rpc_user_other_function_name
The Config-publisher doesn't use the C<arguments> value of the C<%publisher_arguments> hash.
=item publisher => 'pod'
This way of publishing enables one to use a special POD directive C<=for xmlrpc>
lib/Dancer2/RPCPlugin.pm view on Meta::CPAN
sub rpcplugin_tag {
my $full_name = ref($_[0]) ? ref($_[0]) : $_[0];
(my $proto = $full_name) =~ s{.*::}{};
return "\L${proto}";
}
sub dispatch_builder {
my $self = shift;
$self->validate_positional_parameters(
[
$self->parameter(endpoint => $self->Required, {store => \my $endpoint}),
$self->parameter(publish => $self->Required, {store => \my $publish}),
$self->parameter(arguments => $self->Optional, {store => \my $arguments}),
$self->parameter(settings => $self->Optional, {store => \my $settings}),
],
\@_
);
$publish //= 'config';
if ($publish eq 'config') {
return sub {
$self->app->log(
debug => "[build_dispatch_table_from_config]"
);
my $dispatch_builder = Dancer2::RPCPlugin::DispatchFromConfig->new(
plugin_object => $self,
plugin => $self->rpcplugin_tag,
config => $settings,
endpoint => $endpoint,
);
return $dispatch_builder->build_dispatch_table();
};
}
elsif ($publish eq 'pod') {
return sub {
$self->app->log(
debug => "[build_dispatch_table_from_pod]"
);
my $dispatch_builder = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $self,
plugin => $self->rpcplugin_tag,
packages => $arguments,
endpoint => $endpoint,
);
return $dispatch_builder->build_dispatch_table();
};
}
return $publish;
}
sub partial_method_lister {
my $self = shift;
$self->validate_parameters(
{
$self->parameter(protocol => $self->Required, {store => \my $protocol}),
$self->parameter(endpoint => $self->Required, {store => \my $endpoint}),
$self->parameter(methods => $self->Required, {store => \my $methods}),
},
{ @_ }
);
my $lister = Dancer2::RPCPlugin::DispatchMethodList->new();
$lister->set_partial(
protocol => $protocol,
endpoint => $endpoint,
methods => $methods,
);
return $lister;
}
sub code_wrapper {
my $self = shift;
$self->validate_positional_parameters(
[ $self->parameter(config => $self->Required, {store => \my $config}) ],
\@_
lib/Dancer2/RPCPlugin.pm view on Meta::CPAN
=head1 DESCRIPTION
=head2 dispatch_builder(%parameters)
=head3 Parameters
Positional:
=over
=item 1. endpoint
=item 2. publish
=item 3. arguments (list of packages for POD-publishing)
=item 4. settings (config->{plugins}{RPC::proto})
=back
=head2 rpcplugin_tag
lib/Dancer2/RPCPlugin.pm view on Meta::CPAN
Setup the structure for listing the rpc-methods that should be in the dispatch-table.
=head3 Arguments
Named:
=over
=item protocol => $plugin-name
=item endpoint => $endpoint
=item methods => $list_of_methodnames
=back
=head2 code_wrapper
Returns a CodeRef that will be used in the execution of the remote procedure call.
=head3 Arguments
lib/Dancer2/RPCPlugin/DefaultRoute.pm view on Meta::CPAN
Dancer2::RPCPlugin::DefaultRoute - Catch bad-requests and send error-response
=head1 SYNOPSIS
use Dancer2::RPCPlugin::DefaultRoute;
setup_default_route();
=head1 DESCRIPTION
Implements default endpoint to generate -32601 'method not found'
or 'path not found' error_response for non existing endpoints
=head2 setup_default_route
Installs a Dancer route-handler for C<< any qr{.+} >> which tries to return an
appropriate error response to the requestor.
=head3 Responses
All responeses will have B<status: 200 OK>
lib/Dancer2/RPCPlugin/DispatchFromConfig.pm view on Meta::CPAN
has plugin => (
is => 'ro',
isa => sub { $_[0] =~ qr/^(?:jsonrpc|restrpc|xmlrpc)$/ },
required => 1,
);
has config => (
is => 'ro',
isa => sub { ref($_[0]) eq 'HASH' },
required => 1,
);
has endpoint => (
is => 'ro',
isa => sub { $_[0] && !ref($_[0]) },
required => 1,
);
sub build_dispatch_table {
my $self = shift;
my $app = $self->plugin_object->app;
my $config = $self->config->{ $self->endpoint };
my @packages = keys %$config;
my $dispatch;
for my $package (@packages) {
eval "require $package";
if (my $error = $@) {
$app->log(error => "Cannot load '$package': $error");
die "Cannot load $package ($error) in build_dispatch_table_from_config\n";
}
my @rpc_methods = keys %{ $config->{$package} };
for my $rpc_method (@rpc_methods) {
my $subname = $config->{$package}{$rpc_method};
$app->log(
debug => "[bdfc] @{[$self->endpoint]}: $rpc_method => $subname"
);
if (my $handler = $package->can($subname)) {
$dispatch->{$rpc_method} = Dancer2::RPCPlugin::DispatchItem->new(
package => $package,
code => $handler
);
}
else {
die "Handler not found for $rpc_method: $package\::$subname doesn't seem to exist.\n";
}
lib/Dancer2/RPCPlugin/DispatchFromConfig.pm view on Meta::CPAN
Named, list:
=over
=item plugin_object => $plugin
=item plugin => <xmlrpc|jsonrpc|jsonrpc>
=item config => $config_from_plugin
=item endpoint => $endpoint
=back
=head3 Responses
An instantiated object.
=head2 $dtb->build_dispatch_table()
=head3 Parameters
lib/Dancer2/RPCPlugin/DispatchFromPod.pm view on Meta::CPAN
has plugin => (
is => 'ro',
isa => sub { $_[0] =~ qr/^(?:jsonrpc|restrpc|xmlrpc)$/ },
required => 1,
);
has packages => (
is => 'ro',
isa => sub { ref($_[0]) eq 'ARRAY' },
required => 1,
);
has endpoint => (
is => 'ro',
isa => sub { $_[0] && !ref($_[0]) },
required => 1,
);
sub build_dispatch_table {
my $self = shift;
my $app = $self->plugin_object->app;
my $pp = Pod::Simple::PullParser->new();
lib/Dancer2/RPCPlugin/DispatchFromPod.pm view on Meta::CPAN
while (my $token = $p->get_token) {
next if not ($token->is_start && $token->is_tag('for'));
my $label = $token->attr('target');
my $ntoken = $p->get_token;
while (!$ntoken->can('text')) { $ntoken = $p->get_token; }
$app->log(debug => "=for-token $label => ", $ntoken->text);
my ($if_name, $code_name, $ep_name) = split " ", $ntoken->text;
$ep_name //= $self->endpoint;
if (!$code_name) {
$app->log(
error => sprintf(
"[build_dispatcher] POD error $label => %s <=> %s in %s line %u",
$if_name // '>rpcmethod-name-missing<',
'>sub-name-missing<',
$pkg_file,
$token->attr('start_line')
),
);
next;
}
$app->log(debug => "[build_dispatcher] $args{package}\::$code_name => $if_name ($ep_name)");
next if $ep_name ne $self->endpoint;
my $pkg = $args{package};
if (my $handler = $pkg->can($code_name)) {
$dispatch->{$if_name} = Dancer2::RPCPlugin::DispatchItem->new(
package => $pkg,
code => $handler
);
} else {
die "Handler not found for $if_name: $pkg\::$code_name doesn't seem to exist.\n";
}
lib/Dancer2/RPCPlugin/DispatchFromPod.pm view on Meta::CPAN
=head3 Parameters
=over
=item plugin_object => An instance of the current plugin
=item plugin => <jsonrpc|restrpc|xmlrpc>
=item packages => a list (ArrayRef) of package names to be parsed
=item endpoint => $endpoint
=back
=head2 $dfp->build_dispatch_table()
=head3 Parameters
None
=head3 Responses
lib/Dancer2/RPCPlugin/DispatchMethodList.pm view on Meta::CPAN
Dancer2::RPCPlugin::DispatchMethodList - Class for maintaining a global methodlist.
=head1 SYNOPSIS
use Dancer2::RPCPlugin::DispatchMethodList;
my $methods = Dancer2::RPCPlugin::DispatchMethodList->new();
$methods->set_partial(
protocol => <jsonrpc|restrpc|xmlrpc>,
endpoint => </configured>,
methods => [ @method_names ],
);
# Somewhere else
my $dml = Dancer2::RPCPlugin::DispatchMethodList->new();
my $methods = $dml->list_methods(<any|jsonrpc|restrpc|xmlrpc>);
=head1 DESCRIPTION
This class implements a singleton that can hold the collection of all method names.
lib/Dancer2/RPCPlugin/DispatchMethodList.pm view on Meta::CPAN
=head2 $dml->set_partial(%parameters)
=head3 Parameters
Named, list:
=over
=item protocol => <jsonrpc|restrpc|xmlrpc>
=item endpoint => $endpoint
=item methods => \@method_list
=back
=head3 Responses
$self
=cut
sub set_partial {
my $self = shift;
$self->validate_parameters(
{
$self->parameter(protocol => $self->Required, {store => \my $protocol}),
$self->parameter(endpoint => $self->Required, {store => \my $endpoint}),
$self->parameter(methods => $self->Required, {store => \my $methods}),
},
{ @_ }
);
$self->protocol->{$protocol}{$endpoint} = $methods;
return $self;
}
=head2 $dml->list_methods(@parameters)
Method that returns information about the dispatch-table.
=head3 Parameters
Positional, list:
lib/Dancer2/RPCPlugin/DispatchMethodList.pm view on Meta::CPAN
=item $protocol => undef || <any|jsonrpc|restrpc|xmlrpc>
=back
=head3 Responses
In case of no C<$protocol>:
{
xmlrpc => {
$endpoint1 => [ list ],
$endpoint2 => [ list ],
},
jsonrpc => {
$endpoint1 => [ list ],
$endpoint2 => [ list ],
},
}
In case of specified C<$protocol>:
{
$endpoint1 => [ list ],
$endpoint2 => [ list ],
}
=cut
sub list_methods {
my $self = shift;
$self->validate_positional_parameters(
[ $self->parameter(any_protocol => $self->Optional, {store => \my $protocol}) ],
[ @_ ]
);
lib/Dancer2/RPCPlugin/ValidationTemplates.pm view on Meta::CPAN
my $plugin_config = Dict [
publish => Types::Standard::Optional [ Maybe [$publisher] ],
arguments => Types::Standard::Optional [ Maybe [ArrayRef] ],
callback => Types::Standard::Optional [CodeRef],
code_wrapper => Types::Standard::Optional [CodeRef],
plugin_args => Types::Standard::Optional [HashRef],
];
my $plugins = Dancer2::RPCPlugin::PluginNames->new->regex;
my $any_plugin = qr{(?:any|$plugins)};
return {
endpoint => { type => StrMatch [qr{^ [\w/\\%-]+ $}x] },
publish => {
type => Maybe [$publisher],
default => 'config'
},
arguments => { type => Maybe [ArrayRef] },
settings => { type => Maybe [HashRef] },
protocol => { type => StrMatch [$plugins] },
any_protocol => { type => StrMatch [$any_plugin] },
methods => { type => ArrayRef [ StrMatch [qr{ . }x] ] },
config => { type => $plugin_config },
t/010-dispatch-from-config.t view on Meta::CPAN
);
my $plugin = Test::MockObject->new->set_always(
app => $app,
);
{
note('Working dispatch table from configuration');
my $builder = Dancer2::RPCPlugin::DispatchFromConfig->new(
plugin_object => $plugin,
plugin => 'xmlrpc',
endpoint => '/xmlrpc',
config => {
'/xmlrpc' => {
'MyAppCode' => {
'system.ping' => 'do_ping',
'system.version' => 'do_version',
}
}
}
);
isa_ok($builder, 'Dancer2::RPCPlugin::DispatchFromConfig', "Builder")
t/010-dispatch-from-config.t view on Meta::CPAN
}
{
note('Adding non existing code, fails');
like(
exception {
(
my $builder = Dancer2::RPCPlugin::DispatchFromConfig->new(
plugin_object => $plugin,
plugin => 'xmlrpc',
endpoint => '/xmlrpc',
config => {
'/xmlrpc' => {
'MyAppCode' => {
'system.nonexistent' => 'nonexistent',
}
}
},
)
)->build_dispatch_table();
},
t/010-dispatch-from-config.t view on Meta::CPAN
}
{
note('Adding non existing package, fails');
like(
exception {
(
my $builder = Dancer2::RPCPlugin::DispatchFromConfig->new(
plugin_object => $plugin,
plugin => 'xmlrpc',
endpoint => '/xmlrpc',
config => {
'/xmlrpc' => {
'MyNotExistingApp' => {
'system.nonexistent' => 'nonexistent',
}
}
},
)
)->build_dispatch_table();
},
t/015-dispatch-from-pod.t view on Meta::CPAN
app => $app,
);
{
note('Working dispatch table from POD');
my $builder = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => [qw/ MyAppCode /],
endpoint => '/testing',
);
isa_ok($builder, 'Dancer2::RPCPlugin::DispatchFromPod', 'Builder')
or diag("\$builder isa: ", ref $builder);
my $dispatch = $builder->build_dispatch_table();
is_deeply(
$dispatch,
{
'ping' => Dancer2::RPCPlugin::DispatchItem->new(
code => MyAppCode->can('do_ping'),
package => 'MyAppCode',
t/015-dispatch-from-pod.t view on Meta::CPAN
{
note('Adding non existing code, fails');
like(
exception {
(
my $builder = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => [qw/ MyBogusApp /],
endpoint => '/testing',
)
)->build_dispatch_table();
},
qr/Handler not found for bogus.nonexistent: MyBogusApp::nonexistent doesn't seem to exist/,
"Setting a non-existent dispatch target throws an exception"
);
}
{
note('Adding non existing package, fails');
like(
exception {
(
my $builder = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => [qw/ MyNotExistingApp /],
endpoint => '/testing',
)
)->build_dispatch_table();
},
qr/Cannot load MyNotExistingApp .+ in build_dispatch_table_from_pod/s,
"Using a non existing package throws an exception"
);
}
{
note('POD error in =for json');
$logfile = "";
like(
exception {
(
my $builder = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => [qw/ MyPoderrorApp /],
endpoint => '/testing',
)
)->build_dispatch_table();
},
qr/Handler not found for method: MyPoderrorApp::code doesn't seem to exist/,
"Ignore syntax-error in '=for jsonrpc/xmlrpc'"
);
like(
$logfile,
qr/^error .+ >rpcmethod-name-missing< <=> >sub-name-missing</m,
"error log-message method and sub missing"
t/015-dispatch-from-pod.t view on Meta::CPAN
qr/^error .+ <=> >sub-name-missing</m,
"error log-message sub missing"
);
}
{
my $xmlrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'xmlrpc',
packages => [qw/ MixedEndpoints /],
endpoint => '/system',
)->build_dispatch_table();
my $system_call = Dancer2::RPCPlugin::DispatchItem->new(
package => 'MixedEndpoints',
code => MixedEndpoints->can('call_for_system'),
);
my $any_call = Dancer2::RPCPlugin::DispatchItem->new(
package => 'MixedEndpoints',
code => MixedEndpoints->can('call_for_all_endpoints'),
);
is_deeply(
$xmlrpc,
{
'system.call' => $system_call,
'any.call' => $any_call,
},
"picked the /system call for xmlrpc"
) or diag(explain($xmlrpc));
my $jsonrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => [ 'MixedEndpoints' ],
endpoint => '/system',
)->build_dispatch_table();
is_deeply(
$jsonrpc,
{
'system_call' => $system_call,
'any_call' => $any_call,
},
"picked the /system call for jsonrpc"
) or diag(explain($jsonrpc));
my $restrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'restrpc',
packages => ['MixedEndpoints'],
endpoint => '/system',
)->build_dispatch_table();
is_deeply(
$restrpc,
{
'call' => $system_call,
'any-call' => $any_call,
},
"picked the /system call for restrpc"
) or diag(explain($restrpc));
}
{
my $xmlrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'xmlrpc',
packages => ['MixedEndpoints'],
endpoint => '/testing',
)->build_dispatch_table();
my $testing_call = Dancer2::RPCPlugin::DispatchItem->new(
package => 'MixedEndpoints',
code => MixedEndpoints->can('call_for_testing'),
);
my $any_call = Dancer2::RPCPlugin::DispatchItem->new(
package => 'MixedEndpoints',
code => MixedEndpoints->can('call_for_all_endpoints'),
);
is_deeply(
$xmlrpc,
{
'testing.call' => $testing_call,
'any.call' => $any_call,
},
"picked the /testing call for xmlrpc"
) or diag(explain($xmlrpc));
my $jsonrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'jsonrpc',
packages => ['MixedEndpoints'],
endpoint => '/testing',
)->build_dispatch_table();
is_deeply(
$jsonrpc,
{
'testing_call' => $testing_call,
'any_call' => $any_call,
},
"picked the /testing call for jsonrpc"
) or diag(explain($jsonrpc));
my $restrpc = Dancer2::RPCPlugin::DispatchFromPod->new(
plugin_object => $plugin,
plugin => 'restrpc',
packages => ['MixedEndpoints'],
endpoint => '/testing',
)->build_dispatch_table();
is_deeply(
$restrpc,
{
'call' => $testing_call,
'any-call' => $any_call,
},
"picked the /testing call for restrpc"
) or diag(explain($restrpc));
}
t/030-dispatchmethodlist.t view on Meta::CPAN
use t::Test::abeltje;
use Dancer2::RPCPlugin::DispatchMethodList;
note('Instantiate');
{
my $dml = Dancer2::RPCPlugin::DispatchMethodList->new();
isa_ok($dml, 'Dancer2::RPCPlugin::DispatchMethodList');
my $methods = {
jsonrpc => { '/endpoint_j' => [qw/ method1 method2 /] },
xmlrpc => { '/endpoint_x' => [qw/ method3 method4 /] },
};
for my $rpc (keys %$methods) {
for my $ep (keys %{$methods->{$rpc}}) {
$dml->set_partial(
protocol => $rpc,
endpoint => $ep,
methods => $methods->{$rpc}{$ep}
);
}
}
is_deeply(
$dml->list_methods('any'),
$methods,
"all methods (any)"
);
t/030-dispatchmethodlist.t view on Meta::CPAN
"list_methods(xmlrpc)"
);
}
note('Instantiate again');
{
my $dml = Dancer2::RPCPlugin::DispatchMethodList->new();
isa_ok($dml, 'Dancer2::RPCPlugin::DispatchMethodList');
my $methods = {
jsonrpc => { '/endpoint_j' => [qw/ method1 method2 /] },
xmlrpc => { '/endpoint_x' => [qw/ method3 method4 /] },
};
is_deeply(
$dml->list_methods('any'),
$methods,
"all methods (any)"
);
is_deeply(
$dml->list_methods('jsonrpc'),
t/071-default-route-xmlrpc.t view on Meta::CPAN
use Dancer2 qw/:syntax !pass !warning/;
use Plack::Test;
use Dancer2::RPCPlugin::DefaultRoute;
use Dancer2::Plugin::RPC::XMLRPC;
use RPC::XML;
use RPC::XML::ParserFactory;
$RPC::XML::ENCODING = 'utf-8';
my $ENDPOINT = '/system/code_wrapper';
my $UNKNOWN_ENDPOINT = '/system/code_wrapper/undefined_endpoint';
use MyTest::API;
use MyTest::Client;
my $client = MyTest::Client->new(ping_value => 'pong');
my $dispatch = {
'MyTest::API' => MyTest::API->new(test_client => $client),
};
my $config = {
publish => 'config',
t/071-default-route-xmlrpc.t view on Meta::CPAN
'RPC::XMLRPC' => {
$ENDPOINT => {
'MyTest::API' => {
'system_ping' => 'rpc_ping',
'system.exception' => 'rpc_fail',
}
}
}
}
);
set( clients => { test_client => { endpoint => 'somewhere' } });
xmlrpc $ENDPOINT => $config;
my $app = main->to_app();
my $tester = Plack::Test->create($app);
note("Without catchall unknown endpoint errors");
{
my $prefix = "Without catchall";
my $response = _post($ENDPOINT);
is($response->{status}, '200', "$prefix: Known endpoint returns 200 status");
is_deeply(
$response->{content},
[ { result => 'pong' } ],
"$prefix: Known route returns result"
);
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 404, "$prefix: unknown endpoint returns 404 status");
$response = _post($ENDPOINT, { method => 'system.pong'} );
is($response->{status}, 404, "$prefix: Unknown method returns 404 status");
}
setup_default_route();
note('With catchall unknown endpoint errors');
{
my $prefix = "With catchall";
my $response = _post($ENDPOINT);
is($response->{status}, 200, "$prefix: known endpoint returns 200 status");
is_deeply(
$response->{content},
[ { result => 'pong'} ],
"$prefix: Known route returns result"
);
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 200, "$prefix: Unknown route returns 200 status");
is_deeply(
t/071-default-route-xmlrpc.t view on Meta::CPAN
like(
$response->{content}{faultString},
qr/Method '.*' not found/,
sprintf("RPC::XMLRPC: %s - %s", $prefix, $response->{content}{faultCode}),
);
}
abeltje_done_testing();
sub _post {
my ($endpoint, $body) = @_;
my $parser = RPC::XML::ParserFactory->new();
$body //= { method => 'system_ping' };
my $xmlrpc_request = RPC::XML::request->new($body->{method})->as_string();
my $request = HTTP::Request->new(
POST => $endpoint,
[ 'content-type' => 'text/xml' ],
$xmlrpc_request,
);
my $response = $tester->request($request);
my $dancer_response = {
content_type => $response->header('content_type'),
status => $response->code,
content => $response->content,
};
t/072-default-route-jsonrpc.t view on Meta::CPAN
#! perl -I. -w
use t::Test::abeltje;
use Dancer2 qw/!pass !warning/;
use Plack::Test;
use Dancer2::RPCPlugin::DefaultRoute;
use Dancer2::Plugin::RPC::JSONRPC;
my $ENDPOINT = '/system/code_wrapper';
my $UNKNOWN_ENDPOINT = '/system/code_wrapper/undefined_endpoint';
use MyTest::API;
use MyTest::Client;
my $client = MyTest::Client->new(ping_value => 'pong');
my $dispatch = {
'MyTest::API' => MyTest::API->new(test_client => $client),
};
my $config = {
publish => 'config',
t/072-default-route-jsonrpc.t view on Meta::CPAN
'RPC::JSONRPC' => {
$ENDPOINT => {
'MyTest::API' => {
'system_ping' => 'rpc_ping',
'system.exception' => 'rpc_fail',
}
}
}
}
);
set( clients => { test_client => { endpoint => 'somewhere' } });
jsonrpc $ENDPOINT => $config;
my $app = main->to_app();
my $tester = Plack::Test->create($app);
note("Without catchall unknown endpoint errors");
{
my $prefix = "Without catchall";
my $response = _post($ENDPOINT);
is($response->{status}, 200, "$prefix: Known endpoint returns 200 status");
is_deeply(
$response->{content}{result},
[ { result => 'pong' } ],
"$prefix: Known route returns result"
);
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 404, "$prefix: unknown endpoint returns 404 status");
$response = _post(
$ENDPOINT,
{ jsonrpc => "2.0", id => 4242, method => 'system_pong'}
);
is($response->{status}, 200, "$prefix: Unknown method returns 200 status");
is(
$response->{content}{error}{code},
-32601,
"$prefix: Unknown method returns -32601 code"
);
}
setup_default_route();
note('With catchall unknown endpoint errors');
{
my $prefix = "With catchall";
my $response = _post($ENDPOINT);
is($response->{status}, 200, "$prefix: known endpoint returns 200 status");
is_deeply(
$response->{content}{result},
[ { result => 'pong'} ],
"$prefix: Known route returns result"
);
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 200, "$prefix: Unknown route returns 200 status");
is_deeply(
t/072-default-route-jsonrpc.t view on Meta::CPAN
like(
$response->{content}{error}{message},
qr/Method '.*' not found/,
sprintf("RPC::JSONRPC: %s - %s", $prefix, $response->{content}{error}{message}),
);
}
abeltje_done_testing();
sub _post {
my ($endpoint, $body) = @_;
$body //= { jsonrpc => "2.0", id => "42", method => 'system_ping' };
my $jsonrpc_request = to_json($body);
my $request = HTTP::Request->new(
POST => $endpoint,
[ content_type => 'application/json' ],
$jsonrpc_request,
);
my $response = $tester->request($request);
my $dancer_response = {
content_type => $response->header('content_type'),
status => $response->code,
content => $response->content,
};
t/073-default-route-restrpc.t view on Meta::CPAN
#! perl -I. -w
use t::Test::abeltje;
use Dancer2 qw/!pass !warning/;
use Dancer2::RPCPlugin::DefaultRoute;
use Dancer2::Plugin::RPC::RESTRPC;
use Plack::Test;
my $ENDPOINT = '/system/code_wrapper';
my $UNKNOWN_ENDPOINT = '/system/code_wrapper/undefined_endpoint';
use MyTest::API;
use MyTest::Client;
my $client = MyTest::Client->new(ping_value => 'pong');
my $dispatch = {
'MyTest::API' => MyTest::API->new(test_client => $client),
};
my $config = {
t/073-default-route-restrpc.t view on Meta::CPAN
'RPC::RESTRPC' => {
$ENDPOINT => {
'MyTest::API' => {
'system_ping' => 'rpc_ping',
'system.exception' => 'rpc_fail',
}
}
}
}
);
set( clients => { test_client => { endpoint => 'somewhere' } });
restrpc $ENDPOINT => $config;
my $app = main->to_app();
my $tester = Plack::Test->create($app);
note("Without catchall unknown endpoint errors");
{
my $prefix = "Without catchall";
my $response = _post($ENDPOINT);
is($response->{status}, 200, "$prefix: Known endpoint returns 200 status");
is_deeply(
$response->{content},
[ { result => 'pong' } ],
"$prefix: Known route returns result"
);
my $url = $UNKNOWN_ENDPOINT.'/system_ping';
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 404, "$prefix: unknown endpoint returns 404 status");
$response = _post($ENDPOINT, { method => 'system.pong'} );
$url = $ENDPOINT.'/system_pong';
is($response->{status}, 404, "$prefix: Unknown method returns 404 status");
}
setup_default_route();
note('With catchall unknown endpoint errors');
{
my $prefix = "With catchall";
my $response = _post($ENDPOINT);
is($response->{status}, 200, "$prefix: known endpoint returns 200 status");
is_deeply(
$response->{content},
[ {result => 'pong'} ],
"$prefix: Known route returns result"
);
my $url = $UNKNOWN_ENDPOINT.'/system_ping';
$response = _post($UNKNOWN_ENDPOINT);
is($response->{status}, 200, "$prefix: Unknown route returns 200 status");
t/073-default-route-restrpc.t view on Meta::CPAN
like(
$error->{message},
qr/Method '.*' not found/,
sprintf("RPC::RESTRPC: %s - %s", $prefix, $error->{message})
);
}
abeltje_done_testing();
sub _post {
my ($endpoint, $body) = @_;
my $url = sprintf("%s/%s", $endpoint, defined $body ? $body->{method} : 'system_ping');
my $request = HTTP::Request->new(
POST => $url,
[ content_type => 'application/json' ],
to_json( {} ),
);
my $response = $tester->request($request);
my $dancer_response = {
content_type => $response->header('content_type'),
status => $response->code,
t/090-rpcplugin-role.t view on Meta::CPAN
);
is(MyConsumer::RESTRPC->rpcplugin_tag, 'restrpc', "CLASS->rpcplugin_tag()");
is($tst->rpcplugin_tag, 'restrpc', "INSTANCE->rpcplugin_tag()");
}
{
note('Create builder from config');
my $tst = MyConsumer::RESTRPC->new();
isa_ok($tst, 'MyConsumer::RESTRPC');
my $builder = $tst->dispatch_builder(
'/endpoint',
undef,
undef,
{'/endpoint' => {'MyTestConfig' => {method1 => 'sub1'}}}
);
isa_ok($builder, 'CODE');
my $dispatch = $builder->();
is_deeply(
$dispatch,
{
'method1' => Dancer2::RPCPlugin::DispatchItem->new(
code => \&MyTestConfig::sub1,
package => 'MyTestConfig',
),
},
"Dispatch from Config"
);
}
{
note('Create builder from POD');
my $tst = MyConsumer::RESTRPC->new();
isa_ok($tst, 'MyConsumer::RESTRPC');
my $builder = $tst->dispatch_builder(
'/endpoint',
'pod',
['MyTestPod'],
);
isa_ok($builder, 'CODE');
my $dispatch = $builder->();
is_deeply(
$dispatch,
{
'method2' => Dancer2::RPCPlugin::DispatchItem->new(
code => \&MyTestPod::sub2,
t/090-rpcplugin-role.t view on Meta::CPAN
},
"Dispatch from Pod"
) or diag(explain($dispatch));
}
{
note('Dispatch from code');
my $tst = MyConsumer::RESTRPC->new();
isa_ok($tst, 'MyConsumer::RESTRPC');
my $builder = $tst->dispatch_builder(
'/endpoint',
sub {
return {
method1 => Dancer2::RPCPlugin::DispatchItem->new(
code => \&MyTestConfig::sub1,
package => 'MyTestConfig',
),
}
}
);
isa_ok($builder, 'CODE');
t/100-xmlrpc.t view on Meta::CPAN
use HTTP::Request;
use RPC::XML::ParserFactory;
my $p = RPC::XML::ParserFactory->new();
my $app = MyXMLRPCApp->to_app();
my $tester = Plack::Test->create($app);
subtest "XMLRPC ping (POST)" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
[ 'Content-Type' => 'text/xml' ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
my $value = $p->parse($response->decoded_content)->value->value;
is_deeply(
$value,
'pong',
"ping"
) or diag(explain($value));
};
subtest "XMLRPC ping (GET)" => sub {
my $request = HTTP::Request->new(
GET => '/endpoint',
[ 'Content-Type' => 'text/xml' ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
is($response->status_line, "404 Not Found", "Check method POST for xmlrpc");
};
subtest "XMLRPC wrong content-type (404)" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
[ 'Content-Type' => 'application/json', ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
is($response->status_line, "404 Not Found", "Check content-type xmlrpc")
or diag(explain($response));
};
subtest "XMLRPC unknown rpc-method (404)" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
['Content-Type' => 'text/xml'],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>system.doesnotexist</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
is($response->status_line, '404 Not Found', "Check known rpc-methods");
};
subtest "XMLRPC methodList(plugin => 'xmlrpc')" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
['Content-Type' => 'text/xml'],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>methodList</methodName>
<params>
<param>
<struct>
<member>
<name>plugin</name>
t/100-xmlrpc.t view on Meta::CPAN
</methodCall>
EOXML
);
my $response = $tester->request($request);
is($response->status_line, '200 OK', "OK response");
my $methods = $p->parse($response->decoded_content)->value->value;
is_deeply(
$methods,
{
'/endpoint' => [qw/
methodList
ping
version
/]
},
"methodList(plugin => 'xmlrpc')"
) or diag(explain($methods));
};
abeltje_done_testing();
BEGIN {
package MyXMLRPCApp;
use lib 'ex/';
use Dancer2;
use Dancer2::Plugin::RPC::XMLRPC;
BEGIN { set(log => 'error') }
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
};
1;
}
t/110-xmlrpc-callbackfail.t view on Meta::CPAN
use HTTP::Request;
use RPC::XML::ParserFactory;
my $p = RPC::XML::ParserFactory->new();
my $app = MyXMLRPCAppCallbackFail->to_app();
my $tester = Plack::Test->create($app);
subtest "XMLRPC Callback::Fail" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
[ 'Content-Type' => 'text/xml' ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
t/110-xmlrpc-callbackfail.t view on Meta::CPAN
abeltje_done_testing();
BEGIN {
package MyXMLRPCAppCallbackFail;
use lib 'ex/';
use Dancer2;
use Dancer2::Plugin::RPC::XMLRPC;
use Dancer2::RPCPlugin::CallbackResultFactory;
BEGIN { set(log => 'error') }
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
callback => sub {
return callback_fail(
error_code => -32760,
error_message => "Callback failed",
);
},
};
1;
t/115-xmlrpc-callback-die.t view on Meta::CPAN
use HTTP::Request;
use RPC::XML::ParserFactory;
my $p = RPC::XML::ParserFactory->new();
my $app = MyXMLRPCAppCallbackFail->to_app();
my $tester = Plack::Test->create($app);
subtest "XMLRPC Callback::Fail" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
[ 'Content-Type' => 'text/xml' ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
t/115-xmlrpc-callback-die.t view on Meta::CPAN
abeltje_done_testing();
BEGIN {
package MyXMLRPCAppCallbackFail;
use lib 'ex/';
use Dancer2;
use Dancer2::Plugin::RPC::XMLRPC;
use Dancer2::RPCPlugin::CallbackResultFactory;
BEGIN { set(log => 'error') }
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
callback => sub {
die "Callback die()s\n";
},
};
1;
}
t/120-xmlrpc-codewrapper-die.t view on Meta::CPAN
use HTTP::Request;
use RPC::XML::ParserFactory;
my $p = RPC::XML::ParserFactory->new();
my $app = MyXMLRPCAppCallbackFail->to_app();
my $tester = Plack::Test->create($app);
subtest "XMLRPC CodeWrapper" => sub {
my $request = HTTP::Request->new(
POST => '/endpoint',
[ 'Content-Type' => 'text/xml' ],
<<' EOXML',
<?xml version="1.0"?>
<methodCall>
<methodName>ping</methodName>
<params/>
</methodCall>
EOXML
);
my $response = $tester->request($request);
t/120-xmlrpc-codewrapper-die.t view on Meta::CPAN
abeltje_done_testing();
BEGIN {
package MyXMLRPCAppCallbackFail;
use lib 'ex/';
use Dancer2;
use Dancer2::Plugin::RPC::XMLRPC;
use Dancer2::RPCPlugin::CallbackResultFactory;
BEGIN { set(log => 'error') }
xmlrpc '/endpoint' => {
publish => 'pod',
arguments => [qw/ MyAppCode /],
code_wrapper => sub {
die "Codewrapper die()s\n";
}
};
1;
}