Skip to content
17 changes: 17 additions & 0 deletions lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package WebAPI::DBIC::Resource::GenericSetInvoke;

=head1 NAME

WebAPI::DBIC::Resource::GenericSetInvoke - a set of roles to implement a resource for making method calls on a DBIC item

=cut

use Moo;
use namespace::clean;

extends 'WebAPI::DBIC::Resource::GenericCore';
with 'WebAPI::DBIC::Resource::Role::Set',
'WebAPI::DBIC::Resource::Role::SetInvoke',
;

1;
7 changes: 6 additions & 1 deletion lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ sub render_api_as_hal {
}
next unless @parts;

my $title = join(" ", (split /::/, $route->defaults->{result_class})[-3,-1]);
my $title;
if( exists $route->defaults->{result_class}) {
$title = join(" ", (split /::/, $route->defaults->{result_class})[-3,-1]);
} else {
($title) = split( /\?/, $route->path);
}

my $url = $path . join("", @parts);
$links{join("", @parts)} = {
Expand Down
3 changes: 2 additions & 1 deletion lib/WebAPI/DBIC/Resource/Role/DBICParams.pm
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ sub handle_request_params {

my $method = "_handle_${param}_param";
unless ($self->can($method)) {
die "The $param parameter is not supported by the $self resource\n";
warn "The $param parameter is not supported by the $self resource\n";
next;
}
$self->$method($value, $param);
}
Expand Down
84 changes: 84 additions & 0 deletions lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package WebAPI::DBIC::Resource::Role::SetInvoke;

=head1 NAME

WebAPI::DBIC::Resource::Role::SetInvoke - methods for resources representing method calls on item resources

=cut

use Scalar::Util qw(blessed);

use Moo::Role;


requires 'decode_json';
requires 'encode_json';
requires 'render_item_as_plain_hash';
requires 'throwable';
requires 'set';

has method => (
is => 'ro',
required => 1,
);

sub post_is_create { return 0 }

around 'allowed_methods' => sub {
return [ qw(POST) ];
};


sub process_post {
my $self = shift;

# Here's we're calling a method on the set as a simple generic behaviour.
# This is very limited because, for example, the method has no knowledge
# that it's being called inside a web service, thus no way to do redirects
# or provide HTTP specific rich-exceptions.
# If anything more sophisticated is required then it should be implemented
# as a specific resource class for the method (or perhaps a role if there's
# a set of methods that require similar behaviour).

# The POST body content provides a data structure containing the method arguments
# { args => [ (@_) ] }
$self->throwable->throw_bad_request(415, errors => "Request content-type not application/json")
unless $self->request->header('Content-Type') =~ 'application/.*?json';
my $invoke_body_data = $self->decode_json($self->request->content);
$self->throwable->throw_bad_request(400, errors => "Request content not a JSON hash")
unless ref $invoke_body_data eq 'HASH';

my @method_args;
if (my $args = delete $invoke_body_data->{args}) {
$self->throwable->throw_bad_request(400, errors => "The args must be an array")
if ref $args ne 'ARRAY';
@method_args = @$args;
}
$self->throwable->throw_bad_request(400, errors => "Unknown attributes: @{[ keys %$invoke_body_data ]}")
if keys %$invoke_body_data;

my $method_name = $self->method;
# the method is expected to throw an exception on error.
my $result_raw = $self->set->$method_name(@method_args);

my $result_rendered;
# return a DBIC resultset as array of hashes of ALL records (no paging)
if (blessed($result_raw) && $result_raw->isa('DBIx::Class::ResultSet')) {
$result_rendered = [ map { $self->render_item_as_plain_hash($_) } $result_raw->all ];
}
# return a DBIC result row as a hash
elsif (blessed($result_raw) && $result_raw->isa('DBIx::Class::Row')) {
$result_rendered = $self->render_item_as_plain_hash($result_raw);
}
# return anything else as raw JSON wrapped in a hash
else {
# we shouldn't get an object here, but if we do then we
# stringify it here to avoid exposing the guts
$result_rendered = { result => (blessed $result_raw) ? "$result_raw" : $result_raw };
}

$self->response->body( $self->encode_json($result_rendered) );
return 200;
}

1;
34 changes: 30 additions & 4 deletions lib/WebAPI/DBIC/RouteMaker.pm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ has resource_class_for_item_invoke => (is => 'ro', default => 'WebAPI::DBIC::Res
has resource_class_for_set => (is => 'ro', default => 'WebAPI::DBIC::Resource::GenericSet');
has resource_class_for_set_invoke => (is => 'ro', default => 'WebAPI::DBIC::Resource::GenericSetInvoke');
has resource_default_args => (is => 'ro', default => sub { {} });
has resource_extra_roles => (is => 'ro', default => sub { [] });

has type_namer => (
is => 'ro',
Expand Down Expand Up @@ -88,11 +89,16 @@ sub make_routes_for_item {
# and .../:1/:2/:3 etc for a resource with multiple key fields
my $item_path_spec = join "/", map { ":$_" } 1 .. @$key_fields;

my $resource_class_for_item =
$self->adapt_resource_class($self->resource_class_for_item);
my $resource_class_for_item_invoke =
$self->adapt_resource_class($self->resource_class_for_item_invoke);

my @routes;

push @routes, WebAPI::DBIC::Route->new( # item
path => "$path/$item_path_spec",
resource_class => $self->resource_class_for_item,
resource_class => $resource_class_for_item,
resource_args => {
%{ $self->resource_default_args },
set => $set,
Expand All @@ -107,7 +113,7 @@ sub make_routes_for_item {
push @routes, WebAPI::DBIC::Route->new( # method call on item
path => "$path/$item_path_spec/invoke/:method",
validations => { method => _qr_names(@$methods), },
resource_class => $self->resource_class_for_item_invoke,
resource_class => $resource_class_for_item_invoke,
resource_args => {
%{ $self->resource_default_args },
set => $set,
Expand All @@ -124,11 +130,16 @@ sub make_routes_for_set {
$opts ||= {};
my $methods = $opts->{invokable_methods};

my $resource_class_for_set =
$self->adapt_resource_class($self->resource_class_for_set);
my $resource_class_for_set_invoke =
$self->adapt_resource_class($self->resource_class_for_set_invoke);

my @routes;

push @routes, WebAPI::DBIC::Route->new(
path => $path,
resource_class => $self->resource_class_for_set,
resource_class => $resource_class_for_set,
resource_args => {
%{ $self->resource_default_args },
set => $set,
Expand All @@ -139,7 +150,7 @@ sub make_routes_for_set {
push @routes, WebAPI::DBIC::Route->new( # method call on set
path => "$path/invoke/:method",
validations => { method => _qr_names(@$methods) },
resource_class => $self->resource_class_for_set_invoke,
resource_class => $resource_class_for_set_invoke,
resource_args => {
%{ $self->resource_default_args },
set => $set,
Expand All @@ -150,6 +161,21 @@ sub make_routes_for_set {
return @routes;
}

sub adapt_resource_class {
my ($self, $resource_class) = @_;

if(@{ $self->resource_extra_roles }) {
$resource_class = Role::Tiny->create_class_with_roles(
$resource_class,
@{ $self->resource_extra_roles }
);
## Workaround Role::Tiny not setting %INC, which confuses use_module later.
$INC{Module::Runtime::module_notional_filename($resource_class)} = __FILE__;

}

return $resource_class;
}

sub make_root_route {
my $self = shift;
Expand Down
10 changes: 10 additions & 0 deletions lib/WebAPI/DBIC/Router.pm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ has router => (
handles => [ qw(match) ],
);

has extra_routes => (
is => 'ro',
default => sub { [] },
);


sub add_route {
my ($self, %args) = @_;
Expand All @@ -49,6 +54,11 @@ sub add_route {

sub to_psgi_app {
my $self = shift;

foreach my $route (@{ $self->extra_routes }) {
$self->router->add_route( @$route );
}

return Plack::App::Path::Router->new( router => $self->router )->to_app; # return Plack app
}

Expand Down
13 changes: 12 additions & 1 deletion lib/WebAPI/DBIC/WebApp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ which is the same as:
$app = WebAPI::DBIC::WebApp->new({
schema => $schema,
routes => [ $schema->sources ],
extra_routes => [ ],
route_maker => WebAPI::DBIC::RouteMaker->new(
resource_class_for_item => 'WebAPI::DBIC::Resource::GenericItem',
resource_class_for_item_invoke => 'WebAPI::DBIC::Resource::GenericItemInvoke',
resource_class_for_set => 'WebAPI::DBIC::Resource::GenericSet',
resource_class_for_set_invoke => 'WebAPI::DBIC::Resource::GenericSetInvoke',
resource_default_args => { },
resource_extra_roles => [ ],
type_namer => WebAPI::DBIC::TypeNamer->new( # EXPERIMENTAL
type_name_inflect => 'singular', # XXX will change to plural soon
type_name_style => 'under_score', # or 'camelCase' etc
Expand All @@ -41,6 +43,10 @@ The elements in C<routes> are passed to the specified C<route_maker>.
The elements can include any mix of result source names, as in the example above,
resultset objects, and L<WebAPI::DBIC::Route> objects.

The extra_routes arrayref is passed on to L<WebAPI::DBIC::Router>
which will add them to the L<Path::Router> instance as standalone
routes alongside the DBIx::Class based routes.

Result source names are converted to resultset objects.

The L<WebAPI::DBIC::RouteMaker> object converts the resultset objects
Expand Down Expand Up @@ -79,6 +85,11 @@ has routes => (
default => sub { [ sort shift->schema->sources ] },
);

has extra_routes => (
is => 'ro',
default => sub { [] },
);

has route_maker => (
is => 'ro',
lazy => 1,
Expand All @@ -99,7 +110,7 @@ sub _build_route_maker {
sub to_psgi_app {
my ($self) = @_;

my $router = WebAPI::DBIC::Router->new; # XXX
my $router = WebAPI::DBIC::Router->new(extra_routes => $self->extra_routes);

# set the route_maker schema here so users don't have
# to set schema in both WebApp and RouteMaker
Expand Down