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

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

use APISchema::Generator::Markdown::ExampleFormatter;
use APISchema::Generator::Markdown::ResourceResolver;

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

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

    }

    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';
        } elsif ($$x eq 0) {
            $x = 'false';
        }

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

}

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

1;

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

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

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

    return (undef, 0);
}

sub properties {
    my ($self, $resource) = @_;
    return $self->_collect_properties([], $resource);
}

sub example {
    my ($self, $resource) = @_;
    my ($example) = $self->_collect_example([], $resource);

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

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) };
    return { message => "Failed to parse $decode" } if $@;

    my $validator = $validator_class->new($spec->definition);
    my ($valid, $err) = $validator->validate($obj);

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

}

my $JSON = JSON::XS->new->utf8;
sub json {
    my ($self, $body) = @_;
    return $JSON->decode($body);
}

sub url_parameter {
    my ($self, $body) = @_;
    return undef unless defined $body;
    return url_params_mixed($body, 1);
}

1;

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

    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,
    );

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"`|`"number"`'],
        [{'$ref' => '#/resource/foo'} =>  '[`foo`](#resource-foo)'],
        [{oneOf => [{ type =>'object'}, {type =>'number'}]} =>  '`object`|`number`'],
        [{type => 'string', enum => ['a', 'b', 'c']} =>  '`"a"`|`"b"`|`"c"`'],
        [{type => 'number', enum => [1, 2, 3]} =>  '`1`|`2`|`3`'],
    ) {
       is APISchema::Generator::Markdown::Formatter::type($case->[0]), $case->[1], $case->[2] || $case->[1];
    }
}

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

}

sub instantiate : Tests {
    my $schema = APISchema::Schema->new;
    isa_ok $schema, 'APISchema::Schema';
}

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

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

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

sub route : Tests {
    subtest 'Basic' => sub {
        my $schema = APISchema::Schema->new;
        cmp_deeply $schema->get_routes, [];

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

        $schema->register_route();
        is $schema->get_routes->[7]->title, 'empty_route(1)';

        $schema->register_route();
        is $schema->get_routes->[8]->title, 'empty_route(2)';
    };
}

sub title_description : Tests {
    my $schema = APISchema::Schema->new;
    is $schema->title, undef;
    is $schema->description, undef;

    $schema->title('BMI');
    is $schema->title, 'BMI';

    $schema->description('The API to calculate BMI');
    is $schema->description, 'The API to calculate BMI';
}

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

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

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

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

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

            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',
                    message => "Contents do not match resource 'bmi'",
                    encoding => 'url_parameter',
                    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 {

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

}
sub status : Tests {
    my $schema = t::test::fixtures::prepare_status;

    my $middleware_ok = Plack::Middleware::APISchema::ResponseValidator->new(schema => $schema);
    $middleware_ok->wrap(sub {
        my $env = shift;
        my $req = Plack::Request->new($env);
        if ($req->parameters->{success}) {
            return [ 200, [ 'Content-Type' => 'text/plain', ], [ 'OK' ]  ];
        } elsif ($req->parameters->{undefined}) {
            return [ 599, [ 'Content-Type' => 'application/json', ], [
                encode_json({ status => 599, message => 'Something wrong' }),
            ]  ];
        } else {
            return [ 400, [ 'Content-Type' => 'application/json', ], [
                encode_json({ status => 400, message => 'Bad Request' }),
            ]  ];
        }
    });

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

            my $server = shift;
            my $res = $server->(GET '/get');
            is $res->code, 400;
            done_testing;
        };
    };

    subtest 'Undefined status' => sub {
        test_psgi $middleware_ok => sub {
            my $server = shift;
            my $res = $server->(GET '/get?undefined=1');
            is $res->code, 599;
            done_testing;
        };
    };

    my $middleware_ng = Plack::Middleware::APISchema::ResponseValidator->new(schema => $schema);
    $middleware_ng->wrap(sub {
        my $env = shift;
        my $req = Plack::Request->new($env);
        return [ 400, [ 'Content-Type' => 'text/plain', ], [ 'OK' ] ];

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



( run in 3.641 seconds using v1.01-cache-2.11-cpan-d80b1682f3f )