File Coverage

blib/lib/IRI.pm
Criterion Covered Total %
statement 119 177 67.2
branch 42 74 56.7
condition 15 37 40.5
subroutine 15 19 78.9
pod 2 3 66.6
total 193 310 62.2


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
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   2234 use v5.10.1;
  3         11  
82 3     3   14 use warnings;
  3         5  
  3         109  
83             our $VERSION = '0.010';
84 3     3   1697 use Moo;
  3         32343  
  3         14  
85 3     3   5814 use MooX::HandlesVia;
  3         28748  
  3         17  
86 3     3   2164 use Types::Standard qw(Str InstanceOf HashRef Bool);
  3         221829  
  3         30  
87 3     3   3486 use Scalar::Util qw(blessed);
  3         6  
  3         3597  
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             handles_via => 'Hash',
116             handles => {
117             scheme => [ accessor => 'scheme' ],
118             host => [ accessor => 'host' ],
119             port => [ accessor => 'port' ],
120             user => [ accessor => 'user' ],
121             path => [ accessor => 'path' ],
122             fragment => [ accessor => 'fragment' ],
123             query => [ accessor => 'query' ],
124             },
125             );
126              
127             around BUILDARGS => sub {
128             my $orig = shift;
129             my $class = shift;
130             if (scalar(@_) == 1) {
131             return $class->$orig(value => shift);
132             }
133             return $class->$orig(@_);
134             };
135            
136             sub BUILD {
137 24     24 0 1909 my $self = shift;
138 24 50       94 unless ($self->lazy) {
139 24         65 my $comp = $self->_parse_components($self->value);
140             }
141             }
142            
143             before [qw(components as_string abs resolved_components scheme host port user path fragment query)] => sub {
144             my $self = shift;
145             if (not $self->_initialized) {
146             # warn "Lazily initializing IRI";
147             my $comp = $self->_parse_components($self->value);
148             }
149             };
150              
151             # These regexes are (mostly) from the syntax grammar in RFC 3987
152             my $HEXDIG = qr<[0-9A-F]>o;
153             my $ALPHA = qr<[A-Za-z]>o;
154             my $subdelims = qr<[!\$&'()*+,;=]>xo;
155             my $gendelims = qr<[":/?#@] | \[ | \]>xo;
156             my $reserved = qr<${gendelims} | ${subdelims}>o;
157             my $unreserved = qr<${ALPHA} | [0-9] | [-._~]>xo;
158             my $pctencoded = qr<%[0-9A-Fa-f]{2}>o;
159             my $decoctet = qr<
160             [0-9] # 0-9
161             | [1-9][0-9] # 10-99
162             | 1 [0-9]{2} # 100-199
163             | 2 [0-4] [0-9] # 200-249
164             | 25 [0-5] # 250-255
165             >xo;
166             my $IPv4address = qr<
167             # IPv4address
168             ${decoctet}[.]${decoctet}[.]${decoctet}[.]${decoctet}
169             >xo;
170             my $h16 = qr<${HEXDIG}{1,4}>o;
171             my $ls32 = qr<
172             ( ${h16} : ${h16} )
173             | ${IPv4address}
174             >xo;
175             my $IPv6address = qr<
176             # IPv6address
177             ( ( ${h16} : ){6} ${ls32})
178             | ( :: ( ${h16} : ){5} ${ls32})
179             | (( ${h16} )? :: ( ${h16} : ){4} ${ls32})
180             | (( ( ${h16} : ){0,1} ${h16} )? :: ( ${h16} : ){3} ${ls32})
181             | (( ( ${h16} : ){0,2} ${h16} )? :: ( ${h16} : ){2} ${ls32})
182             | (( ( ${h16} : ){0,3} ${h16} )? :: ${h16} : ${ls32})
183             | (( ( ${h16} : ){0,4} ${h16} )? :: ${ls32})
184             | (( ( ${h16} : ){0,5} ${h16} )? :: ${h16})
185             | (( ( ${h16} : ){0,6} ${h16} )? ::)
186             >xo;
187             my $IPvFuture = qrxo;
188             my $IPliteral = qr<\[
189             # IPliteral
190             (${IPv6address} | ${IPvFuture})
191             \]
192             >xo;
193             my $port = qr<(?[0-9]*)>o;
194             my $scheme = qr<(?${ALPHA} ( ${ALPHA} | [0-9] | [+] | [-] | [.] )*)>xo;
195             my $iprivate = qr<[\x{E000}-\x{F8FF}] | [\x{F0000}-\x{FFFFD}] | [\x{100000}-\x{10FFFD}]>xo;
196             my $ucschar = qr<
197             [\x{a0}-\x{d7ff}] | [\x{f900}-\x{fdcf}] | [\x{fdf0}-\x{ffef}]
198             | [\x{10000}-\x{1FFFD}] | [\x{20000}-\x{2FFFD}] | [\x{30000}-\x{3FFFD}]
199             | [\x{40000}-\x{4FFFD}] | [\x{50000}-\x{5FFFD}] | [\x{60000}-\x{6FFFD}]
200             | [\x{70000}-\x{7FFFD}] | [\x{80000}-\x{8FFFD}] | [\x{90000}-\x{9FFFD}]
201             | [\x{A0000}-\x{AFFFD}] | [\x{B0000}-\x{BFFFD}] | [\x{C0000}-\x{CFFFD}]
202             | [\x{D0000}-\x{DFFFD}] | [\x{E1000}-\x{EFFFD}]
203             >xo;
204             my $iunreserved = qr<${ALPHA}|[0-9]|[-._~]|${ucschar}>o;
205             my $ipchar = qr<(${iunreserved})|(${pctencoded})|(${subdelims})|:|@>o;
206             my $ifragment = qr<(?(${ipchar}|/|[?])*)>o;
207             my $iquery = qr<(?(${ipchar}|${iprivate}|/|[?])*)>o;
208             my $isegmentnznc = qr<(${iunreserved}|${pctencoded}|${subdelims}|@)+ # non-zero-length segment without any colon ":"
209             >xo;
210             my $isegmentnz = qr<${ipchar}+>o;
211             my $isegment = qr<${ipchar}*>o;
212             my $ipathempty = qr<>o;
213             my $ipathrootless = qr<(?${isegmentnz}(/${isegment})*)>o;
214             my $ipathnoscheme = qr<(?${isegmentnznc}(/${isegment})*)>o;
215             my $ipathabsolute = qr<(?/(${isegmentnz}(/${isegment})*)?)>o;
216             my $ipathabempty = qr<(?(/${isegment})*)>o;
217             my $ipath = qr<
218             ${ipathabempty} # begins with "/" or is empty
219             | ${ipathabsolute} # begins with "/" but not "//"
220             | ${ipathnoscheme} # begins with a non-colon segment
221             | ${ipathrootless} # begins with a segment
222             | ${ipathempty} # zero characters
223             >xo;
224             my $iregname = qr<(${iunreserved}|${pctencoded}|${subdelims})*>o;
225             my $ihost = qr<(?${IPliteral}|${IPv4address}|${iregname})>o;
226             my $iuserinfo = qr<(?(${iunreserved}|${pctencoded}|${subdelims}|:)*)>o;
227             my $iauthority = qr<(${iuserinfo}@)?${ihost}(:${port})?>o;
228             my $irelativepart = qr<
229             (//${iauthority}${ipathabempty})
230             | ${ipathabsolute}
231             | ${ipathnoscheme}
232             | ${ipathempty}
233             >xo;
234             my $irelativeref = qr<${irelativepart}([?]${iquery})?(#${ifragment})?>o;
235             my $ihierpart = qr<(//${iauthority}${ipathabempty})|(${ipathabsolute})|(${ipathrootless})|(${ipathempty})>o;
236             my $absoluteIRI = qr<${scheme}:${ihierpart}([?]${iquery})?>o;
237             my $IRI = qr<${scheme}:${ihierpart}([?]${iquery})?(#${ifragment})?>o;
238             my $IRIreference = qr<${IRI}|${irelativeref}>o;
239             sub _parse_components {
240 24     24   33 my $self = shift;
241 24         33 my $v = shift;
242 24         30 my $c;
243            
244 24 50       9853 if ($v =~ /^${IRIreference}$/o) {
245 3     3   1506 %$c = %+;
  3         1095  
  3         116  
  24         328  
246             } else {
247 3     3   22 use Data::Dumper;
  3         6  
  3         6071  
248 0         0 die "Not a valid IRI? " . Dumper($v);
249             }
250            
251 24   100     276 $c->{path} //= '';
252 24         80 $self->_set_components($c);
253 24         518 $self->_initialized(1);
254             }
255            
256             sub _merge {
257 4     4   8 my $self = shift;
258 4         6 my $base = shift;
259            
260 4         74 my $bc = $base->components;
261 4         215 my $c = $self->components;
262 4   33     127 my $base_has_authority = ($bc->{user} or $bc->{port} or defined($bc->{host}));
263 4 50 33     23 if ($base_has_authority and not($bc->{path})) {
264 0         0 return "/" . $c->{path};
265             } else {
266 4         12 my $bp = $bc->{path};
267 4         17 my @pathParts = split('/', $bp, -1); # -1 limit means $path='/' splits into ('', '')
268 4         8 pop(@pathParts);
269 4         9 push(@pathParts, $c->{path});
270 4         16 my $path = join('/', @pathParts);
271 4         12 return $path;
272             }
273             }
274              
275             sub _remove_dot_segments {
276 5     5   9 my $self = shift;
277 5         8 my $input = shift;
278 5         7 my @output;
279 5         15 while (length($input)) {
280 13 50       80 if ($input =~ m<^[.][.]/>) {
    50          
    50          
    50          
    100          
    50          
    50          
    50          
281 0         0 substr($input, 0, 3) = '';
282             } elsif ($input =~ m<^[.]/>) {
283 0         0 substr($input, 0, 2) = '';
284             } elsif ($input =~ m<^/[.]/>) {
285 0         0 substr($input, 0, 3) = '/';
286             } elsif ($input eq '/.') {
287 0         0 $input = '/';
288             } elsif ($input =~ m<^/[.][.]/>) {
289 1         3 substr($input, 0, 4) = '/';
290 1         3 pop(@output);
291             } elsif ($input eq '/..') {
292 0         0 $input = '/';
293 0         0 pop(@output);
294             } elsif ($input eq '.') {
295 0         0 $input = '';
296             } elsif ($input eq '..') {
297 0         0 $input = '';
298             } else {
299 12         32 my $leadingSlash = ($input =~ m<^/>);
300 12 50       24 if ($leadingSlash) {
301 12         28 substr($input, 0, 1) = '';
302             }
303 12         30 my ($part, @parts) = split('/', $input, -1);
304 12   100     34 $part //= '';
305 12 100       24 if (scalar(@parts)) {
306 7         15 unshift(@parts, '');
307             }
308 12         27 $input = join('/', @parts);
309 12 50       20 if ($leadingSlash) {
310 12         21 $part = "/$part";
311             }
312 12         35 push(@output, $part);
313             }
314             }
315 5         12 my $newPath = join('', @output);
316 5         19 return $newPath;
317             }
318              
319             sub _resolved_components {
320 13     13   609 my $self = shift;
321 13         33 my $value = $self->value;
322 13 100 66     180 if ($self->has_base and not($self->components->{scheme})) {
323             # Resolve IRI relative to the base IRI
324 8         220 my $base = $self->base;
325 8         14 my $v = $self->value;
326 8         19 my $bv = $base->value;
327             # warn "resolving IRI <$v> relative to the base IRI <$bv>";
328 8         10 my %components = %{ $self->components };
  8         138  
329 8         211 my %base = %{ $base->components };
  8         139  
330 8         228 my %target;
331            
332 8 50       22 if ($components{scheme}) {
333 0         0 foreach my $k (qw(scheme user port host path query)) {
334 0 0       0 if (exists $components{$k}) {
335 0         0 $target{$k} = $components{$k};
336             }
337             }
338             } else {
339 8 50 33     55 if ($components{user} or $components{port} or defined($components{host})) {
      33        
340 0         0 foreach my $k (qw(scheme user port host query)) {
341 0 0       0 if (exists $components{$k}) {
342 0         0 $target{$k} = $components{$k};
343             }
344             }
345 0         0 my $path = $components{path};
346 0         0 $target{path} = $self->_remove_dot_segments($path);
347             } else {
348 8 100       21 if ($components{path} eq '') {
349 3         5 $target{path} = $base{path};
350 3 50       10 if ($components{query}) {
351 0         0 $target{query} = $components{query};
352             } else {
353 3 50       8 if ($base{query}) {
354 0         0 $target{query} = $base{query};
355             }
356             }
357             } else {
358 5 100       17 if ($components{path} =~ m<^/>) {
359 1         2 my $path = $components{path};
360 1         4 $target{path} = $self->_remove_dot_segments($path);
361             } else {
362 4         15 my $path = $self->_merge($base);
363 4         18 $target{path} = $self->_remove_dot_segments($path);
364             }
365 5 50       16 if (defined($components{query})) {
366 0         0 $target{query} = $components{query};
367             }
368             }
369 8 50 33     48 if ($base{user} or $base{port} or defined($base{host})) {
      33        
370 8         20 foreach my $k (qw(user port host)) {
371 24 100       80 if (exists $base{$k}) {
372 8         18 $target{$k} = $base{$k};
373             }
374             }
375             }
376             }
377 8 50       23 if (defined($base{scheme})) {
378 8         17 $target{scheme} = $base{scheme};
379             }
380             }
381            
382 8 100       22 if (defined($components{fragment})) {
383 2         3 $target{fragment} = $components{fragment};
384             }
385            
386 8         171 return \%target;
387             }
388 5         90 return $self->components;
389             }
390            
391             sub _abs {
392 10     10   487 my $self = shift;
393 10         179 my $value = $self->_string_from_components( $self->resolved_components );
394 10         62 return $value;
395             }
396              
397             sub as_string {
398             my $self = shift;
399             if ($self->has_base) {
400             return $self->abs;
401             } else {
402             return $self->value;
403             }
404             }
405            
406             sub _string_from_components {
407 10     10   304 my $self = shift;
408 10         11 my $components = shift;
409 10         18 my $iri = "";
410 10 50       24 if (my $s = $components->{scheme}) {
411 10         24 $iri .= "${s}:";
412             }
413            
414 10 50 33     57 if ($components->{user} or $components->{port} or defined($components->{host})) {
      33        
415             # has authority
416 10         18 $iri .= "//";
417 10 50       37 if (my $u = $components->{user}) {
418 0         0 $iri .= sprintf('%s@', $u);
419             }
420 10 50       38 if (defined(my $h = $components->{host})) {
421 10   50     32 $iri .= $h // '';
422             }
423 10 50       32 if (my $p = $components->{port}) {
424 0         0 $iri .= ":$p";
425             }
426             }
427            
428 10 50       37 if (defined(my $p = $components->{path})) {
429 10         19 $iri .= $p;
430             }
431            
432 10 50       21 if (defined(my $q = $components->{query})) {
433 0         0 $iri .= '?' . $q;
434             }
435            
436 10 100       28 if (defined(my $f = $components->{fragment})) {
437 2         5 $iri .= '#' . $f;
438             }
439            
440 10         21 return $iri;
441             }
442            
443             sub _encode {
444 0     0     my $str = shift;
445 0           $str =~ s~([%])~'%' . sprintf('%02x', ord($1))~ge; # gen-delims
  0            
446 0           $str =~ s~([/:?#@]|\[|\])~'%' . sprintf('%02x', ord($1))~ge; # gen-delims
  0            
447 0           $str =~ s~([$!&'()*+,;=])~'%' . sprintf('%02x', ord($1))~ge; # sub-delims
  0            
448 0           return $str;
449             }
450            
451             sub _unencode {
452 0     0     my $str = shift;
453 0 0         if (defined($str)) {
454 0           $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0            
455             }
456 0           return $str;
457             }
458            
459             =item C<< query_form >>
460              
461             Returns a HASH of key-value mappings for the unencoded, parsed query form data.
462              
463             =cut
464              
465             sub query_form {
466 0     0 1   my $self = shift;
467 0   0       my $q = $self->query // return;
468 0           my @pairs = split(/&/, $q);
469 0           return map { _unencode($_) } map { split(/=/, $_) } @pairs;
  0            
  0            
470             }
471              
472             =item C<< set_query_param ( $key => $value ) >>
473              
474             sets the respective query form value and returns a new L object.
475              
476             =cut
477              
478             sub set_query_param {
479 0     0 1   my $self = shift;
480 0   0       my $q = $self->query // return;
481 0           my %map = map { _unencode($_) } map { split(/=/, $_) } split(/&/, $q);
  0            
  0            
482 0           while (my ($k, $v) = splice(@_, 0, 2)) {
483 0           $map{$k} = $v;
484             }
485            
486 0           my %c = %{ $self->components };
  0            
487 0           my @pairs = map { join('=', (_encode($_), _encode($map{$_}))) } keys %map;
  0            
488 0           warn Dumper(\@pairs);
489 0           $c{query} = join('&', @pairs);
490            
491 0           my $v = $self->_string_from_components(\%c);
492 0           return $self->new( value => $v );
493             }
494             }
495              
496             1;
497              
498             __END__