diff --git a/lib/NGCP/Panel/Controller/API/Contacts.pm b/lib/NGCP/Panel/Controller/API/Contacts.pm index 7204c42fce..4f4043fd58 100644 --- a/lib/NGCP/Panel/Controller/API/Contacts.pm +++ b/lib/NGCP/Panel/Controller/API/Contacts.pm @@ -6,9 +6,7 @@ use Data::HAL qw(); use Data::HAL::Link qw(); use Data::Record qw(); use HTTP::Headers qw(); -use HTTP::Headers::Util qw(split_header_words); use HTTP::Status qw(:constants); -#use JE qw(); use MooseX::ClassAttribute qw(class_has); use NGCP::Panel::Form::Contact::Admin qw(); use NGCP::Panel::Form::Contact::Reseller qw(); @@ -18,9 +16,7 @@ BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; -require Catalyst::ActionRole::QueryParameter; require Catalyst::ActionRole::RequireSSL; -require URI::QueryParam; with 'NGCP::Panel::Role::API'; @@ -36,40 +32,55 @@ __PACKAGE__->config( Does => [qw(ACL CheckTrailingSlash RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - QueryParam => '!id', } } @{ __PACKAGE__->allowed_methods } }, - action_roles => [qw(HTTPMethods QueryParameter)], + action_roles => [qw(HTTPMethods)], ); -sub GET : Allow { +sub GET :Allow { my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; { last if $self->cached($c); my $contacts = $c->model('DB')->resultset('contacts'); $self->last_modified($contacts->get_column('modify_timestamp')->max_rs->single->modify_timestamp); + my $total_count = int($contacts->count); + $contacts = $contacts->search(undef, { + page => $page, + rows => $rows, + }); my (@embedded, @links); for my $contact ($contacts->search({}, {order_by => {-asc => 'me.id'}, prefetch => ['reseller']})->all) { push @embedded, $self->hal_from_contact($contact); push @links, Data::HAL::Link->new( relation => 'ngcp:contacts', - href => sprintf('/api/contacts/?id=%d', $contact->id), + href => sprintf('/api/contacts/%d', $contact->id), ); } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => "/api/contacts/?page=$page&rows=$rows"); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => "/api/contacts/?page=".($page+1)."&rows=$rows"), + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => "/api/contacts/?page=".($page-1)."&rows=$rows"); + } + my $hal = Data::HAL->new( embedded => [@embedded], - links => [ - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => '/api/contacts/'), - @links, - ] + links => [@links], ); + $hal->resource({ + total_count => $total_count, + }); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( (map { # XXX Data::HAL must be able to generate links with multiple relations s|rel="(http://purl.org/sipwise/ngcp-api/#rel-contacts)"|rel="item $1"|; @@ -89,27 +100,27 @@ sub GET : Allow { return; } -sub HEAD : Allow { +sub HEAD :Allow { my ($self, $c) = @_; $c->forward(qw(GET)); $c->response->body(q()); return; } -sub OPTIONS : Allow { +sub OPTIONS :Allow { my ($self, $c) = @_; - my $allowed_methods = $self->allowed_methods->join(q(, )); + my $allowed_methods = $self->allowed_methods; $c->response->headers(HTTP::Headers->new( - Allow => $allowed_methods, + Allow => $allowed_methods->join(', '), Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-contacts', Content_Language => 'en', )); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/allowed_methods.tt', allowed_methods => $allowed_methods); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); return; } -sub POST : Allow { +sub POST :Allow { my ($self, $c) = @_; { @@ -152,24 +163,12 @@ sub POST : Allow { $c->cache->remove($c->request->uri->canonical->as_string); $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/api/contacts/?id=%d', $contact->id)); + $c->response->header(Location => sprintf('/api/contacts/%d', $contact->id)); $c->response->body(q()); } return; } -sub allowed_methods : Private { - my ($self) = @_; - my $meta = $self->meta; - my @allow; - for my $method ($meta->get_method_list) { - push @allow, $meta->get_method($method)->name - if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; - } - return [sort @allow]; -} - - sub hal_from_contact : Private { my ($self, $contact) = @_; # XXX invalid 00-00-00 dates @@ -187,11 +186,11 @@ sub hal_from_contact : Private { ), Data::HAL::Link->new(relation => 'collection', href => '/api/contacts/'), Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => "/api/contacts/?id=$id"), + Data::HAL::Link->new(relation => 'self', href => "/api/contacts/$id"), $contact->reseller ? Data::HAL::Link->new( relation => 'ngcp:resellers', - href => sprintf('/api/resellers/?id=%d', $contact->reseller_id), + href => sprintf('/api/resellers/%d', $contact->reseller_id), ) : (), ], relation => 'ngcp:contacts', diff --git a/lib/NGCP/Panel/Controller/API/ContactsItem.pm b/lib/NGCP/Panel/Controller/API/ContactsItem.pm index 32e0cd5f07..906379f8dc 100644 --- a/lib/NGCP/Panel/Controller/API/ContactsItem.pm +++ b/lib/NGCP/Panel/Controller/API/ContactsItem.pm @@ -9,59 +9,40 @@ use DateTime::Format::HTTP qw(); use DateTime::Format::RFC3339 qw(); use Digest::SHA3 qw(sha3_256_base64); use HTTP::Headers qw(); -use HTTP::Headers::Util qw(split_header_words); -use HTTP::Status qw( - HTTP_BAD_REQUEST - HTTP_NO_CONTENT - HTTP_NOT_FOUND - HTTP_NOT_MODIFIED - HTTP_OK - HTTP_PRECONDITION_FAILED - HTTP_PRECONDITION_REQUIRED - HTTP_UNPROCESSABLE_ENTITY - HTTP_UNSUPPORTED_MEDIA_TYPE -); -use JE qw(); +use HTTP::Status qw(:constants); use JSON qw(); -use JSON::Pointer qw(); use MooseX::ClassAttribute qw(class_has); use NGCP::Panel::Form::Contact::Reseller qw(); use NGCP::Panel::Utils::ValidateJSON qw(); use Path::Tiny qw(path); -use Regexp::Common qw(delimited); # $RE{delimited} use Safe::Isa qw($_isa); -use Types::Standard qw(InstanceOf); BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; -require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; -require Catalyst::ActionRole::QueryParameter; require Catalyst::ActionRole::RequireSSL; -require URI::QueryParam; + +with 'NGCP::Panel::Role::API'; class_has('dispatch_path', is => 'ro', default => '/api/contacts/'); class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-contacts'); -has('last_modified', is => 'rw', isa => InstanceOf['DateTime']); __PACKAGE__->config( action => { map { $_ => { ACLDetachTo => '/api/root/invalid_user', AllowedRole => 'api_admin', - Args => 0, - Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Args => 1, + Does => [qw(ACL RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - QueryParam => 'id', } } @{ __PACKAGE__->allowed_methods } }, - action_roles => [qw(HTTPMethods QueryParameter)], + action_roles => [qw(HTTPMethods)], ); -sub GET : Allow { - my ($self, $c) = @_; +sub GET :Allow { + my ($self, $c, $id) = @_; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last if $self->cached($c); my $contact = $self->contact_by_id($c, $id); @@ -86,15 +67,15 @@ sub GET : Allow { return; } -sub HEAD : Allow { - my ($self, $c) = @_; +sub HEAD :Allow { + my ($self, $c, $id) = @_; $c->forward(qw(GET)); $c->response->body(q()); return; } -sub OPTIONS : Allow { - my ($self, $c) = @_; +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; my $allowed_methods = $self->allowed_methods->join(q(, )); $c->response->headers(HTTP::Headers->new( Allow => $allowed_methods, @@ -106,12 +87,11 @@ sub OPTIONS : Allow { return; } -sub PATCH : Allow { - my ($self, $c) = @_; +sub PATCH :Allow { + my ($self, $c, $id) = @_; my $media_type = 'application/json-patch+json'; my $guard = $c->model('DB')->txn_scope_guard; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last unless $self->forbid_link_header($c); last unless $self->valid_media_type($c, $media_type); @@ -234,12 +214,11 @@ sub PATCH : Allow { return; } -sub PUT : Allow { - my ($self, $c) = @_; +sub PUT :Allow { + my ($self, $c, $id) = @_; my $media_type = 'application/hal+json'; my $guard = $c->model('DB')->txn_scope_guard; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last unless $self->forbid_link_header($c); last unless $self->valid_media_type($c, $media_type); @@ -357,99 +336,11 @@ sub PUT : Allow { return; } -sub allowed_methods : Private { - my ($self) = @_; - my $meta = $self->meta; - my @allow; - for my $method ($meta->get_method_list) { - push @allow, $meta->get_method($method)->name - if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; - } - return [sort @allow]; -} - -sub apply_patch : Private { - my ($self, $c, $entity, $json) = @_; - my $patch = JSON::decode_json($json); - for my $op (@{ $patch }) { - my $coderef = JSON::Pointer->can($op->{op}); - die 'invalid op despite schema validation' unless $coderef; - try { - for ($op->{op}) { - if ('add' eq $_ or 'replace' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); - } elsif ('remove' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{path}); - } elsif ('move' eq $_ or 'copy' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{from}, $op->{path}); - } elsif ('test' eq $_) { - die "test failed - path: $op->{path} value: $op->{value}\n" - unless $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); - } - } - } catch($e) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/unprocessable_entity.tt', error_message => $e); - return; - }; - } - return $entity; -} - -sub cached : Private { - my ($self, $c) = @_; - my $response = $c->cache->get($c->request->uri->canonical->as_string); - unless ($response) { - $c->log->info('not cached'); - return; - } - my $matched_tag = $c->request->header('If-None-Match') && ('*' eq $c->request->header('If-None-Match')) - || (grep {$response->header('ETag') eq $_} Data::Record->new({ - split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")}, - })->records($c->request->header('If-None-Match'))); - my $not_modified = $c->request->header('If-Modified-Since') - && !($self->last_modified < DateTime::Format::HTTP->parse_datetime($c->request->header('If-Modified-Since'))); - if ( - $matched_tag && $not_modified - || $matched_tag - || $not_modified - ) { - $c->response->status(HTTP_NOT_MODIFIED); - $c->response->headers($response->headers); - $c->log->info('cached'); - return 1; - } - $c->log->info('stale'); - return; -} - sub contact_by_id : Private { my ($self, $c, $id) = @_; return $c->model('DB')->resultset('contacts')->find({'me.id' => $id}, {prefetch => ['reseller']}); } -sub etag : Private { - my ($self, $octets) = @_; - return sprintf '"ni:/sha3-256;%s"', sha3_256_base64($octets); -} - -sub expires : Private { - my ($self) = @_; - return DateTime->now->clone->add(years => 1); # XXX insert product end-of-life -} - -sub forbid_link_header : Private { - my ($self, $c) = @_; - return 1 unless $c->request->header('Link'); - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/forbid_link_header.tt'); - return; -} - sub hal_from_contact : Private { my ($self, $contact) = @_; # XXX invalid 00-00-00 dates @@ -487,178 +378,15 @@ sub hal_from_contact : Private { return $hal; } -sub require_body : Private { - my ($self, $c) = @_; - return 1 if $c->request->body; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_body.tt'); - return; -} - -sub require_precondition : Private { - my ($self, $c, $header_name) = @_; - return 1 if $c->request->header($header_name); - $c->response->status(HTTP_PRECONDITION_REQUIRED); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_precondition.tt', header_name => $header_name); - return; -} - -sub require_preference : Private { - my ($self, $c) = @_; - my @preference = grep { 'return' eq $_->[0] } split_header_words($c->request->header('Prefer')); - return $preference[0][1] - if 1 == @preference && ('minimal' eq $preference[0][1] || 'representation' eq $preference[0][1]); - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_preference.tt'); - return; -} - -sub require_valid_patch : Private { - my ($self, $c, $json) = @_; - my $js - = path($c->path_to(qw(share static js api tv4.js)))->slurp - . "\nvar schema = " - . path($c->path_to(qw(share static js api json-patch.json)))->slurp - . ";\nvar data = " - . $json # code injection prevented by asserting well-formedness - . ";\ntv4.validate(data, schema);"; - my $je = JE->new; - unless ($je->eval($js)) { - die "generic JavaScript error: $@" if $@; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/valid_entity.tt', - media_type => 'application/json-patch+json', - error_message => JSON::to_json( - { map { $_ => $je->{tv4}{error}{$_}->value } qw(dataPath message schemaPath) }, - { canonical => 1, pretty => 1, } - ) - ); - return; - }; - return 1; -} - -sub require_wellformed_json : Private { - my ($self, $c, $media_type, $patch) = @_; - try { - NGCP::Panel::Utils::ValidateJSON->new($patch); - } catch($e) { - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/valid_entity.tt', media_type => $media_type, error_message => $e); - return; - }; - return 1; -} - -sub resource_exists : Private { - my ($self, $c, $entity_name, $resource) = @_; - return 1 if $resource; - $c->response->status(HTTP_NOT_FOUND); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/not_found.tt', entity_name => $entity_name); - return; -} - -sub valid_entity : Private { - my ($self, $c, $entity) = @_; - my $js - = path($c->path_to(qw(share static js api tv4.js)))->slurp - . "\nvar schema = " - . path($c->path_to(qw(share static js api properties contacts-item.json)))->slurp - . ";\nvar data = " - . JSON::to_json($entity, { canonical => 1, pretty => 1, utf8 => 1, }) - . ";\ntv4.validate(data, schema);"; - my $je = JE->new; - unless ($je->eval($js)) { - die "generic JavaScript error: $@" if $@; - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/unprocessable_entity.tt', - error_message => JSON::to_json( - { map { $_ => $je->{tv4}{error}{$_}->value } qw(dataPath message schemaPath) }, - { canonical => 1, pretty => 1, } - ) - ); - return; - } - return 1; -} - -sub valid_id : Private { - my ($self, $c, $id) = @_; - return 1 if $id->is_integer; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/invalid_query_parameter.tt', key => 'id'); - return; -} - -sub valid_media_type : Private { - my ($self, $c, $media_type) = @_; - return 1 if $c->request->header('Content-Type') && 0 == index $c->request->header('Content-Type'), $media_type; - $c->response->status(HTTP_UNSUPPORTED_MEDIA_TYPE); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/valid_media_type.tt', media_type => $media_type); - return; -} - -sub valid_precondition : Private { - my ($self, $c, $etag, $entity_name) = @_; - my $if_match = $c->request->header('If-Match'); - return 1 if '*' eq $if_match || grep {$etag eq $_} Data::Record->new({ - split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")}, - })->records($if_match); - $c->response->status(HTTP_PRECONDITION_FAILED); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/precondition_failed.tt', entity_name => $entity_name); - return; -} - sub end : Private { my ($self, $c) = @_; $c->forward(qw(Controller::Root render)); $c->response->content_type('') if $c->response->content_type =~ qr'text/html'; # stupid RenderView getting in the way -use Carp qw(longmess); use DateTime::Format::RFC3339 qw(); use Data::Dumper qw(Dumper); use Convert::Ascii85 qw(); if (@{ $c->error }) { - my $incident = DateTime->from_epoch(epoch => Time::HiRes::time); - my $incident_id = sprintf '%X', $incident->strftime('%s%N'); - my $incident_timestamp = DateTime::Format::RFC3339->new->format_datetime($incident); - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Useqq = 1; - local $Data::Dumper::Deparse = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - my $crash_state = join "\n", @{ $c->error }, longmess, Dumper($c), Dumper($c->config); - $c->log->error( - "Exception id $incident_id at $incident_timestamp crash_state:" . - ($crash_state ? ("\n" . $crash_state) : ' disabled') - ); + my $msg = join ', ', @{ $c->error }; + $c->log->error($msg); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); $c->clear_errors; - $c->stash( - exception_incident => $incident_id, - exception_timestamp => $incident_timestamp, - template => 'api/internal_server_error.tt' - ); - $c->response->status(500); - $c->response->content_type('application/xhtml+xml'); - $c->detach($c->view); } } diff --git a/lib/NGCP/Panel/Controller/API/Contracts.pm b/lib/NGCP/Panel/Controller/API/Contracts.pm index c0080ab161..a3a3d8d84a 100644 --- a/lib/NGCP/Panel/Controller/API/Contracts.pm +++ b/lib/NGCP/Panel/Controller/API/Contracts.pm @@ -9,7 +9,6 @@ use DateTime::Format::HTTP qw(); use DateTime::Format::RFC3339 qw(); use Digest::SHA3 qw(sha3_256_base64); use HTTP::Headers qw(); -use HTTP::Headers::Util qw(split_header_words); use HTTP::Status qw(:constants); use JSON qw(); use MooseX::ClassAttribute qw(class_has); @@ -23,7 +22,6 @@ require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; require Catalyst::ActionRole::QueryParameter; require Catalyst::ActionRole::RequireSSL; -require URI::QueryParam; with 'NGCP::Panel::Role::API'; @@ -39,40 +37,55 @@ __PACKAGE__->config( Does => [qw(ACL CheckTrailingSlash RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - QueryParam => '!id', } } @{ __PACKAGE__->allowed_methods } }, - action_roles => [qw(HTTPMethods QueryParameter)], + action_roles => [qw(HTTPMethods)], ); -sub GET : Allow { +sub GET :Allow :Args(0) { my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; { last if $self->cached($c); my $contracts = $c->model('DB')->resultset('contracts'); $self->last_modified($contracts->get_column('modify_timestamp')->max_rs->single->modify_timestamp); + my $total_count = int($contracts->count); + $contracts = $contracts->search(undef, { + page => $page, + rows => $rows, + }); my (@embedded, @links); for my $contract ($contracts->search({}, {order_by => {-asc => 'me.id'}, prefetch => ['contact']})->all) { push @embedded, $self->hal_from_contract($contract); push @links, Data::HAL::Link->new( relation => 'ngcp:contracts', - href => sprintf('/api/contracts/?id=%d', $contract->id), + href => sprintf('/api/contracts/%d', $contract->id), ); } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => "/api/contracts/?page=$page&rows=$rows"); + + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => "/api/contracts/?page=".($page+1)."&rows=$rows"), + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => "/api/contracts/?page=".($page-1)."&rows=$rows"); + } my $hal = Data::HAL->new( embedded => [@embedded], - links => [ - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => '/api/contracts/'), - @links, - ] + links => [@links], ); + $hal->resource({ + total_count => $total_count, + }); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( (map { # XXX Data::HAL must be able to generate links with multiple relations s|rel="(http://purl.org/sipwise/ngcp-api/#rel-contracts)"|rel="item $1"|; @@ -102,14 +115,14 @@ sub HEAD : Allow { sub OPTIONS : Allow { my ($self, $c) = @_; - my $allowed_methods = $self->allowed_methods->join(q(, )); + my $allowed_methods = $self->allowed_methods; $c->response->headers(HTTP::Headers->new( - Allow => $allowed_methods, + Allow => $allowed_methods->join(', '), Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-contracts', Content_Language => 'en', )); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/allowed_methods.tt', allowed_methods => $allowed_methods); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); return; } @@ -150,23 +163,12 @@ sub POST : Allow { $c->cache->remove($c->request->uri->canonical->as_string); $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/api/contracts/?id=%d', $contract->id)); + $c->response->header(Location => sprintf('/api/contracts/%d', $contract->id)); $c->response->body(q()); } return; } -sub allowed_methods : Private { - my ($self) = @_; - my $meta = $self->meta; - my @allow; - for my $method ($meta->get_method_list) { - push @allow, $meta->get_method($method)->name - if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; - } - return [sort @allow]; -} - sub hal_from_contract : Private { my ($self, $contract) = @_; # XXX invalid 00-00-00 dates @@ -183,11 +185,11 @@ sub hal_from_contract : Private { ), Data::HAL::Link->new(relation => 'collection', href => '/api/contracts/'), Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => "/api/contracts/?id=$id"), + Data::HAL::Link->new(relation => 'self', href => "/api/contracts/$id"), $contract->contact ? Data::HAL::Link->new( relation => 'ngcp:contacts', - href => sprintf('/api/contacts/?id=%d', $contract->contact_id), + href => sprintf('/api/contacts/%d', $contract->contact_id), ) : (), ], relation => 'ngcp:contracts', @@ -217,30 +219,13 @@ sub end : Private { $c->forward(qw(Controller::Root render)); $c->response->content_type('') if $c->response->content_type =~ qr'text/html'; # stupid RenderView getting in the way -use Carp qw(longmess); use DateTime::Format::RFC3339 qw(); use Data::Dumper qw(Dumper); use Convert::Ascii85 qw(); if (@{ $c->error }) { - my $incident = DateTime->from_epoch(epoch => Time::HiRes::time); - my $incident_id = sprintf '%X', $incident->strftime('%s%N'); - my $incident_timestamp = DateTime::Format::RFC3339->new->format_datetime($incident); - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Useqq = 1; - local $Data::Dumper::Deparse = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - my $crash_state = join "\n", @{ $c->error }, longmess, Dumper($c), Dumper($c->config); - $c->log->error( - "Exception id $incident_id at $incident_timestamp crash_state:" . - ($crash_state ? ("\n" . $crash_state) : ' disabled') - ); + my $msg = join ', ', @{ $c->error }; + $c->log->error($msg); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); $c->clear_errors; - $c->stash( - exception_incident => $incident_id, - exception_timestamp => $incident_timestamp, - template => 'api/internal_server_error.tt' - ); - $c->response->status(500); - $c->response->content_type('application/xhtml+xml'); - $c->detach($c->view); } } + + # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/ContractsItem.pm b/lib/NGCP/Panel/Controller/API/ContractsItem.pm index f8ed7667b3..6d3efbd854 100644 --- a/lib/NGCP/Panel/Controller/API/ContractsItem.pm +++ b/lib/NGCP/Panel/Controller/API/ContractsItem.pm @@ -9,21 +9,8 @@ use DateTime::Format::HTTP qw(); use DateTime::Format::RFC3339 qw(); use Digest::SHA3 qw(sha3_256_base64); use HTTP::Headers qw(); -use HTTP::Headers::Util qw(split_header_words); -use HTTP::Status qw( - HTTP_BAD_REQUEST - HTTP_NO_CONTENT - HTTP_NOT_FOUND - HTTP_NOT_MODIFIED - HTTP_OK - HTTP_PRECONDITION_FAILED - HTTP_PRECONDITION_REQUIRED - HTTP_UNPROCESSABLE_ENTITY - HTTP_UNSUPPORTED_MEDIA_TYPE -); -use JE qw(); +use HTTP::Status qw(:constants); use JSON qw(); -use JSON::Pointer qw(); use MooseX::ClassAttribute qw(class_has); use NGCP::Panel::Utils::ValidateJSON qw(); use Path::Tiny qw(path); @@ -32,11 +19,10 @@ use Safe::Isa qw($_isa); use Types::Standard qw(InstanceOf); BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; -require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; -require Catalyst::ActionRole::QueryParameter; require Catalyst::ActionRole::RequireSSL; -require URI::QueryParam; + +with 'NGCP::Panel::Role::API'; class_has('dispatch_path', is => 'ro', default => '/api/contracts/'); class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-contracts'); @@ -47,20 +33,18 @@ __PACKAGE__->config( map { $_ => { ACLDetachTo => '/api/root/invalid_user', AllowedRole => 'api_admin', - Args => 0, - Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Args => 1, + Does => [qw(ACL RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - QueryParam => 'id', } } @{ __PACKAGE__->allowed_methods } }, - action_roles => [qw(HTTPMethods QueryParameter)], + action_roles => [qw(HTTPMethods)], ); -sub GET : Allow { - my ($self, $c) = @_; +sub GET :Allow { + my ($self, $c, $id) = @_; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last if $self->cached($c); my $contract = $self->contract_by_id($c, $id); @@ -85,15 +69,15 @@ sub GET : Allow { return; } -sub HEAD : Allow { - my ($self, $c) = @_; +sub HEAD :Allow { + my ($self, $c, $id) = @_; $c->forward(qw(GET)); $c->response->body(q()); return; } -sub OPTIONS : Allow { - my ($self, $c) = @_; +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; my $allowed_methods = $self->allowed_methods->join(q(, )); $c->response->headers(HTTP::Headers->new( Allow => $allowed_methods, @@ -105,12 +89,11 @@ sub OPTIONS : Allow { return; } -sub PATCH : Allow { - my ($self, $c) = @_; +sub PATCH :Allow { + my ($self, $c, $id) = @_; my $media_type = 'application/json-patch+json'; my $guard = $c->model('DB')->txn_scope_guard; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last unless $self->forbid_link_header($c); last unless $self->valid_media_type($c, $media_type); @@ -218,12 +201,11 @@ sub PATCH : Allow { return; } -sub PUT : Allow { - my ($self, $c) = @_; +sub PUT :Allow { + my ($self, $c, $id) = @_; my $media_type = 'application/hal+json'; my $guard = $c->model('DB')->txn_scope_guard; { - my $id = delete $c->request->query_parameters->{id}; last unless $self->valid_id($c, $id); last unless $self->forbid_link_header($c); last unless $self->valid_media_type($c, $media_type); @@ -326,100 +308,12 @@ sub PUT : Allow { return; } -sub allowed_methods : Private { - my ($self) = @_; - my $meta = $self->meta; - my @allow; - for my $method ($meta->get_method_list) { - push @allow, $meta->get_method($method)->name - if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; - } - return [sort @allow]; -} - -sub apply_patch : Private { - my ($self, $c, $entity, $json) = @_; - my $patch = JSON::decode_json($json); - for my $op (@{ $patch }) { - my $coderef = JSON::Pointer->can($op->{op}); - die 'invalid op despite schema validation' unless $coderef; - try { - for ($op->{op}) { - if ('add' eq $_ or 'replace' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); - } elsif ('remove' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{path}); - } elsif ('move' eq $_ or 'copy' eq $_) { - $entity = $coderef->('JSON::Pointer', $entity, $op->{from}, $op->{path}); - } elsif ('test' eq $_) { - die "test failed - path: $op->{path} value: $op->{value}\n" - unless $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); - } - } - } catch($e) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/unprocessable_entity.tt', error_message => $e); - return; - }; - } - return $entity; -} - -sub cached : Private { - my ($self, $c) = @_; - my $response = $c->cache->get($c->request->uri->canonical->as_string); - unless ($response) { - $c->log->info('not cached'); - return; - } - my $matched_tag = $c->request->header('If-None-Match') && ('*' eq $c->request->header('If-None-Match')) - || (grep {$response->header('ETag') eq $_} Data::Record->new({ - split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")}, - })->records($c->request->header('If-None-Match'))); - my $not_modified = $c->request->header('If-Modified-Since') - && !($self->last_modified < DateTime::Format::HTTP->parse_datetime($c->request->header('If-Modified-Since'))); - if ( - $matched_tag && $not_modified - || $matched_tag - || $not_modified - ) { - $c->response->status(HTTP_NOT_MODIFIED); - $c->response->headers($response->headers); - $c->log->info('cached'); - return 1; - } - $c->log->info('stale'); - return; -} - -sub contract_by_id : Private { +sub contract_by_id :Private { my ($self, $c, $id) = @_; return $c->model('DB')->resultset('contracts')->find({'me.id' => $id}, {prefetch => ['contact']}); } -sub etag : Private { - my ($self, $octets) = @_; - return sprintf '"ni:/sha3-256;%s"', sha3_256_base64($octets); -} - -sub expires : Private { - my ($self) = @_; - return DateTime->now->clone->add(years => 1); # XXX insert product end-of-life -} - -sub forbid_link_header : Private { - my ($self, $c) = @_; - return 1 unless $c->request->header('Link'); - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/forbid_link_header.tt'); - return; -} - -sub hal_from_contract : Private { +sub hal_from_contract :Private { my ($self, $contract) = @_; # XXX invalid 00-00-00 dates my %resource = $contract->get_inflated_columns; @@ -455,178 +349,15 @@ sub hal_from_contract : Private { return $hal; } -sub require_body : Private { - my ($self, $c) = @_; - return 1 if $c->request->body; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_body.tt'); - return; -} - -sub require_precondition : Private { - my ($self, $c, $header_name) = @_; - return 1 if $c->request->header($header_name); - $c->response->status(HTTP_PRECONDITION_REQUIRED); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_precondition.tt', header_name => $header_name); - return; -} - -sub require_preference : Private { - my ($self, $c) = @_; - my @preference = grep { 'return' eq $_->[0] } split_header_words($c->request->header('Prefer')); - return $preference[0][1] - if 1 == @preference && ('minimal' eq $preference[0][1] || 'representation' eq $preference[0][1]); - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/require_preference.tt'); - return; -} - -sub require_valid_patch : Private { - my ($self, $c, $json) = @_; - my $js - = path($c->path_to(qw(share static js api tv4.js)))->slurp - . "\nvar schema = " - . path($c->path_to(qw(share static js api json-patch.json)))->slurp - . ";\nvar data = " - . $json # code injection prevented by asserting well-formedness - . ";\ntv4.validate(data, schema);"; - my $je = JE->new; - unless ($je->eval($js)) { - die "generic JavaScript error: $@" if $@; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/valid_entity.tt', - media_type => 'application/json-patch+json', - error_message => JSON::to_json( - { map { $_ => $je->{tv4}{error}{$_}->value } qw(dataPath message schemaPath) }, - { canonical => 1, pretty => 1, } - ) - ); - return; - }; - return 1; -} - -sub require_wellformed_json : Private { - my ($self, $c, $media_type, $patch) = @_; - try { - NGCP::Panel::Utils::ValidateJSON->new($patch); - } catch($e) { - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/valid_entity.tt', media_type => $media_type, error_message => $e); - return; - }; - return 1; -} - -sub resource_exists : Private { - my ($self, $c, $entity_name, $resource) = @_; - return 1 if $resource; - $c->response->status(HTTP_NOT_FOUND); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/not_found.tt', entity_name => $entity_name); - return; -} - -sub valid_entity : Private { - my ($self, $c, $entity) = @_; - my $js - = path($c->path_to(qw(share static js api tv4.js)))->slurp - . "\nvar schema = " - . path($c->path_to(qw(share static js api properties contracts-item.json)))->slurp - . ";\nvar data = " - . JSON::to_json($entity, { canonical => 1, pretty => 1, utf8 => 1, }) - . ";\ntv4.validate(data, schema);"; - my $je = JE->new; - unless ($je->eval($js)) { - die "generic JavaScript error: $@" if $@; - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/unprocessable_entity.tt', - error_message => JSON::to_json( - { map { $_ => $je->{tv4}{error}{$_}->value } qw(dataPath message schemaPath) }, - { canonical => 1, pretty => 1, } - ) - ); - return; - } - return 1; -} - -sub valid_id : Private { - my ($self, $c, $id) = @_; - return 1 if $id->is_integer; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/invalid_query_parameter.tt', key => 'id'); - return; -} - -sub valid_media_type : Private { - my ($self, $c, $media_type) = @_; - return 1 if $c->request->header('Content-Type') && 0 == index $c->request->header('Content-Type'), $media_type; - $c->response->status(HTTP_UNSUPPORTED_MEDIA_TYPE); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/valid_media_type.tt', media_type => $media_type); - return; -} - -sub valid_precondition : Private { - my ($self, $c, $etag, $entity_name) = @_; - my $if_match = $c->request->header('If-Match'); - return 1 if '*' eq $if_match || grep {$etag eq $_} Data::Record->new({ - split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")}, - })->records($if_match); - $c->response->status(HTTP_PRECONDITION_FAILED); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/precondition_failed.tt', entity_name => $entity_name); - return; -} - sub end : Private { my ($self, $c) = @_; $c->forward(qw(Controller::Root render)); $c->response->content_type('') if $c->response->content_type =~ qr'text/html'; # stupid RenderView getting in the way -use Carp qw(longmess); use DateTime::Format::RFC3339 qw(); use Data::Dumper qw(Dumper); use Convert::Ascii85 qw(); if (@{ $c->error }) { - my $incident = DateTime->from_epoch(epoch => Time::HiRes::time); - my $incident_id = sprintf '%X', $incident->strftime('%s%N'); - my $incident_timestamp = DateTime::Format::RFC3339->new->format_datetime($incident); - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Useqq = 1; - local $Data::Dumper::Deparse = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - my $crash_state = join "\n", @{ $c->error }, longmess, Dumper($c), Dumper($c->config); - $c->log->error( - "Exception id $incident_id at $incident_timestamp crash_state:" . - ($crash_state ? ("\n" . $crash_state) : ' disabled') - ); + my $msg = join ', ', @{ $c->error }; + $c->log->error($msg); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); $c->clear_errors; - $c->stash( - exception_incident => $incident_id, - exception_timestamp => $incident_timestamp, - template => 'api/internal_server_error.tt' - ); - $c->response->status(500); - $c->response->content_type('application/xhtml+xml'); - $c->detach($c->view); } } diff --git a/lib/NGCP/Panel/Controller/API/ResellersItem.pm b/lib/NGCP/Panel/Controller/API/ResellersItem.pm index fc78fb0698..665e76b599 100644 --- a/lib/NGCP/Panel/Controller/API/ResellersItem.pm +++ b/lib/NGCP/Panel/Controller/API/ResellersItem.pm @@ -4,17 +4,15 @@ use namespace::sweep; use DateTime qw(); use DateTime::Format::HTTP qw(); use HTTP::Headers qw(); -use HTTP::Status qw( - HTTP_NOT_IMPLEMENTED -); +use HTTP::Status qw(:constants); use MooseX::ClassAttribute qw(class_has); BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; -require Catalyst::ActionRole::QueryParameter; require Catalyst::ActionRole::RequireSSL; -require URI::QueryParam; + +with 'NGCP::Panel::Role::API'; class_has('dispatch_path', is => 'ro', default => '/api/resellers/'); @@ -23,14 +21,13 @@ __PACKAGE__->config( map { $_ => { ACLDetachTo => '/api/root/invalid_user', AllowedRole => 'api_admin', - Args => 0, - Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Args => 1, + Does => [qw(ACL RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - QueryParam => 'id', } } @{ __PACKAGE__->allowed_methods } }, - action_roles => [qw(HTTPMethods QueryParameter)], + action_roles => [qw(HTTPMethods)], ); sub GET : Allow { @@ -63,14 +60,15 @@ sub OPTIONS : Allow { return; } - -sub allowed_methods : Private { - my ($self) = @_; - my $meta = $self->meta; - my @allow; - for my $method ($meta->get_method_list) { - push @allow, $meta->get_method($method)->name - if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; +sub end : Private { + my ($self, $c) = @_; + $c->forward(qw(Controller::Root render)); + $c->response->content_type('') + if $c->response->content_type =~ qr'text/html'; # stupid RenderView getting in the way + if (@{ $c->error }) { + my $msg = join ', ', @{ $c->error }; + $c->log->error($msg); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); + $c->clear_errors; } - return [sort @allow]; } diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index a68aaa13e0..f1e04eb815 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -11,6 +11,7 @@ use DateTime::Format::HTTP qw(); use DateTime::Format::RFC3339 qw(); use Types::Standard qw(InstanceOf); use Regexp::Common qw(delimited); # $RE{delimited} +use HTTP::Headers::Util qw(split_header_words); use NGCP::Panel::Utils::ValidateJSON qw(); has('last_modified', is => 'rw', isa => InstanceOf['DateTime']); @@ -79,8 +80,6 @@ sub validate_form { return 1; } -# private - sub error { my ($self, $c, $code, $message) = @_; @@ -113,6 +112,22 @@ sub require_body { return; } +sub require_precondition { + my ($self, $c, $header_name) = @_; + return 1 if $c->request->header($header_name); + $self->error($c, HTTP_PRECONDITION_REQUIRED, "This request is required to be conditional, use the '$header_name' header."); + return; +} + +sub require_preference { + my ($self, $c) = @_; + my @preference = grep { 'return' eq $_->[0] } split_header_words($c->request->header('Prefer')); + return $preference[0][1] + if 1 == @preference && ('minimal' eq $preference[0][1] || 'representation' eq $preference[0][1]); + $self->error($c, HTTP_BAD_REQUEST, "This request is required to express an expectation about the response. Use the 'Prefer' header with either 'return=representation' or 'return='minimal' preference."); + return; +} + sub require_wellformed_json { my ($self, $c, $media_type, $patch) = @_; try { @@ -161,5 +176,64 @@ sub expires { return DateTime->now->clone->add(years => 1); # XXX insert product end-of-life } +sub allowed_methods { + my ($self) = @_; + my $meta = $self->meta; + my @allow; + for my $method ($meta->get_method_list) { + push @allow, $meta->get_method($method)->name + if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes; + } + return [sort @allow]; +} + +sub valid_id { + my ($self, $c, $id) = @_; + return 1 if $id->is_integer; + $self->error($c, HTTP_BAD_REQUEST, "Invalid id in request URI"); + return; +} + +sub require_valid_patch { + my ($self, $c, $json) = @_; + + # TODO: implement without JE + return 1; +} + +sub resource_exists { + my ($self, $c, $entity_name, $resource) = @_; + return 1 if $resource; + $self->error($c, HTTP_NOT_FOUND, "Entity '$entity_name' not found."); + return; +} + +sub apply_patch { + my ($self, $c, $entity, $json) = @_; + my $patch = JSON::decode_json($json); + for my $op (@{ $patch }) { + my $coderef = JSON::Pointer->can($op->{op}); + die 'invalid op despite schema validation' unless $coderef; + try { + for ($op->{op}) { + if ('add' eq $_ or 'replace' eq $_) { + $entity = $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); + } elsif ('remove' eq $_) { + $entity = $coderef->('JSON::Pointer', $entity, $op->{path}); + } elsif ('move' eq $_ or 'copy' eq $_) { + $entity = $coderef->('JSON::Pointer', $entity, $op->{from}, $op->{path}); + } elsif ('test' eq $_) { + die "test failed - path: $op->{path} value: $op->{value}\n" + unless $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value}); + } + } + } catch { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "The entity could not be processed: $_"); + return; + }; + } + return $entity; +} + 1; # vim: set tabstop=4 expandtab: diff --git a/share/templates/api/allowed_methods.tt b/share/templates/api/allowed_methods.tt deleted file mode 100644 index 3819b54436..0000000000 --- a/share/templates/api/allowed_methods.tt +++ /dev/null @@ -1,9 +0,0 @@ - - - - Allowed methods - - -

