view release on metacpan or search on metacpan
lib/APISchema/DSL.pm view on Meta::CPAN
use APISchema::Schema;
# core
use Carp ();
# cpan
use Exporter 'import';
use Path::Class qw(file);
my %schema_meta = (
( map { $_ => "${_}_resource" } qw(request response) ),
( map { $_ => $_ } qw(title description destination option) ),
);
our %METHODS = (
( map { $_ => $_ } qw(HEAD GET POST PUT DELETE PATCH) ),
FETCH => [qw(GET HEAD)],
);
our @DIRECTIVES = (qw(include filter resource title description), keys %METHODS);
our @EXPORT = @DIRECTIVES;
my $_directive = {};
sub process (&) {
my $dsl = shift;
lib/APISchema/DSL.pm view on Meta::CPAN
};
my @filters;
local $_directive->{filter} = sub {
push @filters, $_[0];
};
local $_directive->{resource} = sub {
$schema->register_resource(@_);
};
local @$_directive{keys %METHODS} = map {
my $m = $_;
sub {
my ($path, @args) = @_;
for my $filter (reverse @filters) {
local $Carp::CarpLevel += 1;
@args = $filter->(@args);
}
my ($definition, $option) = @args;
$schema->register_route(
( map {
defined $definition->{$_} ?
( $schema_meta{$_} => $definition->{$_} ) : ();
} keys %schema_meta ),
defined $option ? (option => $option) : (),
route => $path,
method => $METHODS{$m},
);
};
} keys %METHODS;
lib/APISchema/Generator/Markdown.pm view on Meta::CPAN
use Text::MicroTemplate::DataSection qw();
sub new {
my ($class) = @_;
my $renderer = Text::MicroTemplate::DataSection->new(
escape_func => undef
);
bless {
renderer => $renderer,
map {
( $_ => $renderer->build_file($_) );
} qw(index toc route resource request response
request_example response_example),
}, $class;
}
sub resolve_encoding ($) {
my ($resources) = @_;
$resources = { body => $resources } unless ref $resources;
my $encoding = $resources->{encoding} // { '' => 'auto' };
lib/APISchema/Generator/Markdown.pm view on Meta::CPAN
schema => $root,
);
return $self->{index}->(
$renderer,
$schema,
$self->{toc}->(
$renderer,
$routes,
$resources,
),
join('', map {
my $route = $_;
my $req = resolve_encoding($route->request_resource);
my $request_resource = $route->canonical_request_resource($root);
my $codes = $route->responsible_codes;
my $default_code = $route->default_responsible_code;
my $response_resource = $route->canonical_response_resource($root, [
$default_code
]);
my $res = $_->response_resource;
$res = $_->responsible_code_is_specified
? { map { $_ => resolve_encoding($res->{$_}) } @$codes }
: { '' => resolve_encoding($res) };
$self->{route}->(
$renderer,
$route,
{
req => $self->{request_example}->(
$renderer,
$route,
APISchema::Generator::Markdown::ExampleFormatter->new(
lib/APISchema/Generator/Markdown.pm view on Meta::CPAN
$route,
$default_code,
APISchema::Generator::Markdown::ExampleFormatter->new(
resolver => $resolver,
spec => $response_resource,
),
),
},
{
req => $self->{request}->($renderer, $route, $req),
res => join("\n", map {
$self->{response}->($renderer, $route, $_, $res->{$_});
} sort keys %$res),
},
);
} @$routes),
join('', map {
my $properties = $resolver->properties($_->definition);
$self->{resource}->($renderer, $resolver, $_, [ map { +{
path => $_,
definition => $properties->{$_},
} } sort keys %$properties ]);
} grep {
( $_->definition->{type} // '' ) ne 'hidden';
} @$resources),
);
}
1;
lib/APISchema/Generator/Markdown/ExampleFormatter.pm view on Meta::CPAN
sub header {
my ($self) = @_;
my $header = $self->spec->{header} or return '';
my $resource = $header->definition or return '';
my $example = $self->example($resource);
return '' unless defined $example;
return '' unless (ref $example) eq 'HASH';
return '' unless scalar keys %$example;
return join "\n", map {
sprintf '%s: %s', $_ =~ s/[_]/-/gr, $example->{$_};
} sort keys %$example;
}
sub parameter {
my ($self) = @_;
my $parameter = $self->spec->{parameter} or return '';
my $resource = $parameter->definition or return '';
my $example = $self->example($resource);
return '' unless defined $example;
return '' unless (ref $example) eq 'HASH';
return '' unless scalar keys %$example;
return '?' . join '&', map {
# TODO multiple values?
sprintf '%s=%s', map { uri_escape_utf8 $_ } $_, $example->{$_};
} sort keys %$example;
}
sub body {
my ($self) = @_;
my $body = $self->spec->{body} or return '';
my $resource = $body->definition or return '';
my $example = $self->example($resource);
return '' unless defined $example;
lib/APISchema/Generator/Markdown/Formatter.pm view on Meta::CPAN
sub type ($); # type has recursive call
sub type ($) {
my $def = shift;
my $bar = '|';
if (ref $def) {
for my $type (qw(oneOf anyOf allOf)) {
if (my $union = $def->{$type}) {
return join($bar, map { type($_) } @$union);
}
}
}
my $ref = ref $def ? $def->{'$ref'} : $def;
if ($ref) {
$ref = $ref =~ s!^#/resource/!!r;
my $ref_text = "`$ref`";
my $name = $ref =~ s!/.*$!!r;
$ref_text = sprintf('[%s](#%s)', $ref_text, anchor(resource => $name)) if $name;
return $ref_text;
}
return join $bar, map { code($_) } @{$def->{enum}} if $def->{enum};
my $type = $def->{type};
if ($type) {
return sprintf '`%s`', $type unless ref $type;
return join $bar, map { code($_) } @{$type} if ref $type eq 'ARRAY';
}
return 'undefined';
}
sub json ($) {
my $x = shift;
if (ref $x eq 'SCALAR') {
if ($$x eq 1) {
$x = 'true';
lib/APISchema/Generator/Markdown/Formatter.pm view on Meta::CPAN
}
sub method ($) {
my $method = shift;
return $method->[0] if (ref $method || '') eq 'ARRAY';
return $method;
}
sub methods ($) {
my $method = shift;
return join ', ', map { _code($_) } @$method
if (ref $method || '') eq 'ARRAY';
return _code($method);
}
sub content_type ($) {
my $type = shift;
return '-' unless length($type);
return "`$type`";
}
lib/APISchema/Generator/Router/Simple.pm view on Meta::CPAN
sub inject_routes {
my ($self, $schema, $router) = @_;
my $router_class = ref $router;
for my $route (@{$schema->get_routes}) {
my $option = $route->option // {};
$option = merge $option, $option->{$router_class} // {};
$router->connect($route->title, $route->route => $route->destination, {
method => $route->method,
map { $_ => $option->{$_} } qw(host on_match),
});
}
$router;
}
1;
lib/APISchema/Route.pm view on Meta::CPAN
$method = "${method}_resource";
my $resource = $self->$method();
for (@$extra_args) {
last unless $resource && ref $resource eq 'HASH';
last unless $resource->{$_};
$resource = $resource->{$_};
}
$resource = { body => $resource } unless ref $resource;
$filter = [qw(header parameter body)] unless scalar @$filter;
my %filter = map { $_ => 1 } grep { $resource->{$_} } @$filter;
return +{
%$resource,
map {
my $name = $resource->{$_};
$_ => APISchema::Resource->new(
title => $name,
definition => ,+{
%$resource_root,
'$ref' => sprintf '#/resource/%s', $name,
},
);
} grep { $filter{$_} } qw(header parameter body),
};
lib/APISchema/Schema.pm view on Meta::CPAN
sub get_resource_by_name {
my ($self, $name) = @_;
$self->{resources}->{$name || ''};
}
sub get_resource_root {
my ($self) = @_;
return +{
resource => +{ map {
$_ => $self->{resources}->{$_}->definition;
} keys %{$self->{resources}} },
properties => {},
};
}
sub _next_title_candidate {
my ($self, $base_title) = @_;
if ($base_title =~ /\(([0-9]+)\)$/) {
my $index = $1 + 1;
lib/APISchema/Validator.pm view on Meta::CPAN
my $encoding = {
body => $body_encoding,
parameter => 'url_parameter',
header => 'perl',
};
my $validator_class = $self->validator_class;
load_class $validator_class;
my $result = APISchema::Validator::Result->new;
$result->merge($_) for map {
my $field = $_;
my $err = _validate($validator_class, map { $_->{$field} } (
$encoding, $target, $resource_spec,
));
$err ? _error_result($field => {
%$err,
encoding => $encoding->{$_},
}) : _valid_result($field);
} @target_keys;
return $result;
}
lib/APISchema/Validator/Result.pm view on Meta::CPAN
use List::MoreUtils qw(all);
# cpan
use Hash::Merge::Simple ();
use Class::Accessor::Lite (
new => 1,
);
sub new_valid {
my ($class, @targets) = @_;
return $class->new(values => { map { ($_ => [1]) } @targets });
}
sub new_error {
my ($class, $target, $err) = @_;
return $class->new(values => { ( $target // '' ) => [ undef, $err] });
}
sub _values { shift->{values} // {} }
sub merge {
my ($self, $other) = @_;
$self->{values} = Hash::Merge::Simple::merge(
$self->_values,
$other->_values,
);
return $self;
}
sub errors {
my $self = shift;
return +{ map {
my $err = $self->_values->{$_}->[1];
$err ? ( $_ => $err ) : ();
} keys %{$self->_values} };
}
sub is_valid {
my $self = shift;
return all { $self->_values->{$_}->[0] } keys %{$self->_values};
}
lib/Plack/Middleware/APISchema/RequestValidator.pm view on Meta::CPAN
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
my ($matched, $route) = $self->router->routematch($env);
$matched or return $self->app->($env);
my $validator = APISchema::Validator->for_request(
validator_class => $self->validator // DEFAULT_VALIDATOR_CLASS,
);
my $result = $validator->validate($route->name => {
header => +{ map {
my $field = lc($_) =~ s/[-]/_/gr;
( $field => $req->header($_) );
} $req->headers->header_field_names },
parameter => $env->{QUERY_STRING},
body => $req->content,
content_type => $req->content_type,
}, $self->schema);
my $errors = $result->errors;
my $status_code = $self->_resolve_status_code($result);
lib/Plack/Middleware/APISchema/ResponseValidator.pm view on Meta::CPAN
my $plack_res = Plack::Response->new(@$res);
my $body;
Plack::Util::foreach($res->[2] // [], sub { $body .= $_[0] });
my $validator_class = $self->validator // DEFAULT_VALIDATOR_CLASS;
my $validator = APISchema::Validator->for_response(
validator_class => $validator_class,
);
my $result = $validator->validate($route->name => {
status_code => $res->[0],
header => +{ map {
my $field = lc($_) =~ s/[-]/_/gr;
( $field => $plack_res->header($_) );
} $plack_res->headers->header_field_names },
body => $body,
content_type => scalar $plack_res->content_type,
}, $self->schema);
my $errors = $result->errors;
if (scalar keys %$errors) {
my $error_cause = join '+', __PACKAGE__, $validator_class;
t/APISchema-Validator.t view on Meta::CPAN
is_deeply $r->errors, { foo => 3, bar => 1 };
};
}
sub _simple_route ($$) {
my ($schema, $keys) = @_;
$keys = [qw(header parameter body)] unless defined $keys;
$schema->register_route(
route => '/endpoint',
request_resource => {
map { $_ => 'figure' } @$keys
},
response_resource => {
map { $_ => 'bmi' } @$keys
},
);
return $schema;
}
sub _forced_route ($$) {
my ($schema, $keys) = @_;
$keys = [qw(header parameter body)] unless defined $keys;
$schema->register_route(
route => '/endpoint',
request_resource => {
encoding => 'json',
map { $_ => 'figure' } @$keys
},
response_resource => {
encoding => 'json',
map { $_ => 'bmi' } @$keys
},
);
return $schema;
}
sub _invalid_encoding_route ($$) {
my ($schema, $keys) = @_;
$keys = [qw(header parameter body)] unless defined $keys;
$schema->register_route(
route => '/endpoint',
request_resource => {
encoding => 'hoge',
map { $_ => 'figure' } @$keys
},
response_resource => {
encoding => 'hoge',
map { $_ => 'bmi' } @$keys
},
);
return $schema;
}
sub _strict_route ($$) {
my ($schema, $keys) = @_;
$keys = [qw(header parameter body)] unless defined $keys;
$schema->register_route(
route => '/endpoint',
request_resource => {
encoding => { 'application/json' => 'json' },
map { $_ => 'figure' } @$keys
},
response_resource => {
encoding => { 'application/json' => 'json' },
map { $_ => 'bmi' } @$keys
},
);
return $schema;
}
sub validate_request : Tests {
subtest 'valid with emtpy schema' => sub {
my $schema = APISchema::Schema->new;
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
t/APISchema-Validator.t view on Meta::CPAN
subtest 'invalid with missing property' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
body => encode_json({weight => 50}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('json') ];
};
subtest 'invalid without body' => sub {
for my $value ({}, '', undef) {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
body => $value,
}, $schema);
t/APISchema-Validator.t view on Meta::CPAN
subtest 'invalid with wrong encoding' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
body => encode_json({weight => 50, height => 1.6}),
content_type => 'application/x-www-form-urlencoded',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('url_parameter') ];
};
subtest 'invalid with invalid encoding' => sub {
my $schema = _invalid_encoding_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
body => encode_json({weight => 50, height => 1.6}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{message} } values %{$result->errors} ],
[ ('Unknown decoding method: hoge') ];
};
subtest 'valid with forced encoding' => sub {
my $schema = _forced_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
body => encode_json({weight => 50, height => 1.6}),
content_type => 'application/x-www-form-urlencoded',
}, $schema);
t/APISchema-Validator.t view on Meta::CPAN
subtest 'invalid with wrong content type' => sub {
my $schema = _strict_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_request;
my $content_type = 'application/x-www-form-urlencoded';
my $result = $validator->validate('/endpoint' => {
body => encode_json({weight => 50, height => 1.6}),
content_type => $content_type,
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{message} } values %{$result->errors} ],
[ ("Wrong content-type: $content_type") ];
};
subtest 'valid parameter' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['parameter'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
parameter => 'weight=50&height=1.6',
}, $schema);
ok $result->is_valid;
};
subtest 'invalid parameter' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['parameter'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
parameter => 'weight=50',
}, $schema);
ok !$result->is_valid;
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('url_parameter') ];
};
subtest 'valid header' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['header'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
header => { weight => 50, height => 1.6 },
}, $schema);
ok $result->is_valid;
};
subtest 'invalid header' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['header'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
header => { weight => 50 },
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'header' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('perl') ];
};
subtest 'all valid' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body', 'parameter', 'header'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
header => { weight => 50, height => 1.6 },
parameter => 'weight=50&height=1.6',
body => encode_json({weight => 50, height => 1.6}),
t/APISchema-Validator.t view on Meta::CPAN
my $result = $validator->validate('/endpoint' => {
header => { weight => 50 },
parameter => 'weight=50',
body => encode_json({weight => 50}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is scalar keys %{$result->errors}, 3;
is_deeply [ sort keys %{$result->errors} ],
[ qw(body header parameter) ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') x 3 ];
is_deeply [ sort map { $_->{encoding} } values %{$result->errors} ],
[ ('json', 'perl', 'url_parameter') ];
};
}
sub validate_response : Tests {
subtest 'valid with emtpy schema' => sub {
my $schema = APISchema::Schema->new;
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
header => { foo => 'bar' },
t/APISchema-Validator.t view on Meta::CPAN
subtest 'invalid with missing property' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
body => encode_json({hoge => 'foo'}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('json') ];
};
subtest 'invalid with wrong encoding' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
body => encode_json({value => 19.5}),
content_type => 'application/x-www-form-urlencoded',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('url_parameter') ];
};
subtest 'invalid with invalid encoding' => sub {
my $schema = _invalid_encoding_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
body => encode_json({value => 19.5}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{message} } values %{$result->errors} ],
[ ('Unknown decoding method: hoge') ];
};
subtest 'valid with forced encoding' => sub {
my $schema = _forced_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
body => encode_json({value => 19.5}),
content_type => 'application/x-www-form-urlencoded',
}, $schema);
t/APISchema-Validator.t view on Meta::CPAN
subtest 'invalid with wrong content type' => sub {
my $schema = _strict_route t::test::fixtures::prepare_bmi, ['body'];
my $validator = APISchema::Validator->for_response;
my $content_type = 'application/x-www-form-urlencoded';
my $result = $validator->validate('/endpoint' => {
body => encode_json({value => 19.5}),
content_type => $content_type,
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
is_deeply [ map { $_->{message} } values %{$result->errors} ],
[ ("Wrong content-type: $content_type") ];
};
subtest 'valid header' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['header'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
header => { value => 19.5 },
}, $schema);
ok $result->is_valid;
};
subtest 'invalid header' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['header'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
header => {},
}, $schema);
ok !$result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'header' ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') ];
is_deeply [ map { $_->{encoding} } values %{$result->errors} ],
[ ('perl') ];
};
subtest 'all valid' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body', 'header'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
header => { value => 19.5 },
body => encode_json({value => 19.5}),
content_type => 'application/json',
t/APISchema-Validator.t view on Meta::CPAN
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('/endpoint' => {
header => {},
body => encode_json({}),
content_type => 'application/json',
}, $schema);
ok !$result->is_valid;
is scalar keys %{$result->errors}, 2;
is_deeply [ sort keys %{$result->errors} ],
[ qw(body header) ];
is_deeply [ map { $_->{attribute} } values %{$result->errors} ],
[ ('Valiemon::Attributes::Required') x 2 ];
is_deeply [ sort map { $_->{encoding} } values %{$result->errors} ],
[ ('json', 'perl') ];
};
subtest 'valid referenced resource' => sub {
my $schema = _forced_route t::test::fixtures::prepare_family, ['body'];
my $validator = APISchema::Validator->for_response;
my $result = $validator->validate('Children GET API' => {
body => encode_json([ {
name => 'Alice',
age => 16,