Fix generate_rs to set model to contents of stash->{class} if present and add tests.
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API.pm
index 2a72e28..b595fb4 100644 (file)
@@ -2,11 +2,11 @@ package Catalyst::Controller::DBIC::API;
 
 #ABSTRACT: Provides a DBIx::Class web service automagically
 use Moose;
-BEGIN { extends 'Catalyst::Controller::ActionRole'; }
+BEGIN { extends 'Catalyst::Controller'; }
 
 use CGI::Expand ();
 use DBIx::Class::ResultClass::HashRefInflator;
-use JSON::Any;
+use JSON ();
 use Test::Deep::NoTest('eq_deeply');
 use MooseX::Types::Moose(':all');
 use Moose::Util;
@@ -15,9 +15,21 @@ use Try::Tiny;
 use Catalyst::Controller::DBIC::API::Request;
 use namespace::autoclean;
 
+has '_json' => (
+    is => 'ro',
+    isa => 'JSON',
+    lazy_build => 1,
+);
+
+sub _build__json {
+    # no ->utf8 here because the request params get decoded by Catalyst
+    return JSON->new;
+}
+
 with 'Catalyst::Controller::DBIC::API::StoredResultSource',
-     'Catalyst::Controller::DBIC::API::StaticArguments',
-     'Catalyst::Controller::DBIC::API::RequestArguments' => { static => 1 };
+     'Catalyst::Controller::DBIC::API::StaticArguments';
+
+with 'Catalyst::Controller::DBIC::API::RequestArguments' => { static => 1 };
 
 __PACKAGE__->config();
 
@@ -30,7 +42,7 @@ __PACKAGE__->config();
   __PACKAGE__->config
     ( action => { setup => { PathPart => 'artist', Chained => '/api/rpc/rpc_base' } }, # define parent chain action and partpath
       class            => 'MyAppDB::Artist',
-      result_class     => 'MyAppDB::ResultSet::Artist',
+      resultset_class  => 'MyAppDB::ResultSet::Artist',
       create_requires  => ['name', 'age'],
       create_allows    => ['nickname'],
       update_allows    => ['name', 'age', 'nickname'],
@@ -69,8 +81,7 @@ 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');
+    Moose::Util::ensure_all_roles($c->req, 'Catalyst::Controller::DBIC::API::Request');
 }
 
 =method_protected setup
