File Coverage

blib/lib/DR/Tarantool/Spaces.pm
Criterion Covered Total %
statement 266 283 93.9
branch 132 192 68.7
condition 26 56 46.4
subroutine 36 37 97.3
pod 7 8 87.5
total 467 576 81.0


line stmt bran cond sub pod time code
1 7     7   53345 use utf8;
  7         10  
  7         31  
2 7     7   176 use strict;
  7         10  
  7         158  
3 7     7   23 use warnings;
  7         15  
  7         180  
4              
5             package DR::Tarantool::Spaces;
6 7     7   24 use Carp;
  7         8  
  7         4200  
7             $Carp::Internal{ (__PACKAGE__) }++;
8              
9             my $LE = $] > 5.01 ? '<' : '';
10              
11             =head1 NAME
12              
13             DR::Tarantool::Spaces - Tarantool schema description
14              
15             =head1 SYNOPSIS
16              
17             use DR::Tarantool::Spaces;
18             my $s = new DR::Tarantool::Spaces({
19             1 => {
20             name => 'users', # space name
21             default_type => 'STR', # undescribed fields
22             fields => [
23             qw(login password role),
24             {
25             name => 'counter',
26             type => 'NUM'
27             },
28             {
29             name => 'something',
30             type => 'UTF8STR',
31             },
32             {
33             name => 'opts',
34             type => 'JSON',
35             }
36             ],
37             indexes => {
38             0 => 'login',
39             1 => [ qw(login password) ],
40             2 => {
41             name => 'my_idx',
42             fields => 'login',
43             },
44             3 => {
45             name => 'my_idx2',
46             fields => [ 'counter', 'something' ]
47             }
48             }
49             },
50              
51             0 => {
52             ...
53             }
54             });
55              
56             my $f = $s->pack_field('users', 'counter', 10);
57             my $f = $s->pack_field('users', 3, 10); # the same
58             my $f = $s->pack_field(1, 3, 10); # the same
59              
60             my $ts = $s->pack_keys([1,2,3] => 'my_idx');
61             my $t = $s->pack_primary_key([1,2,3]);
62              
63              
64             =head1 DESCRIPTION
65              
66             The package describes all spaces used in an application.
67             It supports the following field types:
68              
69             =over
70              
71             =item NUM, NUM64, STR
72              
73             The standard L types.
74              
75             =item UTF8STR
76              
77             The same as B, but the string is utf8-decoded
78             after it's received from the server.
79              
80             =item INT & INT64
81              
82             The same as B and B, but contain signed values.
83              
84             =item JSON
85              
86             The field is encoded with L when putting
87             into a database, and decoded after is received back
88             from the server.
89              
90             =back
91              
92             =head1 METHODS
93              
94             =head2 new
95              
96             my $spaces = DR::Tarantool::Spaces->new( $spaces );
97              
98             =cut
99              
100             sub new {
101 5     5 1 560 my ($class, $spaces, %opts) = @_;
102              
103 5   50     23 $opts{family} ||= 1;
104              
105 5 100       11 $spaces = {} unless defined $spaces;
106 5 100       174 croak 'spaces must be a HASHREF' unless 'HASH' eq ref $spaces;
107              
108 4         4 my (%spaces, %fast);
109 4         12 for (keys %$spaces) {
110 4         20 my $s = new DR::Tarantool::Space($_ => $spaces->{ $_ }, %opts);
111 3         29 $spaces{ $s->name } = $s;
112 3         5 $fast{ $_ } = $s->name;
113             }
114              
115 3   33     28 return bless {
116             spaces => \%spaces,
117             fast => \%fast,
118             family => $opts{family},
119             } => ref($class) || $class;
120             }
121              
122              
123             sub family {
124 3     3 0 13 my ($self, $family) = @_;
125 3 100       11 return $self->{family} if @_ == 1;
126 2         3 $self->{family} = $family;
127 2         2 $_->family($family) for values %{ $self->{spaces} };
  2         8  
128 2         7 return $self->{family};
129             }
130              
131              
132             =head2 space
133              
134             Return space object by number or name.
135              
136             my $space = $spaces->space('name');
137             my $space = $spaces->space(0);
138              
139             =cut
140              
141             sub space {
142 94     94 1 20255 my ($self, $space) = @_;
143 94 100       271 croak 'space name or number is not defined' unless defined $space;
144 93 100       237 if ($space =~ /^\d+$/) {
145 6 50       15 croak "space '$space' is not defined"
146             unless exists $self->{fast}{$space};
147 6         21 return $self->{spaces}{ $self->{fast}{$space} };
148             }
149 87 50       179 croak "space '$space' is not defined"
150             unless exists $self->{spaces}{$space};
151 87         295 return $self->{spaces}{$space};
152             }
153              
154              
155             =head2 space_number
156              
157             Return space number by its name.
158              
159             =cut
160              
161             sub space_number {
162 3     3 1 561 my ($self, $space) = @_;
163 3         7 return $self->space($space)->number;
164             }
165              
166              
167             =head2 pack_field
168              
169             Packs one field into a format suitable for making a database request:
170              
171             my $field = $spaces->pack_field('space', 'field', $data);
172              
173             =cut
174              
175             sub pack_field {
176 23     23 1 8715 my ($self, $space, $field, $value) = @_;
177 23 50       55 croak q{Usage: $spaces->pack_field('space', 'field', $value)}
178             unless @_ == 4;
179 23         86 return $self->space($space)->pack_field($field => $value);
180             }
181              
182              
183             =head2 unpack_field
184              
185             Unpack one field after getting it from the server:
186              
187             my $field = $spaces->unpack_field('space', 'field', $data);
188              
189             =cut
190              
191             sub unpack_field {
192 19     19 1 5778 my ($self, $space, $field, $value) = @_;
193 19 50       52 croak q{Usage: $spaces->unpack_field('space', 'field', $value)}
194             unless @_ == 4;
195              
196 19         32 return $self->space($space)->unpack_field($field => $value);
197             }
198              
199              
200             =head2 pack_tuple
201              
202             Pack a tuple before making database request.
203              
204             my $t = $spaces->pack_tuple('space', [ 1, 2, 3 ]);
205              
206             =cut
207              
208             sub pack_tuple {
209 1     1 1 290 my ($self, $space, $tuple) = @_;
210 1 50       4 croak q{Usage: $spaces->pack_tuple('space', $tuple)} unless @_ == 3;
211 1         3 return $self->space($space)->pack_tuple( $tuple );
212             }
213              
214              
215             =head2 unpack_tuple
216              
217             Unpack a tuple after getting it from the database:
218              
219             my $t = $spaces->unpack_tuple('space', \@fields);
220              
221             =cut
222              
223             sub unpack_tuple {
224 1     1 1 273 my ($self, $space, $tuple) = @_;
225 1 50       5 croak q{Usage: $spaces->unpack_tuple('space', $tuple)} unless @_ == 3;
226 1         10 return $self->space($space)->unpack_tuple( $tuple );
227             }
228              
229             package DR::Tarantool::Space;
230 7     7   35 use Carp;
  7         9  
  7         407  
