From c9b6a1e574ff724c25ec9af6f2dd16e20c768fb6 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 26 Jan 2015 11:05:57 +0000 Subject: [PATCH 1/8] Add initial versions of missing invoke method on set ability --- lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm | 17 ++++ lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm | 84 ++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 lib/WebAPI/DBIC/Resource/GenericSetInvoke.pm create mode 100644 lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm 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/Role/SetInvoke.pm b/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm new file mode 100644 index 0000000..27d5084 --- /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 item 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->item->$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; From 0f18a6d98095182b788c3e7ff5afc98631a23db1 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 29 Jan 2015 12:26:46 +0000 Subject: [PATCH 2/8] Call SetInvoke on correct object --- lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm b/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm index 27d5084..eb88fc9 100644 --- a/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm +++ b/lib/WebAPI/DBIC/Resource/Role/SetInvoke.pm @@ -32,7 +32,7 @@ around 'allowed_methods' => sub { sub process_post { my $self = shift; - # Here's we're calling a method on the item as a simple generic behaviour. + # 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. @@ -59,7 +59,7 @@ sub process_post { my $method_name = $self->method; # the method is expected to throw an exception on error. - my $result_raw = $self->item->$method_name(@method_args); + 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) From 9e3ab2a689e8090ef808133e1cabc6270a39e89a Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 26 Jan 2015 10:59:44 +0000 Subject: [PATCH 3/8] Refactor RouteMaker to allow caller to add extra Roles to the resource classes --- lib/WebAPI/DBIC/RouteMaker.pm | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) 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; From 05018c7daa541a545b346126b936467096db7ecf Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 26 Jan 2015 11:01:26 +0000 Subject: [PATCH 4/8] Enable other Roles to use some of the query params DBICParams steals all the params, make it just skip/ignore unknown ones so that other roles can use them --- lib/WebAPI/DBIC/Resource/Role/DBICParams.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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); } From 0d1eff5ae93e155c1f7804744b4226e5885248cd Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 28 Jan 2015 09:56:50 +0000 Subject: [PATCH 5/8] Allow users of WebAPI-DBIC to add their own routes to the pile --- lib/WebAPI/DBIC/Router.pm | 10 ++++++++++ lib/WebAPI/DBIC/WebApp.pm | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) 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..3466e98 100644 --- a/lib/WebAPI/DBIC/WebApp.pm +++ b/lib/WebAPI/DBIC/WebApp.pm @@ -79,6 +79,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 +104,12 @@ sub _build_route_maker { sub to_psgi_app { my ($self) = @_; - my $router = WebAPI::DBIC::Router->new; # XXX + # ensure our extra routes at least have a copy of the schema object + my $extra_routes = [ map { + my $path = shift @$_; + [$path, (defaults => { schema => $self->schema }, @$_)] + } @{ $self->extra_routes } ]; + my $router = WebAPI::DBIC::Router->new(extra_routes => $extra_routes); # set the route_maker schema here so users don't have # to set schema in both WebApp and RouteMaker From ef26e4be770667ee259463b2807a15a96806bf74 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 28 Jan 2015 10:48:20 +0000 Subject: [PATCH 6/8] Make sure we can display external routes in the hal browser --- lib/WebAPI/DBIC/Resource/HAL/Role/Root.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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)} = { From 9ce0decf016d24a092895d9997701e7d9844f326 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 28 Jan 2015 15:04:17 +0000 Subject: [PATCH 7/8] Remove extra dance supplying schema to extra routes as defaults Turns out we can supply it as resource_args from the .psgi more sanely --- lib/WebAPI/DBIC/WebApp.pm | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/WebAPI/DBIC/WebApp.pm b/lib/WebAPI/DBIC/WebApp.pm index 3466e98..2c8962a 100644 --- a/lib/WebAPI/DBIC/WebApp.pm +++ b/lib/WebAPI/DBIC/WebApp.pm @@ -104,12 +104,7 @@ sub _build_route_maker { sub to_psgi_app { my ($self) = @_; - # ensure our extra routes at least have a copy of the schema object - my $extra_routes = [ map { - my $path = shift @$_; - [$path, (defaults => { schema => $self->schema }, @$_)] - } @{ $self->extra_routes } ]; - my $router = WebAPI::DBIC::Router->new(extra_routes => $extra_routes); + 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 From 11ebda92f09a5798f3982516aa669ad1dc2bec9d Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 29 Jan 2015 12:26:04 +0000 Subject: [PATCH 8/8] Document new extra_routes / roles options --- lib/WebAPI/DBIC/WebApp.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/WebAPI/DBIC/WebApp.pm b/lib/WebAPI/DBIC/WebApp.pm index 2c8962a..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