File Coverage

blib/lib/IRI.pm
Criterion Covered Total %
statement 181 224 80.8
branch 82 104 78.8
condition 42 65 64.6
subroutine 15 19 78.9
pod 3 4 75.0
total 323 416 77.6


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.014
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 5     5   1429380 use v5.10.1;
  5         22  
82 5     5   32 use warnings;
  5         9  
  5         488  
83             our $VERSION = '0.014';
84 5     5   3621 use Moo;
  5         49689  
  5         32  
85 5     5   12356 use MooX::HandlesVia;
  5         79194  
  5         40  
86 5     5   4809 use Types::Standard qw(Str InstanceOf HashRef Bool);
  5         689017  
  5         43  
87 5     5   13967 use Scalar::Util qw(blessed);
  5         11  
  5         6640  
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 189     189 0 20856 my $self = shift;
140 189 100       771 if ($self->has_resolved_components) {
141 2         91 $self->_set_components($self->resolved_components);
142 2         227 $self->_initialized(1);
143             } else {
144 187 50       793 unless ($self->lazy) {
145 187         714 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 189     189   350 my $self = shift;
248 189         351 my $v = shift;
249 189         363 my $c;
250            
251 189 100 66     55867 if ($v =~ /\A(${IRIreference})\Z/mso and length($1) == length($v)) {
252 188         4211 %$c = %+;
253             } else {
254 5     5   35 use Data::Dumper;
  5         6  
  5         13782  
255 1         9 die "Not a valid IRI? " . Dumper($v);
256             }
257            
258 188   100     1694 $c->{path} //= '';
259 188         726 $self->_set_components($c);
260 188         5897 $self->_initialized(1);
261             }
262            
263             sub _merge {
264 21     21   52 my $self = shift;
265 21         37 my $base = shift;
266            
267 21         552 my $bc = $base->components;
268 21         1198 my $c = $self->components;
269 21   33     809 my $base_has_authority = ($bc->{user} or $bc->{port} or defined($bc->{host}));
270 21 50 33     98 if ($base_has_authority and not($bc->{path})) {
271 0         0 return "/" . $c->{path};
272             } else {
273 21         43 my $bp = $bc->{path};
274 21         113 my @pathParts = split('/', $bp, -1); # -1 limit means $path='/' splits into ('', '')
275 21         43 pop(@pathParts);
276 21         59 push(@pathParts, $c->{path});
277 21         67 my $path = join('/', @pathParts);
278 21         88 return $path;
279             }
280             }
281              
282             sub _remove_dot_segments {
283 24     24   40 my $self = shift;
284 24         44 my $input = shift;
285 24         58 my @output;
286 24         63 while (length($input)) {
287 78 50       463 if ($input =~ m<^[.][.]/>) {
    50          
    100          
    100          
    100          
    100          
    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 2         25 substr($input, 0, 3) = '/';
293             } elsif ($input eq '/.') {
294 1         5 $input = '/';
295             } elsif ($input =~ m<^/[.][.]/>) {
296 8         32 substr($input, 0, 4) = '/';
297 8         22 pop(@output);
298             } elsif ($input eq '/..') {
299 2         6 $input = '/';
300 2         5 pop(@output);
301             } elsif ($input eq '.') {
302 0         0 $input = '';
303             } elsif ($input eq '..') {
304 0         0 $input = '';
305             } else {
306 65         172 my $leadingSlash = ($input =~ m<^/>);
307 65 50       144 if ($leadingSlash) {
308 65         145 substr($input, 0, 1) = '';
309             }
310 65         207 my ($part, @parts) = split('/', $input, -1);
311 65   100     199 $part //= '';
312 65 100       140 if (scalar(@parts)) {
313 42         140 unshift(@parts, '');
314             }
315 65         132 $input = join('/', @parts);
316 65 50       125 if ($leadingSlash) {
317 65         103 $part = "/$part";
318             }
319 65         191 push(@output, $part);
320             }
321             }
322 24         58 my $newPath = join('', @output);
323 24         109 return $newPath;
324             }
325              
326             sub _resolved_components {
327 139     139   9809 my $self = shift;
328 139         443 my $value = $self->value;
329 139 100 100     1221 if ($self->has_base and not($self->components->{scheme})) {
330             # Resolve IRI relative to the base IRI
331 30         996 my $base = $self->base;
332 30         69 my $v = $self->value;
333 30         72 my $bv = $base->value;
334             # warn "resolving IRI <$v> relative to the base IRI <$bv>";
335 30         48 my %components = %{ $self->components };
  30         704  
336 30         995 my %base = %{ $base->components };
  30         673  
337 30         1065 my %target;
338            
339 30 50       98 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 30 100 33     215 if ($components{user} or $components{port} or defined($components{host})) {
      66        
347 1         4 foreach my $k (qw(scheme user port host query)) {
348 5 100       15 if (exists $components{$k}) {
349 1         4 $target{$k} = $components{$k};
350             }
351             }
352 1         3 my $path = $components{path};
353 1         5 $target{path} = $self->_remove_dot_segments($path);
354             } else {
355 29 100       82 if ($components{path} eq '') {
356 6         22 $target{path} = $base{path};
357 6 100       34 if ($components{query}) {
358 1         4 $target{query} = $components{query};
359             } else {
360 5 100       21 if ($base{query}) {
361 2         6 $target{query} = $base{query};
362             }
363             }
364             } else {
365 23 100       77 if ($components{path} =~ m<^/>) {
366 2         9 my $path = $components{path};
367 2         11 $target{path} = $self->_remove_dot_segments($path);
368             } else {
369 21         128 my $path = $self->_merge($base);
370 21         68 $target{path} = $self->_remove_dot_segments($path);
371             }
372 23 100       68 if (defined($components{query})) {
373 3         8 $target{query} = $components{query};
374             }
375             }
376 29 50 33     189 if ($base{user} or $base{port} or defined($base{host})) {
      33        
377 29         65 foreach my $k (qw(user port host)) {
378 87 100       206 if (exists $base{$k}) {
379 29         80 $target{$k} = $base{$k};
380             }
381             }
382             }
383             }
384 30 50       71 if (defined($base{scheme})) {
385 30         71 $target{scheme} = $base{scheme};
386             }
387             }
388            
389 30 100       87 if (defined($components{fragment})) {
390 6         17 $target{fragment} = $components{fragment};
391             }
392            
393 30         868 return \%target;
394             }
395 109         2965 return $self->components;
396             }
397            
398             sub _abs {
399 132     132   5555 my $self = shift;
400 132         3421 my $value = $self->_string_from_components( $self->resolved_components );
401 132         2816 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 44     44 1 1636 my $self = shift;
414 44         91 my $base = shift;
415 44         1498 my $rel = IRI->new(value => $self->abs);
416            
417 44 100 50     2933 if (($base->scheme // '') ne ($rel->scheme // '')) {
      50        
418 4         619 return IRI->new(value => $rel->abs);
419             }
420              
421 40         5993 my $scheme = $rel->scheme;
422 40         5242 my $auth = $rel->authority;
423 40         3504 my $path = $rel->path;
424            
425 40 0 33     4880 if (!defined($scheme) and !defined($auth)) {
426 0         0 return $rel;
427             }
428              
429 40         1082 my $bscheme = $base->scheme;
430 40         5406 my $bauth = $base->authority;
431 40         3571 my $bpath = $base->path;
432              
433 40         6364 for ($bscheme, $bauth, $auth) {
434 120 50       290 $_ = '' unless defined($_);
435             }
436            
437 40 50       119 if ($scheme eq $bscheme) {
438 40         1135 $rel->scheme(undef);
439             }
440            
441 40 100 66     4886 unless ($scheme eq $bscheme and $auth eq $bauth) {
442 6         20 return IRI->new(value => $rel->_abs);
443             }
444              
445 34         87 for ($path, $bpath) {
446 68 50       234 $_ = "/$_" unless m{^/};
447             }
448              
449             # Make it relative by eliminating:
450             # the scheme,
451 34         993 $rel->scheme(undef);
452              
453             # ... and authority
454 34         4497 $rel->host(undef);
455 34         4907 $rel->port(undef);
456 34         4623 $rel->user(undef);
457            
458            
459 34         4190 my @rparts = split('/', $path);
460 34         115 my @bparts = split('/', $bpath);
461 34         61 shift(@rparts);
462 34         71 shift(@bparts);
463 34 100 100     242 if (scalar(@rparts) and (scalar(@bparts) and $rparts[0] ne $bparts[0])) {
      100        
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 32         60 my $li = 1;
469 32         47 while (1) {
470 65         130 my $i = index($path, '/', $li);
471 65 100 100     327 last if $i < 0 ||
      66        
472             $i != index($bpath, '/', $li) ||
473             substr($path,$li,$i-$li) ne substr($bpath,$li,$i-$li);
474 33         68 $li=$i+1;
475             }
476            
477             # then we nuke it from both paths
478 32         80 substr($path, 0,$li) = '';
479 32         87 substr($bpath,0,$li) = '';
480              
481              
482 32 100       87 if ($path eq $bpath) {
483 8         258 $rel->path('');
484 8 100 100     1119 if (defined($rel->query) and defined($base->query)) {
    100          
    100          
485 3 100       415 if ($rel->query eq $base->query) {
486 2         1637 $rel->query(undef);
487             } else {
488             #
489             }
490             } elsif (defined($rel->query)) {
491             #
492             } elsif (defined($base->query)) {
493 1         163 $rel->path($path);
494             } else {
495             #
496             }
497             } else {
498             # Add one "../" for each path component left in the base path
499 24         82 $path = ('../' x $bpath =~ tr|/|/|) . $path;
500 24 100       68 $path = "./" if $path eq '';
501 24         699 $rel->path($path);
502             }
503             }
504 34         3660 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 132     132   8314 my $self = shift;
518 132         229 my $components = shift;
519 132         226 my $iri = "";
520 132 100       530 if (my $s = $components->{scheme}) {
521 84         184 $iri .= "${s}:";
522             }
523            
524 132 100 100     928 if ($components->{user} or $components->{port} or defined($components->{host})) {
      100        
525             # has authority
526 90         156 $iri .= "//";
527 90 100       237 if (my $u = $components->{user}) {
528 1         3 $iri .= sprintf('%s@', $u);
529             }
530 90 50       284 if (defined(my $h = $components->{host})) {
531 90   50     238 $iri .= $h // '';
532             }
533 90 100       240 if (my $p = $components->{port}) {
534 10         32 $iri .= ":$p";
535             }
536             }
537            
538 132 50       360 if (defined(my $p = $components->{path})) {
539 132         283 $iri .= $p;
540             }
541            
542 132 100       338 if (defined(my $q = $components->{query})) {
543 26         56 $iri .= '?' . $q;
544             }
545            
546 132 100       322 if (defined(my $f = $components->{fragment})) {
547 38         79 $iri .= '#' . $f;
548             }
549            
550 132         367 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__