File Coverage

lib/Mojolicious/Command/migration.pm
Criterion Covered Total %
statement 36 314 11.4
branch 3 128 2.3
condition 2 27 7.4
subroutine 12 29 41.3
pod 7 14 50.0
total 60 512 11.7


line stmt bran cond sub pod time code
1             package Mojolicious::Command::migration;
2              
3             BEGIN {
4 1   50 1   932 $ENV{MOJO_MIGRATION_TMP } ||= 'tmp';
5 1   50     31 $ENV{MOJO_MIGRATION_SHARE} ||= 'share';
6             };
7              
8 1     1   607 use common::sense;
  1         14  
  1         5  
9 1     1   87 use Mojo::Base 'Mojolicious::Command';
  1         2  
  1         7  
10 1     1   47188 use Getopt::Long qw(GetOptionsFromArray :config no_auto_abbrev no_ignore_case);
  1         2  
  1         11  
11 1     1   216 use File::Basename;
  1         3  
  1         69  
12 1     1   6 use File::Path qw(make_path remove_tree);
  1         3  
  1         49  
13 1     1   729 use Storable qw/nstore retrieve/;
  1         3522  
  1         73  
14 1     1   646 use SQL::Translator;
  1         299823  
  1         39  
15 1     1   605 use SQL::Translator::Diff;
  1         10819  
  1         31  
16 1     1   8 no warnings;
  1         2  
  1         33  
17 1     1   7 use Data::Dumper;
  1         2  
  1         5517  
