package Amon2::Web::Dispatcher::RouterBoom;
use strict;
use warnings;
use utf8;
use 5.008_001;
use Router::Boom::Method;

sub import {
    my $class = shift;
    my %args = @_;
    my $caller = caller(0);

    my $router = Router::Boom::Method->new();

    my $base;

    no strict 'refs';

    *{"${caller}::base"} = sub { $base = $_[0] };

    # functions
    #
    # get( '/path', 'Controller#action')
    # post('/path', 'Controller#action')
    # put('/path', 'Controller#action')
    # delete_('/path', 'Controller#action')
    # any( '/path', 'Controller#action')
    # get( '/path', sub { })
    # post('/path', sub { })
    # put('/path', sub { })
    # delete_('/path', sub { })
    # any( '/path', sub { })
    for my $method (qw(get post put delete_ any)) {
        *{"${caller}::${method}"} = sub {
            my ($path, $dest) = @_;

            my %dest;
            if (ref $dest eq 'CODE') {
                $dest{code} = $dest;
            } else {
                my ($controller, $method) = split('#', $dest);
                $dest{class}      = $base ? "${base}::${controller}" : $controller;
                $dest{method}     = $method if defined $method;
            }

            my $http_method;
            if ($method eq 'get') {
                $http_method = ['GET','HEAD'];
            } elsif ($method eq 'post') {
                $http_method = 'POST';
            } elsif ($method eq 'put') {
                $http_method = 'PUT';
            } elsif ($method eq 'delete_') {
                $http_method = 'DELETE';
            }

            $router->add($http_method, $path, \%dest);
        };
    }

    # class methods
    *{"${caller}::router"} = sub { $router };

    *{"${caller}::dispatch"} = sub {
        my ($class, $c) = @_;

        my $env = $c->request->env;
        if (my ($dest, $captured, $method_not_allowed) = $class->router->match($env->{REQUEST_METHOD}, $env->{PATH_INFO})) {
            if ($method_not_allowed) {
                return $c->res_405();
            }

            my $res = eval {
                if ($dest->{code}) {
                    return $dest->{code}->($c, $captured);
                } else {
                    my $method = $dest->{method};
                    $c->{args} = $captured;
                    return $dest->{class}->$method($c, $captured);
                }
            };
            if ($@) {
                if ($class->can('handle_exception')) {
                    return $class->handle_exception($c, $@);
                } else {
                    print STDERR "$env->{REQUEST_METHOD} $env->{PATH_INFO} [$env->{HTTP_USER_AGENT}]: $@";
                    return $c->res_500();
                }
            }
            return $res;
        } else {
            return $c->res_404();
        }
    };
}

1;
__END__

=head1 NAME

Amon2::Web::Dispatcher::RouterBoom - Router::Boom bindings

=head1 SYNOPSIS

    package MyApp2::Web::Dispatcher;
    use Amon2::Web::Dispatcher::RouterBoom;

    use MyApp::Web::C::Foo;

    base 'MyApp::Web::C';

    get '/' => 'Foo#bar';

    1;

=head1 DESCRIPTION

This is a router class for Amon2. It's based on Router::Boom.

=head1 DSL FUNCTIONS

=over 4

=item C<< get($path:Str, $destnation:Str) >>

=item C<< post($path:Str, $destnation:Str) >>

=item C<< put($path:Str, $destnation:Str) >>

=item C<< delete_($path:Str, $destnation:Str) >>

=item C<< any($path:Str, $destnation:Str) >>

    get  '/' => 'Root#index';
    get  '/:user' => 'User#show';
    any  '/:user/update' => 'User#update';
    post '/:user/blog/post' => 'Blog#post';
    put  '/:user/blog/put'  => 'Blog#put';
    delete_ '/:user/blog/:id' => 'Blog#remove';

Add routes by DSL. First argument is the path pattern in Path::Boom rules.
Second argument is the destination method path.

Destination method pass is C<${class}#${method}> form.

The path declared with get() accepts GET and HEAD.
The path declared with post() accepts POST method.
The path declared with put() accepts PUT method.
The path declared with delete_() accepts DELETE method.
The path declared with any() accepts any methods.

=item C<< base($klass:Str) >>

    base 'My::App::Web::C';

You can specify the base class name for 'Root#index' style definition.

If you are write your dispatcher in following code, then the method for '/' is C<< My::App::Web::C::Root->index >>.

    base 'My::App::Web::C';
    get '/' => 'Root#index';

=item C<< get($path:Str, $destnation:CodeRef) >>

=item C<< post($path:Str, $destnation:CodeRef) >>

=item C<< put($path:Str, $destnation:CodeRef) >>

=item C<< delete_($path:Str, $destnation:CodeRef) >>

=item C<< any($path:Str, $destnation:CodeRef) >>

    get  '/' => sub {
        my ($c) = @_;
        ...
    };
    get  '/:user' => sub {
        my ($c, $args) = @_;
        $c->render(
            'user.tx' => {
                user => $args->{user},
            },
        );
    };

Add routes by DSL. First argument is the path pattern in Path::Boom rules.
Second argument is the destination code.

Callback function's first argument is the context object. Second is the captured values from the router.

=back

=head1 ROUTING RULES

Router::Boom's routing rule is really flexible. You can embed regexp in your rule.

=over 4

=item C<< /foo/bar >>

String literal matches strings.

=item C<< /:foo >>

C<< :foo >> matches C<< qr{[^/]} >>. It's captured.

=item C<< /{foo} >>

C<< {foo} >> is same as C<< :foo >>.

=item C<< /{foo:.*} >>

You can use the custom regexp for capturing.

=item C<< /* >>

C<< * >> is same as C<< {*:.*} >>.

=back

=head1 EXCEPTION HANDLER

You can customize the exception handler. You can define the special named method 'handle_exception'.

    package MyApp::Web::Dispatcher;

    sub handle_exception {
        my ($class, $c, $e) = @_;

        if (UNIVERSAL::isa($e, 'My::Exception::Validation')) {
            return $c->create_simple_status_page(400, 'Bad Request');
        } else {
            return $c->res_500();
        }
    }

=head1 SEE ALSO

L<Amon2>