APISchema

 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,



( run in 1.251 second using v1.01-cache-2.11-cpan-49f99fa48dc )