ngcp-panel/lib/NGCP/Panel/Utils/API.pm

520 lines
18 KiB

package NGCP::Panel::Utils::API;
use strict;
use warnings;
use Sipwise::Base;
use File::Find::Rule;
use JSON qw();
use HTTP::Status qw(:constants);
use Clone qw/clone/;
my $collections_info_cache;
my $collections_files_cache;
sub check_resource_reseller_id {
my($api, $c, $resource, $old_resource) = @_;
if( $resource->{reseller_id}
&& (( ! $old_resource ) || $old_resource->{reseller_id} != $resource->{reseller_id} )) {
my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id});
unless( $reseller ) {
$api->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'");
return;
}
}
return 1;
}
sub apply_resource_reseller_id {
my($c, $resource) = @_;
my $reseller_id;
if($c->user->roles eq "admin") {
try {
$reseller_id = $resource->{reseller_id}
|| $c->user->contract->contact->reseller_id;
}
} elsif($c->user->roles eq "reseller") {
$reseller_id = $c->user->reseller_id;
} elsif($c->user->roles eq "lintercept") {
$reseller_id = $c->user->reseller_id;
}
$resource->{reseller_id} = $reseller_id;
return $resource;
}
sub get_collections {
if ($collections_info_cache) {
my $collections_info_cache_cloned = clone($collections_info_cache);
return @$collections_info_cache_cloned;
}
#get_collections_files in scalar context will return only first value from return array - \@files
my @files = @{get_collections_files()};
my(@collections, @packages, @modules);
foreach my $mod(@files) {
# extract file base from path (e.g. Foo from lib/something/Foo.pm)
$mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/;
my $package = 'NGCP::Panel::Controller::API::'.$mod;
my $rel = lc $mod;
$mod = 'NGCP::Panel::Controller::API::'.$mod;
push @modules, $mod;
push @packages, $package;
push @collections, $rel;
}
$collections_info_cache = [\@files, \@packages, \@collections, \@modules];
return @{clone($collections_info_cache)};
}
sub get_collections_files {
if ($collections_files_cache) {
return clone($collections_files_cache);
}
my($library,$libpath) = @_;
if(!$libpath){
# figure out base path of our api modules
$library ||= "NGCP/Panel/Controller/API/Root.pm";
$libpath = $INC{$library};
$libpath =~ s/\/[^\/]+$/\//;
}
# find all modules not called Root.pm and *Item.pm
# (which should then be just collections)
my $rootrule = File::Find::Rule->new->name('Root.pm');
my $itemrule = File::Find::Rule->new->name('*Item.pm');
my $rule = File::Find::Rule->new
->mindepth(1)
->maxdepth(1)
->name('*.pm')
->not($rootrule)
->not($itemrule);
my @colls = $rule->in($libpath);
$collections_files_cache = \@colls;
return clone($collections_files_cache);
}
sub get_module_by_resource {
my($c, $resource_name, $is_item_resource) = @_;
if ($c->stash->{get_module_by_resource} && $c->stash->{get_module_by_resource}->{$resource_name} ) {
return $c->stash->{get_module_by_resource}->{$resource_name};
}
my($files,$modules,$collections) = get_collections();
my $package = (grep { /::$resource_name$/i } @$files)[0];
$is_item_resource and $package .= 'Item';
$c->stash->{get_module_by_resource}->{$resource_name} = $package;
return $package;
}
sub generate_swagger_datastructure {
my ($collections, $user_role) = @_;
my @tag_descriptions;
my %paths;
my %schemas;
my %responses = (
ErrorResponse => {
description => 'An error',
content => {
"application/json" => {
"schema" => {
type => "object",
properties => {
code => { type => "integer" },
message => { type => "string" },
}
}
}
}
}
);
my %parameters = (
PageParameter => {
name => 'page',
in => 'query',
description => 'Pagination page which should be displayed (default: 1)',
example => 1,
schema => {type => 'integer'}, # schema is required
},
RowsParameter => {
name => 'rows',
in => 'query',
description => 'Number of rows in one pagination page (default: 10)',
example => 10,
schema => {type => 'integer'}, # schema is required
},
ItemIdParameter => {
"name" => "id",
"in" => "path",
"required" => JSON::true,
"schema" => { type => "integer" },
},
);
my %requestBodies = (
PatchBody => {
description => "A JSON patch document specifying modifications",
required => JSON::true,
content => {
'application/json-patch+json' => {
schema => {
# '$ref' => 'http://json.schemastore.org/json-patch.json#/',
# "$ref": "https://raw.githubusercontent.com/fge/sample-json-schemas/master/json-patch/json-patch.json"
'$ref' => '/static/js/schemas/json-patch.json#/',
# type => 'array',
# items => {
# type => 'object',
# }
}
}
}
}
);
my @chapters = sort (keys %{ $collections });
for my $chapter (@chapters) {
my $col = $collections->{$chapter};
my $p = {}; # Path Item Object
my $item_p = {}; # Path Item Object for "NGCP Item"
my $title = $col->{name};
my $entity = $col->{entity_name};
push @tag_descriptions, {
name => "$entity",
description => $col->{description},
};
if (grep {m/^GET$/} @{ $col->{actions} }) {
$p->{get} = {
summary => "Get $entity items",
tags => ["$entity"],
responses => {
"200" => {
description => "$title",
content => {
"application/json" => {
schema => {
type => "array", # I want an Array to $entity objects here
items => {
'$ref' => "#/components/schemas/$entity",
}
}
},
},
}
}
};
for my $query_param (@{ $col->{query_params} // [] }) {
push @{$p->{get}{parameters} }, {
name => $query_param->{param},
description => $query_param->{description},
in => 'query',
schema => {type => 'string'}, # schema is required
};
}
if ($col->{sorting_cols} && @{ $col->{sorting_cols} }) {
push @{$p->{get}{parameters} }, {
name => 'order_by',
description => 'Order collection by a specific attribute.',
in => 'query',
schema => {
type => 'string',
enum => [ @{ $col->{sorting_cols} } ],
}
},{
name => 'order_by_direction',
description => 'Direction which the collection should be ordered by. Possible values are: asc (default), desc.',
in => 'query',
example => 'asc',
schema => {
type => 'string',
enum => [ 'asc', 'desc' ],
}
};
}
push @{ $p->{get}{parameters} }, {
'$ref' => '#/components/parameters/PageParameter',
},{
'$ref' => '#/components/parameters/RowsParameter',
};
}
if (grep {m/^POST$/} @{ $col->{actions} }) {
$p->{post} = {
# description => "Creates a new item of $title",
summary => "Create a new $entity",
tags => ["$entity"],
requestBody => {
required => JSON::true,
content => {
"application/json" => {
schema => {
'$ref' => "#/components/schemas/$entity",
}
}
}
},
responses => {
"201" => {
description => "The newly created item or empty",
content => {
"application/json" => {
schema => {
type => "array",
items => {
'$ref' => "#/components/schemas/$entity",
}
}
},
# "*/*" => {
# schema => { type => "string", maxLength => 0 }
# }
},
headers => {
"Location" => {
"description" => "Location of the newly created item (as a relative path)",
"schema" => {
"type" => "string",
}
}
}
},
"422" => { '$ref' => "#/components/responses/ErrorResponse" },
}
};
}
if (grep {m/^GET$/} @{ $col->{item_actions} }) {
my $action_config = $col->{item_config}->{action}->{GET};
my $produces = $action_config->{ReturnContentType}
? ref $action_config->{ReturnContentType} eq 'ARRAY'
? $action_config->{ReturnContentType}
: $action_config->{ReturnContentType} eq 'binary' ? ['application/octet-stream'] : undef
: undef;
$item_p->{get} = {
summary => "Get a specific $entity",
tags => ["$entity"],
$produces ? ( produces => $produces ) : (),
responses => {
"200" => {
description => "$title",
$produces ? ( content => { map {
$_ => {
schema => {
$_ ne 'application/json'
? (type => 'string')
: ('$ref' => "#/components/schemas/$entity"),
}
} } @$produces } )
: (
content => {
"application/json" => {
schema => {
'$ref' => "#/components/schemas/$entity",
}
},
}
),
}
}
};
}
if (grep {m/^PUT$/} @{ $col->{item_actions} }) {
$item_p->{put} = {
summary => "Replace/change a specific $entity",
tags => ["$entity"],
requestBody => {
required => JSON::true,
content => {
"application/json" => {
schema => {
'$ref' => "#/components/schemas/$entity",
}
}
}
},
responses => {
"200" => {
description => "$title",
content => {
"application/json" => {
schema => {
'$ref' => "#/components/schemas/$entity",
}
},
},
},
"204" => {
description => "Put successful",
# empty content
},
}
};
}
if (grep {m/^PATCH$/} @{ $col->{item_actions} }) {
$item_p->{patch} = {
summary => "Change a specific $entity",
tags => ["$entity"],
requestBody => {
'$ref' => '#/components/requestBodies/PatchBody',
},
responses => {
"200" => {
description => "$title",
content => {
"application/json" => {
schema => {
'$ref' => "#/components/schemas/$entity",
}
},
},
},
"204" => {
description => "Patch successful",
# empty content
},
}
};
}
if (grep {m/^DELETE$/} @{ $col->{item_actions} }) {
$item_p->{delete} = {
summary => "Delete a specific $entity",
tags => ["$entity"],
responses => {
"204" => {
description => "Deletion successful",
# empty content
},
}
};
}
#push @paths, $p;
$paths{'/'.$chapter.'/'} = $p;
if (keys %{ $item_p }) {
$item_p->{description} = $col->{description};
$item_p->{parameters} = [
{ '$ref' => '#/components/parameters/ItemIdParameter' },
];
$paths{'/'.$chapter.'/{id}'} = $item_p;
}
# ---------------------------------------------
# possible values for types: null, (select options), Number, Boolean, Array, Object, String
my $e = _fields_to_swagger_schema($col->{fields});
$schemas{$entity} = $e;
}
my $role = "".$user_role;
my $result = {
"openapi" => "3.0.0",
"info" => {
"title" => "NGCP API",
"description" => "Sipwise NGCP API (role $role)",
"version" => "1.0.1",
},
"servers" => [ { "url" => "/api" } ],
"paths" => \%paths,
"tags" => \@tag_descriptions,
"components" => {
"schemas" => \%schemas,
"responses" => \%responses,
"parameters" => \%parameters,
"requestBodies" => \%requestBodies,
},
};
return $result;
}
# this is recursive to parse subfields
sub _fields_to_swagger_schema {
my ($fields) = @_;
my $e = {
type => "object",
properties => {},
required => [],
};
for my $f (@{ $fields }) {
my $p = {};
if ($f->{type_original} eq "Select" ||
($f->{type_original} =~ m/\+NGCP::Panel::Field::.*Select$/ && $f->{enum})) {
$p->{type} = "string";
$p->{enum} = [ map {$_->{value}} @{ $f->{enum} // [] } ];
} elsif ($f->{type_original} eq 'IntRange') {
$p->{type} = "number";
$p->{enum} = [ map {$_->{value}} @{ $f->{enum} // [] } ];
} elsif ($f->{type_original} eq "Boolean") {
$p->{type} = "boolean";
} elsif (grep {m/^Number$/} @{$f->{types}}) {
$p->{type} = "number";
} elsif ($f->{type_original} eq '+NGCP::Panel::Field::EmailList' ||
$f->{type_original} eq 'Email') {
$p->{type} = "string";
$p->{format} = "email"; # not the same as emaillist but that's nit-picky
} elsif ($f->{type_original} eq '+NGCP::Panel::Field::DateTime') {
$p->{type} = "string";
$p->{format} = "date-time"; # actually a slightly different format
} elsif ($f->{type_original} eq "Text" || grep {m/^String$/} @{$f->{types}}) {
$p->{type} = "string";
} elsif ($f->{type_original} eq "Repeatable" || grep {m/^Array$/} @{$f->{types}}) {
$p->{type} = "array";
if ($f->{subfields}) {
$p->{items} = _fields_to_swagger_schema($f->{subfields});
} else {
$p->{items}{type} = "object"; # content of array basically unspecified
}
} elsif ($f->{subfields}) { # object with subfields
$p = _fields_to_swagger_schema($f->{subfields});
} else {
$p->{type} = "object"; # object or uncategorizable
}
$p->{description} = $f->{description};
unless (grep {m/^null$/} @{ $f->{types} // [] }) {
push @{ $e->{required} }, $f->{name};
}
$e->{properties}{$f->{name}} = $p;
}
unless (@{ $e->{required} }) {
delete $e->{required}; # empty required is not allowed
}
unless (keys %{ $e->{properties} }) {
delete $e->{properties}; # try delete empty properties (then it's a valid Free Form Object)
}
return $e;
}
1;
=head1 NAME
NGCP::Panel::Utils::API
=head1 DESCRIPTION
A helper to manipulate REST API related data
=head1 METHODS
=head1 AUTHOR
Irina Peshinskaya
=head1 LICENSE
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# vim: set tabstop=4 expandtab: