File Coverage

blib/lib/IRI.pm
Criterion Covered Total %
statement 125 227 55.0
branch 49 104 47.1
condition 17 62 27.4
subroutine 15 20 75.0
pod 3 4 75.0
total 209 417 50.1


line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =head1 NAME
4              
5             IRI - Internationalized Resource Identifiers
6              
7             =head1 VERSION
8              
9             This document describes IRI version 0.010_01
10              
11             =head1 SYNOPSIS
12              
13             use IRI;
14            
15             my $i = IRI->new(value => 'https://example.org:80/index#frag');
16             say $i->scheme; # 'https'
17             say $i->path; # '/index'
18              
19             my $base = IRI->new(value => "http://www.hestebedg\x{e5}rd.dk/");
20             my $i = IRI->new(value => '#frag', base => $base);
21             say $i->abs; # 'http://www.hestebedgĂ„rd.dk/#frag'
22              
23             # Defer parsing of the IRI until necessary
24             my $i = IRI->new(value => "http://www.hestebedg\x{e5}rd.dk/", lazy => 1);
25             say $i->path; # path is parsed here
26              
27             =head1 DESCRIPTION
28              
29             The IRI module provides an object representation for Internationalized
30             Resource Identifiers (IRIs) as defined by
31             L and supports their parsing,
32             serializing, and base resolution.
33              
34             =head1 ATTRIBUTES
35              
36             =over 4
37              
38             =item C<< lazy >>
39              
40             A boolean value indicating whether the IRI should be parsed (and validated)
41             during object construction (false), or parsed only when an IRI component is
42             accessed (true). If no components are ever needed (e.g. an IRI is constructed
43             with a C<< value >> and C<< value >> is the only accessor ever called), no
44             parsing will take place.
45              
46             =back
47              
48             =head1 METHODS
49              
50             =over 4
51              
52             =item C<< as_string >>
53              
54             Returns the absolute IRI string resolved against the base IRI, if present;
55             the relative IRI string otherwise.
56              
57             =item C<< abs >>
58              
59             Returns the absolute IRI string (resolved against the base IRI if present).
60              
61             =item C<< scheme >>
62              
63             =item C<< host >>
64              
65             =item C<< port >>
66              
67             =item C<< user >>
68              
69             =item C<< path >>
70              
71             =item C<< fragment >>
72              
73             =item C<< query >>
74              
75             Returns the respective component of the parsed IRI.
76              
77             =cut
78              
79             {
80             package IRI;
81 3     3   2225 use v5.10.1;
  3         10  
82 3     3   16 use warnings;
  3         6  
  3         108  
83             our $VERSION = '0.010_01';
84 3     3   1716 use Moo;
  3         32254  
  3         12  
85 3     3   5646 use MooX::HandlesVia;
  3         28177  
  3         21  
86 3     3   2103 use Types::Standard qw(Str InstanceOf HashRef Bool);
  3         222974  
  3         27  
87 3     3   3495 use Scalar::Util qw(blessed);
  3         6  
  3         3389  
88            
89             # class_type 'URI';
90             # coerce 'IRI' => from 'Str' => via { IRI->new( value => $_ ) };
91             # coerce 'IRI' => from 'URI' => via { IRI->new( value => $_->as_string ) };
92              
93             has 'lazy' => (is => 'ro', isa => Bool, default => 0);
94             has '_initialized' => (is => 'rw', isa => Bool, default => 0, init_arg => undef);
95             has 'base' => (is => 'ro', isa => InstanceOf['IRI'], predicate => 'has_base', coerce => sub {
96             my $base = shift;
97             if (blessed($base)) {
98             if ($base->isa('IRI')) {
99             return $base;
100             } elsif ($base->isa('URI')) {
101             return IRI->new( value => $base->as_string );
102             }
103             } else {
104             return IRI->new($base);
105             }
106             });
107             has 'value' => (is => 'ro', isa => Str, default => '');
108             has 'components' => (is => 'ro', writer => '_set_components');
109             has 'abs' => (is => 'ro', lazy => 1, builder => '_abs');
110             has 'resolved_components' => (
111             is => 'ro',
112             isa => HashRef,
113             lazy => 1,
114             builder => '_resolved_components',
115             predicate => 1,
116             handles_via => 'Hash',
117             handles => {
118             authority => [ accessor => 'authority' ],
119             scheme => [ accessor => 'scheme' ],
120             host => [ accessor => 'host' ],
121             port => [ accessor => 'port' ],
122             user => [ accessor => 'user' ],
123             path => [ accessor => 'path' ],
124             fragment => [ accessor => 'fragment' ],
125             query => [ accessor => 'query' ],
126             },
127             );
128              
129             around BUILDARGS => sub {
130             my $orig = shift;
131             my $class = shift;
132             if (scalar(@_) == 1) {
133             return $class->$orig(value => shift);
134             }
135             return $class->$orig(@_);
136             };
137            
138             sub BUILD {
139 26     26 0 2136 my $self = shift;
140 26 100       90 if ($self->has_resolved_components) {
141 2         41 $self->_set_components($self->resolved_components);
142 2         156 $self->_initialized(1);
143             } else {
144 24 50       83 unless ($self->lazy) {
145 24         62 my $comp = $self->_parse_components($self->value);
146             }
147             }
148             }
149            
150             before [qw(components as_string abs resolved_components scheme host port user path fragment query)] => sub {
151             my $self = shift;
152             if (not $self->_initialized) {
153             # warn "Lazily initializing IRI";
154             my $comp = $self->_parse_components($self->value);
155             }
156             };
157              
158             # These regexes are (mostly) from the syntax grammar in RFC 3987
159             my $HEXDIG = qr<[0-9A-F]>o;
160             my $ALPHA = qr<[A-Za-z]>o;
161             my $subdelims = qr<[!\$&'()*+,;=]>xo;
162             my $gendelims = qr<[":/?#@] | \[ | \]>xo;
163             my $reserved = qr<${gendelims} | ${subdelims}>o;
164             my $unreserved = qr<${ALPHA} | [0-9] | [-._~]>xo;
165             my $pctencoded = qr<%[0-9A-Fa-f]{2}>o;
166             my $decoctet = qr<
167             [0-9] # 0-9
168             | [1-9][0-9] # 10-99
169             | 1 [0-9]{2} # 100-199
170             | 2 [0-4] [0-9] # 200-249
171             | 25 [0-5] # 250-255
172             >xo;
173             my $IPv4address = qr<
174             # IPv4address
175             ${decoctet}[.]${decoctet}[.]${decoctet}[.]${decoctet}
176             >xo;
177             my $h16 = qr<${HEXDIG}{1,4}>o;
178             my $ls32 = qr<
179             ( ${h16} : ${h16} )
180             | ${IPv4address}
181             >xo;
182             my $IPv6address = qr<
183             # IPv6address
184             ( ( ${h16} : ){6} ${ls32})
185             | ( :: ( ${h16} : ){5} ${ls32})
186             | (( ${h16} )? :: ( ${h16} : ){4} ${ls32})
187             | (( ( ${h16} : ){0,1} ${h16} )? :: ( ${h16} : ){3} ${ls32})
188             | (( ( ${h16} : ){0,2} ${h16} )? :: ( ${h16} : ){2} ${ls32})
189             | (( ( ${h16} : ){0,3} ${h16} )? :: ${h16} : ${ls32})
190             | (( ( ${h16} : ){0,4} ${h16} )? :: ${ls32})
191             | (( ( ${h16} : ){0,5} ${h16} )? :: ${h16})
192             | (( ( ${h16} : ){0,6} ${h16} )? ::)
193             >xo;
194             my $IPvFuture = qrxo;
195             my $IPliteral = qr<\[
196             # IPliteral
197             (${IPv6address} | ${IPvFuture})
198             \]
199             >xo;
200             my $port = qr<(?[0-9]*)>o;
201             my $scheme = qr<(?${ALPHA} ( ${ALPHA} | [0-9] | [+] | [-] | [.] )*)>xo;
202             my $iprivate = qr<[\x{E000}-\x{F8FF}] | [\x{F0000}-\x{FFFFD}] | [\x{100000}-\x{10FFFD}]>xo;
203             my $ucschar = qr<
204             [\x{a0}-\x{d7ff}] | [\x{f900}-\x{fdcf}] | [\x{fdf0}-\x{ffef}]
205             | [\x{10000}-\x{1FFFD}] | [\x{20000}-\x{2FFFD}] | [\x{30000}-\x{3FFFD}]
206             | [\x{40000}-\x{4FFFD}] | [\x{50000}-\x{5FFFD}] | [\x{60000}-\x{6FFFD}]
207             | [\x{70000}-\x{7FFFD}] | [\x{80000}-\x{8FFFD}] | [\x{90000}-\x{9FFFD}]
208             | [\x{A0000}-\x{AFFFD}] | [\x{B0000}-\x{BFFFD}] | [\x{C0000}-\x{CFFFD}]
209             | [\x{D0000}-\x{DFFFD}] | [\x{E1000}-\x{EFFFD}]
210             >xo;
211             my $iunreserved = qr<${ALPHA}|[0-9]|[-._~]|${ucschar}>o;
212             my $ipchar = qr<(${iunreserved})|(${pctencoded})|(${subdelims})|:|@>o;
213             my $ifragment = qr<(?(${ipchar}|/|[?])*)>o;
214             my $iquery = qr<(?(${ipchar}|${iprivate}|/|[?])*)>o;
215             my $isegmentnznc = qr<(${iunreserved}|${pctencoded}|${subdelims}|@)+ # non-zero-length segment without any colon ":"
216             >xo;
217             my $isegmentnz = qr<${ipchar}+>o;
218             my $isegment = qr<${ipchar}*>o;
219             my $ipathempty = qr<>o;
220             my $ipathrootless = qr<(?${isegmentnz}(/${isegment})*)>o;
221             my $ipathnoscheme = qr<(?${isegmentnznc}(/${isegment})*)>o;
222             my $ipathabsolute = qr<(?/(${isegmentnz}(/${isegment})*)?)>o;
223             my $ipathabempty = qr<(?(/${isegment})*)>o;
224             my $ipath = qr<
225             ${ipathabempty} # begins with "/" or is empty
226             | ${ipathabsolute} # begins with "/" but not "//"
227             | ${ipathnoscheme} # begins with a non-colon segment
228             | ${ipathrootless} # begins with a segment
229             | ${ipathempty} # zero characters
230             >xo;
231             my $iregname = qr<(${iunreserved}|${pctencoded}|${subdelims})*>o;
232             my $ihost = qr<(?${IPliteral}|${IPv4address}|${iregname})>o;
233             my $iuserinfo = qr<(?(${iunreserved}|${pctencoded}|${subdelims}|:)*)>o;
234             my $iauthority = qr<(?(${iuserinfo}@)?${ihost}(:${port})?)>o;
235             my $irelativepart = qr<
236             (//${iauthority}${ipathabempty})
237             | ${ipathabsolute}
238             | ${ipathnoscheme}
239             | ${ipathempty}
240             >xo;
241             my $irelativeref = qr<${irelativepart}([?]${iquery})?(#${ifragment})?>o;
242             my $ihierpart = qr<(//${iauthority}${ipathabempty})|(${ipathabsolute})|(${ipathrootless})|(${ipathempty})>o;
243             my $absoluteIRI = qr<${scheme}:${ihierpart}([?]${iquery})?>o;
244             my $IRI = qr<${scheme}:${ihierpart}([?]${iquery})?(#${ifragment})?>o;
245             my $IRIreference = qr<${IRI}|${irelativeref}>o;
246             sub _parse_components {
247 26     26   36 my $self = shift;
248 26         37 my $v = shift;
249 26         33 my $c;
250            
251 26 50       9838 if ($v =~ /^${IRIreference}$/o) {
252 3     3   1429 %$c = %+;
  3         1137  
  3         115  
  26         380  
253             } else {
254 3     3   21 use Data::Dumper;
  3         6  
  3         7707  
255 0         0 die "Not a valid IRI? " . Dumper($v);
256             }
257            
258 26   100     288 $c->{path} //= '';
259 26         69 $self->_set_components($c);
260 26         596 $self->_initialized(1);
261             }
262            
263             sub _merge {
264 4     4   7 my $self = shift;
265 4         7 my $base = shift;
266            
267 4         77 my $bc = $base->components;
268 4         183 my $c = $self->components;
269 4   33     118 my $base_has_authority = ($bc->{user} or $bc->{port} or defined($bc->{host}));
270 4 50 33     23 if ($base_has_authority and not($bc->{path})) {
271 0         0 return "/" . $c->{path};
272             } else {
273 4         7 my $bp = $bc->{path};
274 4         18 my @pathParts = split('/', $bp, -1); # -1 limit means $path='/' splits into ('', '')
275 4         8 pop(@pathParts);
276 4         9 push(@pathParts, $c->{path});
277 4         12 my $path = join('/', @pathParts);
278 4         14 return $path;
279             }
280             }
281              
282             sub _remove_dot_segments {
283 5     5   10 my $self = shift;
284 5         9 my $input = shift;
285 5         9 my @output;
286 5         17 while (length($input)) {
287 13 50       82 if ($input =~ m<^[.][.]/>) {
    50          
    50          
    50          
    100          
    50          
    50          
    50          
288 0         0 substr($input, 0, 3) = '';
289             } elsif ($input =~ m<^[.]/>) {
290 0         0 substr($input, 0, 2) = '';
291             } elsif ($input =~ m<^/[.]/>) {
292 0         0 substr($input, 0, 3) = '/';
293             } elsif ($input eq '/.') {
294 0         0 $input = '/';
295             } elsif ($input =~ m<^/[.][.]/>) {
296 1         2 substr($input, 0, 4) = '/';
297 1         4 pop(@output);
298             } elsif ($input eq '/..') {
299 0         0 $input = '/';
300 0         0 pop(@output);
301             } elsif ($input eq '.') {
302 0         0 $input = '';
303             } elsif ($input eq '..') {
304 0         0 $input = '';
305             } else {
306 12         29 my $leadingSlash = ($input =~ m<^/>);
307 12 50       30 if ($leadingSlash) {
308 12         23 substr($input, 0, 1) = '';
309             }
310 12         38 my ($part, @parts) = split('/', $input, -1);
311 12   100     31 $part //= '';
312 12 100       26 if (scalar(@parts)) {
313 7         15 unshift(@parts, '');
314             }
315 12         26 $input = join('/', @parts);
316 12 50       25 if ($leadingSlash) {
317 12         19 $part = "/$part";
318             }
319 12         47 push(@output, $part);
320             }
321             }
322 5         14 my $newPath = join('', @output);
323 5         17 return $newPath;
324             }
325              
326             sub _resolved_components {
327 13     13   608 my $self = shift;
328 13         39 my $value = $self->value;
329 13 100 66     180 if ($self->has_base and not($self->components->{scheme})) {
330             # Resolve IRI relative to the base IRI
331 8         230 my $base = $self->base;
332 8         18 my $v = $self->value;
333 8         19 my $bv = $base->value;
334             # warn "resolving IRI <$v> relative to the base IRI <$bv>";
335 8         10 my %components = %{ $self->components };
  8         139  
336 8         213 my %base = %{ $base->components };
  8         139  
337 8         215 my %target;
338            
339 8 50       23 if ($components{scheme}) {
340 0         0 foreach my $k (qw(scheme user port host path query)) {
341 0 0       0 if (exists $components{$k}) {
342 0         0 $target{$k} = $components{$k};
343             }
344             }
345             } else {
346 8 50 33     57 if ($components{user} or $components{port} or defined($components{host})) {
      33        
347 0         0 foreach my $k (qw(scheme user port host query)) {
348 0 0       0 if (exists $components{$k}) {
349 0         0 $target{$k} = $components{$k};
350             }
351             }
352 0         0 my $path = $components{path};
353 0         0 $target{path} = $self->_remove_dot_segments($path);
354             } else {
355 8 100       21 if ($components{path} eq '') {
356 3         10 $target{path} = $base{path};
357 3 50       10 if ($components{query}) {
358 0         0 $target{query} = $components{query};
359             } else {
360 3 50       9 if ($base{query}) {
361 0         0 $target{query} = $base{query};
362             }
363             }
364             } else {
365 5 100       19 if ($components{path} =~ m<^/>) {
366 1         3 my $path = $components{path};
367 1         3 $target{path} = $self->_remove_dot_segments($path);
368             } else {
369 4         21 my $path = $self->_merge($base);
370 4         17 $target{path} = $self->_remove_dot_segments($path);
371             }
372 5 50       23 if (defined($components{query})) {
373 0         0 $target{query} = $components{query};
374             }
375             }
376 8 50 33     45 if ($base{user} or $base{port} or defined($base{host})) {
      33        
377 8         19 foreach my $k (qw(user port host)) {
378 24 100       50 if (exists $base{$k}) {
379 8         18 $target{$k} = $base{$k};
380             }
381             }
382             }
383             }
384 8 50       21 if (defined($base{scheme})) {
385 8         19 $target{scheme} = $base{scheme};
386             }
387             }
388            
389 8 100       26 if (defined($components{fragment})) {
390 2         5 $target{fragment} = $components{fragment};
391             }
392            
393 8         204 return \%target;
394             }
395 5         91 return $self->components;
396             }
397            
398             sub _abs {
399 14     14   659 my $self = shift;
400 14         248 my $value = $self->_string_from_components( $self->resolved_components );
401 14         87 return $value;
402             }
403              
404             =item C<< rel ( $base ) >>
405              
406             Returns a new relative IRI object which, when resolved against the C<< $base >>
407             IRI, is equal to this IRI.
408              
409             =cut
410              
411             sub rel {
412             # based on code in URI
413 0     0 1 0 my $self = shift;
414 0         0 my $base = shift;
415 0         0 my $rel = IRI->new(value => $self->abs);
416            
417 0 0 0     0 if (($base->scheme // '') ne ($rel->scheme // '')) {
      0        
418 0         0 return IRI->new(value => $rel->abs);
419             }
420              
421 0         0 my $scheme = $rel->scheme;
422 0         0 my $auth = $rel->authority;
423 0         0 my $path = $rel->path;
424            
425 0 0 0     0 if (!defined($scheme) and !defined($auth)) {
426 0         0 return $rel;
427             }
428              
429 0         0 my $bscheme = $base->scheme;
430 0         0 my $bauth = $base->authority;
431 0         0 my $bpath = $base->path;
432              
433 0         0 for ($bscheme, $bauth, $auth) {
434 0 0       0 $_ = '' unless defined($_);
435             }
436            
437 0 0       0 if ($scheme eq $bscheme) {
438 0         0 $rel->scheme(undef);
439             }
440            
441 0 0 0     0 unless ($scheme eq $bscheme and $auth eq $bauth) {
442 0         0 return IRI->new(value => $rel->_abs);
443             }
444              
445 0         0 for ($path, $bpath) {
446 0 0       0 $_ = "/$_" unless m{^/};
447             }
448              
449             # Make it relative by eliminating:
450             # the scheme,
451 0         0 $rel->scheme(undef);
452              
453             # ... and authority
454 0         0 $rel->host(undef);
455 0         0 $rel->port(undef);
456 0         0 $rel->user(undef);
457            
458            
459 0         0 my @rparts = split('/', $path);
460 0         0 my @bparts = split('/', $bpath);
461 0         0 shift(@rparts);
462 0         0 shift(@bparts);
463 0 0 0     0 if (scalar(@rparts) and (scalar(@bparts) and $rparts[0] ne $bparts[0])) {
      0        
464             # use an absolute path, because $rel differs from $base at the very beginning
465             } else {
466             # This loop is based on code from Nicolai Langfeldt .
467             # First we calculate common initial path components length ($li).
468 0         0 my $li = 1;
469 0         0 while (1) {
470 0         0 my $i = index($path, '/', $li);
471 0 0 0     0 last if $i < 0 ||
      0        
472             $i != index($bpath, '/', $li) ||
473             substr($path,$li,$i-$li) ne substr($bpath,$li,$i-$li);
474 0         0 $li=$i+1;
475             }
476            
477             # then we nuke it from both paths
478 0         0 substr($path, 0,$li) = '';
479 0         0 substr($bpath,0,$li) = '';
480              
481              
482 0 0       0 if ($path eq $bpath) {
483 0         0 $rel->path('');
484 0 0 0     0 if (defined($rel->query) and defined($base->query)) {
    0          
    0          
485 0 0       0 if ($rel->query eq $base->query) {
486 0         0 $rel->query(undef);
487             } else {
488             #
489             }
490             } elsif (defined($rel->query)) {
491             #
492             } elsif (defined($base->query)) {
493 0         0 $rel->path($path);
494             } else {
495             #
496             }
497             } else {
498             # Add one "../" for each path component left in the base path
499 0         0 $path = ('../' x $bpath =~ tr|/|/|) . $path;
500 0 0       0 $path = "./" if $path eq '';
501 0         0 $rel->path($path);
502             }
503             }
504 0         0 return IRI->new(value => $rel->_abs);
505             }
506              
507             sub as_string {
508             my $self = shift;
509             if ($self->has_base || $self->has_resolved_components) {
510             return $self->abs;
511             } else {
512             return $self->value;
513             }
514             }
515            
516             sub _string_from_components {
517 14     14   615 my $self = shift;
518 14         22 my $components = shift;
519 14         23 my $iri = "";
520 14 100       39 if (my $s = $components->{scheme}) {
521 13         29 $iri .= "${s}:";
522             }
523            
524 14 100 66     82 if ($components->{user} or $components->{port} or defined($components->{host})) {
      66        
525             # has authority
526 13         37 $iri .= "//";
527 13 100       51 if (my $u = $components->{user}) {
528 1         5 $iri .= sprintf('%s@', $u);
529             }
530 13 50       38 if (defined(my $h = $components->{host})) {
531 13   50     37 $iri .= $h // '';
532             }
533 13 100       35 if (my $p = $components->{port}) {
534 1         3 $iri .= ":$p";
535             }
536             }
537            
538 14 50       30 if (defined(my $p = $components->{path})) {
539 14         24 $iri .= $p;
540             }
541            
542 14 100       34 if (defined(my $q = $components->{query})) {
543 1         2 $iri .= '?' . $q;
544             }
545            
546 14 100       30 if (defined(my $f = $components->{fragment})) {
547 3         7 $iri .= '#' . $f;
548             }
549            
550 14         28 return $iri;
551             }
552            
553             sub _encode {
554 0     0     my $str = shift;
555 0           $str =~ s~([%])~'%' . sprintf('%02x', ord($1))~ge; # gen-delims
  0            
556 0           $str =~ s~([/:?#@]|\[|\])~'%' . sprintf('%02x', ord($1))~ge; # gen-delims
  0            
557 0           $str =~ s~([$!&'()*+,;=])~'%' . sprintf('%02x', ord($1))~ge; # sub-delims
  0            
558 0           return $str;
559             }
560            
561             sub _unencode {
562 0     0     my $str = shift;
563 0 0         if (defined($str)) {
564 0           $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0            
565             }
566 0           return $str;
567             }
568            
569             =item C<< query_form >>
570              
571             Returns a HASH of key-value mappings for the unencoded, parsed query form data.
572              
573             =cut
574              
575             sub query_form {
576 0     0 1   my $self = shift;
577 0   0       my $q = $self->query // return;
578 0           my @pairs = split(/&/, $q);
579 0           return map { _unencode($_) } map { split(/=/, $_) } @pairs;
  0            
  0            
580             }
581              
582             =item C<< set_query_param ( $key => $value ) >>
583              
584             sets the respective query form value and returns a new L object.
585              
586             =cut
587              
588             sub set_query_param {
589 0     0 1   my $self = shift;
590 0   0       my $q = $self->query // return;
591 0           my %map = map { _unencode($_) } map { split(/=/, $_) } split(/&/, $q);
  0            
  0            
592 0           while (my ($k, $v) = splice(@_, 0, 2)) {
593 0           $map{$k} = $v;
594             }
595            
596 0           my %c = %{ $self->components };
  0            
597 0           my @pairs = map { join('=', (_encode($_), _encode($map{$_}))) } keys %map;
  0            
598 0           warn Dumper(\@pairs);
599 0           $c{query} = join('&', @pairs);
600            
601 0           my $v = $self->_string_from_components(\%c);
602 0           return $self->new( value => $v );
603             }
604             }
605              
606             1;
607              
608             __END__