view release on metacpan or search on metacpan
- Prettify the output of examples (aereal++)
1.01 2015-09-04T09:32:37Z
- Recognize HTTP PATCH method (aereal++)
1.00 2015-08-27T10:48:03Z
- Support schema which contains unicode characters
- Enable `use utf8` in DSL file
0.03 2015-08-18T02:06:45Z
- Validation error has more details. expected: expected schema, actual: actual input data
0.02 2015-03-16T02:35:36Z
- Add a status_code parameter to P::MW::AS::RequestValidator
0.01 2015-02-18T10:41:05Z
- original version
t/APISchema.t
t/Plack-App-APISchema-Document.t
t/Plack-App-APISchema-MockServer.t
t/Plack-Middleware-APISchema-RequestValidator.t
t/Plack-Middleware-APISchema-ResponseValidator.t
t/fixtures/author.def
t/fixtures/bmi.def
t/fixtures/boolean.def
t/fixtures/example-null.def
t/fixtures/family.def
t/fixtures/runtime-error.def
t/fixtures/status.def
t/fixtures/syntax-error.def
t/fixtures/user.def
t/test.pm
t/test/InheritedDocument.pm
t/test/fixtures.pm
META.yml
MANIFEST
lib/APISchema/Validator.pm view on Meta::CPAN
my $class = shift;
return $class->_new(@_, fetch_resource_method => 'canonical_request_resource');
}
sub for_response {
my $class = shift;
return $class->_new(@_, fetch_resource_method => 'canonical_response_resource');
}
sub _valid_result { APISchema::Validator::Result->new_valid(@_) }
sub _error_result { APISchema::Validator::Result->new_error(@_) }
sub _resolve_encoding {
my ($content_type, $encoding_spec) = @_;
# TODO handle charset?
$content_type = $content_type =~ s/\s*;.*$//r;
$encoding_spec //= DEFAULT_ENCODING_SPEC;
if (ref $encoding_spec) {
$encoding_spec = $encoding_spec->{$content_type};
return ( undef, { message => "Wrong content-type: $content_type" } )
lib/APISchema/Validator.pm view on Meta::CPAN
[ @target_keys ],
);
@target_keys = grep { $resource_spec->{$_} } @target_keys;
my $body_encoding = $resource_spec->{body} && do {
my ($enc, $err) = _resolve_encoding(
$target->{content_type} // '',
$resource_spec->{encoding},
);
if ($err) {
return _error_result(body => $err);
}
$enc;
};
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;
}
1;
__END__
lib/APISchema/Validator/Result.pm view on Meta::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 $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);
return [
$status_code,
[ 'Content-Type' => 'application/json' ],
[ encode_json_canonical($errors) ],
] if scalar keys %$errors;
$self->app->($env);
}
sub router {
my ($self) = @_;
$self->{router} //= do {
my $generator = APISchema::Generator::Router::Simple->new;
$generator->generate_router($self->schema);
};
}
sub _resolve_status_code {
my ($self, $validation_result) = @_;
my $error_message = $validation_result->errors->{body}->{message} // '';
return $error_message =~ m/Wrong content-type/ ? HTTP_UNSUPPORTED_MEDIA_TYPE : HTTP_UNPROCESSABLE_ENTITY;
}
1;
lib/Plack/Middleware/APISchema/ResponseValidator.pm view on Meta::CPAN
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;
@$res = (
500,
[ 'Content-Type' => 'application/json', 'X-Error-Cause' => $error_cause ],
[ encode_json_canonical($errors) ],
);
return;
}
$res->[2] = [ $body ];
});
}
sub router {
my ($self) = @_;
t/APISchema-DSL.t view on Meta::CPAN
};
dies_ok {
my $schema = APISchema::DSL::process {
include 'not-such-file';
};
};
dies_ok {
my $schema = APISchema::DSL::process {
include 't/fixtures/syntax-error.def';
};
};
dies_ok {
my $schema = APISchema::DSL::process {
include 't/fixtures/runtime-error.def';
};
};
}
sub with_unicode : Tests {
my $schema = APISchema::DSL::process {
include 't/fixtures/user.def';
};
isa_ok $schema, 'APISchema::Schema';
t/APISchema-Validator.t view on Meta::CPAN
is $validator->fetch_resource_method, 'canonical_response_resource';
};
subtest 'Result' => sub {
my $r = APISchema::Validator::Result->new;
isa_ok $r, 'APISchema::Validator::Result';
my $valid = APISchema::Validator::Result->new_valid;
isa_ok $valid, 'APISchema::Validator::Result';
my $error = APISchema::Validator::Result->new_valid;
isa_ok $error, 'APISchema::Validator::Result';
};
}
sub result : Tests {
subtest 'empty' => sub {
my $r = APISchema::Validator::Result->new;
ok $r->is_valid;
is_deeply $r->errors, {};
};
subtest 'valid without target' => sub {
my $r = APISchema::Validator::Result->new_valid;
ok $r->is_valid;
is_deeply $r->errors, {};
};
subtest 'valid with targets' => sub {
my $r = APISchema::Validator::Result->new_valid(qw(foo));
ok $r->is_valid;
is_deeply $r->errors, {};
};
subtest 'error without target' => sub {
my $r = APISchema::Validator::Result->new_error;
ok !$r->is_valid;
};
subtest 'error without target' => sub {
my $r = APISchema::Validator::Result->new_error(foo => 'bar');
ok !$r->is_valid;
is_deeply $r->errors, { foo => 'bar' };
};
subtest 'merge' => sub {
my $r = APISchema::Validator::Result->new;
$r->merge(APISchema::Validator::Result->new_valid());
ok $r->is_valid;
is_deeply $r->errors, {};
$r->merge(APISchema::Validator::Result->new_valid(qw(foo)));
ok $r->is_valid;
is_deeply $r->errors, {};
$r->merge(APISchema::Validator::Result->new_error(bar => 1));
ok !$r->is_valid;
is_deeply $r->errors, { bar => 1 };
$r->merge(APISchema::Validator::Result->new_error(foo => 3));
ok !$r->is_valid;
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
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);
ok ! $result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'body' ];
}
};
subtest 'invalid without parameter' => sub {
for my $value ({}, '', undef) {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['parameter'];
my $validator = APISchema::Validator->for_request;
my $result = $validator->validate('/endpoint' => {
parameter => $value,
}, $schema);
ok ! $result->is_valid;
is_deeply [ keys %{$result->errors} ], [ 'parameter' ];
}
};
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
subtest 'many invalid' => 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 },
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
subtest 'many invalid' => sub {
my $schema = _simple_route t::test::fixtures::prepare_bmi, ['body', 'header'];
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,
t/fixtures/status.def view on Meta::CPAN
description => 'Empty object',
properties => {},
};
resource ok => {
type => 'string',
description => 'Success',
example => 'Succeeded!',
};
resource error => {
type => 'object',
description => 'Failure',
properties => {
status => {
type => 'number',
example => 400,
},
message => {
type => 'string',
example => 'Bad Request',
t/fixtures/status.def view on Meta::CPAN
title => 'Get API',
description => 'Get something',
destination => {},
request => 'none',
response => {
200 => {
body => 'ok',
encoding => 'perl',
},
400 => {
body => 'error',
encoding => 'json',
},
},
};
t/fixtures/syntax-error.def view on Meta::CPAN
title 'Incorrect API definition';
description 'API definition with a syntax error';
resource value => {
type => 'number',
description => 'value' # , missing here
example => 1,
};
GET '/value' => {
title => 'Value API',
description => 'Endpoint for test.',