APISchema

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for Perl extension APISchema

1.37 2018-02-08T10:31:50Z
  - Allow `example: undef` and serve document

1.36 2017-12-25T12:45:24Z
  - show enum values in type column

1.35 2017-12-13T14:30:28Z
  - MockServer serves default status codes in definitions

1.34 2017-11-30T16:29:26Z
  - Define prototype of type before calling it

1.33 2017-11-30T14:39:09Z
  - Resolve anyOf, allOf and oneOf keywords when serving Mock Server and API Document

1.32 2017-11-30T06:08:51Z
  - MockServer always returns bytes

1.31 2017-11-29T02:30:47Z
  - MockServer support `response => {body => 'bmi', encoding => 'json'}` syntax

LICENSE  view on Meta::CPAN

    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA  02110-1301 USA


Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) 19xx name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the
appropriate parts of the General Public License.  Of course, the
commands you use may be called something other than `show w' and `show
c'; they could even be mouse-clicks or menu items--whatever suits your
program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here a sample; alter the names:

README.md  view on Meta::CPAN

[![Build Status](https://travis-ci.org/hitode909/APISchema.svg?branch=master)](https://travis-ci.org/hitode909/APISchema)
# NAME

APISchema - Schema for API

# SYNOPSIS

    # bmi.def

    resource figure => {
        type => 'object',
        description => 'Figure, which includes weight and height',
        properties => {
            weight  => {
                type => 'number',
                description => 'Weight(kg)',
                example => 50,
            },
            height  => {
                type => 'number',
                description => 'Height(m)',
                example => 1.6,
            },
        },
        required => ['weight', 'height'],
    };

    resource bmi => {
        type => 'object',
        description => 'Body mass index',
        properties => {
            value  => {
                type => 'number',
                description => 'bmi value',
                example => 19.5,
            },
        },
        required => ['value'],
    };

    POST '/bmi/' => {
        title           => 'BMI API',
        description     => 'This API calculates your BMI.',

eg/bmi.psgi  view on Meta::CPAN

Sample Application

=head1 HOW TO USE

Start the appliction.

    carton exec -- plackup bmi.psgi

Then,

    % curl -X POST -H "Content-type: application/json" -d '{"weight": 60, "height": '1.7'}' http://localhost:5000/bmi
    {"value":20.7612456747405}

Requests and Reponses to the API are validated by Middlewares.

    % curl -X POST -H "Content-type: application/json" -d 'hello' http://localhost:5000/bmi
    {"attribute":"Valiemon::Attributes::Required","position":"/required"}

    % curl -X POST -H "Content-type: application/json" -d '{"weight": 60, "height": 'a'}' http://localhost:5000/bmi
    {"attribute":"Valiemon::Attributes::Required","position":"/required"}

    % curl -X POST -H "Content-type: application/json" -d '{"weight": 60, "height": 'a'}' http://localhost:5000/bmi
    {"attribute":"Valiemon::Attributes::Required","position":"/required"}

    % curl -X POST -H "Content-type: application/json" -d '{"weight": 60}' http://localhost:5000/bmi
    {"attribute":"Valiemon::Attributes::Required","position":"/required"}

You can read the document of API at L<http://localhost:5000/doc/>

lib/APISchema.pm  view on Meta::CPAN


=head1 NAME

APISchema - Schema for API

=head1 SYNOPSIS

    # bmi.def

    resource figure => {
        type => 'object',
        description => 'Figure, which includes weight and height',
        properties => {
            weight  => {
                type => 'number',
                description => 'Weight(kg)',
                example => 50,
            },
            height  => {
                type => 'number',
                description => 'Height(m)',
                example => 1.6,
            },
        },
        required => ['weight', 'height'],
    };

    resource bmi => {
        type => 'object',
        description => 'Body mass index',
        properties => {
            value  => {
                type => 'number',
                description => 'bmi value',
                example => 19.5,
            },
        },
        required => ['value'],
    };

    POST '/bmi/' => {
        title           => 'BMI API',
        description     => 'This API calculates your BMI.',

lib/APISchema/Generator/Markdown.pm  view on Meta::CPAN

                },
            );
        } @$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;
__DATA__
@@ index
? my ($schema, $toc_text, $routes_text, $resources_text) = @_;
# <?= $schema->title || '' ?>

lib/APISchema/Generator/Markdown.pm  view on Meta::CPAN


@@ toc
? my ($routes, $resources) = @_;

- [Routes](#routes)
? for (@$routes) {
    - [<?= $_->title ?>](#<?= anchor(route => $_->title) ?>) - <?= methods($_->method) ?> <?= $_->route ?>
? }
- [Resources](#resources)
? for (@$resources) {
?   next if ( $_->definition->{type} // '' ) eq 'hidden';
    - [<?= $_->title ?>](#<?= anchor(resource => $_->title) ?>)
? }

@@ route
? my ($route, $example, $text) = @_;
### <a name="<?= anchor(route => $route) ?>"></a> <?= $route->title ?>

?= $example->{req}
?= $example->{res}
?= $route->description || ''

lib/APISchema/Generator/Markdown.pm  view on Meta::CPAN

@@ request
? my ($route, $req) = @_;
#### Request  <?= methods($route->method) ?>
?= $req->{description} // ''

? if (scalar grep { $req->{$_} } qw(header parameter body)) {
|Part|Resource|Content-Type|Encoding|
|----|--------|------------|--------|
?   for (qw(header parameter)) {
?     next unless $req->{$_};
|<?= $_ ?>|<?= type($req->{$_}) ?>|-|-|
?   } # for
?   if ($req->{body}) {
?     for (sort keys %{$req->{encoding}}) {
|body|<?= type($req->{body}) ?>|<?= content_type($_) ?>|<?= content_type($req->{encoding}->{$_}) ?>|
?     } # for
?   } # $req->{body}
? } # scalar keys %$req

@@ response
? my ($route, $code, $res) = @_;
#### Response <?= http_status_code($code) ?>
?= $res->{description} // ''

? if (scalar grep { $res->{$_} } qw(header parameter body)) {
|Part|Resource|Content-Type|Encoding|
|----|--------|------------|--------|
?   for (qw(header)) {
?     next unless $res->{$_};
|<?= $_ ?>|<?= type($res->{$_}) ?>|-|-|
?   } # for
?   if ($res->{body}) {
?     for (sort keys %{$res->{encoding}}) {
|body|<?= type($res->{body}) ?>|<?= content_type($_) ?>|<?= content_type($res->{encoding}->{$_}) ?>|
?     } # for
?   } # $res->{body}
? } # scalar keys %$res

@@ resource
? my ($r, $resource, $properties) = @_;
### <a name="<?= anchor(resource => $resource) ?>"></a> `<?= $resource->title ?>` : <?= type($resource->definition) ?>
```javascript
<?= pretty_json $r->example($resource->definition) ?>
```

?= $resource->definition->{description} || ''

#### Properties

? if (scalar @$properties) {
|Property|Type|Default|Example|Restrictions|Description|
|--------|----|-------|-------|------------|-----------|
?   for my $prop (@$properties) {
?     my $def = $prop->{definition};
|`<?= $prop->{path} ?>` |<?= type($def) ?> |<?= code($def->{default}) ?> |<?= code($def->{example}, exists $def->{example}) ?> |<?= restriction($def) ?> |<?= desc($def->{description}) ?> |
?   } # $prop
? } # scalar @$properties

lib/APISchema/Generator/Markdown/Formatter.pm  view on Meta::CPAN

package APISchema::Generator::Markdown::Formatter;
use 5.014;
use strict;
use warnings;

# core
use Exporter qw(import);
our @EXPORT = qw(type json pretty_json code restriction desc anchor method methods content_type http_status http_status_code);

# cpan
use HTTP::Status qw(status_message);
use URI::Escape qw(uri_escape_utf8);
use JSON::XS ();
my $JSON = JSON::XS->new->canonical(1);

use constant +{
    RESTRICTIONS => [qw(required max_items min_items max_length min_length maximum minimum pattern)],
    SHORT_DESCRIPTION_LENGTH => 100,
};

sub type ($); # type has recursive call

sub type ($) {
    my $def = shift;
    my $bar = '&#124;';

    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

    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`";
}

sub http_status ($) {
    my $code = shift;
    return undef unless $code;
    return join(' ', $code, status_message($code));
}

sub http_status_code {
    return _code http_status shift;

lib/APISchema/Generator/Markdown/ResourceResolver.pm  view on Meta::CPAN

    new => 1,
    ro  => [qw(schema)],
);

sub _foreach_properties($$&) {
    my ($name_path, $definition, $callback) = @_;
    return unless (ref $definition || '') eq 'HASH';

    if ($definition->{items}) {
        my $items = $definition->{items};
        my $type = ref $items || '';
        if ($type eq 'HASH') {
            $callback->([@$name_path, '[]'], $items);
        } elsif ($type eq 'ARRAY') {
            $callback->([@$name_path, "[$_]"], $items->{$_}) for (0..$#$items);
        }
    }

    if ($definition->{properties}) {
        my $items = $definition->{properties};
        my $type = ref $items || '';
        if ($type eq 'HASH') {
            $callback->([@$name_path, $_], $items->{$_}) for keys %$items;
        }
    }
}

sub _property_name (@) {
    my @name_path = @_;
    return '.' . join '.', @name_path;
}

lib/APISchema/Generator/Markdown/ResourceResolver.pm  view on Meta::CPAN

    }

    my $ref = $definition->{'$ref'};
    if ($ref) {
        $ref = $ref =~ s/^#//r;
        my $def = JSON::Pointer->get($self->schema, $ref);
        return ($self->_collect_example($path, $def), 1) if $def;
    }

    my %result;
    my $type = $definition->{type} || '';
    _foreach_properties($path, $definition, sub {
        my ($example, $exists) = $self->_collect_example(@_);
        unless ($exists) {
            if (exists $_[1]->{default}) {
                $example = $_[1]->{default};
                $exists = 1;
            }
        }
        $result{$_[0]->[-1]} = $example if $exists;
    });

    return (\%result, 1) if $type eq 'object';

    if ($type eq 'array') {
        return ([ $result{'[]'} ], 1) if $result{'[]'};

        my @result;
        for (keys %result) {
            next unless $_ =~ /\A\[([0-9]+)\]\z/;
            $result[$1] = $result{$_};
        }
        return (\@result, 1);
    }

lib/APISchema/Validator.pm  view on Meta::CPAN


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" } )
            unless $encoding_spec;
    }

    my $method = $encoding_spec;
    return ( undef, {
        message      => "Unknown decoding method: $method",
        content_type => $content_type,
    } )
        unless APISchema::Validator::Decoder->new->can($method);

    return ($method, undef);
}

sub _validate {
    my ($validator_class, $decode, $target, $spec) = @_;

    my $obj = eval { APISchema::Validator::Decoder->new->$decode($target) };

lib/APISchema/Validator.pm  view on Meta::CPAN

    my $resource_root = $schema->get_resource_root;
    my $resource_spec = $route->$method(
        $resource_root,
        $target->{status_code} ? [ $target->{status_code} ] : [],
        [ @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,

lib/Plack/Middleware/APISchema/RequestValidator.pm  view on Meta::CPAN

    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);
    return [
        $status_code,
        [ 'Content-Type' => 'application/json' ],
        [ encode_json_canonical($errors) ],
    ] if scalar keys %$errors;

lib/Plack/Middleware/APISchema/RequestValidator.pm  view on Meta::CPAN


    $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 $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;
            @$res = (
                500,
                [ 'Content-Type' => 'application/json', 'X-Error-Cause' => $error_cause ],
                [ encode_json_canonical($errors) ],
            );

t/APISchema-DSL.t  view on Meta::CPAN

        };
    };

    subtest 'Validation should be returned' => sub {
        lives_ok {
            my $schema = APISchema::DSL::process {
                title 'BMI API';
                description 'The API to calculate BMI';

                resource figure => {
                    type => 'object',
                    description => 'Figure, which includes weight and height',
                    properties => {
                        weight  => {
                            type => 'number',
                            description => 'Weight(kg)',
                            example => 50,
                        },
                        height  => {
                            type => 'number',
                            description => 'Height(m)',
                            example => 1.6,
                        },
                    },
                    required => ['weight', 'height'],
                };

                resource bmi => {
                    type => 'object',
                    description => 'Body mass index',
                    properties => {
                        value  => {
                            type => 'number',
                            description => 'bmi value',
                            example => 19.5,
                        },
                    },
                    required => ['value'],
                };

                POST '/bmi' => {
                    title           => 'BMI API',
                    description     => 'This API calculates your BMI.',

t/APISchema-DSL.t  view on Meta::CPAN

                    on_match => sub { 1 },
                };
            };
            isa_ok $schema, 'APISchema::Schema';

            is_deeply [ sort {
                $a->title cmp $b->title;
            } @{$schema->get_resources} ], [ {
                title => 'bmi',
                definition => {
                    type => 'object',
                    description => 'Body mass index',
                    properties => {
                        value  => {
                            type => 'number',
                            description => 'bmi value',
                            example => 19.5,
                        },
                    },
                    required => ['value'],
                },
            }, {
                title => 'figure',
                definition => {
                    type => 'object',
                    description => 'Figure, which includes weight and height',
                    properties => {
                        weight  => {
                            type => 'number',
                            description => 'Weight(kg)',
                            example => 50,
                        },
                        height  => {
                            type => 'number',
                            description => 'Height(m)',
                            example => 1.6,
                        },
                    },
                    required => ['weight', 'height'],
                },
            } ];

            is $schema->title, 'BMI API';
            is $schema->description, 'The API to calculate BMI';

t/APISchema-DSL.t  view on Meta::CPAN

        isa_ok $schema, 'APISchema::Schema';

        is $schema->title, 'BMI API';
        is $schema->description, 'The API to calculate BMI';

        is_deeply [ sort {
            $a->title cmp $b->title;
        } @{$schema->get_resources} ], [ {
            title => 'bmi',
            definition => {
                type => 'object',
                description => 'Body mass index',
                properties => {
                    value  => {
                        type => 'number',
                        description => 'bmi value',
                        example => 19.5,
                    },
                },
                required => ['value'],
            },
        }, {
            title => 'figure',
            definition => {
                type => 'object',
                description => 'Figure, which includes weight and height',
                properties => {
                    weight  => {
                        type => 'number',
                        description => 'Weight(kg)',
                        example => 50,
                    },
                    height  => {
                        type => 'number',
                        description => 'Height(m)',
                        example => 1.6,
                    },
                },
                required => ['weight', 'height'],
            },
        } ];

        my $routes = $schema->get_routes;
        is scalar @$routes, 1;

t/APISchema-DSL.t  view on Meta::CPAN

    my $schema = APISchema::DSL::process {
        include 't/fixtures/user.def';
    };

    isa_ok $schema, 'APISchema::Schema';

    is $schema->title, decode_utf8('ユーザー');
    is $schema->description, decode_utf8('ユーザーの定義');

    cmp_deeply $schema->get_resource_by_name('user')->{definition}, {
        type => 'object',
        description => decode_utf8('ユーザー'),
        properties => {
            first_name  => {
                type => 'string',
                description => decode_utf8('å§“'),
                example => decode_utf8('小飼'),
            },
            last_name  => {
                type => 'string',
                description => decode_utf8('名'),
                example => decode_utf8('å¼¾'),
            },
        },
        required => ['first_name', 'last_name'],
    };
}

t/APISchema-Generator-Markdown-Formatter.t  view on Meta::CPAN

package t::APISchema::Generator::Markdown::Formatter;
use lib '.';
use t::test;
use t::test::fixtures;

use APISchema::Generator::Markdown::Formatter ();

sub _type : Tests {
    for my $case (
        [{} => 'undefined'],
        [{type => 'object'} => '`object`'],
        [{type => ['object', 'number']} =>  '`"object"`&#124;`"number"`'],
        [{'$ref' => '#/resource/foo'} =>  '[`foo`](#resource-foo)'],
        [{oneOf => [{ type =>'object'}, {type =>'number'}]} =>  '`object`&#124;`number`'],
        [{type => 'string', enum => ['a', 'b', 'c']} =>  '`"a"`&#124;`"b"`&#124;`"c"`'],
        [{type => 'number', enum => [1, 2, 3]} =>  '`1`&#124;`2`&#124;`3`'],
    ) {
       is APISchema::Generator::Markdown::Formatter::type($case->[0]), $case->[1], $case->[2] || $case->[1];
    }
}

t/APISchema-Resource.t  view on Meta::CPAN

sub _require : Test(startup => 1) {
    my ($self) = @_;

    use_ok 'APISchema::Resource';
}

sub instantiate : Tests {
    my $resource = APISchema::Resource->new(
        title => 'Human',
        definition => {
            type => 'object',
            properties => {
                name  => { type => 'string'  },
                age => { type => 'integer' },
            },
            required => ['name', 'age'],
        },
    );
    cmp_deeply $resource, isa('APISchema::Resource') & methods(
        title => 'Human',
        definition => {
            type => 'object',
            properties => {
                name  => { type => 'string'  },
                age => { type => 'integer' },
            },
            required => ['name', 'age'],
        },
    );
}

t/APISchema-Schema.t  view on Meta::CPAN

}

sub resource : Tests {
    my $schema = APISchema::Schema->new;

    is $schema->get_resource_by_name('user'), undef;

    cmp_deeply $schema->get_resources, [];

    $schema->register_resource('user' => {
        type => 'object',
        properties => {
            name  => { type => 'string'  },
            age => { type => 'integer' },
        },
        required => ['name', 'age'],
    });

    cmp_deeply $schema->get_resource_by_name('user'), isa('APISchema::Resource') & methods(
        title => 'user',
        definition => {
            type => 'object',
            properties => {
                name  => { type => 'string'  },
                age => { type => 'integer' },
            },
            required => ['name', 'age'],
        },
    );

    is $schema->get_resource_by_name('not_user'), undef;

    cmp_deeply $schema->get_resources, [
        $schema->get_resource_by_name('user'),
    ];

t/APISchema-Validator.t  view on Meta::CPAN

}

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' => {
            header => { foo => 'bar' },
            parameter => 'foo&bar',
            body => '{"foo":"bar"}',
            content_type => 'application/json',
        }, $schema);
        ok $result->is_valid;
    };

    subtest 'valid with empty target' => sub {
        my $schema = _simple_route t::test::fixtures::prepare_bmi, [];
        my $validator = APISchema::Validator->for_request;
        my $result = $validator->validate('/endpoint' => {}, $schema);
        ok $result->is_valid;
    };

    subtest 'valid with some target and schema' => 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/json',
        }, $schema);
        ok $result->is_valid;
    };

    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 {

t/APISchema-Validator.t  view on Meta::CPAN

            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);
        ok $result->is_valid;
    };

    subtest 'valid with strict content-type check' => sub {
        my $schema = _strict_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;
    };

    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;
    };

t/APISchema-Validator.t  view on Meta::CPAN

            [ ('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}),
            content_type => 'application/json',
        }, $schema);
        ok $result->is_valid;
    };

    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' },
            body => '{"foo":"bar"}',
            content_type => 'application/json',
        }, $schema);
        ok $result->is_valid;
    };

    subtest 'valid with empty target' => sub {
        my $schema = _simple_route t::test::fixtures::prepare_bmi, [];
        my $validator = APISchema::Validator->for_response;
        my $result = $validator->validate('/endpoint' => {}, $schema);
        ok $result->is_valid;
    };

    subtest 'valid with some target and schema' => 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/json',
        }, $schema);
        ok $result->is_valid;
    };

    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);
        ok $result->is_valid;
    };

    subtest 'valid with strict content-type check' => sub {
        my $schema = _strict_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;
    };

    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;
    };

t/APISchema-Validator.t  view on Meta::CPAN

        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',
        }, $schema);
        ok $result->is_valid;
    };

    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') ];
    };

t/APISchema-Validator.t  view on Meta::CPAN

        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,
            }, {
                name => 'Charlie',
                age  => 14,
            } ]),
            content_type => 'application/json',
        }, $schema);
        ok $result->is_valid;
    };

    subtest 'invalid 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,
            }, {
                age  => 14,
            } ]),
            content_type => 'application/json',
        }, $schema);
        ok !$result->is_valid;
    };

 SKIP: {
    skip 'Recursive dereference is not implemented in Valiemon', 2;
    subtest 'valid recursively referenced resource' => sub {
        my $schema = _forced_route t::test::fixtures::prepare_family, ['body'];
        my $validator = APISchema::Validator->for_request;
        my $result = $validator->validate('Children GET API' => {

t/Plack-App-APISchema-Document.t  view on Meta::CPAN


sub serve_document : Tests {
     my $schema = t::test::fixtures::prepare_bmi;
     my $app = Plack::App::APISchema::Document->new(schema => $schema)->to_app;

     subtest 'when valid request' => sub {
         test_psgi $app => sub {
             my $server = shift;
             my $res = $server->(GET '/');
             is $res->code, 200;
             is $res->header('content-type'), 'text/html; charset=utf-8';
             like $res->content, qr{<h3 id="toc_8"><a name="resource-figure"></a> <code>figure</code> : <code>object</code></h3>};
             done_testing;
         }
     };
}

sub mojibake : Tests {
    my $schema = t::test::fixtures::prepare_author;
    my $app = Plack::App::APISchema::Document->new(schema => $schema)->to_app;

     subtest 'when valid request' => sub {
         test_psgi $app => sub {
             my $server = shift;
             my $res = $server->(GET '/');
             is $res->code, 200;
             is $res->header('content-type'), 'text/html; charset=utf-8';
             like $res->content, qr{td>著者</td>};
             done_testing;
         }
     };
}

sub inheritable : Tests {
    my $schema = t::test::fixtures::prepare_bmi;
    my $app = t::test::InheritedDocument->new(schema => $schema)->to_app;

    subtest 'Document is inheritable' => sub {
        test_psgi $app => sub {
            my $server = shift;
            my $res = $server->(GET '/');
            is $res->code, 200;
            is $res->header('content-type'), 'text/html; charset=utf-8';
            like $res->content, qr{pink};
            done_testing;
        };
    };
}

t/Plack-App-APISchema-MockServer.t  view on Meta::CPAN


sub serve_document_bmi : Tests {
     my $schema = t::test::fixtures::prepare_bmi;
     my $app = Plack::App::APISchema::MockServer->new(schema => $schema)->to_app;

     subtest 'when valid request' => sub {
         test_psgi $app => sub {
             my $server = shift;
             my $res = $server->(POST '/bmi');
             is $res->code, 200;
             is $res->header('content-type'), 'application/json; charset=utf-8';
             is $res->content, q!{"value":19.5}!;
         }
     };

     subtest 'when invalid request' => sub {
         test_psgi $app => sub {
             my $server = shift;
             my $res = $server->(POST '/notfound');
             is $res->code, 404;
             is $res->header('content-type'), 'text/plain; charset=utf-8';
             is $res->content, q!not found!;
         }
     };
}

sub when_encoding_is_specified : Tests {
    my $schema = t::test::fixtures::prepare_bmi;
    $schema->register_route(
        method => 'POST',
        route => '/bmi_force_json',

t/Plack-App-APISchema-MockServer.t  view on Meta::CPAN

        response_resource => {
            encoding => 'json',
            body => 'bmi',
        },
    );
    my $app = Plack::App::APISchema::MockServer->new(schema => $schema)->to_app;
    test_psgi $app => sub {
        my $server = shift;
        my $res = $server->(POST '/bmi_force_json');
        is $res->code, 200;
        is $res->header('content-type'), 'application/json; charset=utf-8';
        is $res->content, q!{"value":19.5}!;
    }
}

sub with_wide_character : Tests {
    my $schema = t::test::fixtures::prepare_author;
    $schema->register_route(
        method => 'GET',
        route => '/author',
        response_resource => {

t/Plack-App-APISchema-MockServer.t  view on Meta::CPAN

            body => 'author',
        },
    );

    my $app = Plack::App::APISchema::MockServer->new(schema => $schema)->to_app;

    test_psgi $app => sub {
        my $server = shift;
        my $res = $server->(GET '/author');
        is $res->code, 200;
        is $res->header('content-type'), 'application/json; charset=utf-8';
        is $res->content, q!{"author_name":"著者"}!;
    };
}

sub one_of : Tests {
    my $schema = t::test::fixtures::prepare_bmi;
    $schema->register_resource(maybe_bmi => {
        oneOf => [
            {
                type => 'object',
                '$ref' => '#/resource/bmi',
            },
            {
                type => 'null',
            },
        ],
    });
    $schema->register_route(
        method => 'GET',
        route => '/maybe_bmi',
        response_resource => {
            encoding => 'json',
            body => 'maybe_bmi',
        },
    );

    my $app = Plack::App::APISchema::MockServer->new(schema => $schema)->to_app;

    test_psgi $app => sub {
        my $server = shift;
        my $res = $server->(GET '/maybe_bmi');
        is $res->code, 200;
        is $res->header('content-type'), 'application/json; charset=utf-8';
        is $res->content, q!{"value":19.5}!;
    };
}

sub status_201 : Tests {
    my $schema = t::test::fixtures::prepare_bmi;
    $schema->register_route(
        method => 'PUT',
        route => '/put_bmi',
        request_resource => {

t/Plack-Middleware-APISchema-RequestValidator.t  view on Meta::CPAN

        },
    );
    $schema->register_route(
        method => 'POST',
        route => '/bmi_by_parameter',
        request_resource => {
            parameter => 'figure',
        },
    );
    $schema->register_resource(figure_header => {
        type => 'object',
        properties => {
            'x_weight' => { type => 'number' },
            'x_height' => { type => 'number' },
        },
        required => ['x_weight', 'x_height'],
    });
    $schema->register_route(
        method => 'POST',
        route => '/bmi_by_header',
        request_resource => {
            header => 'figure_header',
        },
    );

t/Plack-Middleware-APISchema-RequestValidator.t  view on Meta::CPAN

            cmp_deeply $res->content, json({
                body => {
                    message => "Failed to parse json",
                    encoding => 'json',
                },
            });
            done_testing;
        }
    };

    subtest 'when content-type is incorrect' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $content_type = 'application/x-www-form-urlencoded';
            my $res = $server->(
                POST '/bmi',
                Content_Type => $content_type,
                Content => encode_json({weight => 50, height => 1.6}),
            );
            is $res->code, HTTP_UNPROCESSABLE_ENTITY;
            cmp_deeply $res->content, json({
                body => {
                    attribute => 'Valiemon::Attributes::Required',
                    position => '/$ref/required',
                    message => "Contents do not match resource 'figure'",
                    encoding => 'url_parameter',
                    actual => isa('HASH'),
                    # XXX: Hash order randomization
                    # actual => { "{\"weight\":50,\"height\":1.6}" => undef }
                    expected => $schema->get_resource_by_name('figure')->definition,
                },
            });
            done_testing;
        }
    };

    subtest 'when content-type is incorrect with forced encoding' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $content_type = 'application/x-www-form-urlencoded';
            my $res = $server->(
                POST '/bmi_force_json',
                Content_Type => $content_type,
                Content => encode_json({weight => 50, height => 1.6}),
            );
            is $res->code, 200;
            done_testing;
        }
    };

    subtest 'when content-type is incorrect with strict content-type check' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $content_type = 'application/x-www-form-urlencoded';
            my $res = $server->(
                POST '/bmi_strict',
                Content_Type => $content_type,
                Content => encode_json({weight => 50, height => 1.6}),
            );
            is $res->code, HTTP_UNSUPPORTED_MEDIA_TYPE;
            cmp_deeply $res->content, json({
                body => { message => "Wrong content-type: $content_type" },
            });
            done_testing;
        }
    };

    subtest 'when valid request by parameter' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $res = $server->(
                POST '/bmi_by_parameter?weight=50&height=1.6',

t/Plack-Middleware-APISchema-RequestValidator.t  view on Meta::CPAN

            is $res->code, HTTP_UNPROCESSABLE_ENTITY;
            cmp_deeply $res->content, json({
                header => {
                    attribute => 'Valiemon::Attributes::Required',
                    position => '/$ref/required',
                    message => "Contents do not match resource 'figure_header'",
                    encoding => 'perl',
                    actual => {
                        content_length => 0,
                        x_weight       => 50,
                        content_type   => "application/x-www-form-urlencoded",
                        host           => "localhost",
                    },
                    expected => $schema->get_resource_by_name('figure_header')->definition,
                },
            });
            done_testing;
        }
    };
}

t/Plack-Middleware-APISchema-RequestValidator.t  view on Meta::CPAN

                Content => encode_json({
                    first_name => 'Bill',
                    last_name => [],
                }),
            );
            is $res->code, HTTP_UNPROCESSABLE_ENTITY;
            cmp_deeply $res->content, json({
                body  => {
                    actual    => [],
                    attribute => 'Valiemon::Attributes::Type',
                    position  => '/$ref/properties/last_name/type',
                    expected  => $schema->get_resource_by_name('user')->definition->{properties}->{last_name},
                    encoding  => 'json',
                    message   => "Contents do not match resource 'user'",
                },
            });
            done_testing;
        };
    };
}

t/Plack-Middleware-APISchema-ResponseValidator.t  view on Meta::CPAN

    );
    $schema->register_route(
        method => 'POST',
        route => '/bmi_force_json',
        response_resource => {
            encoding => 'json',
            body => 'bmi',
        },
    );
    $schema->register_resource(bmi_header => {
        type => 'object',
        properties => {
            'x_value' => { type => 'number' },
        },
        required => ['x_value'],
    });
    $schema->register_route(
        method => 'POST',
        route => '/bmi_by_header',
        response_resource => {
            header => 'bmi_header',
        },
    );

    my $content_type;
    my $json;
    my $middleware = Plack::Middleware::APISchema::ResponseValidator->new(schema => $schema);
    my $header = [];
    $middleware->wrap(sub {
        [200, [ 'Content-Type' => $content_type, @$header ], [ $json ]  ]
    });

    subtest 'when valid response' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/json';
            $json = encode_json({value => 19.5});
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi');
            is $res->code, 200;
            done_testing;
        }
    };

    subtest 'when invalid response' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/json';
            $json = encode_json({value => 'aaa'});
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                body => {
                    attribute => "Valiemon::Attributes::Type",
                    position => '/$ref/properties/value/type',
                    expected => $schema->get_resource_by_name('bmi')->definition->{properties}->{value},
                    actual => 'aaa',
                    message => "Contents do not match resource 'bmi'",
                    encoding => 'json',
                },
            });
            done_testing;
        }
    };

    subtest 'when wrong content-type' => sub {
        test_psgi $middleware => sub {
            $content_type = 'text/plain';
            $json = encode_json({value => 19.5});
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                body => { message => "Wrong content-type: text/plain" },
            });
            done_testing;
        }
    };

    subtest 'when response is not a JSON' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/json';
            $json = 'aaa';
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                body => {
                    message => "Failed to parse json",
                    encoding => 'json',
                },
            });
            done_testing;
        }
    };

    subtest 'when content-type is incorrect' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/x-www-form-urlencoded';
            $json = encode_json({value => 19.5});
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                body => {
                    attribute => 'Valiemon::Attributes::Required',
                    position => '/$ref/required',

t/Plack-Middleware-APISchema-ResponseValidator.t  view on Meta::CPAN

                    actual => {
                        '{"value":19.5}' => undef,
                    },
                    expected => $schema->get_resource_by_name('bmi')->definition,
                },
            });
            done_testing;
        }
    };

    subtest 'when content-type is incorrect with forced encoding' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/x-www-form-urlencoded';
            $json = encode_json({value => 19.5});
            $header = [];
            my $server = shift;
            my $res = $server->(POST '/bmi_force_json');
            is $res->code, 200;
            done_testing;
        }
    };

    subtest 'when content-type is incorrect with strict content-type check' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/x-www-form-urlencoded';
            $json = encode_json({value => 19.5});
            my $server = shift;
            my $res = $server->(POST '/bmi_strict');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                body => { message => "Wrong content-type: $content_type" },
            });
            done_testing;
        }
    };

    subtest 'other endpoints are not affected' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $res = $server->(GET '/other/');
            is $res->code, 200;
        }
    };

    subtest 'when valid response by header' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/json';
            $json = encode_json({});
            $header = [ 'X-Value' => 19.5 ];
            my $server = shift;
            my $res = $server->(POST '/bmi_by_header');
            is $res->code, 200;
            done_testing;
        }
    };

    subtest 'when invalid response by header' => sub {
        test_psgi $middleware => sub {
            $content_type = 'application/json';
            $json = encode_json({});
            $header = [ 'X-Foo' => 1 ];
            my $server = shift;
            my $res = $server->(POST '/bmi_by_header');
            is $res->code, 500;
            is $res->header('X-Error-Cause'), 'Plack::Middleware::APISchema::ResponseValidator+Valiemon';
            cmp_deeply $res->content, json({
                header => {
                    attribute => "Valiemon::Attributes::Required",
                    position => '/$ref/required',
                    message => "Contents do not match resource 'bmi_header'",
                    encoding => 'perl',
                    expected => $schema->get_resource_by_name('bmi_header')->definition,
                    actual => {
                        content_type => "application/json",
                        "x_foo" => 1,
                    },
                },
            });
            done_testing;
        }
    };
}
sub status : Tests {
    my $schema = t::test::fixtures::prepare_status;

t/Plack-Middleware-APISchema-ResponseValidator.t  view on Meta::CPAN

    });

    subtest 'invalid response with utf8' => sub {
        test_psgi $middleware => sub {
            my $server = shift;
            my $res = $server->(GET '/user');
            is $res->code, 500;
            cmp_deeply $res->content, json({
                body => {
                    attribute => "Valiemon::Attributes::Type",
                    position => '/$ref/properties/last_name/type',
                    expected => $schema->get_resource_by_name('user')->definition->{properties}->{last_name},
                    actual => [],
                    message => "Contents do not match resource 'user'",
                    encoding => 'json',
                },
            });
            done_testing;
        };
    };
}

t/fixtures/author.def  view on Meta::CPAN

resource author => {
    type => 'object',
    description => '著者',
    properties => {
        author_name  => {
            type => 'string',
            description => '著者',
            example => '著者',
        },
    },
};

t/fixtures/bmi.def  view on Meta::CPAN

title 'BMI API';
description 'The API to calculate BMI';

resource figure => {
    type => 'object',
    description => 'Figure, which includes weight and height',
    properties => {
        weight  => {
            type => 'number',
            description => 'Weight(kg)',
            example => 50,
        },
        height  => {
            type => 'number',
            description => 'Height(m)',
            example => 1.6,
        },
    },
    required => ['weight', 'height'],
};

resource bmi => {
    type => 'object',
    description => 'Body mass index',
    properties => {
        value  => {
            type => 'number',
            description => 'bmi value',
            example => 19.5,
        },
    },
    required => ['value'],
};

POST '/bmi' => {
    title           => 'BMI API',
    description     => 'This API calculates your BMI.',

t/fixtures/boolean.def  view on Meta::CPAN

title 'Object with Boolean';

resource value => {
    type => 'object',
    description => 'the result',
    properties => {
        value  => {
            type => 'boolean',
            description => 'The Value',
            example => \1,
        },
    },
    required => ['value'],
};

t/fixtures/example-null.def  view on Meta::CPAN

title 'Example with null';

resource value => {
    type => 'object',
    description => 'the result',
    properties => {
        value  => {
            type => 'null',
            description => 'The Value',
            example => undef,
        },
    },
    required => ['value'],
};

t/fixtures/family.def  view on Meta::CPAN

title 'Family API';
description 'The API to define or exaplain a family';

resource person => {
    type => 'object',
    description => 'A person',
    properties => {
        name => {
            type => 'string',
            description => 'The name of the person',
            example => 'Alice',
        },
        age => {
            type => 'integer',
            description => 'The age of the person',
            example => 16,
        },
    },
    required => ['name', 'age'],
};

resource target => {
    type => 'object',
    description => 'Target of retrieving information',
    properties => {
        name => { '$ref' => '#/resource/person/properties/name' },
    },
};

resource parent => {
    '$ref' => '#/resource/target',
    description => 'Target of retrieving/defining children',
    example => { name => 'Bob' },
};

resource people => {
    type => 'array',
    description => 'Some people',
    items => {
        '$ref' => '#/resource/person',
    },
    example => [ {
        name => 'Alice',
        age  => 16,
    }, {
        name => 'Charlie',
        age  => 14,
    } ],
};

resource result => {
    type => 'object',
    description => 'Result of an operation',
    properties => {
        status => {
            enum => [ 'success', 'failure' ],
            example => 'success',
        },
        message => {
            type => 'string',
            example => 'OK',
        },
    },
};

PUT '/person' => {
    title => 'Person PUT API',
    description => 'Define a new person',
    destination => {},
    request => 'person',

t/fixtures/status.def  view on Meta::CPAN

title 'Status';
description 'Return various HTTP status';

resource none => {
    type => 'object',
    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',
        },
    },
};

GET '/get' => {
    title => 'Get API',
    description => 'Get something',
    destination => {},
    request => 'none',

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.',
    response        => 'value',
};

t/fixtures/user.def  view on Meta::CPAN

title 'ユーザー';
description 'ユーザーの定義';

resource user => {
    type => 'object',
    description => 'ユーザー',
    properties => {
        first_name  => {
            type => 'string',
            description => 'å§“',
            example => '小飼',
        },
        last_name  => {
            type => 'string',
            description => '名',
            example => 'å¼¾',
        },
    },
    required => ['first_name', 'last_name'],
};



( run in 1.779 second using v1.01-cache-2.11-cpan-df04353d9ac )