File Coverage

lib/PAGI/App/URLMap.pm
Criterion Covered Total %
statement 40 45 88.8
branch 4 6 66.6
condition 5 10 50.0
subroutine 7 8 87.5
pod 2 4 50.0
total 58 73 79.4


line stmt bran cond sub pod time code
1             package PAGI::App::URLMap;
2              
3 1     1   199391 use strict;
  1         1  
  1         32  
4 1     1   3 use warnings;
  1         5  
  1         41  
5 1     1   4 use Future::AsyncAwait;
  1         2  
  1         6  
6              
7             =head1 NAME
8              
9             PAGI::App::URLMap - Mount apps at URL path prefixes
10              
11             =head1 SYNOPSIS
12              
13             use PAGI::App::URLMap;
14              
15             my $map = PAGI::App::URLMap->new;
16             $map->mount('/api' => $api_app);
17             $map->mount('/static' => $static_app);
18             my $app = $map->to_app;
19              
20             =cut
21              
22             sub new {
23 4     4 0 209362 my ($class, %args) = @_;
24              
25             return bless {
26             mounts => [],
27             default => $args{default},
28 4         16 }, $class;
29             }
30              
31             sub mount {
32 6     6 1 58 my ($self, $path, $app) = @_;
33              
34 6         23 $path =~ s{/+$}{}; # Remove trailing slashes
35 6         7 push @{$self->{mounts}}, [$path, $app];
  6         20  
36             # Keep sorted by length (longest first) for proper matching
37 6         7 @{$self->{mounts}} = sort { length($b->[0]) <=> length($a->[0]) } @{$self->{mounts}};
  6         12  
  2         5  
  6         13  
38 6         8 return $self;
39             }
40              
41             sub map {
42 0     0 1 0 my ($self, $mapping) = @_;
43              
44 0         0 while (my ($path, $app) = each %$mapping) {
45 0         0 $self->mount($path, $app);
46             }
47 0         0 return $self;
48             }
49              
50             sub to_app {
51 4     4 0 14 my ($self) = @_;
52              
53 4         4 my @mounts = @{$self->{mounts}};
  4         9  
54 4         7 my $default = $self->{default};
55              
56 4     4   84 return async sub {
57 4         5 my ($scope, $receive, $send) = @_;
58 4   50     11 my $path = $scope->{path} // '/';
59              
60 4         6 for my $mount (@mounts) {
61 4         20 my ($prefix, $app) = @$mount;
62              
63 4 100 33     67 if ($prefix eq '' || $path eq $prefix || $path =~ /^\Q$prefix\E\//) {
      66        
64             # Match found - adjust path for mounted app
65 3         5 my $new_path = $path;
66 3         17 $new_path =~ s/^\Q$prefix\E//;
67 3 50       5 $new_path = '/' if $new_path eq '';
68              
69             my $new_scope = {
70             %$scope,
71             path => $new_path,
72 3   50     19 script_name => ($scope->{script_name} // '') . $prefix,
73             };
74              
75 3         6 await $app->($new_scope, $receive, $send);
76 3         332 return;
77             }
78             }
79              
80             # No match - use default or 404
81 1 50       3 if ($default) {
82 0         0 await $default->($scope, $receive, $send);
83             } else {
84 1         5 await $send->({
85             type => 'http.response.start',
86             status => 404,
87             headers => [['content-type', 'text/plain']],
88             });
89 1         65 await $send->({ type => 'http.response.body', body => 'Not Found', more => 0 });
90             }
91 4         18 };
92             }
93              
94             1;
95              
96             __END__