fix environment changes within subdispatch and arrange for middleware to uplevel...
[catagits/Web-Simple.git] / lib / Web / Dispatch.pm
1 package Web::Dispatch;
2
3 use Sub::Quote;
4 use Scalar::Util qw(blessed);
5
6 sub MAGIC_MIDDLEWARE_KEY { __PACKAGE__.'.middleware' }
7
8 use Moo;
9 use Web::Dispatch::Parser;
10 use Web::Dispatch::Node;
11
12 with 'Web::Dispatch::ToApp';
13
14 has app => (is => 'ro', required => 1);
15 has parser_class => (
16   is => 'ro', default => quote_sub q{ 'Web::Dispatch::Parser' }
17 );
18 has node_class => (
19   is => 'ro', default => quote_sub q{ 'Web::Dispatch::Node' }
20 );
21 has node_args => (is => 'ro', default => quote_sub q{ {} });
22 has _parser => (is => 'lazy');
23
24 sub _build__parser {
25   my ($self) = @_;
26   $self->parser_class->new;
27 }
28
29 sub call {
30   my ($self, $env) = @_;
31   $self->_dispatch($env, $self->app);
32 }
33
34 sub _dispatch {
35   my ($self, $env, @match) = @_;
36   while (my $try = shift @match) {
37     if (ref($try) eq 'HASH') {
38       $env = { %$env, %$try };
39       next;
40     } elsif (ref($try) eq 'ARRAY') {
41       return $try;
42     }
43     my @result = $self->_to_try($try, \@match)->($env, @match);
44     next unless @result and defined($result[0]);
45     if (ref($result[0]) eq 'ARRAY') {
46       if (@{$result[0]} == 1 and ref($result[0][0]) eq 'CODE') {
47         return $result[0][0];
48       }
49       return $result[0];
50     } elsif (blessed($result[0]) && $result[0]->isa('Plack::Middleware')) {
51       die "Multiple results but first one is a middleware ($result[0])"
52         if @result > 1;
53       # middleware needs to uplevel exactly once to wrap the rest of the
54       # level it was created for - next elsif unwraps it
55       return { MAGIC_MIDDLEWARE_KEY, $result[0] };
56       my $mw = $result[0];
57     } elsif (
58       ref($result[0]) eq 'HASH'
59       and my $mw = $result[0]->{+MAGIC_MIDDLEWARE_KEY}
60     ) {
61       $mw->app(sub { $self->_dispatch($_[0], @match) });
62       return $mw->to_app->($env);
63     } elsif (blessed($result[0]) && !$result[0]->can('to_app')) {
64       return $result[0];
65     } else {
66       # make a copy so we don't screw with it assigning further up
67       my $env = $env;
68       unshift @match, sub { $self->_dispatch($env, @result) };
69     }
70   }
71   return;
72 }
73
74 sub _to_try {
75   my ($self, $try, $more) = @_;
76   if (ref($try) eq 'CODE') {
77     if (defined(my $proto = prototype($try))) {
78       $self->_construct_node(
79         match => $self->_parser->parse($proto), run => $try
80       )->to_app;
81     } else {
82       $try
83     }
84   } elsif (!ref($try) and ref($more->[0]) eq 'CODE') {
85     $self->_construct_node(
86       match => $self->_parser->parse($try), run => shift(@$more)
87     )->to_app;
88   } elsif (blessed($try) && $try->can('to_app')) {
89     $try->to_app;
90   } else {
91     die "No idea how we got here with $try";
92   }
93 }
94
95 sub _construct_node {
96   my ($self, %args) = @_;
97   $self->node_class->new({ %{$self->node_args}, %args });
98 }
99
100 1;