231             $Carp::Internal{ (__PACKAGE__) }++;
232 7     7   4370 use JSON::XS ();
  7         33125  
  7         140  
233 7     7   34 use Digest::MD5 ();
  7         8  
  7         5516  
234              
235              
236             =head1 SPACES methods
237              
238             =head2 new
239              
240             constructor
241              
242             use DR::Tarantool::Spaces;
243             my $space = DR::Tarantool::Space->new($no, $space);
244              
245             =cut
246              
247             sub new {
248 4     4   8 my ($class, $no, $space, %opts) = @_;
249              
250 4   50     9 $opts{family} ||= 1;
251 4 100 66     123 croak 'space number must conform the regexp qr{^\d+}'
252             unless defined $no and $no =~ /^\d+$/;
253 3 50       9 croak "'fields' not defined in space hash"
254             unless 'ARRAY' eq ref $space->{fields};
255 3 50 33     16 croak "wrong 'indexes' hash"
256             if !$space->{indexes} or 'HASH' ne ref $space->{indexes};
257              
258 3         4 my $name = $space->{name};
259 3 0 33     15 croak 'wrong space name: ' . (defined($name) ? $name : 'undef')
    50          
260             unless $name and $name =~ /^[a-z_]\w*$/i;
261              
262              
263 3         9 my $fqr = qr{^(?:STR|NUM|NUM64|INT|INT64|UTF8STR|JSON|MONEY|BIGMONEY)$};
264              
265 3         4 my (@fields, %fast, $default_type);
266 3   100     10 $default_type = $space->{default_type} || 'STR';
267 3 50       17 croak "wrong 'default_type'" unless $default_type =~ $fqr;
268              
269 3         5 for (my $no = 0; $no < @{ $space->{fields} }; $no++) {
  18         37  
270 15         16 my $f = $space->{ fields }[ $no ];
271              
272 15 100       24 if (ref $f eq 'HASH') {
    50          
273 9   33     27 push @fields => {
274             name => $f->{name} || "f$no",
275             idx => $no,
276             type => $f->{type}
277             };
278             } elsif(ref $f) {
279 0         0 croak 'wrong field name or description';
280             } else {
281 6         12 push @fields => {
282             name => $f,
283             idx => $no,
284             type => $default_type,
285             }
286             }
287              
288 15         14 my $s = $fields[ -1 ];
289 15 0 33     76 croak 'unknown field type: ' .
    50          
290             (defined($s->{type}) ? $s->{type} : 'undef')
291             unless $s->{type} and $s->{type} =~ $fqr;
292              
293 15 0 33     57 croak 'wrong field name: ' .
    50          
294             (defined($s->{name}) ? $s->{name} : 'undef')
295             unless $s->{name} and $s->{name} =~ /^[a-z_]\w*$/i;
296              
297 15 50       42 croak "Duplicate field name: $s->{name}" if exists $fast{ $s->{name} };
298 15         24 $fast{ $s->{name} } = $no;
299             }
300              
301 3         4 my %indexes;
302 3 50       9 if ($space->{indexes}) {
303 3         4 for my $no (keys %{ $space->{indexes} }) {
  3         9  
304 6         9 my $l = $space->{indexes}{ $no };
305 6 50       21 croak "wrong index number: $no" unless $no =~ /^\d+$/;
306              
307 6         4 my ($name, $fields);
308              
309 6 100       19 if ('ARRAY' eq ref $l) {
    100          
310 2         4 $name = "i$no";
311 2         3 $fields = $l;
312             } elsif ('HASH' eq ref $l) {
313 1   33     3 $name = $l->{name} || "i$no";
314 1         3 $fields =
315 1 50       2 [ ref($l->{fields}) ? @{ $l->{fields} } : $l->{fields} ];
316             } else {
317 3         5 $name = "i$no";
318 3         6 $fields = [ $l ];
319             }
320              
321 6 50       19 croak "wrong index name: $name" unless $name =~ /^[a-z_]\w*$/i;
322              
323 6         8 for (@$fields) {
324 10 50       22 croak "field '$_' is presend in index but isn't in fields"
325             unless exists $fast{ $_ };
326             }
327              
328 6         25 $indexes{ $name } = {
329             no => $no,
330             name => $name,
331             fields => $fields
332             };
333              
334             }
335             }
336              
337 3         31 my $tuple_class = 'DR::Tarantool::Tuple::Instance' .
338             Digest::MD5::md5_hex( join "\0", sort keys %fast );
339              
340 3   33     40 bless {
341             fields => \@fields,
342             fast => \%fast,
343             name => $name,
344             number => $no,
345             default_type => $default_type,
346             indexes => \%indexes,
347             tuple_class => $tuple_class,
348             family => $opts{family},
349             } => ref($class) || $class;
350              
351             }
352              
353              
354             sub family {
355 21     21   22 my ($self, $family) = @_;
356 21 100       72 return $self->{family} if @_ == 1;
357 4         8 return $self->{family} = $family;
358             }
359              
360              
361             =head2 tuple_class
362              
363             Create (or return) a class to hold tuple data.
364             The class is a descendant of L. Returns a unique class
365             (package) name. If a package with such name is already exists, the method
366             doesn't recreate it.
367              
368             =cut
369              
370             sub tuple_class {
371 11     11   13 my ($self) = @_;
372 11         14 my $class = $self->{tuple_class};
373              
374              
375 7     7   35 no strict 'refs';
  7         8  
  7         13468  
376 11 100       12 return $class if ${ $class . '::CREATED' };
  11         68  
377              
378 3 50   2   186 die unless eval "package $class; use base 'DR::Tarantool::Tuple'; 1";
  2     1   9  
  2         2  
  2         721  
  1         4  
  1         1  
  1         60  
379              
380 3         32 for my $fname (keys %{ $self->{fast} }) {
  3         13  
381 15         22 my $fnumber = $self->{fast}{$fname};
382              
383 15         629 *{ $class . '::' . $fname } = eval "sub { \$_[0]->raw($fnumber) }";
  15         76  
384             }
385              
386 3         14 ${ $class . '::CREATED' } = time;
  3         10  
387              
388 3         8 return $class;
389             }
390              
391              
392             =head2 name
393              
394             Get a space name.
395              
396             =cut
397              
398 13     13   347 sub name { $_[0]{name} }
399              
400              
401             =head2 number
402              
403             Get a space number.
404              
405             =cut
406              
407 3     2   11 sub number { $_[0]{number} }
408              
409             sub _field {
410 106     105   101 my ($self, $field) = @_;
411              
412 105 50       136 croak 'field name or number is not defined' unless defined $field;
413 105 100       261 if ($field =~ /^\d+$/) {
414 24 50       25 return $self->{fields}[ $field ] if $field < @{ $self->{fields} };
  20         60  
415 0         0 return undef;
416             }
417 85 50       140 croak "field with name '$field' is not defined in this space"
418             unless exists $self->{fast}{$field};
419 85         144 return $self->{fields}[ $self->{fast}{$field} ];
420             }
421              
422              
423             =head2 field_number
424              
425             Return field index by field name.
426              
427             =cut
428              
429             sub field_number {
430 4     4   6 my ($self, $field) = @_;
431 4 50       6 croak 'field name or number is not defined' unless defined $field;
432 4 100       16 return $self->{fast}{$field} if exists $self->{fast}{$field};
433 1         76 croak "Can't find field '$field' in this space";
434             }
435              
436              
437             =head2 tail_index
438              
439             Return index of the first element that is not described in the space.
440              
441             =cut
442              
443             sub tail_index {
444 3     3   3 my ($self) = @_;
445 3         4 return scalar @{ $self->{fields} };
  3         14  
446             }
447              
448              
449             =head2 pack_field
450              
451             Pack a field before making a database request.
452              
453             =cut
454              
455             sub pack_field {
456 52     52   59 my ($self, $field, $value) = @_;
457 52 50       84 croak q{Usage: $space->pack_field('field', $value)}
458             unless @_ == 3;
459              
460 52         67 my $f = $self->_field($field);
461              
462 52 50       96 my $type = $f ? $f->{type} : $self->{default_type};
463              
464 52 100       91 if ($type eq 'JSON') {
465 8         10 my $v = eval { JSON::XS->new->allow_nonref->utf8->encode( $value ) };
  8         68  
466 8 50       28 croak "Can't pack json: $@" if $@;
467 8         25 return $v;
468             }
469              
470 44         24 my $v = $value;
471 44 100       100 utf8::encode( $v ) if utf8::is_utf8( $v );
472 44 100 100     173 return $v if $type eq 'STR' or $type eq 'UTF8STR';
473 37 100       190 return pack "L$LE" => $v if $type eq 'NUM';
474 8 100       19 return pack "l$LE" => $v if $type eq 'INT';
475 7 50       12 return pack "Q$LE" => $v if $type eq 'NUM64';
476 7 50       10 return pack "q$LE" => $v if $type eq 'INT64';
477              
478 7 50 33     16 if ($type eq 'MONEY' or $type eq 'BIGMONEY') {
479 7         15 my ($r, $k) = split /\./, $v;
480 7         9 for ($k) {
481 7 100       14 $_ = '.00' unless defined $_;
482 7         13 s/^\.//;
483 7 100       11 $_ .= '0' if length $_ < 2;
484 7         15 $_ = substr $_, 0, 2;
485             }
486 7   100     14 $r ||= 0;
487              
488 7 100       14 if ($r < 0) {
489 2         4 $v = $r * 100 - $k;
490             } else {
491 5         9 $v = $r * 100 + $k;
492             }
493              
494 7 50       41 return pack "l$LE", $v if $type eq 'MONEY';
495 0         0 return pack "q$LE", $v;
496             }
497              
498              
499 0         0 croak 'Unknown field type:' . $type;
500             }
501              
502              
503             =head2 unpack_field
504              
505             Unpack a single field in a server response.
506              
507             =cut
508              
509             sub unpack_field {
510 27     27   30 my ($self, $field, $value) = @_;
511 27 50       44 croak q{Usage: $space->pack_field('field', $value)}
512             unless @_ == 3;
513              
514 27         45 my $f = $self->_field($field);
515              
516 27 50       57 my $type = $f ? $f->{type} : $self->{default_type};
517              
518 27         25 my $v = $value;
519 27 100       57 utf8::encode( $v ) if utf8::is_utf8( $v );
520              
521 27 100       43 if ($type eq 'JSON') {
522 8         58 $v = JSON::XS->new->allow_nonref->utf8->decode( $v );
523 8 50       26 croak "Can't unpack json: $@" if $@;
524 8         29 return $v;
525             }
526              
527 19 100       50 $v = unpack "L$LE" => $v if $type eq 'NUM';
528 19 100       36 $v = unpack "l$LE" => $v if $type eq 'INT';
529 19 50       26 $v = unpack "Q$LE" => $v if $type eq 'NUM64';
530 19 50       28 $v = unpack "q$LE" => $v if $type eq 'INT64';
531 19 100       28 utf8::decode( $v ) if $type eq 'UTF8STR';
532 19 100 66     62 if ($type eq 'MONEY' or $type eq 'BIGMONEY') {
533 4 50       14 $v = unpack "l$LE" => $v if $type eq 'MONEY';
534 4 50       7 $v = unpack "q$LE" => $v if $type eq 'BIGMONEY';
535 4         4 my $s = '';
536 4 100       8 if ($v < 0) {
537 1         2 $v = -$v;
538 1         1 $s = '-';
539             }
540 4         6 my $k = $v % 100;
541 4         7 my $r = ($v - $k) / 100;
542 4         14 $v = sprintf '%s%d.%02d', $s, $r, $k;
543             }
544 19         52 return $v;
545             }
546              
547              
548             =head2 pack_tuple
549              
550             Pack a tuple to the binary protocol format:
551              
552             =cut
553              
554             sub pack_tuple {
555 1     1   2 my ($self, $tuple) = @_;
556 1 50       4 croak 'tuple must be ARRAYREF' unless 'ARRAY' eq ref $tuple;
557 1         1 my @res;
558 1 50       3 if ($self->family == 1) {
559 1         3 for (my $i = 0; $i < @$tuple; $i++) {
560 6         9 push @res => $self->pack_field($i, $tuple->[ $i ]);
561             }
562             } else {
563 0         0 @res = @$tuple;
564             }
565 1         4 return \@res;
566             }
567              
568              
569             =head2 unpack_tuple
570              
571             Unpack a tuple in a server response.
572              
573             =cut
574              
575             sub unpack_tuple {
576 2     2   4 my ($self, $tuple) = @_;
577 2 50       7 croak 'tuple must be ARRAYREF' unless 'ARRAY' eq ref $tuple;
578 2         2 my @res;
579 2 50       6 if ($self->family == 1) {
580 2         8 for (my $i = 0; $i < @$tuple; $i++) {
581 8         18 push @res => $self->unpack_field($i, $tuple->[ $i ]);
582             }
583             } else {
584 0         0 @res = @$tuple;
585             }
586 2         10 return \@res;
587             }
588              
589              
590             sub _index {
591 16     16   17 my ($self, $index) = @_;
592 16 100       46 if ($index =~ /^\d+$/) {
593 5         5 for (values %{ $self->{indexes} }) {
  5         17  
594 19 100       46 return $_ if $_->{no} == $index;
595             }
596 0         0 croak "index $index is undefined";
597             }
598              
599 11 100       43 return $self->{indexes}{$index} if exists $self->{indexes}{$index};
600 1         74 croak "index `$index' is undefined";
601             }
602              
603              
604             =head2 index_number
605              
606             returns index number by its name.
607              
608             =cut
609              
610             sub index_number {
611 4     4   5 my ($self, $idx) = @_;
612 4 100       87 croak "index name is undefined" unless defined $idx;
613 3         7 return $self->_index( $idx )->{no};
614             }
615              
616              
617             =head2 index_name
618              
619             returns index name by its number.
620              
621             =cut
622              
623             sub index_name {
624 2     2   3 my ($self, $idx) = @_;
625 2 50       4 croak "index number is undefined" unless defined $idx;
626 2         5 return $self->_index( $idx )->{name};
627             }
628              
629              
630             sub pack_keys {
631 11     11   13 my ($self, $keys, $idx, $disable_warn) = @_;
632              
633 11         19 $idx = $self->_index($idx);
634 11         11 my $ksize = @{ $idx->{fields} };
  11         17  
635              
636 11 100       25 $keys = [[ $keys ]] unless 'ARRAY' eq ref $keys;
637 11 100       20 unless('ARRAY' eq ref $keys->[0]) {
638 4 100       9 if ($ksize == @$keys) {
639 3         5 $keys = [ $keys ];
640 3 100 66     201 carp "Ambiguous keys list (it was used as ONE key), ".
641             "Use brackets to solve the trouble."
642             if $ksize > 1 and !$disable_warn;
643             } else {
644 1         2 $keys = [ map { [ $_ ] } @$keys ];
  3         5  
645             }
646             }
647              
648 11         111 my @res;
649 11         15 for my $k (@$keys) {
650 13 100       260 croak "key must have $ksize elements" unless $ksize >= @$k;
651 10         4 my @packed;
652 10         22 for (my $i = 0; $i < @$k; $i++) {
653 14         29 my $f = $self->_field($idx->{fields}[$i]);
654 14         36 push @packed => $self->pack_field($f->{name}, $k->[$i])
655             }
656 10         25 push @res => \@packed;
657             }
658 8         20 return \@res;
659             }
660              
661             sub pack_primary_key {
662 0     0   0 my ($self, $key) = @_;
663              
664 0 0 0     0 croak 'wrong key format'
665             if 'ARRAY' eq ref $key and 'ARRAY' eq ref $key->[0];
666              
667 0         0 my $t = $self->pack_keys($key, 0, 1);
668 0         0 return $t->[0];
669             }
670              
671             sub pack_operation {
672 12     12   13 my ($self, $op) = @_;
673 12 50 33     233 croak 'wrong operation' unless 'ARRAY' eq ref $op and @$op > 1;
674              
675 12 50       24 if ($self->family == 1) {
676 12         16 my $fno = $op->[0];
677 12         12 my $opname = $op->[1];
678              
679 12         19 my $f = $self->_field($fno);
680              
681 12 100       23 if ($opname eq 'delete') {
682 1 50       3 croak 'wrong operation' unless @$op == 2;
683 1         4 return [ $f->{idx} => $opname ];
684             }
685              
686 11 100       40 if ($opname =~ /^(?:set|insert|add|and|or|xor)$/) {
687 9 50       13 croak 'wrong operation' unless @$op == 3;
688 9         19 return [ $f->{idx} => $opname, $self->pack_field($fno, $op->[2]) ];
689             }
690              
691 2 50       6 if ($opname eq 'substr') {
692 2 50       6 croak 'wrong operation11' unless @$op >= 4;
693 2 50       8 croak 'wrong offset in substr operation' unless $op->[2] =~ /^\d+$/;
694 2 50       7 croak 'wrong length in substr operation' unless $op->[3] =~ /^\d+$/;
695 2         7 return [ $f->{idx}, $opname, $op->[2], $op->[3], $op->[4] ];
696             }
697 0         0 croak "unknown operation: $opname";
698             }
699              
700 0         0 my $fno = $op->[1];
701 0         0 my $f = $self->_field($fno);
702 0         0 my @res = @$op;
703 0         0 splice @res, 1, 1, $f->{idx};
704 0         0 return \@res;
705             }
706              
707             sub pack_operations {
708 2     2   4 my ($self, $ops) = @_;
709              
710 2 50 33     13 croak 'wrong operation' unless 'ARRAY' eq ref $ops and @$ops >= 1;
711 2 100       7 $ops = [ $ops ] unless 'ARRAY' eq ref $ops->[ 0 ];
712              
713 2         2 my @res;
714 2         6 push @res => $self->pack_operation( $_ ) for @$ops;
715 2         7 return \@res;
716             }
717              
718             =head1 COPYRIGHT AND LICENSE
719              
720             Copyright (C) 2011 Dmitry E. Oboukhov
721             Copyright (C) 2011 Roman V. Nikolaev
722              
723             This program is free software, you can redistribute it and/or
724             modify it under the terms of the Artistic License.
725              
726             =head1 VCS
727              
728             The project is placed git repo on github:
729             L.
730              
731             =cut
732              
733             1;