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.013
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   1400358 use v5.10.1;
  5         20  
82 5     5   34 use warnings;
  5         8  
  5         561  
83             our $VERSION = '0.013';
84 5     5   3812 use Moo;
  5         47487  
  5         27  
85 5     5   12051 use MooX::HandlesVia;
  5         79253  
  5         41  
86 5     5   4554 use Types::Standard qw(Str InstanceOf HashRef Bool);
  5         786247  
  5         64  
87 5     5   19606 use Scalar::Util qw(blessed);
  5         15  
  5         9347  
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 19464 my $self = shift;
140 189 100       657 if ($self->has_resolved_components) {
141 2         67 $self->_set_components($self->resolved_components);
142 2         184 $self->_initialized(1);
143             } else {
144 187 50       599 unless ($self->lazy) {
145 187         568 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   362 my $self = shift;
248 189         328 my $v = shift;
249 189         260 my $c;
250            
251 189 100 66     54587 if ($v =~ /\A(${IRIreference})\Z/mso and length($1) == length($v)) {
252 188         3299 %$c = %+;
253             } else {
254 5     5   49 use Data::Dumper;
  5         10  
  5         17934  
255 1         15 die "Not a valid IRI? " . Dumper($v);
256             }
257            
258 188   100     1411 $c->{path} //= '';
259 188         506 $self->_set_components($c);
260 188         4492 $self->_initialized(1);
261             }
262            
263             sub _merge {
264 21     21   29 my $self = shift;
265 21         38 my $base = shift;
266            
267 21         358 my $bc = $base->components;
268 21         799 my $c = $self->components;
269 21   33     519 my $base_has_authority = ($bc->{user} or $bc->{port} or defined($bc->{host}));
270 21 50 33     71 if ($base_has_authority and not($bc->{path})) {
271 0         0 return "/" . $c->{path};
272             } else {
273 21         41 my $bp = $bc->{path};
274 21         78 my @pathParts = split('/', $bp, -1); # -1 limit means $path='/' splits into ('', '')
275 21         33 pop(@pathParts);
276 21         45 push(@pathParts, $c->{path});
277 21         57 my $path = join('/', @pathParts);
278 21         67 return $path;
279             }
280             }
281              
282             sub _remove_dot_segments {
283 24     24   28 my $self = shift;
284 24         49 my $input = shift;
285 24         37 my @output;
286 24         52 while (length($input)) {
287 78 50       367 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         5 substr($input, 0, 3) = '/';
293             } elsif ($input eq '/.') {
294 1         2 $input = '/';
295             } elsif ($input =~ m<^/[.][.]/>) {
296 8         15 substr($input, 0, 4) = '/';
297 8         13 pop(@output);
298             } elsif ($input eq '/..') {
299 2         4 $input = '/';
300 2         9 pop(@output);
301             } elsif ($input eq '.') {
302 0         0 $input = '';
303             } elsif ($input eq '..') {
304 0         0 $input = '';
305             } else {
306 65         113 my $leadingSlash = ($input =~ m<^/>);
307 65 50       124 if ($leadingSlash) {
308 65         137 substr($input, 0, 1) = '';
309             }
310 65         148 my ($part, @parts) = split('/', $input, -1);
311 65   100     148 $part //= '';
312 65 100       124 if (scalar(@parts)) {
313 42         68 unshift(@parts, '');
314             }
315 65         129 $input = join('/', @parts);
316 65 50       84 if ($leadingSlash) {
317 65         80 $part = "/$part";
318             }
319 65         150 push(@output, $part);
320             }
321             }
322 24         46 my $newPath = join('', @output);
323 24         91 return $newPath;
324             }
325              
326             sub _resolved_components {
327 139     139   7859 my $self = shift;
328 139         330 my $value = $self->value;
329 139 100 100     1018 if ($self->has_base and not($self->components->{scheme})) {
330             # Resolve IRI relative to the base IRI
331 30         777 my $base = $self->base;
332 30         54 my $v = $self->value;
333 30         54 my $bv = $base->value;
334             # warn "resolving IRI <$v> relative to the base IRI <$bv>";
335 30         34 my %components = %{ $self->components };
  30         505  
336 30         821 my %base = %{ $base->components };
  30         555  
337 30         878 my %target;
338            
339 30 50       69 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     172 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       14 if (exists $components{$k}) {
349 1         4 $target{$k} = $components{$k};
350             }
351             }
352 1         4 my $path = $components{path};
353 1         4 $target{path} = $self->_remove_dot_segments($path);
354             } else {
355 29 100       88 if ($components{path} eq '') {
356 6         16 $target{path} = $base{path};
357 6 100       20 if ($components{query}) {
358 1         3 $target{query} = $components{query};
359             } else {
360 5 100       38 if ($base{query}) {
361 2         5 $target{query} = $base{query};
362             }
363             }
364             } else {
365 23 100       60 if ($components{path} =~ m<^/>) {
366 2         8 my $path = $components{path};
367 2         9 $target{path} = $self->_remove_dot_segments($path);
368             } else {
369 21         80 my $path = $self->_merge($base);
370 21         58 $target{path} = $self->_remove_dot_segments($path);
371             }
372 23 100       60 if (defined($components{query})) {
373 3         7 $target{query} = $components{query};
374             }
375             }
376 29 50 33     142 if ($base{user} or $base{port} or defined($base{host})) {
      33        
377 29         70 foreach my $k (qw(user port host)) {
378 87 100       158 if (exists $base{$k}) {
379 29         70 $target{$k} = $base{$k};
380             }
381             }
382             }
383             }
384 30 50       110 if (defined($base{scheme})) {
385 30         59 $target{scheme} = $base{scheme};
386             }
387             }
388            
389 30 100       66 if (defined($components{fragment})) {
390 6         34 $target{fragment} = $components{fragment};
391             }
392            
393 30         728 return \%target;
394             }
395 109         2409 return $self->components;
396             }
397            
398             sub _abs {
399 132     132   4600 my $self = shift;
400 132         2965 my $value = $self->_string_from_components( $self->resolved_components );
401 132         2166 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 1352 my $self = shift;
414 44         72 my $base = shift;
415 44         1256 my $rel = IRI->new(value => $self->abs);
416            
417 44 100 50     2341 if (($base->scheme // '') ne ($rel->scheme // '')) {
      50        
418 4         462 return IRI->new(value => $rel->abs);
419             }
420              
421 40         4751 my $scheme = $rel->scheme;
422 40         4478 my $auth = $rel->authority;
423 40         2965 my $path = $rel->path;
424            
425 40 0 33     3894 if (!defined($scheme) and !defined($auth)) {
426 0         0 return $rel;
427             }
428              
429 40         906 my $bscheme = $base->scheme;
430 40         4352 my $bauth = $base->authority;
431 40         2979 my $bpath = $base->path;
432              
433 40         3730 for ($bscheme, $bauth, $auth) {
434 120 50       269 $_ = '' unless defined($_);
435             }
436            
437 40 50       104 if ($scheme eq $bscheme) {
438 40         869 $rel->scheme(undef);
439             }
440            
441 40 100 66     4776 unless ($scheme eq $bscheme and $auth eq $bauth) {
442 6         18 return IRI->new(value => $rel->_abs);
443             }
444              
445 34         84 for ($path, $bpath) {
446 68 50       217 $_ = "/$_" unless m{^/};
447             }
448              
449             # Make it relative by eliminating:
450             # the scheme,
451 34         854 $rel->scheme(undef);
452              
453             # ... and authority
454 34         3989 $rel->host(undef);
455 34         4050 $rel->port(undef);
456 34         4076 $rel->user(undef);
457            
458            
459 34         3687 my @rparts = split('/', $path);
460 34         121 my @bparts = split('/', $bpath);
461 34         65 shift(@rparts);
462 34         62 shift(@bparts);
463 34 100 100     201 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         69 my $li = 1;
469 32         45 while (1) {
470 65         120 my $i = index($path, '/', $li);
471 65 100 100     322 last if $i < 0 ||
      66        
472             $i != index($bpath, '/', $li) ||
473             substr($path,$li,$i-$li) ne substr($bpath,$li,$i-$li);
474 33         66 $li=$i+1;
475             }
476            
477             # then we nuke it from both paths
478 32         109 substr($path, 0,$li) = '';
479 32         66 substr($bpath,0,$li) = '';
480              
481              
482 32 100       72 if ($path eq $bpath) {
483 8         196 $rel->path('');
484 8 100 100     1448 if (defined($rel->query) and defined($base->query)) {
    100          
    100          
485 3 100       447 if ($rel->query eq $base->query) {
486 2         260 $rel->query(undef);
487             } else {
488             #
489             }
490             } elsif (defined($rel->query)) {
491             #
492             } elsif (defined($base->query)) {
493 1         120 $rel->path($path);
494             } else {
495             #
496             }
497             } else {
498             # Add one "../" for each path component left in the base path
499 24         83 $path = ('../' x $bpath =~ tr|/|/|) . $path;
500 24 100       71 $path = "./" if $path eq '';
501 24         591 $rel->path($path);
502             }
503             }
504 34         3188 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   6823 my $self = shift;
518 132         214 my $components = shift;
519 132         220 my $iri = "";
520 132 100       404 if (my $s = $components->{scheme}) {
521 84         173 $iri .= "${s}:";
522             }
523            
524 132 100 100     794 if ($components->{user} or $components->{port} or defined($components->{host})) {
      100        
525             # has authority
526 90         142 $iri .= "//";
527 90 100       209 if (my $u = $components->{user}) {
528 1         3 $iri .= sprintf('%s@', $u);
529             }
530 90 50       244 if (defined(my $h = $components->{host})) {
531 90   50     222 $iri .= $h // '';
532             }
533 90 100       209 if (my $p = $components->{port}) {
534 10         23 $iri .= ":$p";
535             }
536             }
537            
538 132 50       316 if (defined(my $p = $components->{path})) {
539 132         242 $iri .= $p;
540             }
541            
542 132 100       362 if (defined(my $q = $components->{query})) {
543 26         53 $iri .= '?' . $q;
544             }
545            
546 132 100       292 if (defined(my $f = $components->{fragment})) {
547 38         72 $iri .= '#' . $f;
548             }
549            
550 132         313 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__