Allowed methods: [% allowed_methods | html %]

- - diff --git a/share/templates/api/invalid_query_parameter.tt b/share/templates/api/invalid_query_parameter.tt deleted file mode 100644 index bc4931a624..0000000000 --- a/share/templates/api/invalid_query_parameter.tt +++ /dev/null @@ -1,9 +0,0 @@ - - - - Invalid query parameter - - -

Invalid query parameter value for key [% key | html %]

- - diff --git a/share/templates/api/not_found.tt b/share/templates/api/not_found.tt deleted file mode 100644 index 39a87a1a6e..0000000000 --- a/share/templates/api/not_found.tt +++ /dev/null @@ -1,9 +0,0 @@ - - - - Not found - - -

Entity [% entity_name | html %] not found

- - diff --git a/share/templates/api/require_body.tt b/share/templates/api/require_body.tt deleted file mode 100644 index c371e6ddd6..0000000000 --- a/share/templates/api/require_body.tt +++ /dev/null @@ -1,9 +0,0 @@ - - - - Bad request - - -

This request is missing a message body.

- - diff --git a/share/templates/api/require_precondition.tt b/share/templates/api/require_precondition.tt deleted file mode 100644 index d2134a767e..0000000000 --- a/share/templates/api/require_precondition.tt +++ /dev/null @@ -1,9 +0,0 @@ - - - - Precondition required - - -

This request is required to be conditional, use the [% header_name | html %] header.

- -