File Coverage

blib/lib/URI/Template.pm
Criterion Covered Total %
statement 101 174 58.0
branch 34 104 32.6
condition 4 17 23.5
subroutine 19 23 82.6
pod 7 8 87.5
total 165 326 50.6


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