File Coverage

blib/lib/PAGI/Middleware/Rewrite.pm
Criterion Covered Total %
statement 56 61 91.8
branch 12 16 75.0
condition 7 14 50.0
subroutine 9 9 100.0
pod 1 1 100.0
total 85 101 84.1


line stmt bran cond sub pod time code
1             package PAGI::Middleware::Rewrite;
2             $PAGI::Middleware::Rewrite::VERSION = '0.002000';
3 1     1   241817 use strict;
  1         1  
  1         41  
4 1     1   4 use warnings;
  1         1  
  1         59  
5 1     1   373 use parent 'PAGI::Middleware';
  1         376  
  1         8  
6 1     1   53 use Future::AsyncAwait;
  1         1  
  1         6  
7              
8             =head1 NAME
9              
10             PAGI::Middleware::Rewrite - URL rewriting middleware
11              
12             =head1 SYNOPSIS
13              
14             use PAGI::Middleware::Builder;
15              
16             my $app = builder {
17             enable 'Rewrite',
18             rules => [
19             { from => qr{^/old/(.*)}, to => '/new/$1' },
20             { from => '/legacy', to => '/modern' },
21             ];
22             $my_app;
23             };
24              
25             =head1 DESCRIPTION
26              
27             PAGI::Middleware::Rewrite rewrites request paths before passing to the
28             inner application. Supports both exact matches and regex patterns.
29              
30             =head1 CONFIGURATION
31              
32             =over 4
33              
34             =item * rules (required)
35              
36             Arrayref of rewrite rules. Each rule is a hashref with:
37              
38             { from => '/old-path', to => '/new-path' }
39             { from => qr{^/user/(\d+)}, to => '/users/$1' }
40              
41             =item * redirect (default: 0)
42              
43             If true, send redirect response instead of rewriting internally.
44              
45             =item * redirect_code (default: 301)
46              
47             HTTP status code for redirects.
48              
49             =back
50              
51             =cut
52              
53             sub _init {
54 4     4   10 my ($self, $config) = @_;
55              
56             $self->{rules} = $config->{rules}
57 4   50     19 // die "Rewrite middleware requires 'rules' option";
58 4   100     19 $self->{redirect} = $config->{redirect} // 0;
59 4   100     16 $self->{redirect_code} = $config->{redirect_code} // 301;
60             }
61              
62             sub wrap {
63 4     4 1 59 my ($self, $app) = @_;
64              
65 4     4   166 return async sub {
66 4         12 my ($scope, $receive, $send) = @_;
67 4 50       16 if ($scope->{type} ne 'http') {
68 0         0 await $app->($scope, $receive, $send);
69 0         0 return;
70             }
71              
72 4         11 my $path = $scope->{path};
73 4         12 my $new_path = $self->_apply_rules($path);
74              
75             # No rewrite needed
76 4 100       19 if ($new_path eq $path) {
77 1         5 await $app->($scope, $receive, $send);
78 1         184 return;
79             }
80              
81             # Redirect mode
82 3 100       10 if ($self->{redirect}) {
83 1         4 my $location = $new_path;
84 1 50 33     5 if (defined $scope->{query_string} && $scope->{query_string} ne '') {
85 0         0 $location .= '?' . $scope->{query_string};
86             }
87 1         6 await $self->_send_redirect($send, $location);
88 1         88 return;
89             }
90              
91             # Internal rewrite
92             my $new_scope = $self->modify_scope($scope, {
93             path => $new_path,
94 2   33     33 original_path => $scope->{original_path} // $path,
95             });
96              
97 2         12 await $app->($new_scope, $receive, $send);
98 4         19 };
99             }
100              
101             sub _apply_rules {
102 4     4   10 my ($self, $path) = @_;
103              
104 4         8 for my $rule (@{$self->{rules}}) {
  4         13  
105 4         8 my $from = $rule->{from};
106 4         9 my $to = $rule->{to};
107              
108 4 100       13 if (ref $from eq 'Regexp') {
109 1 50       9 if ($path =~ $from) {
110 1         5 my @captures = ($path =~ $from);
111 1         6 my $new_path = $to;
112 1         6 for my $i (0 .. $#captures) {
113 1         3 my $n = $i + 1;
114 1         25 $new_path =~ s/\$$n/$captures[$i]/g;
115             }
116 1         6 return $new_path;
117             }
118             } else {
119 3 100       10 if ($path eq $from) {
120 2         9 return $to;
121             }
122             # Also check prefix match for directory-like rules
123 1 50       38 if ($path =~ m{^\Q$from\E(/.*)?$}) {
124 0   0     0 my $suffix = $1 // '';
125 0         0 return $to . $suffix;
126             }
127             }
128             }
129              
130 1         5 return $path;
131             }
132              
133 1     1   2 async sub _send_redirect {
134 1         4 my ($self, $send, $location) = @_;
135              
136 1         4 my $status = $self->{redirect_code};
137 1         3 my $body = "Redirecting to $location";
138              
139 1         11 await $send->({
140             type => 'http.response.start',
141             status => $status,
142             headers => [
143             ['Content-Type', 'text/plain'],
144             ['Content-Length', length($body)],
145             ['Location', $location],
146             ],
147             });
148 1         89 await $send->({
149             type => 'http.response.body',
150             body => $body,
151             more => 0,
152             });
153             }
154              
155             1;
156              
157             __END__