Atto

 view release on metacpan or  search on metacpan

lib/Atto.pm  view on Meta::CPAN

package Atto;
$Atto::VERSION = '0.005';
# ABSTRACT: A tiny microservice builder

use 5.008001;
use warnings;
use strict;

use Carp qw(croak);
use JSON::MaybeXS ();
use WWW::Form::UrlEncoded qw(parse_urlencoded);
use Plack::Request;

my %methods_for_package;

sub import {
    my ($class, @methods) = @_;
    my $package = caller;
    $methods_for_package{$package} = { map { $_ => undef } @methods };
}

sub psgi {
    my $package = caller;

    my $methods = $methods_for_package{$package};
    for my $method (keys %$methods) {
        my $coderef = do { no strict 'refs'; *{$package.'::'.$method}{CODE} };
        croak "method $method not found in $package" unless $coderef;
        $methods->{$method} = $coderef;
    }

    my $json = JSON::MaybeXS->new->utf8->allow_nonref;

    my $response = sub {
        my ($code, $raw) = @_;
        my $body = [ eval { $json->encode($raw) } ];
        if ($@) {
            $code = 500;
            $body = [ $json->encode("couldn't encode response: $@") ];
        }

        [ $code, [ 'Content-type' => 'application/json' ], $body ]
    };

    sub {
        my ($env) = @_;

        return $response->(405, "request method must be POST or GET (not $env->{REQUEST_METHOD})") unless grep { $env->{REQUEST_METHOD} eq $_ } qw(POST GET);

        my ($method) = $env->{REQUEST_URI} =~ m{^/([^/?]+)};
        return $response->(400, "method not found in request URL") unless defined $method;

        return $response->(404, "method not found") unless $methods->{$method};

        my $args = {};

        if ($env->{REQUEST_METHOD} eq 'GET') {
            my $req = Plack::Request->new($env);
            %$args = $req->query_parameters->flatten;
        }

        elsif ($env->{REQUEST_METHOD} eq 'POST') {
            my $len = 0+($env->{CONTENT_LENGTH} || 0);

            if ($len > 0) {
                return $response->(400, "content type not provided") unless defined $env->{CONTENT_TYPE};

                if ($env->{CONTENT_TYPE} eq 'application/json') {
                    my $nread = $env->{'psgi.input'}->read(my $content, $len);
                    return $response->(400, sprintf("expected %d bytes (from content-length), got %d", $len, $nread)) if $nread != $len;

                    $args = eval { $json->decode($content) };
                    return $response->(400, $@) if $@;
                }
                elsif ($env->{CONTENT_TYPE} eq 'application/x-www-form-urlencoded') {
                    my $nread = $env->{'psgi.input'}->read(my $content, $len);
                    return $response->(400, sprintf("expected %d bytes (from content-length), got %d", $len, $nread)) if $nread != $len;

                    %$args = parse_urlencoded($content);
                    return $response->(400, $@) if $@;
                }
                else {
                    return $response->(400, "unknown content type");
                }
            }
        }

        else {
            return $response->(405, "request method must be POST or GET (not $env->{REQUEST_METHOD})");
        }


        # XXX prototypes

        my @args =
            ref $args eq 'ARRAY' ? @$args :
            ref $args eq 'HASH'  ? %$args :
            ($args);

        my $ret = eval { $methods->{$method}->(@args) };
        return $response->(500, "method call failed: $@") if $@;

        return $response->(200, $ret);
    }
}

1;
__END__

=pod

=encoding UTF-8



( run in 1.533 second using v1.01-cache-2.11-cpan-98e64b0badf )