Make tests pass for new definitive chaining
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API.pm
index c173dcd..e257ed1 100644 (file)
@@ -56,23 +56,23 @@ __PACKAGE__->config();
   # /api/rpc/artist/id/[id]/update
 =cut
 
-=method_protected begin
+=method_private begin
 
  :Private
 
-A begin method is provided to apply the L<Catalyst::Controller::DBIC::API::Request> role to $c->request, and perform deserialization and validation of request parameters
+begin is provided in the base class to setup the Catalyst Request object, by applying the DBIC::API::Request role.
 
 =cut
 
 sub begin :Private
 {
     my ($self, $c) = @_;
-    
+
     Catalyst::Controller::DBIC::API::Request->meta->apply($c->req)
         unless Moose::Util::does_role($c->req, 'Catalyst::Controller::DBIC::API::Request');
-    $c->forward('deserialize');
 }
 
+
 =method_protected setup
 
  :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config')
@@ -80,7 +80,8 @@ sub begin :Private
 This action is the chain root of the controller. It must either be overridden or configured to provide a base pathpart to the action and also a parent action. For example, for class MyAppDB::Track you might have
 
   package MyApp::Controller::API::RPC::Track;
-  use base qw/Catalyst::Controller::DBIC::API::RPC/;
+  use Moose; 
+  BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC'; }
 
   __PACKAGE__->config
     ( action => { setup => { PathPart => 'track', Chained => '/api/rpc/rpc_base' } }, 
@@ -95,103 +96,16 @@ This action is the chain root of the controller. It must either be overridden or
     $self->next::method($c);
   }
 
-This action will populate $c->req->current_result_set with $self->stored_result_source->resultset for other actions in the chain to use.
-
-=cut
-
-sub setup :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config')
-{
-    my ($self, $c) = @_;
-
-    $c->req->_set_current_result_set($self->stored_result_source->resultset);
-}
-
-=method_protected object
-
- :Chained('setup') :CaptureArgs(1) :PathPart('')
-
-This action is the chain root for all object level actions (such as delete and update). If an identifier is passed it will be used to find that particular object and add it to the request's store of objects. Otherwise, the data stored at the data_root of the request_data will be interpreted as an array of objects on which to operate. If the hashes are missing an 'id' key, they will be considered a new object to be created, otherwise, the values in the hash will be used to perform an update. Please see L<Catalyst::Controller::DBIC::API::Context> for more details on the stored objects.
-
-=cut
-
-sub object :Chained('setup') :CaptureArgs(1) :PathPart('')
-{
-       my ($self, $c, $id) = @_;
-
-    my $vals = $c->req->request_data->{$self->data_root};
-    unless(defined($vals))
-    {
-        # no data root, assume the request_data itself is the payload
-        $vals = [$c->req->request_data];
-    }
-    elsif(reftype($vals) eq 'HASH')
-    {
-        $vals = [ $vals ];
-    }
-
-    if(defined($id))
-    {
-        try
-        {
-            # there can be only one set of data
-            $c->req->add_object([$self->object_lookup($c, $id), $vals->[0]]);
-        }
-        catch
-        {
-            $c->log->error($_);
-            $self->push_error($c, { message => $_ });
-            $c->detach();
-        }
-    }
-    else
-    {
-        unless(reftype($vals) eq 'ARRAY')
-        {
-            $c->log->error('Invalid request data');
-            $self->push_error($c, { message => 'Invalid request data' });
-            $c->detach();
-        }
-
-        foreach my $val (@$vals)
-        {
-            unless(exists($val->{id}))
-            {
-                $c->req->add_object([$c->req->current_result_set->new_result({}), $val]);
-                next;
-            }
-
-            try
-            {
-                $c->req->add_object([$self->object_lookup($c, $val->{id}), $val]);
-            }
-            catch
-            {
-                $c->log->error($_);
-                $self->push_error($c, { message => $_ });
-                $c->detach();
-            }
-        }
-    }
-}
-
-=method_protected object_lookup
-
-This method provides the look up functionality for an object based on 'id'. It is passed the current $c and the $id to be used to perform the lookup. Dies if there is no provided $id or if no object was found.
+This action does nothing by default.
 
 =cut
 
-sub object_lookup
-{
-    my ($self, $c, $id) = @_;
-
-    die 'No valid ID provided for look up' unless defined $id and length $id;
-    my $object = $c->req->current_result_set->find($id);
-    die "No object found for id '$id'" unless defined $object;
-    return $object;
-}
+sub setup :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config') {}
 
 =method_protected deserialize
 
+ :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('Deserialize')
+
 deserialize absorbs the request data and transforms it into useful bits by using CGI::Expand->expand_hash and a smattering of JSON::Any->from_json for a handful of arguments. Current only the following arguments are capable of being expressed as JSON:
 
     search_arg
@@ -201,11 +115,11 @@ deserialize absorbs the request data and transforms it into useful bits by using
     grouped_by_arg
     prefetch_arg
 
-It should be noted that arguments can used mixed modes in with some caveats. Each top level arg can be expressed as CGI::Expand with their immediate child keys expressed as JSON.
+It should be noted that arguments can used mixed modes in with some caveats. Each top level arg can be expressed as CGI::Expand with their immediate child keys expressed as JSON when sending the data application/x-www-form-urlencoded. Otherwise, you can send content as raw json and it will be deserialized as is with no CGI::Expand expasion.
 
 =cut
 
-sub deserialize :ActionClass('Deserialize')
+sub deserialize :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('Deserialize')
 {
     my ($self, $c) = @_;
     my $req_params;
@@ -260,7 +174,7 @@ sub deserialize :ActionClass('Deserialize')
 }
 
 =method_protected inflate_request