@@ -106,7 +117,7 @@ sub setup :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('spec
 
  :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:
+deserialize absorbs the request data and transforms it into useful bits by using CGI::Expand->expand_hash and a smattering of JSON->decode for a handful of arguments. Current only the following arguments are capable of being expressed as JSON:
 
     search_arg
     count_arg
@@ -142,9 +153,12 @@ sub deserialize :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('De
             {
                 for my $key ( keys %{$req_params->{$param}} )
                 {
+                    # copy the value because JSON::XS will alter it
+                    # even if decoding failed
+                    my $value = $req_params->{$param}->{$key};
                     try
                     {
-                        my $deserialized = JSON::Any->from_json($req_params->{$param}->{$key});
+                        my $deserialized = $self->_json->decode($value);
                         $req_params->{$param}->{$key} = $deserialized;
                     }
                     catch
@@ -158,7 +172,8 @@ sub deserialize :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('De
             {
                 try
                 {
-                    my $deserialized = JSON::Any->from_json($req_params->{$param});
+                    my $value = $req_params->{$param};
+                    my $deserialized = $self->_json->decode($value);
                     $req_params->{$param} = $deserialized;
                 }
                 catch
@@ -175,16 +190,19 @@ sub deserialize :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('De
 
 =method_protected generate_rs
 
-generate_rs is used by inflate_request to generate the resultset stored in the current request. It receives $c as its only argument. And by default it merely returns the resultset from the stored_result_source on the controller. Override this method if you need to manipulate the default implementation of getting the resultset from the controller.
+generate_rs is used by inflate_request to get a resultset for the current
+request. It receives $c as its only argument.
+By default it returns a resultset of the controller's class.
+Override this method if you need to manipulate the default implementation of
+getting a resultset.
 
 =cut
 
 sub generate_rs
 {
-    #my ($self, $c) = @_;
-    my ($self) = @_;
+    my ($self, $c) = @_;
 
-    return $self->stored_result_source->resultset;
+    return $c->model($self->class || $c->stash->{class});
 }
 
 =method_protected inflate_request
@@ -639,7 +657,7 @@ sub validate_object
             }
 
             # check for multiple values
-            if (ref($value) && !(reftype($value) eq reftype(JSON::Any::true)))
+            if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
             {
                 require Data::Dumper;
                 die "Multiple values for '${key}': ${\Data::Dumper::Dumper($value)}";
@@ -734,7 +752,7 @@ sub update_object_from_params
     foreach my $key (keys %$params)
     {
         my $value = $params->{$key};
-        if (ref($value) && !(reftype($value) eq reftype(JSON::Any::true)))
+        if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
         {
             $self->update_object_relation($c, $object, delete $params->{$key}, $key);
         }
@@ -766,7 +784,7 @@ sub update_object_relation
     if ($row) {
         foreach my $key (keys %$related_params) {
             my $value = $related_params->{$key};
-            if (ref($value) && !(reftype($value) eq reftype(JSON::Any::true)))
+            if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
             {
                 $self->update_object_relation($c, $row, delete $related_params->{$key}, $key);
             }
@@ -799,12 +817,18 @@ sub insert_object_from_params
     my ($self, undef, $object, $params) = @_;
 
     my %rels;
-    while (my ($k, $v) = each %{ $params }) {
-        if (ref($v) && !(reftype($v) eq reftype(JSON::Any::true))) {
-            $rels{$k} = $v;
+    while (my ($key, $value) = each %{ $params }) {
+        if (ref($value) && !(reftype($value) eq reftype(JSON::true))) {
+            $rels{$key} = $value;
         }
+        # accessor = colname
+        elsif ($object->can($key)) {
+            $object->$key($value);
+        }
+        # accessor != colname
         else {
-            $object->set_column($k => $v);
+            my $accessor = $object->result_source->column_info($key)->{accessor};
+            $object->$accessor($value);
         }
     }
 
@@ -852,34 +876,32 @@ sub end :Private
 {
     my ($self, $c) = @_;
 
-    # check for errors
-    my $default_status;
-
-    # Check for errors caught elsewhere
-    if ( $c->res->status and $c->res->status != 200 ) {
-        $default_status = $c->res->status;
-        $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::Any::false : 'false';
-    } elsif ($self->get_errors($c)) {
-        $c->stash->{$self->stash_key}->{messages} = $self->get_errors($c);
-        $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::Any::false : 'false';
-        $default_status = 400;
-    } else {
-        $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::Any::true : 'true';
-        $default_status = 200;
+    # don't change the http status code if already set elsewhere
+    unless ($c->res->status && $c->res->status != 200) {
+        if ($self->has_errors($c)) {
+            $c->res->status(400);
+        }
+        else {
+            $c->res->status(200);
+        }
     }
 
-    unless ($default_status == 200)
-    {
-        delete $c->stash->{$self->stash_key}->{$self->data_root};
+    if ($c->res->status == 200) {
+        $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::true : 'true';
+        if($self->return_object && $c->req->has_objects) {
+            my $returned_objects = [];
+            push(@$returned_objects, $self->each_object_inflate($c, $_)) for map { $_->[0] } $c->req->all_objects;
+            $c->stash->{$self->stash_key}->{$self->data_root} = scalar(@$returned_objects) > 1 ? $returned_objects : $returned_objects->[0];
+        }
     }
-    elsif($self->return_object && $c->req->has_objects)
-    {
-        my $returned_objects = [];
-        push(@$returned_objects, $self->each_object_inflate($c, $_)) for map { $_->[0] } $c->req->all_objects;
-        $c->stash->{$self->stash_key}->{$self->data_root} = scalar(@$returned_objects) > 1 ? $returned_objects : $returned_objects->[0];
+    else {
+        $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::false : 'false';
+        $c->stash->{$self->stash_key}->{messages} = $self->get_errors($c)
+            if $self->has_errors($c);
+        # don't return data for error responses
+        delete $c->stash->{$self->stash_key}->{$self->data_root};
     }
 
-    $c->res->status( $default_status || 200 );
     $c->forward('serialize');
 }
 
@@ -917,6 +939,8 @@ push_error stores an error message into the stash to be later retrieved by L</en
 sub push_error
 {
     my ( $self, $c, $params ) = @_;
+    die 'Catalyst app object missing'
+        unless defined $c;
     my $error = 'unknown error';
     if (exists $params->{message}) {
         $error = $params->{message};
@@ -936,9 +960,24 @@ get_errors returns all of the errors stored in the stash
 sub get_errors
 {
     my ( $self, $c ) = @_;
+    die 'Catalyst app object missing'
+        unless defined $c;
     return $c->stash->{_dbic_crud_errors};
 }
 
+=method_protected has_errors
+
+returns returns true if errors are stored in the stash
+
+=cut
+
+sub has_errors {
+    my ( $self, $c ) = @_;
+    die 'Catalyst app object missing'
+        unless defined $c;
+    return exists $c->stash->{_dbic_crud_errors};
+}
+
 =head1 DESCRIPTION
 
 Easily provide common API endpoints based on your L<DBIx::Class> schema classes. Module provides both RPC and REST interfaces to base functionality. Uses L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize> to serialise response and/or deserialise request.
@@ -996,7 +1035,7 @@ By default, the response data is serialized into $c->stash->{$self->stash_key}->
 
 =head3 use_json_boolean
 
-By default, the response success status is set to a string value of "true" or "false". If this attribute is true, JSON::Any's true() and false() will be used instead. Note, this does not effect other internal processing of boolean values.
+By default, the response success status is set to a string value of "true" or "false". If this attribute is true, JSON's true() and false() will be used instead. Note, this does not effect other internal processing of boolean values.
 
 =head3 count_arg, page_arg, select_arg, search_arg, grouped_by_arg, ordered_by_arg, prefetch_arg, as_arg, total_entries_arg
 
@@ -1126,7 +1165,7 @@ If more extensive customization is required, it is recommened to peer into the r
 
 It should be noted that version 1.004 and above makes a rapid depature from the status quo. The internals were revamped to use more modern tools such as Moose and its role system to refactor functionality out into self-contained roles.
 
-To this end, internally, this module now understands JSON boolean values (as represented by JSON::Any) and will Do The Right Thing in handling those values. This means you can have ColumnInflators installed that can covert between JSON::Any booleans and whatever your database wants for boolean values.
+To this end, internally, this module now understands JSON boolean values (as represented by the JSON module) and will Do The Right Thing in handling those values. This means you can have ColumnInflators installed that can covert between JSON booleans and whatever your database wants for boolean values.
 
 Validation for various *_allows or *_exposes is now accomplished via Data::DPath::Validator with a lightly simplified, via subclass, Data::DPath::Validator::Visitor. The rough jist of the process goes as follows: Arguments provided to those attributes are fed into the Validator and Data::DPaths are generated. Then, incoming requests are validated against these paths generated. The validator is set in "loose" mode meaning only one path is required to match. For more information, please see L<Data::DPath::Validator> and more specifically L<Catalyst::Controller::DBIC::API::Validator>.