APISchema

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

    - 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

MANIFEST  view on Meta::CPAN

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.',



( run in 0.381 second using v1.01-cache-2.11-cpan-65fba6d93b7 )