18              
19             our $VERSION = 0.17;
20              
21             has description => 'MySQL migration tool';
22             has usage => sub { shift->extract_usage };
23             has config => sub { shift->app->config->{db}->{mysql} };
24             has paths => sub {+{
25             deploy_status => "$ENV{MOJO_MIGRATION_TMP}/.deploy_status",
26             source_deploy => "$ENV{MOJO_MIGRATION_SHARE}/migrations/_source/deploy",
27             db_deploy => "$ENV{MOJO_MIGRATION_SHARE}/migrations/MySQL/deploy",
28             db_upgrade => "$ENV{MOJO_MIGRATION_SHARE}/migrations/MySQL/upgrade",
29             db_downgrade => "$ENV{MOJO_MIGRATION_SHARE}/migrations/MySQL/downgrade",
30             }};
31             has deployed => sub {
32             my $self = shift;
33             return {} unless -e $self->paths->{deploy_status};
34             return retrieve $self->paths->{deploy_status};
35             };
36             has db => sub {
37             my $self = shift;
38             return $self->app->db if $self->app->can('db');
39             DBI->connect('dbi:mysql:'.$self->config->{datasource}->{database},
40             $self->config->{user },
41             $self->config->{password},
42             {
43             host => $self->config->{datasource}->{host} || '', port => $self->config->{datasource}->{port},
44             }
45             );
46             };
47             has params => sub {{}};
48              
49             sub run {
50 2     2 1 44251 my $self = shift;
51 2         6 my @args = @_;
52              
53 2 100       10 die $self->usage unless my $action = shift @args;
54 1 50       24 die $self->usage unless $action ~~ [qw/status prepare install upgrade downgrade rm diff/];
55              
56             GetOptionsFromArray \@args,
57 0     0     'to-version=s' => sub { $self->params->{'to-version'} = $_[1] },
58 0     0     'version=s' => sub { $self->params->{'version' } = $_[1] },
59 0     0     'force' => sub { $self->params->{force } = 1 },
60 0           ;
61              
62 0           $self->$action;
63              
64 0           $self->params({});
65             }
66              
67             sub install {
68 0     0 1   my $self = shift;
69 0           my $paths = $self->paths;
70              
71 0           my $last_version = $self->get_last_version;
72              
73 0 0         unless ($last_version) {
74 0           say "Migration dont initialized. Please run ";
75              
76 0           return;
77             }
78              
79 0           say "Schema version: $last_version";
80              
81 0 0         if (my $version = $self->deployed->{version}) {
82 0           say "Deployed database is $version";
83 0           say "A versioned schema has already been deployed, try upgrade instead.";
84              
85 0           return;
86             }
87              
88 0 0 0       if (!$self->params->{force} && !$self->db_is_empty) {
89 0           say "Database is not empty. Installing is dangerous. Try --force to skip installation";
90              
91 0           return;
92             }
93              
94 0 0         $last_version = $self->params->{'to-version'} if $self->params->{'to-version'};
95              
96 0 0         unless (-s "$paths->{source_deploy}/$last_version/001_auto.yml") {
97 0           say "Schema $last_version not exists";
98              
99 0           return;
100             }
101              
102 0 0         if ($self->db_is_empty) {
103 0           say "Deploy database to $last_version";
104              
105 0           my $source = $self->deployment_statements(
106             type => 'install',
107             version => $last_version,
108             );
109              
110 0           for my $line(@$source) {
111 0           eval { $self->db->do($line) };
  0            
112              
113 0 0         if ($@) {
114 0           die "Deploy failed: $@";
115             }
116             }
117              
118 0           $self->deployed->{version} = $last_version;
119 0           $self->save_deployed;
120              
121 0           return;
122             } else {
123 0           say "Force deploy to $last_version";
124 0           $self->deployed->{version} = $last_version;
125 0           $self->save_deployed;
126              
127 0           return;
128             }
129             }
130              
131             sub upgrade {
132 0     0 1   my $self = shift;
133 0           my $paths = $self->paths;
134              
135 0           my $to_version = $self->get_last_version;
136              
137 0 0         unless ($to_version) {
138 0           say "Migration dont initialized. Please run ";
139              
140 0           return;
141             }
142              
143 0           say "Schema version: $to_version";
144              
145 0 0         unless ($self->deployed->{version}) {
146 0           say "Database is not installed. Please run ";
147              
148 0           return;
149             }
150              
151 0 0         if ($self->deployed->{version} == $to_version) {
152 0           say "Database is already up-to-date.";
153              
154 0           return;
155             }
156              
157 0 0 0       if ($self->params->{'to-version'} && $self->params->{'to-version'} > $to_version) {
158 0           say "Schema not exists.";
159              
160 0           return;
161             }
162              
163 0 0         $to_version = $self->params->{'to-version'} if $self->params->{'to-version'};
164              
165 0 0         if ($self->deployed->{version} == $to_version) {
166 0           say "Database is already deployed to $to_version";
167              
168 0           return;
169             }
170              
171 0           say "Database version: ".$self->deployed->{version};
172              
173 0 0         if ($self->params->{force}) {
174 0           say "Force upgrade to $to_version";
175 0           $self->deployed->{version} = $to_version;
176 0           $self->save_deployed;
177 0           return;
178             }
179              
180 0           my $current = $self->deployed->{version};
181 0           for my $upgrade ($self->deployed->{version} + 1 .. $to_version) {
182 0           say "Upgrade to $upgrade";
183 0           say "+++++++++ "."$paths->{db_upgrade}/$current-$upgrade/*";
184 0           my @files = sort {$a cmp $b} glob("$paths->{db_upgrade}/$current-$upgrade/*");
  0            
185 0 0         say "Upgrade is empty" unless @files;
186              
187 0           for my $file (@files) {
188 0 0         next unless -s $file;
189 0           say "Exec file: $file";
190              
191 0           my $source = $self->deployment_statements(
192             filename => $file,
193             );
194              
195 0           for my $line(@$source) {
196 0 0         next unless $line;
197              
198 0           say "Exec SQL: $line";
199              
200 0           eval { $self->db->do($line) };
  0            
201              
202 0 0         if ($@) {
203 0           die "SQL failed: $@";
204             }
205             }
206             }
207              
208 0           $self->deployed->{version} = $upgrade;
209 0           $self->save_deployed;
210 0           ++$current;
211             }
212             }
213              
214             sub downgrade {
215 0     0 1   my $self = shift;
216 0           my $paths = $self->paths;
217              
218 0           my $last_version = $self->get_last_version;
219              
220 0 0         unless ($last_version) {
221 0           say "Migration dont initialized. Please run ";
222              
223 0           return;
224             }
225              
226 0           say "Schema version: $last_version";
227              
228 0 0         unless ($self->deployed->{version}) {
229 0           say "Database is not installed. Please run ";
230              
231 0           return;
232             }
233              
234 0 0 0       if ($self->params->{'to-version'} && $self->params->{'to-version'} > $last_version) {
235 0           say "Schema not exists.";
236              
237 0           return;
238             }
239              
240 0   0       my $to_version = $self->params->{'to-version'} || $self->deployed->{version} - 1;
241              
242 0 0         unless ($to_version > 0) {
243 0           say "Nothing to downgrade.";
244              
245 0           return;
246             }
247              
248 0 0         if ($self->deployed->{version} == $to_version) {
249 0           say "Database is already deployed to $to_version";
250              
251 0           return;
252             }
253              
254 0           say "Database version: ".$self->deployed->{version};
255              
256 0 0         if ($self->params->{force}) {
257 0           say "Force downgrade to $to_version";
258 0           $self->deployed->{version} = $to_version;
259 0           $self->save_deployed;
260 0           return;
261             }
262              
263 0           my $current = $self->deployed->{version};
264 0           for my $downgrade ($self->deployed->{version} - 1 .. $to_version) {
265 0           say "Downgrade to $downgrade";
266 0           my @files = sort {$a cmp $b} glob("$paths->{db_downgrade}/$current-$downgrade/*");
  0            
267 0 0         say "Downgrade is empty" unless @files;
268              
269 0           for my $file (@files) {
270 0 0         next unless -s $file;
271 0           say "Exec file: $file";
272              
273 0           my $source = $self->deployment_statements(
274             filename => $file,
275             );
276              
277 0           for my $line(@$source) {
278 0 0         next unless $line;
279              
280 0           say "Exec SQL: $line";
281              
282 0           eval { $self->db->do($line) };
  0            
283              
284 0 0         if ($@) {
285 0           die "SQL failed: $@";
286             }
287             }
288             }
289              
290 0           $self->deployed->{version} = $downgrade;
291 0           $self->save_deployed;
292             }
293             }
294              
295             sub status {
296 0     0 1   my $self = shift;
297              
298 0           my $last_version = $self->get_last_version;
299              
300 0 0         unless ($last_version) {
301 0           say "Migration dont initialized. Please run ";
302              
303 0           return;
304             }
305 0           say "Schema version: $last_version";
306              
307 0 0         if (my $version = $self->deployed->{version}) {
308 0           say "Deployed database is $version";
309             } else {
310 0           say "Database is not deployed";
311             }
312              
313             }
314              
315             sub save_deployed {
316 0     0 0   my $self = shift;
317 0           nstore $self->deployed, $self->paths->{deploy_status};
318             }
319              
320             sub prepare {
321 0     0 1   my $self = shift;
322 0           my $paths = $self->paths;
323              
324 0           my $last_version = $self->get_last_version;
325 0 0         my $new_version = $last_version ? $last_version + 1 : 1;
326              
327 0 0         if ($new_version == 1) {
328 0           say "Initialization";
329             } else {
330 0           say "Schema version: $last_version";
331             }
332              
333 0 0         if (my $version = $self->deployed->{version}) {
334 0           say "Deployed database is $version";
335             }
336              
337 0 0         if ($self->db_is_empty) {
338 0           say "Nothing to prepare. Database is empty.";
339              
340 0           return;
341             }
342              
343 0           my $deploy = $self->get_schema(to => 'MySQL');
344             my $error = $self->save_migration(
345             path => "$paths->{db_deploy}/$new_version/001_auto.sql",
346 0           data => join '', @{ $deploy->{data} },
  0            
347             );
348 0 0         die "Cant create MySQL deploy: $error" if $error;
349              
350 0           my $deploy = $self->get_schema(to => 'YAML');
351             my $error = $self->save_migration(
352             path => "$paths->{source_deploy}/$new_version/001_auto.yml",
353 0           data => join '', @{ $deploy->{data} },
  0            
354             );
355 0 0         die "Cant create YML deploy: $error" if $error;
356              
357 0           $deploy->{schema}->name("$paths->{source_deploy}/$new_version/001_auto.yml");
358              
359 0 0         if ($new_version > 1) {
360 0           my $target_schema = $deploy->{schema};
361             my $source_schema = $self->get_schema(
362             from => 'YAML',
363             filename => "$paths->{source_deploy}/$last_version/001_auto.yml",
364 0           )->{schema};
365              
366 0           my $diff = $self->_diff($target_schema, $source_schema);
367            
368 0 0         if ($diff =~ /No differences/) {
369 0           say "Nothing to upgrade. Exit";
370              
371 0           remove_tree "$paths->{source_deploy}/$new_version";
372 0           remove_tree "$paths->{db_deploy}/$new_version";
373              
374 0           return;
375             } else {
376 0           my $error = $self->save_migration(
377             path => "$paths->{db_upgrade}/$last_version-$new_version/001_auto.sql",
378             data => $diff,
379             );
380 0 0         die "Cant create MySQL upgrade: $error" if $error;
381              
382 0           my $diff = SQL::Translator::Diff->new({
383             output_db => 'MySQL',
384             source_schema => $target_schema,
385             target_schema => $source_schema,
386             ignore_index_names => 1,
387             ignore_constraint_names => 1,
388             caseopt => 1,
389             })->compute_differences->produce_diff_sql;
390              
391 0           my $error = $self->save_migration(
392             path => "$paths->{db_downgrade}/$new_version-$last_version/001_auto.sql",
393             data => $diff,
394             );
395 0 0         die "Cant create MySQL downgrade: $error" if $error;
396             }
397             }
398              
399 0           say "New schema version: $new_version";
400 0           say "Deploy to $new_version";
401 0           $self->deployed->{version} = $new_version;
402 0           $self->save_deployed;
403              
404 0           say "Done";
405             }
406              
407             sub diff {
408 0     0 0   my $self = shift;
409 0           my $paths = $self->paths;
410              
411 0           my $last_version = $self->get_last_version;
412              
413 0           say "Schema version: $last_version";
414              
415 0 0         if (my $version = $self->deployed->{version}) {
416 0           say "Deployed database is $version";
417             }
418              
419 0 0         if ($self->db_is_empty) {
420 0           say "Nothing to diff. Database is empty.";
421              
422 0           return;
423             }
424              
425 0           my $schema1 = $self->get_schema(to => 'YAML')->{schema};
426             my $schema2 = $self->get_schema(
427             from => 'YAML',
428             filename => "$paths->{source_deploy}/$last_version/001_auto.yml",
429 0           )->{schema};
430              
431 0           my $diff = $self->_diff($schema1, $schema2);
432 0           say "==== BEGIN SQL ====";
433 0           say $diff;
434 0           say "==== END SQL ====";
435             }
436              
437             sub _diff {
438 0     0     my $self = shift;
439 0   0       my $schema1 = shift || return;
440 0   0       my $schema2 = shift || return;
441              
442 0           for ($schema1->get_tables) {
443 0           $_->{options} = [grep {!$_->{'AUTO_INCREMENT'}} @{ $_->{options} }];
  0            
  0            
444             }
445 0           for ($schema2->get_tables) {
446 0           $_->{options} = [grep {!$_->{'AUTO_INCREMENT'}} @{ $_->{options} }];
  0            
  0            
447             }
448 0           my $diff = SQL::Translator::Diff->new({
449             output_db => 'MySQL',
450             source_schema => $schema2,
451             target_schema => $schema1,
452             ignore_index_names => 1,
453             ignore_constraint_names => 1,
454             caseopt => 1
455             })->compute_differences;
456              
457 0           my $h = {};
458 0 0         for my $table(keys %{ $diff->{table_diff_hash} || {} }) {
  0            
459 0           for my $field (@{$diff->{table_diff_hash}->{$table}->{fields_to_create}}) {
  0            
460 0           $h->{$table}->{$field->name} = [grep {$_->order == $field->{order} - 1} $field->table->get_fields]->[0]->{name};
  0            
461             }
462             }
463 0           $diff = $diff->produce_diff_sql;
464              
465 0 0         if (%$h) {
466 0           my @res = split "\n\n", $diff;
467 0           for my $s (@res) {
468 0           my ($t, $a) = $s =~ /ALTER TABLE ([^\s]+) ([^;]+)/;
469              
470 0           for ($a =~ /ADD COLUMN ([^\s]+) /g) {
471 0           $s =~ s/ADD COLUMN $_ (.*)([\,\;])/ADD COLUMN $_ $1 AFTER $h->{$t}->{$_}$2/g;
472             }
473             }
474              
475 0           $diff = join "\n\n", @res;
476             }
477              
478 0           return $diff;
479             }
480              
481             sub get_last_version {
482 0     0 0   my $self = shift;
483              
484 0           my $path = $self->paths->{source_deploy};
485              
486 0           my $last_version;
487 0 0         if (-e $path) {
488 0 0         opendir my $dh, $path or die "can't opendir $path: $!";
489 0           ($last_version) = sort {$b <=> $a} readdir $dh;
  0            
490 0           closedir $dh;
491             }
492              
493 0           return $last_version;
494             }
495              
496 0 0   0 0   sub db_is_empty { @{ shift->db->selectall_arrayref('show tables', { Slice => {} }) } ? 0 : 1 }
  0            
