use
constant
DEBUG
=>
$ENV
{SWAGGER2_DEBUG};
use
constant
IO_LOGGING
=>
$ENV
{SWAGGER2_IO_LOGGING};
my
$SKIP_OP_RE
=
qr(By|From|For|In|Of|To|With)
;
has
url
=>
''
;
has
_validator
=>
sub
{ Swagger2::SchemaValidator->new; };
has
_json_validator
=>
sub
{ JSON::Validator->new; };
sub
dispatch_to_swagger {
return
undef
unless
$_
[1]->{op} and
$_
[1]->{id} and
ref
$_
[1]->{params} eq
'HASH'
;
my
(
$c
,
$data
) =
@_
;
my
$self
=
$c
->stash(
'swagger.plugin'
);
my
$reply
=
sub
{
$_
[0]->
send
({
json
=> {
code
=>
$_
[2] || 200,
id
=>
$data
->{id},
body
=>
$_
[1]}}) };
my
$defaults
=
$self
->{route_defaults}{
$data
->{op}}
or
return
$c
->
$reply
(_error(
'Unknown operationId.'
), 400);
my
(
$e
,
$input
,
@errors
);
return
$c
->
$reply
(_error(
$e
), 501)
if
$e
= _find_action(
$c
,
$defaults
);
for
my
$p
(@{
$defaults
->{swagger_operation_spec}{parameters} || []}) {
my
$name
=
$p
->{name};
my
$value
=
$data
->{params}{
$name
} //
$p
->{
default
};
my
@e
=
$self
->_validate_input_value(
$p
,
$name
=>
$value
);
$input
->{
$name
} =
$value
unless
@e
;
push
@errors
,
@e
;
}
return
$c
->
$reply
({
errors
=> \
@errors
}, 400)
if
@errors
;
return
Mojo::IOLoop->delay(
sub
{
my
$delay
=
shift
;
my
$action
=
$defaults
->{action};
my
$sc
=
$delay
->data->{sc} =
$defaults
->{controller}->new(
%$c
);
$sc
->stash(
swagger_operation_spec
=>
$defaults
->{swagger_operation_spec});
$sc
->
$action
(
$input
,
$delay
->begin);
},
sub
{
my
$delay
=
shift
;
my
$data
=
shift
;
my
$status
=
shift
|| 200;
my
@errors
=
$self
->_validate_response(
$c
,
$data
,
$defaults
->{swagger_operation_spec},
$status
);
return
$c
->
$reply
(
$data
,
$status
)
unless
@errors
;
warn
"[Swagger2] Invalid response: @errors\n"
if
DEBUG;
$c
->
$reply
({
errors
=> \
@errors
}, 500);
},
);
}
sub
render_swagger {
my
(
$c
,
$err
,
$data
,
$status
) =
@_
;
return
$c
->render(
json
=>
$err
,
status
=>
$status
)
if
%$err
;
return
$c
->render(
ref
$data
? (
json
=>
$data
) : (
text
=>
$data
),
status
=>
$status
);
}
sub
register {
my
(
$self
,
$app
,
$config
) =
@_
;
my
(
$base_path
,
$paths
,
$r
,
$swagger
);
$swagger
=
$config
->{swagger} || Swagger2->new->load(
$config
->{url} ||
'"url" is missing'
);
$swagger
=
$swagger
->expand;
$paths
=
$swagger
->api_spec->get(
'/paths'
) || {};
if
(
$config
->{validate} // 1) {
my
@errors
=
$swagger
->validate;
die
join
"\n"
,
"Swagger2: Invalid spec:"
,
@errors
if
@errors
;
}
if
(
$app
->plugins->has_subscribers(
'swagger_route_added'
)) {
warn
}
else
{
$app
->hook(
swagger_route_added
=> \
&_on_route_added
);
}
local
$config
->{coerce} =
$config
->{coerce} ||
$ENV
{SWAGGER_COERCE_VALUES};
$self
->_validator->coerce(
$config
->{coerce})
if
$config
->{coerce};
$self
->url(
$swagger
->url);
$app
->helper(
dispatch_to_swagger
=> \
&dispatch_to_swagger
)
unless
$app
->renderer->get_helper(
'dispatch_to_swagger'
);
$app
->helper(
render_swagger
=> \
&render_swagger
)
unless
$app
->renderer->get_helper(
'render_swagger'
);
$r
=
$config
->{route};
if
(
$r
and !
$r
->pattern->unparsed) {
$r
->to(
swagger
=>
$swagger
);
$r
=
$r
->any(
$swagger
->base_url->path->to_string);
}
if
(!
$r
) {
$r
=
$app
->routes->any(
$swagger
->base_url->path->to_string);
$r
->to(
swagger
=>
$swagger
);
}
if
(
my
$ws
=
$config
->{ws}) {
$ws
->to(
'swagger.plugin'
=>
$self
);
}
$base_path
=
$swagger
->api_spec->data->{basePath} =
$r
->to_string;
$base_path
=~ s!/$!!;
for
my
$path
(
sort
{
length
$a
<=>
length
$b
}
keys
%$paths
) {
my
$around_action
=
$paths
->{
$path
}{
'x-mojo-around-action'
} ||
$swagger
->api_spec->get(
'/x-mojo-around-action'
);
my
$controller
=
$paths
->{
$path
}{
'x-mojo-controller'
} ||
$swagger
->api_spec->get(
'/x-mojo-controller'
);
for
my
$http_method
(
grep
{ !/^x-/ }
keys
%{
$paths
->{
$path
}}) {
my
$op_spec
=
$paths
->{
$path
}{
$http_method
};
my
$route_path
=
$path
;
my
%parameters
=
map
{ (
$_
->{name},
$_
) } @{
$op_spec
->{parameters} || []};
$route_path
=~ s/{([^}]+)}/{
my
$name
= $1;
my
$type
=
$parameters
{
$name
}{
'x-mojo-placeholder'
} ||
':'
;
"($type$name)"
;
}/ge;
$op_spec
->{
'x-mojo-around-action'
} =
$around_action
if
!
$op_spec
->{
'x-mojo-around-action'
} and
$around_action
;
$op_spec
->{
'x-mojo-controller'
} =
$controller
if
!
$op_spec
->{
'x-mojo-controller'
} and
$controller
;
$app
->plugins->emit(
swagger_route_added
=>
$r
->
$http_method
(
$route_path
=>
$self
->_generate_request_handler(
$op_spec
)));
warn
"[Swagger2] Add route $http_method $base_path$route_path\n"
if
DEBUG;
}
}
if
(
my
$spec_path
=
$config
->{spec_path} //
'/'
) {
my
$title
=
$swagger
->api_spec->get(
'/info/title'
);
$title
=~ s!\W!_!g;
$r
->get(
$spec_path
)->to(
cb
=>
sub
{ _render_spec(
shift
,
$swagger
) })->name(
lc
$title
);
}
if
(
$config
->{ensure_swagger_response}) {
$self
->_ensure_swagger_response(
$app
,
$config
->{ensure_swagger_response},
$swagger
);
}
}
sub
_ensure_swagger_response {
my
(
$self
,
$app
,
$responses
,
$swagger
) =
@_
;
my
$base_path
=
$swagger
->api_spec->data->{basePath};
$responses
->{exception} ||=
'Internal server error.'
;
$responses
->{not_found} ||=
'Not found.'
;
$base_path
=
qr{^$base_path}
;
$app
->hook(
before_render
=>
sub
{
my
(
$c
,
$args
) =
@_
;
return
unless
my
$template
=
$args
->{template};
return
unless
my
$msg
=
$responses
->{
$template
};
return
unless
$c
->req->url->path->to_string =~
$base_path
;
$args
->{json} = _error(
$msg
);
}
);
}
sub
_generate_request_handler {
my
(
$self
,
$op_spec
) =
@_
;
my
$defaults
= {
swagger_operation_spec
=>
$op_spec
};
my
$handler
=
sub
{
my
$c
=
shift
;
my
(
$e
,
$v
,
$input
);
return
$c
->render_swagger(_error(
$e
), {}, 501)
if
$e
= _find_action(
$c
,
$defaults
);
$c
=
$defaults
->{controller}->new(
%$c
);
(
$v
,
$input
) =
$self
->_validate_input(
$c
,
$op_spec
);
_io_error(
$c
,
Input
=>
$v
->{errors})
if
IO_LOGGING and @{
$v
->{errors}};
return
$c
->render_swagger(
$v
, {}, 400)
if
@{
$v
->{errors}};
return
$c
->delay(
sub
{
my
$action
=
$defaults
->{action};
$c
->app->
log
->debug(
qq(Swagger2 routing to controller "$defaults->{controller}" and action "$action")
);
$c
->
$action
(
$input
,
shift
->begin);
},
sub
{
my
$delay
=
shift
;
my
$data
=
shift
;
my
$status
=
shift
|| 200;
my
@errors
=
$self
->_validate_response(
$c
,
$data
,
$op_spec
,
$status
);
return
$c
->render_swagger({},
$data
,
$status
)
unless
@errors
;
_io_error(
$c
,
Output
=> \
@errors
)
if
IO_LOGGING and
@errors
;
$c
->render_swagger({
errors
=> \
@errors
},
$data
, 500);
},
);
};
for
my
$p
(@{
$op_spec
->{parameters} || []}) {
$defaults
->{
$p
->{name}} =
$p
->{
default
}
if
$p
->{in} eq
'path'
and
defined
$p
->{
default
};
}
if
(
my
$around_action
=
$op_spec
->{
'x-mojo-around-action'
}) {
my
$next
=
$handler
;
$handler
=
sub
{
my
$c
=
shift
;
my
$around
=
$c
->can(
$around_action
) ||
$around_action
;
$around
->(
$next
,
$c
,
$op_spec
);
};
}
$self
->{route_defaults}{
$op_spec
->{operationId}} =
$defaults
;
return
$defaults
,
$handler
;
}
sub
_on_route_added {
my
(
$self
,
$r
) =
@_
;
my
$op_spec
=
$r
->pattern->defaults->{swagger_operation_spec};
my
$controller
=
$op_spec
->{
'x-mojo-controller'
};
my
$route_name
;
$route_name
=
$controller
? decamelize
join
'::'
,
map
{
ucfirst
$_
}
$controller
,
$op_spec
->{operationId}
: decamelize
$op_spec
->{operationId};
$route_name
=~ s/\W+/_/g;
$r
->name(
$route_name
);
}
sub
_render_spec {
my
(
$c
,
$swagger
) =
@_
;
my
$spec
=
$swagger
->api_spec->data;
local
$spec
->{id};
delete
$spec
->{id};
local
$spec
->{host} =
$c
->req->url->to_abs->host_port;
$c
->render(
json
=>
$spec
);
}
sub
_validate_input {
my
(
$self
,
$c
,
$op_spec
) =
@_
;
my
(
%cache
,
%input
,
@errors
);
for
my
$p
(@{
$op_spec
->{parameters} || []}) {
my
(
$in
,
$name
,
$type
) =
@$p
{
qw(in name type)
};
my
(
$exists
,
$value
);
if
(
$in
eq
'body'
) {
$value
=
$c
->req->json;
$exists
=
$c
->req->body_size;
}
else
{
$value
=
$cache
{
$in
} ||=
do
{
$in
eq
'query'
?
$c
->req->url->query->to_hash
:
$in
eq
'path'
?
$c
->match->stack->[-1]
:
$in
eq
'formData'
?
$c
->req->body_params->to_hash
:
$in
eq
'header'
?
$c
->req->headers->to_hash
: {};
};
$exists
=
exists
$value
->{
$name
};
$value
=
$value
->{
$name
};
}
if
(
ref
$p
->{items} eq
'HASH'
and
$p
->{collectionFormat}) {
$value
= _coerce_by_collection_format(
$value
,
$p
);
}
if
(
$type
and
defined
(
$value
//=
$p
->{
default
})) {
if
((
$type
eq
'integer'
or
$type
eq
'number'
) and
$value
=~ /^-?\d/) {
$value
+= 0;
}
elsif
(
$type
eq
'boolean'
) {
$value
= (!
$value
or
$value
eq
'false'
) ? Mojo::JSON->false : Mojo::JSON->true;
}
}
my
@e
=
$self
->_validate_input_value(
$p
,
$name
=>
$value
);
$input
{
$name
} =
$value
if
!
@e
and (
$exists
or
exists
$p
->{
default
});
push
@errors
,
@e
;
}
return
{
errors
=> \
@errors
}, \
%input
;
}
sub
_validate_input_value {
my
(
$self
,
$p
,
$name
,
$value
) =
@_
;
my
$type
=
$p
->{type} ||
'object'
;
my
@e
;
return
if
!
defined
$value
and !Swagger2::_is_true(
$p
->{required});
my
$schema
= {
properties
=> {
$name
=>
$p
->{
'x-json-schema'
} ||
$p
->{schema} ||
$p
},
required
=> [
$p
->{required} ? (
$name
) : ()]
};
my
$in
=
$p
->{in};
if
(
$in
eq
'body'
) {
warn
"[Swagger2] Validate $in $name\n"
if
DEBUG;
if
(
$p
->{
'x-json-schema'
}) {
return
$self
->_json_validator->validate({
$name
=>
$value
},
$schema
);
}
else
{
return
$self
->_validator->validate_input({
$name
=>
$value
},
$schema
);
}
}
elsif
(
defined
$value
) {
warn
"[Swagger2] Validate $in $name=$value\n"
if
DEBUG;
return
$self
->_validator->validate_input({
$name
=>
$value
},
$schema
);
}
else
{
warn
"[Swagger2] Validate $in $name=undef\n"
if
DEBUG;
return
$self
->_validator->validate_input({
$name
=>
$value
},
$schema
);
}
return
;
}
sub
_validate_response {
my
(
$self
,
$c
,
$data
,
$op_spec
,
$status
) =
@_
;
my
$headers
=
$c
->res->headers;
my
@errors
;
if
(
my
$blueprint
=
$op_spec
->{responses}{
$status
} ||
$op_spec
->{responses}{
default
}) {
my
$input
=
$headers
->to_hash(1);
for
my
$n
(
keys
%{
$blueprint
->{headers} || {}}) {
my
$p
=
$blueprint
->{headers}{
$n
};
if
(
$p
->{type} eq
'array'
) {
push
@errors
,
$self
->_validator->validate(
$input
->{
$n
},
$p
);
}
elsif
(
$input
->{
$n
}) {
push
@errors
,
$self
->_validator->validate(
$input
->{
$n
}[0],
$p
);
$headers
->header(
$n
=>
$input
->{
$n
}[0] ?
'true'
:
'false'
)
if
$p
->{type} eq
'boolean'
and !
@errors
;
}
}
if
(
$blueprint
->{
'x-json-schema'
}) {
warn
"[Swagger2] Validate using x-json-schema\n"
if
DEBUG;
push
@errors
,
$self
->_json_validator->validate(
$data
,
$blueprint
->{
'x-json-schema'
});
}
elsif
(
$blueprint
->{schema}) {
warn
"[Swagger2] Validate using schema\n"
if
DEBUG;
push
@errors
,
$self
->_validator->validate(
$data
,
$blueprint
->{schema});
}
}
else
{
push
@errors
,
$self
->_validator->validate(
$data
, {});
}
return
@errors
;
}
sub
_coerce_by_collection_format {
my
(
$value
,
$schema
) =
@_
;
my
$re
=
$Swagger2::SchemaValidator::COLLECTION_RE
{
$schema
->{collectionFormat}} ||
''
;
my
$type
=
$schema
->{items}{type} ||
''
;
my
@data
;
return
[
ref
$value
?
@$value
:
$value
]
unless
$re
;
defined
and
push
@data
,
split
/
$re
/
for
ref
$value
?
@$value
:
$value
;
return
[
map
{
$_
+ 0 }
@data
]
if
$type
eq
'integer'
or
$type
eq
'number'
;
return
\
@data
;
}
sub
_error {
return
{
errors
=> [{
message
=>
$_
[0],
path
=>
'/'
}]};
}
sub
_find_action {
return
if
$_
[1]->{controller};
my
(
$c
,
$defaults
) =
@_
;
my
$op
=
$defaults
->{swagger_operation_spec}{operationId} or
return
'operationId is missing.'
;
my
$can
=
sub
{
$defaults
->{controller}->can(
$defaults
->{action})
?
''
:
qq(Method "$defaults->{action}" not implemented.)
;
};
@$defaults
{
qw(action controller)
}
= _load(
$c
,
$op
,
$defaults
->{swagger_operation_spec}{
'x-mojo-controller'
});
return
$can
->()
if
$defaults
->{controller};
@$defaults
{
qw(action controller)
} = _load(
$c
,
split
$SKIP_OP_RE
,
$op
);
return
$can
->()
if
$defaults
->{controller};
$op
=~ s!
$SKIP_OP_RE
.*$!!;
@$defaults
{
qw(action controller)
} = _load(
$c
,
$op
=~ /^([a-z_]+)([A-Z]\w+)$/);
return
$can
->()
if
$defaults
->{controller};
return
qq(Controller from operationId "$defaults->{swagger_operation_spec}{operationId}" could not be loaded.)
;
}
sub
_io_error {
my
$c
=
shift
;
my
$level
= IO_LOGGING;
$c
->app->
log
->
$level
(
sprintf
'%s error: %s'
,
shift
, Mojo::JSON::encode_json(
shift
));
}
sub
_load {
my
(
$c
,
$action
,
$controller
) =
@_
;
my
(
@classes
,
%uniq
);
return
unless
$controller
;
$action
= decamelize
ucfirst
$action
;
if
(
$controller
=~ /::/) {
push
@classes
,
$controller
;
}
else
{
my
$singular
=
$controller
;
$singular
=~ s!s$!!;
push
@classes
,
grep
{ !
$uniq
{
$_
}++ }
map
{ (
"${_}::$controller"
,
"${_}::$singular"
) } @{
$c
->app->routes->namespaces};
}
while
(
$controller
=
shift
@classes
) {
my
$e
= Mojo::Loader::load_class(
$controller
);
warn
qq([Swagger2] Load "$controller": @{[ref $e ? $e : $e ? "Can't locate class" : "Success"]}.\n)
if
DEBUG;
return
(
$action
,
$controller
)
if
$controller
->can(
'new'
);
}
return
;
}
1;