File Coverage

blib/lib/URI/Template.pm
Criterion Covered Total %
statement 95 165 57.5
branch 33 100 33.0
condition 4 17 23.5
subroutine 16 20 80.0
pod 6 6 100.0
total 154 308 50.0


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