File Coverage

blib/lib/Mojolicious/Plugin/Util/Endpoint.pm
Criterion Covered Total %
statement 110 114 96.4
branch 42 50 84.0
condition 15 22 68.1
subroutine 9 9 100.0
pod 1 1 100.0
total 177 196 90.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Util::Endpoint;
2 2     2   14884 use Mojo::Base 'Mojolicious::Plugin';
  2         6  
  2         15  
3 2     2   428 use Mojo::ByteStream 'b';
  2         4  
  2         127  
4 2     2   12 use Scalar::Util qw/blessed/;
  2         4  
  2         107  
5 2     2   14 use Mojo::URL;
  2         5  
  2         18  
6              
7             our $VERSION = '0.24';
8              
9             # Todo: Support alternative bases for https-paths
10             # Todo: Update to https://tools.ietf.org/html/rfc6570
11             # Todo: Allow for changing scheme, port, host etc. afterwards
12              
13             # Endpoint hash
14             our %endpoints;
15              
16             # Register Plugin
17             sub register {
18 2     2 1 129 my ($plugin, $mojo) = @_;
19              
20             # Add 'endpoints' command
21 2         6 push @{$mojo->commands->namespaces}, __PACKAGE__;
  2         22  
22              
23             # Add 'endpoint' shortcut
24             $mojo->routes->add_shortcut(
25             endpoint => sub {
26 14     14   8238 my ($route, $name, $param) = @_;
27              
28             # Endpoint already defined
29 14 100       69 if (exists $endpoints{$name}) {
30 1         5 $mojo->log->debug(qq{Route endpoint "$name" already defined});
31 1         839 return $route;
32             };
33              
34 13   100     66 $param //= {};
35              
36             # Route defined
37 13         55 $param->{route} = $route->name;
38              
39             # Set to stash
40 13   50     154 $endpoints{$name} = $param // {};
41              
42             # Return route for piping
43 13         40 return $route;
44             }
45 2         241 );
46              
47              
48             # Add 'endpoint' helper
49             $mojo->helper(
50             endpoint => sub {
51 59     59   66990 my ($c, $name, $values) = @_;
52 59   100     299 $values ||= {};
53              
54             # Define endpoint by string
55 59 100 100     398 unless (ref $values) {
56 4         65 return ($endpoints{$name} = Mojo::URL->new($values));
57             }
58              
59             # Define endpoint by Mojo::URL
60             elsif (blessed $values && $values->isa('Mojo::URL')) {
61             return ($endpoints{$name} = $values->clone);
62             };
63              
64             # Set values
65             my %values = (
66 54 50       292 $c->isa('Mojolicious::Controller') ? %{$c->stash} : %{$c->defaults},
  54         234  
  0         0  
67             format => undef,
68             %$values
69             );
70              
71             # Endpoint undefined
72 54 100       761 unless (defined $endpoints{$name}) {
73              
74             # Named route
75 7 100       52 if ($name !~ m!^([^:]+:)?/?/!) {
76 1         29 return $c->url_for($name)->to_abs->to_string;
77             };
78              
79             # Interpolate string
80 6         22 return _interpolate($name, \%values, $values);
81             };
82              
83             # Return interpolated string
84 47 100 66     268 if (blessed $endpoints{$name} && $endpoints{$name}->isa('Mojo::URL')) {
85             return _interpolate(
86 8         43 $endpoints{$name}->to_abs->to_string,
87             \%values,
88             $values
89             );
90             };
91              
92             # The following is based on url_for of Mojolicious::Controller
93             # and parts of path_for in Mojolicious::Routes::Route
94             # Get match object
95 39         70 my $match;
96 39 50       130 unless ($match = $c->match) {
97 0         0 $match = Mojolicious::Routes::Match->new(get => '/');
98 0         0 $match->root($c->app->routes);
99             };
100              
101             # Base
102 39         853 my $url = Mojo::URL->new;
103 39         412 my $req = $c->req;
104 39         831 $url->base($req->url->base->clone);
105 39         1777 my $base = $url->base;
106 39         246 $base->userinfo(undef);
107              
108             # Get parameters
109 39         244 my $param = $endpoints{$name};
110              
111             # Set parameters to url
112 39 100       143 $url->scheme($param->{scheme}) if $param->{scheme};
113 39 100       148 $url->port($param->{port}) if $param->{port};
114              
115 39 100       117 if ($param->{host}) {
116 18         67 $url->host($param->{host});
117 18 100       161 $url->port(undef) unless $param->{port};
118 18 100       119 $url->scheme('http') unless $url->scheme;
119             };
120              
121             # Clone query
122 39 100       308 $url->query( [@{$param->{query}}] ) if $param->{query};
  28         206  
123              
124             # Get path
125 39         1646 my $path = $url->path;
126              
127             # Lookup match
128 39         688 my $r = $match->root->find($param->{route});
129              
130             # Interpolate path
131 39         4449 my @parts;
132 39         139 while ($r) {
133 84         306 my $p = '';
134              
135 84         140 foreach my $part (@{$r->pattern->tree}) {
  84         195  
136 71         432 my $t = $part->[0];
137              
138             # Slash
139 71 100       277 if ($t eq 'slash') {
    100          
    50          
140 19         41 $p .= '/';
141             }
142              
143             # Text
144             elsif ($t eq 'text') {
145 32         91 $p .= $part->[1];
146             }
147              
148             # Various wildcards
149             elsif ($t =~ m/^(?:wildcard|placeholder|relaxed)$/) {
150              
151             # Support different tree variants in Mojolicious (before and after 9)
152 20 50       66 my $n = ref $part->[1] eq 'ARRAY' ? $part->[1]->[0] : $part->[1];
153              
154 20 50       58 if (exists $values{$part->[1]}) {
155 0         0 $p .= $values{$n};
156             }
157             else {
158 20         61 $p .= "{$n}";
159             };
160             };
161             };
162              
163             # Prepend to path array
164 84         423 unshift(@parts, $p);
165              
166             # Go up one level till root
167 84         220 $r = $r->parent;
168             };
169              
170             # Set path
171 39 50       348 $path->parse(join('', @parts)) if @parts;
172              
173             # Fix trailing slash
174 39 50 33     557 $path->trailing_slash(1)
      33        
175             if (!$name || $name eq 'current')
176             && $req->url->path->trailing_slash;
177              
178             # Make path absolute
179 39         114 my $base_path = $base->path;
180 39         562 unshift @{$path->parts}, @{$base_path->parts};
  39         114  
  39         2723  
181 39         1597 $base_path->parts([]);
182              
183             # Interpolate url for query parameters
184 39         478 return _interpolate($url->to_abs->to_string, \%values, $values);
185             }
186 2         252 );
187              
188              
189             # Add 'get_endpoints' helper
190             $mojo->helper(
191             get_endpoints => sub {
192 1     1   146 my $c = shift;
193              
194             # Get all endpoints
195 1         4 my %endpoint_hash;
196 1         7 foreach (keys %endpoints) {
197 7         33 $endpoint_hash{$_} = $c->endpoint($_);
198             };
199              
200             # Return endpoint hash
201 1         7 return \%endpoint_hash;
202 2         267 });
203             };
204              
205              
206             # Interpolate templates
207             sub _interpolate {
208 53     53   26404 my $endpoint = shift;
209              
210             # Decode escaped symbols
211 53         310 $endpoint =~
212 106         2109 s/\%7[bB](.+?)\%7[dD]/'{' . b($1)->url_unescape . '}'/ge;
213              
214 53         1062 my $param = shift;
215 53         108 my $orig_param = shift;
216              
217             # Interpolate template
218 53         176 pos($endpoint) = 0;
219 53         306 while ($endpoint =~ /\{([^\}\?}\?]+)\??\}/g) {
220              
221             # Save search position
222             # Todo: That's not exact!
223 117         514 my $val = $1;
224 117         255 my $p = pos($endpoint) - length($val) - 1;
225              
226 117         204 my $fill = undef;
227              
228             # Look in param
229 117 100       355 if ($param->{$val}) {
    100          
230 37         121 $fill = b($param->{$val})->url_escape;
231 37         1651 $endpoint =~ s/\{$val\??\}/$fill/;
232             }
233              
234             # unset specific parameters
235             elsif (exists $orig_param->{$val}) {
236              
237             # Delete specific parameters
238 3         9 for ($endpoint) {
239 3 100       148 if (s/(?<=[\&\?])[^\}][^=]*?=\{$val\??\}//g) {
240 2         16 s/([\?\&])\&*/$1/g;
241 2         5 s/\&$//g;
242             };
243 3         73 s/^([^\?]+?)([\/\.])\{$val\??\}\2/$1$2/g;
244 3         50 s/^([^\?]+?)\{$val\??\}/$1/g;
245             };
246             };
247              
248             # Reset search position
249             # Todo: (not exact if it was optional)
250 117   100     815 pos($endpoint) = $p + length($fill || '');
251             };
252              
253             # Ignore optional placeholders
254 53 100 66     348 if (exists $param->{'?'} &&
255             !defined $param->{'?'}) {
256 7         21 for ($endpoint) {
257 7 50       83 s/(?<=[\&\?])[^\}][^=]*?=\{[^\?\}]+?\?\}//g or last;
258 7         50 s/([\?\&])\&*/$1/g;
259 7         30 s/\&$//g;
260             };
261             };
262              
263             # Strip empty query marker
264 53         128 $endpoint =~ s/\?$//;
265 53         807 return $endpoint;
266             };
267              
268              
269             1;
270              
271              
272             __END__