diff --git a/lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm b/lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm new file mode 100644 index 0000000..4c1638f --- /dev/null +++ b/lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm @@ -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; diff --git a/lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm b/lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm index b9f5ddf..c231b4f 100644 --- a/lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm +++ b/lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm @@ -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)} = { diff --git a/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm b/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm index ecaa12e..3032338 100644 --- a/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm +++ b/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm @@ -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); } diff --git a/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm b/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm new file mode 100644 index 0000000..eb88fc9 --- /dev/null +++ b/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm @@ -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; diff --git a/lib/WebAPI/DBIC/RouteMaker.pm b/lib/WebAPI/DBIC/RouteMaker.pm index fc37b3f..839dd4d 100644 --- a/lib/WebAPI/DBIC/RouteMaker.pm +++ b/lib/WebAPI/DBIC/RouteMaker.pm @@ -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', @@ -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, @@ -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, @@ -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, @@ -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, @@ -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; diff --git a/lib/WebAPI/DBIC/Router.pm b/lib/WebAPI/DBIC/Router.pm index fd550c4..2311d2e 100644 --- a/lib/WebAPI/DBIC/Router.pm +++ b/lib/WebAPI/DBIC/Router.pm @@ -29,6 +29,11 @@ has router => ( handles => [ qw(match) ], ); +has extra_routes => ( + is => 'ro', + default => sub { [] }, +); + sub add_route { my ($self, %args) = @_; @@ -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 } diff --git a/lib/WebAPI/DBIC/WebApp.pm b/lib/WebAPI/DBIC/WebApp.pm index 84d6652..bf06204 100644 --- a/lib/WebAPI/DBIC/WebApp.pm +++ b/lib/WebAPI/DBIC/WebApp.pm @@ -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 @@ -41,6 +43,10 @@ The elements in C are passed to the specified C. The elements can include any mix of result source names, as in the example above, resultset objects, and L objects. +The extra_routes arrayref is passed on to L +which will add them to the L instance as standalone +routes alongside the DBIx::Class based routes. + Result source names are converted to resultset objects. The L object converts the resultset objects @@ -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, @@ -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