File Coverage

blib/lib/Mojolicious/Plugin/Util/Endpoint.pm
Criterion Covered Total %
statement 110 113 97.3
branch 42 48 87.5
condition 15 22 68.1
subroutine 9 9 100.0
pod 1 1 100.0
total 177 193 91.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Util::Endpoint;
2 2     2   14274 use Mojo::Base 'Mojolicious::Plugin';
  2         4  
  2         14  
3 2     2   411 use Mojo::ByteStream 'b';
  2         4  
  2         111  
4 2     2   12 use Scalar::Util qw/blessed/;
  2         9  
  2         120  
5 2     2   13 use Mojo::URL;
  2         5  
  2         21  
6              
7             our $VERSION = '0.23';
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 85 my ($plugin, $mojo) = @_;
19              
20             # Add 'endpoints' command
21 2         4 push @{$mojo->commands->namespaces}, __PACKAGE__;
  2         17  
22              
23             # Add 'endpoint' shortcut
24             $mojo->routes->add_shortcut(
25             endpoint => sub {
26 14     14   6307 my ($route, $name, $param) = @_;
27              
28             # Endpoint already defined
29 14 100       51 if (exists $endpoints{$name}) {
30 1         3 $mojo->log->debug(qq{Route endpoint "$name" already defined});
31 1         362 return $route;
32             };
33              
34 13   100     46 $param //= {};
35              
36             # Route defined
37 13         36 $param->{route} = $route->name;
38              
39             # Set to stash
40 13   50     115 $endpoints{$name} = $param // {};
41              
42             # Return route for piping
43 13         35 return $route;
44             }
45 2         182 );
46              
47              
48             # Add 'endpoint' helper
49             $mojo->helper(
50             endpoint => sub {
51 59     59   61947 my ($c, $name, $values) = @_;
52 59   100     214 $values ||= {};
53              
54             # Define endpoint by string
55 59 100 100     282 unless (ref $values) {
56 4         18 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       206 $c->isa('Mojolicious::Controller') ? %{$c->stash} : %{$c->defaults},
  54         165  
  0         0  
67             format => undef,
68             %$values
69             );
70              
71             # Endpoint undefined
72 54 100       643 unless (defined $endpoints{$name}) {
73              
74             # Named route
75 7 100       50 if ($name !~ m!^([^:]+:)?/?/!) {
76 1         23 return $c->url_for($name)->to_abs->to_string;
77             };
78              
79             # Interpolate string
80 6         16 return _interpolate($name, \%values, $values);
81             };
82              
83             # Return interpolated string
84 47 100 66     558 if (blessed $endpoints{$name} && $endpoints{$name}->isa('Mojo::URL')) {
85             return _interpolate(
86 8         32 $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         58 my $match;
96 39 50       105 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         703 my $url = Mojo::URL->new;
103 39         366 my $req = $c->req;
104 39         650 $url->base($req->url->base->clone);
105 39         1547 my $base = $url->base;
106 39         209 $base->userinfo(undef);
107              
108             # Get parameters
109 39         225 my $param = $endpoints{$name};
110              
111             # Set parameters to url
112 39 100       126 $url->scheme($param->{scheme}) if $param->{scheme};
113 39 100       117 $url->port($param->{port}) if $param->{port};
114              
115 39 100       100 if ($param->{host}) {
116 18         59 $url->host($param->{host});
117 18 100       129 $url->port(undef) unless $param->{port};
118 18 100       145 $url->scheme('http') unless $url->scheme;
119             };
120              
121             # Clone query
122 39 100       208 $url->query( [@{$param->{query}}] ) if $param->{query};
  28         133  
123              
124             # Get path
125 39         1470 my $path = $url->path;
126              
127             # Lookup match
128 39         585 my $r = $match->root->find($param->{route});
129              
130             # Interpolate path
131 39         4020 my @parts;
132 39         113 while ($r) {
133 84         271 my $p = '';
134 84         125 foreach my $part (@{$r->pattern->tree}) {
  84         179  
135 71         368 my $t = $part->[0];
136              
137             # Slash
138 71 100       216 if ($t eq 'slash') {
    100          
    50          
139 19         43 $p .= '/';
140             }
141              
142             # Text
143             elsif ($t eq 'text') {
144 32         77 $p .= $part->[1];
145             }
146              
147             # Various wildcards
148             elsif ($t =~ m/^(?:wildcard|placeholder|relaxed)$/) {
149 20 100       48 if (exists $values{$part->[1]}) {
150 10         22 $p .= $values{$part->[1]};
151             }
152             else {
153 10         27 $p .= '{' . $part->[1] . '}';
154             };
155             };
156             };
157              
158             # Prepend to path array
159 84         428 unshift(@parts, $p);
160              
161             # Go up one level till root
162 84         187 $r = $r->parent;
163             };
164              
165             # Set path
166 39 50       357 $path->parse(join('', @parts)) if @parts;
167              
168             # Fix trailing slash
169 39 50 33     499 $path->trailing_slash(1)
      33        
170             if (!$name || $name eq 'current')
171             && $req->url->path->trailing_slash;
172              
173             # Make path absolute
174 39         119 my $base_path = $base->path;
175 39         499 unshift @{$path->parts}, @{$base_path->parts};
  39         88  
  39         2191  
176 39         1560 $base_path->parts([]);
177              
178             # Interpolate url for query parameters
179 39         429 return _interpolate($url->to_abs->to_string, \%values, $values);
180             }
181 2         210 );
182              
183              
184             # Add 'get_endpoints' helper
185             $mojo->helper(
186             get_endpoints => sub {
187 1     1   92 my $c = shift;
188              
189             # Get all endpoints
190 1         2 my %endpoint_hash;
191 1         5 foreach (keys %endpoints) {
192 7         50 $endpoint_hash{$_} = $c->endpoint($_);
193             };
194              
195             # Return endpoint hash
196 1         5 return \%endpoint_hash;
197 2         246 });
198             };
199              
200              
201             # Interpolate templates
202             sub _interpolate {
203 53     53   23506 my $endpoint = shift;
204              
205             # Decode escaped symbols
206 53         257 $endpoint =~
207 96         1645 s/\%7[bB](.+?)\%7[dD]/'{' . b($1)->url_unescape . '}'/ge;
208              
209 53         980 my $param = shift;
210 53         87 my $orig_param = shift;
211              
212             # Interpolate template
213 53         136 pos($endpoint) = 0;
214 53         274 while ($endpoint =~ /\{([^\}\?}\?]+)\??\}/g) {
215              
216             # Save search position
217             # Todo: That's not exact!
218 107         375 my $val = $1;
219 107         209 my $p = pos($endpoint) - length($val) - 1;
220              
221 107         167 my $fill = undef;
222              
223             # Look in param
224 107 100       283 if ($param->{$val}) {
    100          
225 27         78 $fill = b($param->{$val})->url_escape;
226 27         984 $endpoint =~ s/\{$val\??\}/$fill/;
227             }
228              
229             # unset specific parameters
230             elsif (exists $orig_param->{$val}) {
231              
232             # Delete specific parameters
233 3         8 for ($endpoint) {
234 3 100       122 if (s/(?<=[\&\?])[^\}][^=]*?=\{$val\??\}//g) {
235 2         15 s/([\?\&])\&*/$1/g;
236 2         7 s/\&$//g;
237             };
238 3         67 s/^([^\?]+?)([\/\.])\{$val\??\}\2/$1$2/g;
239 3         50 s/^([^\?]+?)\{$val\??\}/$1/g;
240             };
241             };
242              
243             # Reset search position
244             # Todo: (not exact if it was optional)
245 107   100     673 pos($endpoint) = $p + length($fill || '');
246             };
247              
248             # Ignore optional placeholders
249 53 100 66     296 if (exists $param->{'?'} &&
250             !defined $param->{'?'}) {
251 7         17 for ($endpoint) {
252 7 50       77 s/(?<=[\&\?])[^\}][^=]*?=\{[^\?\}]+?\?\}//g or last;
253 7         41 s/([\?\&])\&*/$1/g;
254 7         26 s/\&$//g;
255             };
256             };
257              
258             # Strip empty query marker
259 53         106 $endpoint =~ s/\?$//;
260 53         749 return $endpoint;
261             };
262              
263              
264             1;
265              
266              
267             __END__