diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index 75ab5054db..4fbf6b7825 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -2,6 +2,8 @@ package NGCP::Panel::Role::API; use Sipwise::Base; +use parent qw/NGCP::Panel::Role::Journal/; + use NGCP::Panel::Utils::Generic qw(:all); use boolean qw(true); use Safe::Isa qw($_isa); @@ -20,10 +22,6 @@ use Data::HAL qw(); use Data::HAL::Link qw(); use NGCP::Panel::Utils::ValidateJSON qw(); use NGCP::Panel::Utils::Journal qw(); -use parent qw/NGCP::Panel::Role::Journal/; - -#fun - there is no one usage of the last_modified through all ngcp-panel sources. even in javascript -#has('last_modified', is => 'rw', isa => InstanceOf['DateTime']); sub get_valid_post_data { my ($self, %params) = @_; @@ -242,9 +240,6 @@ sub valid_media_type { sub require_body { my ($self, $c) = @_; return 1 if length $c->stash->{body}; - - - $self->error($c, HTTP_BAD_REQUEST, "This request is missing a message body."); return; } @@ -617,8 +612,8 @@ sub apply_query_params { } } return $item_rs; - } + sub get_query_callbacks{ my ($self, $query_param_spec) = @_; #while believe that there is only one parameter @@ -656,8 +651,9 @@ sub delay_commit { sub hal_from_item { my ($self, $c, $item, $form) = @_; - my %resource = $item->get_inflated_columns; - $resource{contract_id} = delete $resource{peering_contract_id}; + my $resource = {$item->get_inflated_columns}; + $resource = $self->process_hal_resource($c, $item, $resource, $form); + my $links = $self->hal_links($c, $item, $resource, $form) // []; my $hal = Data::HAL->new( links => [ Data::HAL::Link->new( @@ -669,6 +665,7 @@ sub hal_from_item { Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)), Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), + @$links ], relation => 'ngcp:'.$self->resource_name, ); @@ -677,16 +674,21 @@ sub hal_from_item { $self->validate_form( c => $c, - resource => \%resource, + resource => $resource, form => $form, run => 0, ); - $resource{id} = int($item->id); - $hal->resource({%resource}); + $resource->{id} = int($item->id); + $hal->resource({%$resource}); return $hal; } +sub hal_links { + my($self, $c, $item, $resource, $form) = @_; + return []; +} + sub item_by_id { my ($self, $c, $id) = @_; my $item_rs = $self->item_rs($c); @@ -703,22 +705,41 @@ sub update_item { resource => $resource, ); last unless $resource; - $resource = $self->process_resource($c, $item, $old_resource, $resource, $form); + $resource = $self->process_form_resource($c, $item, $old_resource, $resource, $form); last unless $resource; last unless $self->check_duplicate($c, $item, $old_resource, $resource, $form); $item = $self->update($c, $item, $old_resource, $resource, $form); return $item; } -sub process_resource{ + +sub process_form_resource { my($self, $c, $item, $old_resource, $resource, $form) = @_; return $resource; } -sub update{ + +sub process_hal_resource { + my($self, $c, $item, $resource, $form) = @_; + return $resource; +} + +sub update { my($self, $c, $item, $old_resource, $resource, $form) = @_; $item->update($resource); return $item; } +#------ accessors --- +sub dispatch_path { + my $self = shift; + return '/api/'.$self->resource_name.'/'; +} + +sub relation { + my $self = shift; + return 'http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name; +} +#------ /accessors --- + sub return_csv(){ my($self,$c) = @_; try{ diff --git a/lib/NGCP/Panel/Role/Entities.pm b/lib/NGCP/Panel/Role/Entities.pm new file mode 100644 index 0000000000..f934609a67 --- /dev/null +++ b/lib/NGCP/Panel/Role/Entities.pm @@ -0,0 +1,162 @@ +package NGCP::Panel::Role::Entities; + +use parent qw/Catalyst::Controller/; +use boolean qw(true); +use Safe::Isa qw($_isa); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use Data::HAL qw(); +use Data::HAL::Link qw(); +#use Path::Tiny qw(path); + +sub set_config { + my $self = shift; + $self->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => $self->dispatch_path, + } } @{ $self->allowed_methods } + }, + action_roles => [qw(HTTPMethods)], + ); +} + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub get { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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 => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); + } + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub head { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub options { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub post { + my ($self) = shift; + my ($c) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + $resource = $self->process_form_resource($c, undef, undef, $resource, $form); + last unless $resource; + last unless $self->check_duplicate($c, undef, undef, $resource, $form); + + my $item = $self->create_item($c, $resource, $form); + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); + $c->response->body(q()); + } + return; +} + +sub end :Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +sub GET { + my ($self) = shift; + return $self->get(@_); +} + +sub HEAD { + my ($self) = shift; + return $self->head(@_); +} + +sub OPTIONS { + my ($self) = shift; + return $self->options(@_); +} + +sub POST { + my ($self) = shift; + return $self->post(@_); +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/EntitiesItem.pm b/lib/NGCP/Panel/Role/EntitiesItem.pm new file mode 100644 index 0000000000..b09523fb93 --- /dev/null +++ b/lib/NGCP/Panel/Role/EntitiesItem.pm @@ -0,0 +1,219 @@ +package NGCP::Panel::Role::EntitiesItem; + +use parent qw/Catalyst::Controller/; + +use boolean qw(true); +use Safe::Isa qw($_isa); +use Path::Tiny qw(path); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use NGCP::Panel::Utils::Generic qw(:all); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); + +sub set_config { + my $self = shift; + $self->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => $self->dispatch_path, + } } @{ $self->allowed_methods } + }, + action_roles => [qw(HTTPMethods)], + ); +} + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub get { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, $self->item_name => $item); + + my $hal = $self->hal_from_item($c, $item); + + 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-resellers)"|rel="item $1"|; + s/rel=self/rel="item self"/; + $_ + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub head { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub options { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub patch { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ); + last unless $json; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, $self->item_name => $item); + my $old_resource = { $item->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + my $hal = $self->hal_from_item($c, $item, $form); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub put { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, $self->item_name => $item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $item->get_inflated_columns }; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + my $hal = $self->hal_from_item($c, $item, $form); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub delete { + my ($self, $c, $id) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, $self->item_name => $item); + $self->delete_item($c, $item ); + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end :Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +sub GET { + my ($self) = shift; + return $self->get(@_); +} + +sub HEAD { + my ($self) = shift; + return $self->head(@_); +} + +sub OPTIONS { + my ($self) = shift; + return $self->options(@_); +} + +sub PUT { + my ($self) = shift; + return $self->put(@_); +} + +sub PATCH { + my ($self) = shift; + return $self->patch(@_); +} + +sub DELETE { + my ($self) = shift; + return $self->delete(@_); +} + +1; + +# vim: set tabstop=4 expandtab: