File Coverage

blib/lib/URI/Template.pm
Criterion Covered Total %
statement 96 169 56.8
branch 34 104 32.6
condition 4 17 23.5
subroutine 16 20 80.0
pod 6 6 100.0
total 156 316 49.3


line stmt bran cond sub pod time code
1             package URI::Template;
2              
3 3     3   1676 use strict;
  3         5  
  3         62  
4 3     3   10 use warnings;
  3         3  
  3         85  
5              
6             our $VERSION = '0.23';
7              
8 3     3   1255 use URI;
  3         10096  
  3         59  
9 3     3   13 use URI::Escape ();
  3         5  
  3         30  
10 3     3   1242 use Unicode::Normalize ();
  3         4925  
  3         88  
11 3     3   14 use overload '""' => \&template;
  3         5  
  3         23  
12              
13             my $RESERVED = q(:/?#\[\]\@!\$\&'\(\)\*\+,;=);
14             my %TOSTRING = (
15             '' => \&_tostring,
16             '+' => \&_tostring,
17             '#' => \&_tostring,
18             ';' => \&_tostring_semi,
19             '?' => \&_tostring_query,
20             '&' => \&_tostring_query,
21             '/' => \&_tostring_path,
22             '.' => \&_tostring_path,
23             );
24              
25             sub new {
26 8     8 1 13977 my $class = shift;
27 8         18 my $templ = shift;
28 8 100       22 $templ = '' unless defined $templ;
29 8         26 my $self = bless { template => $templ, _vars => {} } => $class;
30              
31 8         24 $self->_study;
32              
33 8         24 return $self;
34             }
35              
36             sub _quote {
37 14     14   51 my ( $val, $safe ) = @_;
38 14   50     59 $safe ||= '';
39 14         26 my $unsafe = '^A-Za-z0-9\-\._' . $safe;
40              
41             ## Where RESERVED are allowed to pass-through, so are
42             ## already-pct-encoded values
43 14 50       29 if( $safe ) {
44 0         0 my (@chunks) = split(/(%[0-9A-Fa-f]{2})/, $val);
45              
46             # even chunks are not %xx, odd chunks are
47             return join '',
48 0 0       0 map { $_ % 2
  0         0  
49             ? $chunks[$_]
50             : URI::Escape::uri_escape_utf8( Unicode::Normalize::NFKC($chunks[$_]), $unsafe ) } 0..$#chunks;
51              
52             }
53              
54             # try to mirror python's urllib quote
55 14         102 return URI::Escape::uri_escape_utf8( Unicode::Normalize::NFKC( $val ),
56             $unsafe );
57             }
58              
59             sub _tostring {
60 14     14   32 my ( $var, $value, $exp ) = @_;
61 14         29 my $safe = $exp->{ safe };
62              
63 14 50       84 if ( ref $value eq 'ARRAY' ) {
    50          
    50          
64 0         0 return join( ',', map { _quote( $_, $safe ) } @$value );
  0         0  
65             }
66             elsif ( ref $value eq 'HASH' ) {
67             return join(
68             ',',
69             map {
70 0         0 _quote( $_, $safe )
71             . ( $var->{ explode } ? '=' : ',' )
72 0 0       0 . _quote( $value->{ $_ }, $safe )
73             } sort keys %$value
74             );
75             }
76             elsif ( defined $value ) {
77             return _quote(
78 14   33     106 substr( $value, 0, $var->{ prefix } || length( $value ) ),
79             $safe );
80             }
81              
82 0         0 return;
83             }
84              
85             sub _tostring_semi {
86 0     0   0 my ( $var, $value, $exp ) = @_;
87 0         0 my $safe = $exp->{ safe };
88 0         0 my $join = $exp->{ op };
89 0 0       0 $join = '&' if $exp->{ op } eq '?';
90              
91 0 0       0 if ( ref $value eq 'ARRAY' ) {
    0          
    0          
92 0 0       0 if ( $var->{ explode } ) {
93             return join( $join,
94 0         0 map { $var->{ name } . '=' . _quote( $_, $safe ) } @$value );
  0         0  
95             }
96             else {
97             return $var->{ name } . '='
98 0         0 . join( ',', map { _quote( $_, $safe ) } @$value );
  0         0  
99             }
100             }
101             elsif ( ref $value eq 'HASH' ) {
102 0 0       0 if ( $var->{ explode } ) {
103             return join(
104             $join,
105             map {
106 0         0 _quote( $_, $safe ) . '='
107 0         0 . _quote( $value->{ $_ }, $safe )
108             } sort keys %$value
109             );
110             }
111             else {
112             return $var->{ name } . '=' . join(
113             ',',
114             map {
115 0         0 _quote( $_, $safe ) . ','
116 0         0 . _quote( $value->{ $_ }, $safe )
117             } sort keys %$value
118             );
119             }
120             }
121             elsif ( defined $value ) {
122 0 0       0 return $var->{ name } unless length( $value );
123             return
124             $var->{ name } . '='
125             . _quote(
126 0   0     0 substr( $value, 0, $var->{ prefix } || length( $value ) ),
127             $safe );
128             }
129              
130 0         0 return;
131             }
132              
133             sub _tostring_query {
134 0     0   0 my ( $var, $value, $exp ) = @_;
135 0         0 my $safe = $exp->{ safe };
136 0         0 my $join = $exp->{ op };
137 0 0       0 $join = '&' if $exp->{ op } =~ /[?&]/;
138              
139 0 0       0 if ( ref $value eq 'ARRAY' ) {
    0          
    0          
140 0 0       0 return if !@$value;
141 0 0       0 if ( $var->{ explode } ) {
142             return join( $join,
143 0         0 map { $var->{ name } . '=' . _quote( $_, $safe ) } @$value );
  0         0  
144             }
145             else {
146             return $var->{ name } . '='
147 0         0 . join( ',', map { _quote( $_, $safe ) } @$value );
  0         0  
148             }
149             }
150             elsif ( ref $value eq 'HASH' ) {
151 0 0       0 return if !keys %$value;
152 0 0       0 if ( $var->{ explode } ) {
153             return join(
154             $join,
155             map {
156 0         0 _quote( $_, $safe ) . '='
157 0         0 . _quote( $value->{ $_ }, $safe )
158             } sort keys %$value
159             );
160             }
161             else {
162             return $var->{ name } . '=' . join(
163             ',',
164             map {
165 0         0 _quote( $_, $safe ) . ','
166 0         0 . _quote( $value->{ $_ }, $safe )
167             } sort keys %$value
168             );
169             }
170             }
171             elsif ( defined $value ) {
172 0 0       0 return $var->{ name } . '=' unless length( $value );
173             return
174             $var->{ name } . '='
175             . _quote(
176 0   0     0 substr( $value, 0, $var->{ prefix } || length( $value ) ),
177             $safe );
178             }
179             }
180              
181             sub _tostring_path {
182 0     0   0 my ( $var, $value, $exp ) = @_;
183 0         0 my $safe = $exp->{ safe };
184 0         0 my $join = $exp->{ op };
185              
186 0 0       0 if ( ref $value eq 'ARRAY' ) {
    0          
    0          
187 0 0       0 return unless @$value;
188             return join(
189             ( $var->{ explode } ? $join : ',' ),
190 0 0       0 map { _quote( $_, $safe ) } @$value
  0         0  
191             );
192             }
193             elsif ( ref $value eq 'HASH' ) {
194             return join(
195             ( $var->{ explode } ? $join : ',' ),
196             map {
197 0 0       0 _quote( $_, $safe )
198             . ( $var->{ explode } ? '=' : ',' )
199 0 0       0 . _quote( $value->{ $_ }, $safe )
200             } sort keys %$value
201             );
202             }
203             elsif ( defined $value ) {
204             return _quote(
205 0   0     0 substr( $value, 0, $var->{ prefix } || length( $value ) ),
206             $safe );
207             }
208              
209 0         0 return;
210             }
211              
212             sub _study {
213 10     10   17 my ( $self ) = @_;
214 10 50       18 my @hunks = grep { defined && length } split /(\{.+?\})/, $self->template;
  34         112  
215 10         20 my $pos = 1;
216 10         20 for ( @hunks ) {
217 34 100       100 next unless /^\{(.+?)\}$/;
218 14         52 $_ = $self->_compile_expansion( $1, $pos++ );
219             }
220 10         23 $self->{ studied } = \@hunks;
221             }
222              
223             sub _compile_expansion {
224 14     14   43 my ( $self, $str, $pos ) = @_;
225              
226 14         58 my %exp = ( op => '', vars => [], str => $str );
227 14 50       41 if ( $str =~ /^([+#.\/;?&|!\@])(.+)/ ) {
228 0         0 $exp{ op } = $1;
229 0         0 $exp{ str } = $2;
230             }
231              
232 14 50       35 $exp{ safe } = $RESERVED if $exp{ op } =~ m{[+#]};
233              
234 14         40 for my $varspec ( split( ',', delete $exp{ str } ) ) {
235 14         33 my %var = ( name => $varspec );
236 14 50       35 if ( $varspec =~ /=/ ) {
237 0         0 @var{ 'name', 'default' } = split( /=/, $varspec, 2 );
238             }
239 14 50       44 if ( $var{ name } =~ s{\*$}{} ) {
    50          
240 0         0 $var{ explode } = 1;
241             }
242             elsif ( $var{ name } =~ /:/ ) {
243 0         0 @var{ 'name', 'prefix' } = split( /:/, $var{ name }, 2 );
244 0 0       0 if ( $var{ prefix } =~ m{[^0-9]} ) {
245 0         0 die 'Non-numeric prefix specified';
246             }
247             }
248              
249             # remove "optional" flag (for opensearch compatibility)
250 14         21 $var{ name } =~ s{\?$}{};
251 14         34 $self->{ _vars }->{ $var{ name } } = $pos;
252              
253 14         15 push @{ $exp{ vars } }, \%var;
  14         46  
254             }
255              
256 14         30 my $join = $exp{ op };
257 14         21 my $start = $exp{ op };
258              
259 14 50       55 if ( $exp{ op } eq '+' ) {
    50          
    50          
    50          
    50          
260 0         0 $start = '';
261 0         0 $join = ',';
262             }
263             elsif ( $exp{ op } eq '#' ) {
264 0         0 $join = ',';
265             }
266             elsif ( $exp{ op } eq '?' ) {
267 0         0 $join = '&';
268             }
269             elsif ( $exp{ op } eq '&' ) {
270 0         0 $join = '&';
271             }
272             elsif ( $exp{ op } eq '' ) {
273 14         22 $join = ',';
274             }
275              
276 14 50       30 if ( !exists $TOSTRING{ $exp{ op } } ) {
277 0         0 die 'Invalid operation "' . $exp{ op } . '"';
278             }
279              
280             return sub {
281 16     16   30 my $variables = shift;
282              
283 16         20 my @return;
284 16         19 for my $var ( @{ $exp{ vars } } ) {
  16         37  
285 16         25 my $value;
286 16 100       42 if ( exists $variables->{ $var->{ name } } ) {
287 14         30 $value = $variables->{ $var->{ name } };
288             }
289 16 100       36 $value = $var->{ default } if !defined $value;
290              
291 16 100       31 next unless defined $value;
292              
293 14         43 my $expand = $TOSTRING{ $exp{ op } }->( $var, $value, \%exp );
294              
295 14 50       936 push @return, $expand if defined $expand;
296             }
297              
298 16 100       83 return $start . join( $join, @return ) if @return;
299 2         7 return '';
300 14         71 };
301             }
302              
303             sub template {
304 17     17 1 925 my $self = shift;
305 17         21 my $templ = shift;
306              
307             # Update template
308 17 100 66     40 if ( defined $templ && $templ ne $self->{ template } ) {
309 2         3 $self->{ template } = $templ;
310 2         4 $self->{ _vars } = {};
311 2         5 $self->_study;
312 2         3 return $self;
313             }
314              
315 15         120 return $self->{ template };
316             }
317              
318             sub variables {
319 4     4 1 1158 my @vars = sort {$_[ 0 ]->{ _vars }->{ $a } <=> $_[ 0 ]->{ _vars }->{ $b } } keys %{ $_[ 0 ]->{ _vars } };
  15         34  
  4         25  
320 4         24 return @vars;
321             }
322              
323             sub expansions {
324 0     0 1 0 my $self = shift;
325 0         0 return grep { ref } @{ $self->{ studied } };
  0         0  
  0         0  
326             }
327              
328             sub process {
329 10     10 1 1846 my $self = shift;
330 10         23 return URI->new( $self->process_to_string( @_ ) );
331             }
332              
333             sub process_to_string {
334 11     11 1 1144 my $self = shift;
335 11 50       41 my $arg = @_ == 1 ? $_[ 0 ] : { @_ };
336 11         21 my $str = '';
337              
338 11         13 for my $hunk ( @{ $self->{ studied } } ) {
  11         27  
339 40 100       96 if ( !ref $hunk ) { $str .= $hunk; next; }
  24         44  
  24         42  
340              
341 16         37 $str .= $hunk->( $arg );
342             }
343              
344 11         55 return $str;
345             }
346              
347             1;
348              
349             __END__