520 lines
18 KiB
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:
|