File Coverage

lib/Badger/URL.pm
Criterion Covered Total %
statement 111 121 91.7
branch 44 62 70.9
condition 23 49 46.9
subroutine 21 23 91.3
pod 20 20 100.0
total 219 275 79.6


line stmt bran cond sub pod time code
1             package Badger::URL;
2              
3             use Badger::Class
4 1         14 version => 0.02,
5             debug => 0,
6             base => 'Badger::Base',
7             import => 'CLASS class',
8             utils => 'textlike is_object',
9             codec => 'uri', # we use URI encoding for parameters
10             as_text => \&text,
11             is_true => 1, # not sure about this
12             constants => 'HASH BLANK',
13             constant => {
14             SLASH => '/',
15             TEXT => 0,
16             SCHEME => 1,
17             AUTHORITY => 2,
18             USER => 3,
19             HOST => 4,
20             PORT => 5,
21             PATH => 6,
22             QUERY => 7,
23             FRAGMENT => 8,
24             PARAMS => 9,
25             },
26             alias => {
27             url => \&text,
28             },
29             exports => {
30             any => 'URL',
31 1     1   534 };
  1         2  
32              
33              
34             #------------------------------------------------------------------------
35             # Example URL:
36             #
37             # scheme authority path query fragment
38             # __ ___________________ _________ _________ __
39             # / \ / \/ \ / \ / \
40             # http://user@example.com:8042/over/there?name=ferret#nose
41             # \__/ \_________/ \__/
42             # user host port
43             #
44             #------------------------------------------------------------------------
45              
46             our @ELEMENTS = qw(
47             scheme authority user host port path query fragment params
48             );
49             our $N_ELEMS = 1; # slot 0 holds source text, so slot 1 is first field
50             our $ELEMENT = {
51             map { $_ => $N_ELEMS++ }
52             @ELEMENTS
53             };
54              
55             # regexen to match basic tokens
56             our $MATCH_SCHEME = qr{ ( [a-zA-Z][a-zA-Z0-9.+\-]* ) : }x;
57             our $MATCH_USER = qr{ ([^@]*) @ }x;
58             our $MATCH_HOST = qr{ ( [^:\/]* ) }x;
59             our $MATCH_PORT = qr{ : (\d*) }x;
60             our $MATCH_PATH = qr{ ( [^ \? \#]* ) }x;
61             our $MATCH_QUERY = qr{ \? ( [^ \#]* ) }x;
62             our $MATCH_FRAGMENT = qr{ \# ( .* ) }x;
63              
64             # compound regexen to match authority
65             our $MATCH_AUTHORITY = qr{
66             // ( # $1 - authority
67             (?: $MATCH_USER )? # $2 - user
68             $MATCH_HOST # $3 - host
69             (?: $MATCH_PORT )? # $4 - port
70             )
71             }x;
72              
73             # compound regexen to match complete URL
74             our $MATCH_URL = qr{
75             ^ (?: $MATCH_SCHEME )? # $1 - scheme
76             (?: $MATCH_AUTHORITY )? # $2,$3,$4,$5 - authority,user,host,port
77             $MATCH_PATH # $6 - path
78             (?: $MATCH_QUERY )? # $7 - query
79             (?: $MATCH_FRAGMENT )? # $8 - fragment
80             }x;
81              
82              
83              
84             #------------------------------------------------------------------------
85             # Constructor function and methods.
86             #------------------------------------------------------------------------
87              
88             sub URL {
89 4 100   4 1 45 return CLASS unless @_;
90 1 50 33     9 return @_ == 1 && is_object(CLASS, $_[0])
91             ? $_[0]->copy # copy existing URL object
92             : CLASS->new(@_); # or construct a new one
93             }
94              
95              
96             sub new {
97 4     4 1 7 my $class = shift;
98 4 100       14 my $args = @_ == 1 ? shift : { @_ };
99 4         5 my $self;
100              
101 4   33     14 $class = ref $class || $class;
102              
103 4 100       11 if (textlike $args) {
    50          
104 3         42 $self = bless [$args, $args =~ $MATCH_URL], $class;
105             }
106             elsif (ref $args eq HASH) {
107 1         3 $self = bless [ ], $class;
108 1         3 $self->set($args);
109              
110             }
111             else {
112 0         0 return $class->error("Invalid URL specification: $_[0]");
113             }
114              
115 4         21 return $self;
116             }
117              
118              
119             sub copy {
120 6     6 1 17 my $self = shift;
121 6         23 my $copy = bless [ @$self ], ref $self;
122             return @_
123 6 100       20 ? $copy->set(@_)
124             : $copy;
125             }
126              
127              
128             sub set {
129 5     5 1 7 my $self = shift;
130 5 100 66     65 my $args = @_ && ref $_[0] eq HASH ? shift : { @_ };
131 5         9 my ($k, $v, $n);
132              
133 5         24 while (($k, $v) = each %$args) {
134 11   50     21 $n = $ELEMENT->{ $k } || next;
135 11         25 $self->[$n] = $v;
136             }
137              
138             # The authority is comprised of the user, host and port fields.
139             # We need to split any authority specified, or merge together the user,
140             # host and port if any of them have been changed
141             $self->split_authority
142 5 50       11 if exists $args->{ authority };
143              
144             $self->join_authority
145             if exists $args->{ user }
146             or exists $args->{ host }
147 5 50 66     48 or exists $args->{ port };
      33        
148              
149             # similar thing for query/params
150             $self->split_query
151 5 100       10 if exists $args->{ query };
152              
153             $self->join_query
154 5 50       11 if exists $args->{ params };
155              
156             # finally reconstruct the complete url
157 5         12 $self->join_url;
158              
159 5         40 return $self;
160             }
161              
162              
163             #-----------------------------------------------------------------------
164             # split/join methods
165             #-----------------------------------------------------------------------
166              
167             sub split_authority {
168 0     0 1 0 my $self = shift;
169 0 0       0 $self->[AUTHORITY] = BLANK
170             unless defined $self->[AUTHORITY];
171              
172             # this regex shouldn't ever fail as everything is optional
173 0         0 @$self[AUTHORITY,USER,HOST,PORT]
174             = $self->[AUTHORITY] =~ $MATCH_AUTHORITY;
175             }
176              
177              
178             sub join_authority {
179 5     5 1 20 my $self = shift;
180 5         14 my ($user, $host, $port) = @$self[USER,HOST,PORT];
181              
182 5 100 66     22 $user = (defined $user && length $user) ? $user . '@' : BLANK;
183 5 50 33     19 $port = (defined $port && length $port) ? ':' . $port : BLANK;
184 5 50       9 $host = BLANK unless defined $host;
185              
186 5         12 return ($self->[AUTHORITY] = $user.$host.$port);
187             }
188              
189              
190             sub split_query {
191 4     4 1 8 my $self = shift;
192 4 50       11 $self->[QUERY] = ''
193             unless defined $self->[QUERY];
194              
195             return ($self->[PARAMS] = {
196             map {
197 4         15 map { decode($_) }
  4         9  
  8         15  
198             split(/=/, $_, 2)
199             }
200             split(/[&;]/, $self->[QUERY])
201             });
202             }
203              
204              
205             sub join_query {
206 2     2 1 3 my $self = shift;
207 2   50     6 my $params = $self->[PARAMS] || { } ; # should we call split_query()?
208              
209             return ($self->[QUERY] = join(
210             '&',
211 2         10 map { $_ . '=' . encode( $params->{ $_ } ) }
  6         17  
212             sort keys %$params # sorted makes debugging easier
213             ));
214             }
215              
216              
217             sub join_url {
218 17     17 1 24 my $self = shift;
219 17         25 my $scheme = $self->[SCHEME];
220 17         19 my $auth = $self->[AUTHORITY];
221 17         20 my $query = $self->[QUERY];
222 17         20 my $frag = $self->[FRAGMENT];
223              
224 17 50 33     54 $scheme = (defined $scheme && length $scheme) ? $scheme . ':' : BLANK;
225 17 50 33     47 $auth = (defined $auth && length $auth) ? '//' . $auth : BLANK;
226 17 100 66     46 $query = (defined $query && length $query) ? '?' . $query : BLANK;
227 17 100 66     46 $frag = (defined $frag && length $frag) ? '#' . $frag : BLANK;
228              
229 17         37 return ($self->[TEXT] = $scheme.$auth.$self->[PATH].$query.$frag);
230             }
231              
232              
233              
234             #-----------------------------------------------------------------------
235             # accessor/mutator methods
236             #-----------------------------------------------------------------------
237              
238             sub text {
239 17     17 1 40 $_[0]->[TEXT];
240             }
241              
242              
243             sub scheme {
244 2     2 1 12 my $self = shift;
245 2 100       6 if (@_) {
246 1         4 $self->[SCHEME] = shift;
247 1         3 $self->join_url;
248             }
249 2         9 return $self->[SCHEME];
250             }
251              
252              
253             sub authority {
254 2     2 1 16 my $self = shift;
255 2 50       5 if (@_) {
256 0         0 $self->[AUTHORITY] = shift;
257 0         0 $self->split_authority;
258 0         0 $self->join_url;
259             }
260 2         24 return $self->[AUTHORITY];
261             }
262              
263              
264             sub query {
265 3     3 1 12 my $self = shift;
266 3 100       8 if (@_) {
267 2         7 $self->[QUERY] = shift;
268 2         14 $self->split_query;
269 2         5 $self->join_url;
270             }
271 3         10 return $self->[QUERY];
272             }
273              
274              
275             sub params {
276 3     3 1 16 my $self = shift;
277 3   66     21 my $params = $self->[PARAMS] || $self->split_query;
278 3 100       19 if (@_) {
279 2         8 my $extra = Badger::Utils::params(@_);
280             # NOTE: this doesn't account for multi-valued params
281 2         12 @$params{ keys %$extra } = values %$extra;
282 2         7 $self->join_query;
283 2         6 $self->join_url;
284             }
285 3         6 return $params;
286             }
287              
288              
289             sub server {
290 8     8 1 10 my $self = shift;
291 8         13 my $scheme = $self->[SCHEME];
292 8         7 my $auth = $self->[AUTHORITY];
293              
294 8 50 33     29 $scheme = (defined $scheme && length $scheme) ? $scheme.':' : BLANK;
295 8 50 33     26 $auth = (defined $auth && length $auth) ? '//'.$auth : BLANK;
296              
297 8         33 return $scheme.$auth;
298             }
299              
300              
301             sub service {
302 6     6 1 8 my $self = shift;
303 6         9 return $self->server
304             . $self->[PATH];
305             }
306              
307              
308             sub request {
309 2     2 1 4 my $self = shift;
310 2         5 my $service = $self->service;
311 2         5 my $query = $self->[QUERY];
312              
313 2 50 33     12 $query = (defined $query && length $query) ? '?'.$query : BLANK;
314              
315 2         4 return $self->service
316             . $query;
317             }
318              
319              
320             # This is a quick-hack implementation of relative() that bodges the paths.
321             # This should be replaced with a more robust implementation. It also needs
322             # to be integrated with the work on Badger::Filesystem::Universal.
323              
324             sub relative {
325 2     2 1 4 my $self = shift;
326 2         5 my $path = join(SLASH, @_);
327 2         4 my $base = $self->[PATH];
328 2         4 $base =~ s{/$}{};
329 2 100       11 $path = join(SLASH, $base, $path)
330             unless $path =~ m{^/};
331 2         4 return $self->copy( path => $path );
332             }
333              
334              
335             sub absolute {
336 2     2 1 15 my $self = shift;
337 2         4 my $path = shift;
338 2         11 $path =~ s{^/*}{/};
339 2         6 return $self->copy( path => $path );
340             }
341              
342              
343             sub dump {
344 0     0 1 0 my $self = shift;
345 0 0       0 return '[URL:' . join('|', map { defined($_) ? $_ : '' } @$self) . ']';
  0         0  
346             }
347              
348              
349              
350             #-----------------------------------------------------------------------
351             # generated accessor/mutator methods for those with similar functionality
352             #-----------------------------------------------------------------------
353              
354             class->methods(
355             map {
356             my ($name, $slot) = @$_;
357             $name => sub {
358 7     7   14 my $self = shift;
359 7 100       15 if (@_) {
360             # if any of user, host or port are updated then we must
361             # reconstruct the authority and complete URL
362 4         10 $self->[$slot] = shift;
363 4         11 $self->join_authority;
364 4         7 $self->join_url;
365             }
366 7         32 return $self->[$slot];
367             }
368             }
369             [user => USER],
370             [host => HOST],
371             [port => PORT],
372             );
373              
374             class->methods(
375             map {
376             my ($name, $slot) = @$_;
377             $name => sub {
378 5     5   14 my $self = shift;
379 5 100       11 if (@_) {
380             # if either of the path or fragment are updated then we
381             # must regenerate the complete URL
382 3         6 $self->[$slot] = shift;
383 3         5 $self->join_url;
384             }
385 5         21 return $self->[$slot];
386             }
387             }
388             [path => PATH],
389             [fragment => FRAGMENT],
390             );
391              
392              
393             1;
394             __END__