-
 inflate_request is called at the end of deserialize to populate key portions of the request with the useful bits
 
 =cut
@@ -276,6 +190,9 @@ sub inflate_request
 
         # set request arguments
         $c->req->_set_request_data($params);
+
+        # set the current resultset
+        $c->req->_set_current_result_set($self->stored_result_source->resultset);
         
     }
     catch
@@ -284,7 +201,117 @@ sub inflate_request
         $self->push_error($c, { message => $_ });
         $c->detach();
     }
+}
+
+=method_protected object_with_id
+
+ :Chained('deserialize') :CaptureArgs(1) :PathPart('')
+
+This action is the chain root for all object level actions (such as delete and update) that operate on a single identifer. The provided identifier will be used to find that particular object and add it to the request's store of objects. Please see L<Catalyst::Controller::DBIC::API::Context> for more details on the stored objects.
+
+=cut
+
+sub object_with_id :Chained('deserialize') :CaptureArgs(1) :PathPart('')
+{
+       my ($self, $c, $id) = @_;
+
+    my $vals = $c->req->request_data->{$self->data_root};
+    unless(defined($vals))
+    {
+        # no data root, assume the request_data itself is the payload
+        $vals = $c->req->request_data;
+    }
+
+    try
+    {
+        # there can be only one set of data
+        $c->req->add_object([$self->object_lookup($c, $id), $vals]);
+    }
+    catch
+    {
+        $c->log->error($_);
+        $self->push_error($c, { message => $_ });
+        $c->detach();
+    }
+}
+
+=method_protected objects_no_id
+
+ :Chained('deserialize') :CaptureArgs(0) :PathPart('')
+
+This action is the chain root for object level actions (such as create, update, or delete) that can involve more than one object. The data stored at the data_root of the request_data will be interpreted as an array of hashes on which to operate. If the hashes are missing an 'id' key, they will be considered a new object to be created. Otherwise, the values in the hash will be used to perform an update. As a special case, a single hash sent will be coerced into an array. Please see L<Catalyst::Controller::DBIC::API::Context> for more details on the stored objects.
+
+=cut
+
+sub objects_no_id :Chained('deserialize') :CaptureArgs(0) :PathPart('')
+{
+    my ($self, $c) = @_;
     
+    if($c->req->has_request_data)
+    {
+        my $data = $c->req->request_data;
+        my $vals;
+        
+        if(exists($data->{$self->data_root}) && defined($data->{$self->data_root}))
+        {
+            my $root = $data->{$self->data_root};
+            if(reftype($root) eq 'ARRAY')
+            {
+                $vals = $root;
+            }
+            elsif(reftype($root) eq 'HASH')
+            {
+                $vals = [$root];
+            }
+            else
+            {
+                $c->log->error('Invalid request data');
+                $self->push_error($c, { message => 'Invalid request data' });
+                $c->detach();
+            }
+        }
+        else
+        {
+            # no data root, assume the request_data itself is the payload
+            $vals = [$c->req->request_data];
+        }
+
+        foreach my $val (@$vals)
+        {
+            unless(exists($val->{id}))
+            {
+                $c->req->add_object([$c->req->current_result_set->new_result({}), $val]);
+                next;
+            }
+
+            try
+            {
+                $c->req->add_object([$self->object_lookup($c, $val->{id}), $val]);
+            }
+            catch
+            {
+                $c->log->error($_);
+                $self->push_error($c, { message => $_ });
+                $c->detach();
+            }
+        }
+    }
+}
+
+=method_protected object_lookup
+
+This method provides the look up functionality for an object based on 'id'. It is passed the current $c and the $id to be used to perform the lookup. Dies if there is no provided $id or if no object was found.
+
+=cut
+
+sub object_lookup
+{
+    my ($self, $c, $id) = @_;
+
+    die 'No valid ID provided for look up' unless defined $id and length $id;
+    my $object = $c->req->current_result_set->find($id);
+    die "No object found for id '$id'" unless defined $object;
+    return $object;
 }
 
 =method_protected list
@@ -330,6 +357,9 @@ sub list :Private
     $self->list_munge_parameters($c);
     $self->list_perform_search($c);
     $self->list_format_output($c);
+
+    # make sure there are no objects lingering
+    $c->req->clear_objects(); 
 }
 
 =method_protected list_munge_parameters
@@ -425,6 +455,31 @@ sub row_format_output
     return $row; # passthrough by default
 }
 
+=method_protected item
+ :Private
+
+item will return a single object called by identifier in the uri. It will be inflated via each_object_inflate.
+
+=cut
+
+sub item :Private 
+{
+    my ($self, $c) = @_;
+
+    if($c->req->count_objects != 1)
+    {
+        $c->log->error($_);
+        $self->push_error($c, { message => 'No objects on which to operate' });
+        $c->detach();
+    }
+    else
+    {
+        $c->stash->{response}->{$self->item_root} = $self->each_object_inflate($c, $c->req->get_object(0)->[0]);
+    }
+}
+
+
 =method_protected update_or_create
 
  :Private
@@ -990,8 +1045,6 @@ For example if you wanted create to return the JSON for the newly created object
 
 It should be noted that the return_object attribute will produce the above result for you, free of charge.
 
-For REST the only difference besides the class names would be that create should be :Private rather than an endpoint.
-
 Similarly you might want create, update and delete to all forward to the list action once they are done so you can refresh your view. This should also be simple enough.
 
 If more extensive customization is required, it is recommened to peer into the roles that comprise the system and make use