497              
498             sub save_migration {
499 0     0 0   my $self = shift;
500 0           my $p = {@_};
501              
502 0           my $dir = dirname $p->{path};
503 0 0         make_path $dir unless -d $dir;
504              
505 0 0         return 'No input data to save!' unless $p->{data};
506              
507 0 0         open my $fh, '>', $p->{path} or return $!;
508 0           print $fh $p->{data};
509 0           close $fh;
510              
511 0           return;
512             }
513              
514             sub get_schema {
515 0     0 0   my $self = shift;
516 0           my $p = {@_};
517              
518             my $args = join ';',
519             $self->config->{datasource}->{host} ? 'host='.$self->config->{datasource}->{host} : (),
520 0 0         $self->config->{datasource}->{port} ? 'port='.$self->config->{datasource}->{port} : (),
    0          
521             ;
522 0 0         $args = ":$args" if $args;
523              
524             my $translator = SQL::Translator->new(
525             debug => 1,
526             no_comments => $p->{no_comments} || 0,
527             $p->{filename}
528             ?
529             ()
530             :
531             (
532             parser_args => {
533             dsn => 'dbi:mysql:'.$self->config->{datasource}->{database}.$args,
534             db_user => $self->config->{user },
535             db_password => $self->config->{password},
536             },
537             )
538 0 0 0       );
539 0   0       $translator->parser($p->{from} || 'DBI');
540              
541             my @output = $translator->translate(
542             producer => $p->{to},
543             $p->{filename}
544             ?
545             (filename => $p->{filename})
546 0 0         :
    0          
547             ()
548             ) or die "Error: " . $translator->error;
549              
550 0           my $schema = $translator->schema;
551 0 0         if ($p->{filename}) {
552 0           $schema->name($p->{filename});
553             }
554              
555             return {
556 0           schema => $schema,
557             data => \@output,
558             };
559             }
560              
561             sub deployment_statements {
562 0     0 0   my $self = shift;
563 0           my $p = {@_};
564 0           my $paths = $self->paths;
565              
566 0 0         if ($p->{type} eq 'install') {
567             return $self->get_schema(
568             from => 'YAML',
569             to => 'MySQL',
570             filename => "$paths->{source_deploy}/$p->{version}/001_auto.yml",
571             no_comments => 1,
572 0           )->{data};
573             } else {
574 0   0       my $filename = $p->{filename} || "$paths->{db_$p->{type}}/$p->{from}-$p->{to}/001_auto.sql";
575 0 0         if(-f $filename) {
576 0           my $file;
577 0 0         open $file, "<$filename" or die "Can't open $filename ($!)";
578 0           my @rows = <$file>;
579 0           close $file;
580              
581             return [
582             grep {
583 0           s/\n//g;
  0            
584 0 0         /(^--|^BEGIN|^COMMIT|^\s*$)/ ? 0 : 1
585             }
586             split
587             /\s*--.*\n|;\n/,
588             join '', @rows
589             ];
590             }
591             }
592              
593 0           return [];
594             }
595              
596             sub rm {
597 0     0 1   my $self = shift;
598 0           my $paths = $self->paths;
599 0 0         say 'Params --version in required' unless my $version = $self->params->{version};
600              
601 0           remove_tree "$paths->{source_deploy}/$version";
602 0           remove_tree "$paths->{db_deploy}/$version";
603 0           remove_tree "$paths->{db_upgrade}/".($version-1)."-$version";
604 0           remove_tree "$paths->{db_downgrade}/$version-".($version-1);
605             }
606              
607             1;
608              
609              
610             =pod
611              
612             =encoding utf8
613            
614             =head1 NAME
615            
616             Mojolicious::Command::migration — MySQL migration tool for Mojolicious
617              
618             =head1 VERSION
619              
620             version 0.17
621              
622             =head1 SYNOPSIS
623            
624             Usage: APPLICATION migration [COMMAND] [OPTIONS]
625            
626             mojo migration prepare
627            
628             Commands:
629             status : Current database and schema version
630             diff : SQL diff with last version.
631             install : Install a version to the database.
632             prepare : Makes deployment files for your database
633             upgrade : Upgrade the database.
634             downgrade : Downgrade the database.
635             rm : Remove files of migration by version number.
636            
637             =head1 DESCRIPTION
638            
639             L MySQL migration tool.
640            
641              
642             =head1 USAGE
643              
644             L uses app->db for mysql connection and following configuration:
645              
646             {
647             'user' => 'USER',
648             'password' => 'PASSWORD',
649             'datasource' => { 'database' => 'DB_NAME'},
650             }
651              
652             from
653              
654             $ app->config->{db}->{mysql}
655              
656             Use can force command without saving state with param --force. Example:
657             $ app migration downgrade --force
658              
659             All deploy files saves to relative directory 'share/'. You can change it with 'MOJO_MIGRATION_SHARE' environment.
660             Current project state saves to 'tmp/.deploy_status' file. You can change directory with 'MOJO_MIGRATION_TMP' environment.
661              
662             Note: we create directories automatically
663              
664             =head1 COMMANDS
665            
666             =head2 status
667            
668             $ app migration status
669             Schema version: 21
670             Deployed database is 20
671              
672             Returns the state of the deployed database (if it is deployed) and the state of the current schema version. Sends this as a string to STDOUT
673              
674             =head2 rm
675            
676             $ app migration rm --version 123
677              
678             =head2 prepare
679              
680             Makes deployment files for the current schema. If deployment files exist, will fail unless you "overwrite_migrations".
681              
682             # have changes
683             $ app migration prepare
684             Schema version: 21
685             New version is 22
686             Deploy to 22
687            
688             # no changes
689             $ app migration prepare
690             Schema version: 21
691             Nothing to upgrade. Exit
692              
693             =head2 install
694              
695             Installs either the current schema version (if already prepared) or the target version specified via any to_version flags.
696              
697             If you try to install to a database that has already been installed (not empty), you'll get an error. Use flag force to set current database to schema version without changes database.
698              
699             # last
700             $ app migration install
701             Schema version: 21
702             Deploy database to 21
703            
704             # target version
705             $ app migration install --to-version 10
706             Schema version: 21
707             Deploy database to 10
708              
709             # force install
710             $ app migration install --force
711             Schema version: 21
712             Force deploy to 21
713              
714             =head2 upgrade
715              
716              
717             Use flag --force to set current database to schema version without changes database.
718              
719             # last
720             $ app migration upgrade
721             Schema version: 21
722             Database version: 20
723             Upgrade to 21
724            
725             # target version
726             $ app migration upgrade --to-version 10
727             Schema version: 21
728             Database version: 8
729             Upgrade to 10
730              
731             # force upgrade
732             $ app migration upgrade --force
733             Schema version: 21
734             Database version: 8
735             Force upgrade to 21
736              
737             =head2 downgrade
738              
739              
740             Use flag --force to set current database to schema version without changes database.
741              
742             # last
743             $ app migration downgrade
744             Schema version: 21
745             Database version: 20
746             Downgrade to 21
747            
748             # target version
749             $ app migration downgrade --to-version 10
750             Schema version: 21
751             Database version: 8
752             Downgrade to 10
753              
754             # force downgrade
755             $ app migration downgrade --force
756             Schema version: 21
757             Database version: 8
758             Force downgrade to 21
759              
760             =head1 Custom upgrade and downgrade
761              
762             You can customize upgrade and downgrade by adding additional SQL scripts to path of action. All scripts will be executed in alphabetical order.
763              
764             # share/migration/MySQL/upgrade/10-11/001_auto.sql is automatic
765             # share/migration/MySQL/upgrade/10-11/002_some_script.sql is additional sctipt
766             $ app migration upgrade
767             Schema version: 11
768             Database version: 10
769             Upgrade to 11
770             Exec file: share/migrations/MySQL/upgrade/10-11/001_auto.sql
771             Exec file: share/migrations/MySQL/upgrade/10-11/002_some_script.sql
772              
773             =head1 SOURCE REPOSITORY
774              
775             L
776              
777             =head1 AUTHOR
778              
779             Alexey Likhatskiy,
780              
781             =head1 LICENSE AND COPYRIGHT
782              
783             Copyright (C) 2015 "Alexey Likhatskiy"
784              
785             This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.