File Coverage

blib/lib/IRI.pm
Criterion Covered Total %
statement 183 227 80.6
branch 81 104 77.8
condition 40 62 64.5
subroutine 16 20 80.0
pod 3 4 75.0
total 323 417 77.4


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.011
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   199878 use v5.10.1;
  5         40  
82 5     5   30 use warnings;
  5         10  
  5         203  
83             our $VERSION = '0.011';
84 5     5   2827 use Moo;
  5         57956  
  5         26  
85 5     5   10141 use MooX::HandlesVia;
  5         49380  
  5         38  
86 5     5   3870 use Types::Standard qw(Str InstanceOf HashRef Bool);
  5         384452  
  5         59  
87 5     5   6769 use Scalar::Util qw(blessed);
  5         22  
  5         5994  
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 188     188 0 15376 my $self = shift;
140 188 100       556 if ($self->has_resolved_components) {
141 2         43 $self->_set_components($self->resolved_components);
142 2         151 $self->_initialized(1);
143             } else {
144 186 50       542 unless ($self->lazy) {
145 186         434 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 188     188   237 my $self = shift;
248 188         251 my $v = shift;
249 188         239 my $c;
250            
251 188 50       22321 if ($v =~ /^${IRIreference}$/o) {
252 5     5   2801 %$c = %+;
  5         2198  
  5         202  
  188         2971  
253             } else {
254 5     5   34 use Data::Dumper;
  5         12  
  5         13226  
255 0         0 die "Not a valid IRI? " . Dumper($v);
256             }
257            
258 188   100     1119 $c->{path} //= '';
259 188         411 $self->_set_components($c);
260 188         3841 $self->_initialized(1);
261             }
262            
263             sub _merge {
264 21     21   32 my $self = shift;
265 21         31 my $base = shift;
266            
267 21         410 my $bc = $base->components;
268 21         836 my $c = $self->components;
269 21   33     569 my $base_has_authority = ($bc->{user} or $bc->{port} or defined($bc->{host}));
270 21 50 33     90 if ($base_has_authority and not($bc->{path})) {
271 0         0 return "/" . $c->{path};
272             } else {
273 21         34 my $bp = $bc->{path};
274 21         76 my @pathParts = split('/', $bp, -1); # -1 limit means $path='/' splits into ('', '')
275 21         35 pop(@pathParts);
276 21         45 push(@pathParts, $c->{path});
277 21         62 my $path = join('/', @pathParts);
278 21         58 return $path;
279             }
280             }
281              
282             sub _remove_dot_segments {
283 24     24   33 my $self = shift;
284 24         33 my $input = shift;
285 24         33 my @output;
286 24         57 while (length($input)) {
287 78 50       369 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         8 substr($input, 0, 3) = '/';
293             } elsif ($input eq '/.') {
294 1         3 $input = '/';
295             } elsif ($input =~ m<^/[.][.]/>) {
296 8         18 substr($input, 0, 4) = '/';
297 8         17 pop(@output);
298             } elsif ($input eq '/..') {
299 2         4 $input = '/';
300 2         3 pop(@output);
301             } elsif ($input eq '.') {
302 0         0 $input = '';
303             } elsif ($input eq '..') {
304 0         0 $input = '';
305             } else {
306 65         144 my $leadingSlash = ($input =~ m<^/>);
307 65 50       116 if ($leadingSlash) {
308 65         109 substr($input, 0, 1) = '';
309             }
310 65         172 my ($part, @parts) = split('/', $input, -1);
311 65   100     137 $part //= '';
312 65 100       118 if (scalar(@parts)) {
313 42         98 unshift(@parts, '');
314             }
315 65         120 $input = join('/', @parts);
316 65 50       107 if ($leadingSlash) {
317 65         148 $part = "/$part";
318             }
319 65         190 push(@output, $part);
320             }
321             }
322 24         55 my $newPath = join('', @output);
323 24         112 return $newPath;
324             }
325              
326             sub _resolved_components {
327 139     139   6744 my $self = shift;
328 139         327 my $value = $self->value;
329 139 100 100     990 if ($self->has_base and not($self->components->{scheme})) {
330             # Resolve IRI relative to the base IRI
331 30         795 my $base = $self->base;
332 30         50 my $v = $self->value;
333 30         60 my $bv = $base->value;
334             # warn "resolving IRI <$v> relative to the base IRI <$bv>";
335 30         36 my %components = %{ $self->components };
  30         505  
336 30         778 my %base = %{ $base->components };
  30         540  
337 30         795 my %target;
338            
339 30 50       65 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     169 if ($components{user} or $components{port} or defined($components{host})) {
      66        
347 1         3 foreach my $k (qw(scheme user port host query)) {
348 5 100       13 if (exists $components{$k}) {
349 1         3 $target{$k} = $components{$k};
350             }
351             }
352 1         2 my $path = $components{path};
353 1         4 $target{path} = $self->_remove_dot_segments($path);
354             } else {
355 29 100       63 if ($components{path} eq '') {
356 6         16 $target{path} = $base{path};
357 6 100       19 if ($components{query}) {
358 1         3 $target{query} = $components{query};
359             } else {
360 5 100       15 if ($base{query}) {
361 2         5 $target{query} = $base{query};
362             }
363             }
364             } else {
365 23 100       58 if ($components{path} =~ m<^/>) {
366 2         6 my $path = $components{path};
367 2         8 $target{path} = $self->_remove_dot_segments($path);
368             } else {
369 21         61 my $path = $self->_merge($base);
370 21         60 $target{path} = $self->_remove_dot_segments($path);
371             }
372 23 100       66 if (defined($components{query})) {
373 3         7 $target{query} = $components{query};
374             }
375             }
376 29 50 33     153 if ($base{user} or $base{port} or defined($base{host})) {
      33        
377 29         59 foreach my $k (qw(user port host)) {
378 87 100       157 if (exists $base{$k}) {
379 29         73 $target{$k} = $base{$k};
380             }
381             }
382             }
383             }
384 30 50       64 if (defined($base{scheme})) {
385 30         58 $target{scheme} = $base{scheme};
386             }
387             }
388            
389 30 100       65 if (defined($components{fragment})) {
390 6         12 $target{fragment} = $components{fragment};
391             }
392            
393 30         625 return \%target;
394             }
395 109         2132 return $self->components;
396             }
397            
398             sub _abs {
399 132     132   3917 my $self = shift;
400 132         2305 my $value = $self->_string_from_components( $self->resolved_components );
401 132         1754 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 1154 my $self = shift;
414 44         60 my $base = shift;
415 44         983 my $rel = IRI->new(value => $self->abs);
416            
417 44 100 50     1967 if (($base->scheme // '') ne ($rel->scheme // '')) {
      50        
418 4         389 return IRI->new(value => $rel->abs);
419             }
420              
421 40         3916 my $scheme = $rel->scheme;
422 40         3689 my $auth = $rel->authority;
423 40         2488 my $path = $rel->path;
424            
425 40 0 33     3142 if (!defined($scheme) and !defined($auth)) {
426 0         0 return $rel;
427             }
428              
429 40         693 my $bscheme = $base->scheme;
430 40         3573 my $bauth = $base->authority;
431 40         2339 my $bpath = $base->path;
432              
433 40         3086 for ($bscheme, $bauth, $auth) {
434 120 50       218 $_ = '' unless defined($_);
435             }
436            
437 40 50       89 if ($scheme eq $bscheme) {
438 40         751 $rel->scheme(undef);
439             }
440            
441 40 100 66     3253 unless ($scheme eq $bscheme and $auth eq $bauth) {
442 6         18 return IRI->new(value => $rel->_abs);
443             }
444              
445 34         69 for ($path, $bpath) {
446 68 50       216 $_ = "/$_" unless m{^/};
447             }
448              
449             # Make it relative by eliminating:
450             # the scheme,
451 34         678 $rel->scheme(undef);
452              
453             # ... and authority
454 34         3230 $rel->host(undef);
455 34         3482 $rel->port(undef);
456 34         3235 $rel->user(undef);
457            
458            
459 34         2756 my @rparts = split('/', $path);
460 34         84 my @bparts = split('/', $bpath);
461 34         48 shift(@rparts);
462 34         52 shift(@bparts);
463 34 100 100     178 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         44 my $li = 1;
469 32         46 while (1) {
470 65         108 my $i = index($path, '/', $li);
471 65 100 100     279 last if $i < 0 ||
      66        
472             $i != index($bpath, '/', $li) ||
473             substr($path,$li,$i-$li) ne substr($bpath,$li,$i-$li);
474 33         62 $li=$i+1;
475             }
476            
477             # then we nuke it from both paths
478 32         67 substr($path, 0,$li) = '';
479 32         54 substr($bpath,0,$li) = '';
480              
481              
482 32 100       66 if ($path eq $bpath) {
483 8         161 $rel->path('');
484 8 100 100     757 if (defined($rel->query) and defined($base->query)) {
    100          
    100          
485 3 100       281 if ($rel->query eq $base->query) {
486 2         187 $rel->query(undef);
487             } else {
488             #
489             }
490             } elsif (defined($rel->query)) {
491             #
492             } elsif (defined($base->query)) {
493 1         94 $rel->path($path);
494             } else {
495             #
496             }
497             } else {
498             # Add one "../" for each path component left in the base path
499 24         73 $path = ('../' x $bpath =~ tr|/|/|) . $path;
500 24 100       55 $path = "./" if $path eq '';
501 24         494 $rel->path($path);
502             }
503             }
504 34         2594 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   5743 my $self = shift;
518 132         180 my $components = shift;
519 132         170 my $iri = "";
520 132 100       315 if (my $s = $components->{scheme}) {
521 84         180 $iri .= "${s}:";
522             }
523            
524 132 100 100     721 if ($components->{user} or $components->{port} or defined($components->{host})) {
      100        
525             # has authority
526 90         167 $iri .= "//";
527 90 100       210 if (my $u = $components->{user}) {
528 1         6 $iri .= sprintf('%s@', $u);
529             }
530 90 50       209 if (defined(my $h = $components->{host})) {
531 90   50     213 $iri .= $h // '';
532             }
533 90 100       190 if (my $p = $components->{port}) {
534 10         23 $iri .= ":$p";
535             }
536             }
537            
538 132 50       301 if (defined(my $p = $components->{path})) {
539 132         202 $iri .= $p;
540             }
541            
542 132 100       257 if (defined(my $q = $components->{query})) {
543 26         49 $iri .= '?' . $q;
544             }
545            
546 132 100       301 if (defined(my $f = $components->{fragment})) {
547 38         68 $iri .= '#' . $f;
548             }
549            
550 132         264 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__