File Coverage

blib/lib/HTTP/Request/Generator.pm
Criterion Covered Total %
statement 190 291 65.2
branch 46 66 69.7
condition 17 20 85.0
subroutine 22 25 88.0
pod 5 8 62.5
total 280 410 68.2


line stmt bran cond sub pod time code
1             package HTTP::Request::Generator;
2 6     6   78580 use strict;
  6         47  
  6         188  
3 6     6   2869 use Filter::signatures;
  6         162303  
  6         46  
4 6     6   233 use feature 'signatures';
  6         14  
  6         189  
5 6     6   34 no warnings 'experimental::signatures';
  6         14  
  6         193  
6 6     6   3567 use Algorithm::Loops 'NestedLoops';
  6         14240  
  6         406  
7 6     6   5241 use List::MoreUtils 'zip';
  6         80896  
  6         40  
8 6     6   9730 use URI;
  6         27854  
  6         189  
9 6     6   41 use URI::Escape;
  6         15  
  6         357  
10 6     6   39 use Exporter 'import';
  6         14  
  6         159  
11 6     6   34 use Carp 'croak';
  6         27  
  6         22457  
12              
13             =head1 NAME
14              
15             HTTP::Request::Generator - generate HTTP requests
16              
17             =head1 SYNOPSIS
18              
19             use HTTP::Request::Generator 'generate_requests';
20              
21             @requests = generate_requests(
22             method => 'GET',
23             pattern => 'https://example.com/{bar,foo,gallery}/[00..99].html',
24             );
25              
26             # generates 300 requests from
27             # https://example.com/bar/00.html to
28             # https://example.com/gallery/99.html
29              
30             @requests = generate_requests(
31             method => 'POST',
32             host => ['example.com','www.example.com'],
33             path => '/profiles/:name',
34             url_params => {
35             name => ['Corion','Co-Rion'],
36             },
37             query_params => {
38             stars => [2,3],
39             },
40             body_params => {
41             comment => ['Some comment', 'Another comment, A++'],
42             },
43             headers => [
44             {
45             "Content-Type" => 'text/plain; encoding=UTF-8',
46             Cookie => 'my_session_id',
47             },
48             {
49             "Content-Type" => 'text/plain; encoding=Latin-1',
50             Cookie => 'my_session_id',
51             },
52             ],
53             );
54             # Generates 32 requests out of the combinations
55              
56             for my $req (@requests) {
57             $ua->request( $req );
58             };
59              
60             =cut
61              
62             our $VERSION = '0.10';
63             our @EXPORT_OK = qw( generate_requests as_dancer as_plack as_http_request
64             expand_curl_pattern
65             );
66              
67 150     150 0 193 sub unwrap($item,$default) {
  150         268  
  150         196  
  150         185  
68 150 100       391 defined $item
    100          
69             ? (ref $item ? $item : [$item])
70             : $default
71             }
72              
73 24     24 0 39 sub fetch_all( $iterator, $limit=0 ) {
  24         37  
  24         53  
  24         42  
74 24         35 my @res;
75 24         46 while( my @r = $iterator->()) {
76 186         369 push @res, @r;
77 186 100 100     533 if( $limit && (@res > $limit )) {
78 1         3 splice @res, $limit;
79             last
80 1         3 };
81             };
82             return @res
83 24         835 };
84              
85             our %defaults = (
86             method => ['GET'],
87             path => ['/'],
88             host => [''],
89             port => [0],
90             scheme => ['http'],
91              
92             # How can we specify various values for the headers?
93             headers => [{}],
94              
95             #query_params => [],
96             #body_params => [],
97             #url_params => [],
98             #values => [[]], # the list over which to iterate for *_params
99             );
100              
101             # We want to skip a set of values if they make a test fail
102             # if a value appears anywhere with a failing test, skip it elsewhere
103             # or look at the history to see whether that value has passing tests somewhere
104             # and then keep it?!
105              
106 70528     70528 0 88985 sub fill_url( $url, $values, $raw=undef ) {
  70528         100332  
  70528         89045  
  70528         91530  
  70528         86168  
107 70528 50       121401 if( $values ) {
108 70528 100       111589 if( $raw ) {
109 70480 100       158927 $url =~ s!:(\w+)!exists $values->{$1} ? $values->{$1} : ":$1"!ge;
  52793         189393  
110             } else {
111 48 50       127 $url =~ s!:(\w+)!exists $values->{$1} ? uri_escape($values->{$1}) : ":$1"!ge;
  24         302  
112             };
113             };
114 70528         182558 $url
115             };
116              
117             # Convert nonref arguments to arrayrefs
118             sub _makeref {
119             map {
120 100 100   100   181 ref $_ ne 'ARRAY' ? [$_] : $_
  183         404  
121             } @_
122             }
123              
124 15     15   20 sub _extract_enum( $name, $item ) {
  15         28  
  15         17  
  15         21  
125             # Explicitly enumerate all ranges
126 15 50       26 my @res = @{ $defaults{ $name } || []};
  15         69  
127              
128 15 100       35 if( $item ) {
129             # Expand all ranges into the enumerated lists
130 1         18 $item =~ s!\[([^.\]]+?)\.\.([^.\]]+?)\]!"{" . join(",", $1..$2 )."}"!ge;
  0         0  
131              
132             # Explode all enumerated items into their list
133             # We should punt this into a(nother) iterator, maybe?!
134 1 50       12 if( $item =~ /\{.*\}/ ) {
135 0         0 my $changed = 1;
136 0         0 @res = $item;
137 0         0 while ($changed) {
138 0         0 undef $changed;
139             @res = map {
140 0         0 my $i = $_;
  0         0  
141 0         0 my @r;
142 0 0       0 if( $i =~ /^([^{]*)\{([^}]+)\}([^{]*)/ ) {
143 0         0 my($pre, $m, $post) = ($1,$2,$3);
144 0         0 $changed = 1;
145 0         0 @r = map { "$pre$_$post" } split /,/, $m, -1;
  0         0  
146             } else {
147 0         0 @r = $i
148             };
149             @r
150 0         0 } @res;
151             }
152             } else {
153 1         4 @res = $item;
154             };
155             };
156             return \@res
157 15         36 }
158              
159 15     15   22 sub _extract_enum_query( $query ) {
  15         24  
  15         22  
160 15         32 my $items = _extract_enum( 'query', $query );
161 15         26 my %parameters;
162 15         33 for my $q (@$items) {
163 1         5 my $u = URI->new('example.com', 'http');
164 1         59 $u->query($q);
165 1         21 my %f = $u->query_form;
166 1         71 for my $k (keys %f) {
167 2         9 $parameters{ $k }->{ $f{ $k }} = 1;
168             };
169             };
170              
171 15         39 for my $v (values %parameters) {
172 2         8 $v = [ sort keys %$v ];
173             };
174              
175 15         82 \%parameters
176             }
177              
178              
179             =head2 C<< expand_curl_pattern >>
180              
181             my %res = expand_curl_pattern( 'https://' );
182             #
183              
184             Expands a curl-style pattern to a pattern using positional placeholders.
185             See the C documentation on the patterns.
186              
187             =cut
188              
189 15     15 1 24 sub expand_curl_pattern( $pattern ) {
  15         24  
  15         26  
190 15         21 my %ranges;
191              
192             # Split up the URL pattern into a scheme, host(pattern), port number and
193             # path (pattern)
194             #use Regexp::Debugger;
195             #use re 'debug';
196 15         157 my( $scheme, $host, $port, $path, $query )
197             = $pattern =~ m!^(?:([^:]+):)? # scheme
198             /?/? # optional? slashes
199             ( # hostname
200             \[(?:[:\da-fA-F]+)\] # ipv6
201             |[^/:\[]+
202             (?:\[[^/\]]+\][^/:\[]*)*
203             (?=[:/]|$) # plain, or expansion
204             |\[[^:\]]+\][^/:]* # expansion
205             )
206             (?::(\d+))? # optional port
207             ([^?]*) # path
208             (?:\?(.*))? # optional query part
209             $!x;
210             #my( $scheme, $host, $port, $path, $query )
211             # = $pattern =~ m!^(?:([^:]+):)?/?/?(\[(?:[:\da-fA-F]+)\]|[^/:\[]+(?:\[[^/\]]+\][^/:\[]*)*(?=[:/]|$)|\[[^:\]]+\][^/:]*)(?::(\d+))?([^?]*)(?:\?(.*))?$!;
212              
213             #no Regexp::Debugger;
214              
215             # Explicitly enumerate all ranges
216 15         36 my $idx = 0;
217              
218 15 100       34 if( $scheme ) {
219 14         28 $scheme =~ s!\[([^.\]]+?)\.\.([^.\[]+?)\]!$ranges{$idx} = [$1..$2]; ":".$idx++!ge;
  0         0  
  0         0  
220 14         35 $scheme =~ s!\{([^\}]*)\}!$ranges{$idx} = [split /,/, $1, -1]; ":".$idx++!ge;
  2         16  
  2         13  
221             };
222              
223 15 50       37 if( $host ) {
224 15         45 $host =~ s!\[([^.\]]+?)\.\.([^.\[]+?)\]!$ranges{$idx} = [$1..$2]; ":".$idx++!ge;
  6         56  
  6         31  
225 15         36 $host =~ s!\{([^\}]*)\}!$ranges{$idx} = [split /,/, $1, -1]; ":".$idx++!ge;
  1         34  
  1         5  
226             };
227              
228 15 100       37 if( $port ) {
229 4         6 $port =~ s!\[([^.\]]+?)\.\.([^.\[]+?)\]!$ranges{$idx} = [$1..$2]; ":".$idx++!ge;
  0         0  
  0         0  
230 4         5 $port =~ s!\{([^\}]*)\}!$ranges{$idx} = [split /,/, $1, -1]; ":".$idx++!ge;
  0         0  
  0         0  
231             };
232              
233 15 100       34 if( $path ) {
234 11         46 $path =~ s!\[([^.\]]+?)\.\.([^.\[]+?)\]!$ranges{$idx} = [$1..$2]; ":".$idx++!ge;
  7         47  
  7         31  
235             # Move all explicitly enumerated parts into lists:
236 11         37 $path =~ s!\{([^\}]*)\}!$ranges{$idx} = [split /,/, $1, -1]; ":".$idx++!ge;
  4         22  
  4         17  
237             };
238              
239 15         46 my %res = (
240             url_params => \%ranges,
241             host => $host,
242             scheme => $scheme,
243             port => $port,
244             path => $path,
245             query_params => _extract_enum_query( $query ),
246             raw_params => 1,
247             );
248 15         114 %res
249             }
250              
251 25     25   43 sub _generate_requests_iter(%options) {
  25         54  
  25         38  
252 25 100 50 17762   163 my $wrapper = delete $options{ wrap } || sub { wantarray ? @_ : $_[0]};
  17762         58480  
253 25         203 my @keys = sort keys %defaults;
254              
255 25 100       83 if( my $pattern = delete $options{ pattern }) {
256 15         55 %options = (%options, expand_curl_pattern( $pattern ));
257             };
258              
259 25   100     88 my $query_params = $options{ query_params } || {};
260 25   100     87 my $body_params = $options{ body_params } || {};
261 25   100     67 my $url_params = $options{ url_params } || {};
262              
263             $options{ "fixed_$_" } ||= {}
264 25   50     363 for @keys;
265              
266             # Now only iterate over the non-empty lists
267 25         59 my %args = map { my @v = unwrap($options{ $_ }, [@{$defaults{ $_ }}]);
  150         276  
  150         358  
268 150 50       463 @v ? ($_ => @v) : () }
269             @keys;
270 25         170 @keys = sort keys %args; # somewhat predictable
271             $args{ $_ } ||= {}
272 25   50     202 for qw(query_params body_params url_params);
273 25         94 my @loops = _makeref @args{ @keys };
274             #use Data::Dumper; warn Dumper \@loops, \@keys, \%options;
275              
276             # Turn all query_params into additional loops for each entry in keys %$query_params
277             # Turn all body_params into additional loops over keys %$body_params
278 25         73 my @query_params = keys %$query_params;
279 25         57 push @loops, _makeref values %$query_params;
280 25         52 my @body_params = keys %$body_params;
281 25         44 push @loops, _makeref values %$body_params;
282 25         76 my @url_params = keys %$url_params;
283 25         54 push @loops, _makeref values %$url_params;
284              
285             #use Data::Dumper; warn "Looping over " . Dumper \@loops;
286              
287 25         94 my $iter = NestedLoops(\@loops,{});
288              
289             # Set up the fixed parts
290 25         1573 my %template;
291              
292 25         52 for(qw(query_params body_params headers)) {
293 75   100     293 $template{ $_ } = $options{ "fixed_$_" } || {};
294             };
295              
296             return sub {
297 17786     17786   91552 my @v = $iter->();
298 17786 100       371206 return unless @v;
299             #use Data::Dumper; warn Dumper \@v;
300              
301             # Patch in the new values
302 17762         52515 my %values = %template;
303 17762         40833 my @vv = splice @v, 0, 0+@keys;
304 17762         68690 @values{ @keys } = @vv;
305              
306             # Now add the query_params, if any
307 17762 100       35796 if(@query_params) {
308 122         247 my @get_values = splice @v, 0, 0+@query_params;
309 122         182 $values{ query_params } = { (%{ $values{ query_params } }, zip( @query_params, @get_values )) };
  122         714  
310             };
311             # Now add the body_params, if any
312 17762 100       33494 if(@body_params) {
313 8         18 my @values = splice @v, 0, 0+@body_params;
314 8         15 $values{ body_params } = { %{ $values{ body_params } }, zip @body_params, @values };
  8         34  
315             };
316              
317             # Recreate the URL with the substituted values
318 17762 100       33672 if( @url_params ) {
319 17632         23893 my %v;
320 17632         41131 @v{ @url_params } = splice @v, 0, 0+@url_params;
321             #use Data::Dumper; warn Dumper \%values;
322 17632         35414 for my $key (qw(scheme host port path )) {
323 70528         138009 $values{ $key } = fill_url($values{ $key }, \%v, $options{ raw_params });
324             };
325             };
326              
327 17762         36402 $values{ url } = _build_uri( \%values );
328              
329             # Merge the headers as well
330             #warn "Merging headers: " . Dumper($values{headers}). " + " . (Dumper $template{headers});
331 17762 50       27509 %{$values{headers}} = (%{$template{headers}}, %{$values{headers} || {}});
  17762         30336  
  17762         32740  
  17762         40434  
332 17762         38028 return $wrapper->(\%values);
333 25         178 };
334             }
335              
336 17762     17762   23618 sub _build_uri( $req ) {
  17762         23717  
  17762         22256  
337 17762         46491 my $uri = URI->new( '', $req->{scheme} );
338 17762 100       894678 if( $req->{host}) {
339 17630         48613 $uri->host( $req->{host});
340 17630         1085405 $uri->scheme( $req->{scheme});
341 17630 100 100     1329143 $uri->port( $req->{port}) if( $req->{port} and $req->{port} != $uri->default_port );
342             };
343 17762         51787 $uri->path( $req->{path});
344             # We want predictable URIs, so we sort the keys here instead of
345             # just passing the hash reference
346 17762         448402 $uri->query_form( map { $_ => $req->{query_params}->{$_} } sort keys %{ $req->{query_params} });
  456         1094  
  17762         72466  
347             #$uri->query_form( $req->{query_params});
348 17762         268843 $uri
349             }
350              
351             =head2 C<< generate_requests( %options ) >>
352              
353             my $g = generate_requests(
354             url => '/profiles/:name',
355             url_params => ['Mark','John'],
356             wrap => sub {
357             my( $req ) = @_;
358             # Fix up some values
359             $req->{headers}->{'Content-Length'} = 666;
360             return $req;
361             },
362             );
363             while( my $r = $g->()) {
364             send_request( $r );
365             };
366              
367             This function creates data structures that are suitable for sending off
368             a mass of similar but different HTTP requests. All array references are expanded
369             into the cartesian product of their contents. The above example would create
370             two requests:
371              
372             url => '/profiles/Mark,
373             url => '/profiles/John',
374              
375             C returns an iterator in scalar context. In list context, it
376             returns the complete list of requests:
377              
378             my @requests = generate_requests(
379             url => '/profiles/:name',
380             url_params => ['Mark','John'],
381             wrap => sub {
382             my( $req ) = @_;
383             # Fix up some values
384             $req->{headers}->{'Content-Length'} = 666;
385             return $req;
386             },
387             );
388             for my $r (@requests) {
389             send_request( $r );
390             };
391              
392             Note that returning a list instead of the iterator will use up quite some memory
393             quickly, as the list will be the cartesian product of the input parameters.
394              
395             There are helper functions
396             that will turn that data into a data structure suitable for your HTTP framework
397             of choice.
398              
399             {
400             method => 'GET',
401             url => '/profiles/Mark',
402             scheme => 'http',
403             port => 80,
404             headers => {},
405             body_params => {},
406             query_params => {},
407             }
408              
409             As a shorthand for creating lists, you can use the C option, which
410             will expand a string into a set of requests. C<{}> will expand into alternatives
411             while C<[xx..yy]> will expand into the range C to C. Note that these
412             lists will be expanded in memory.
413              
414             =head3 Options
415              
416             =over 4
417              
418             =item B
419              
420             pattern => 'https://example.{com,org,net}/page_[00..99].html',
421              
422             Generate URLs from this pattern instead of C, C
423             and C.
424              
425             =item B
426              
427             URL template to use.
428              
429             =item B
430              
431             Parameters to replace in the C template.
432              
433             =item B
434              
435             Parameters to replace in the POST body.
436              
437             =item B
438              
439             Parameters to replace in the GET request.
440              
441             =item B
442              
443             Hostname(s) to use.
444              
445             =item B
446              
447             Port(s) to use.
448              
449             =item B
450              
451             Headers to use. Currently, no templates are generated for the headers. You have
452             to specify complete sets of headers for each alternative.
453              
454             =item B
455              
456             Limit the number of requests generated.
457              
458             =back
459              
460             =cut
461              
462 25     25 1 30300 sub generate_requests(%options) {
  25         79  
  25         42  
463             croak "Option 'protocol' is now named 'scheme'."
464 25 50       96 if $options{ protocol };
465              
466 25         80 my $i = _generate_requests_iter(%options);
467 25 100       65 if( wantarray ) {
468 24         78 return fetch_all($i, $options{ limit });
469             } else {
470 1         4 return $i
471             }
472             }
473              
474             =head2 C<< as_http_request >>
475              
476             generate_requests(
477             method => 'POST',
478             url => '/feedback/:item',
479             wrap => \&HTTP::Request::Generator::as_http_request,
480             )
481              
482             Converts the request data to a L object.
483              
484             =cut
485              
486 0     0 1   sub as_http_request($req) {
  0            
  0            
487 0           require HTTP::Request;
488 0           HTTP::Request->VERSION(6); # ->flatten()
489 0           require URI;
490 0           require URI::QueryParam;
491              
492 0           my $body = '';
493 0           my $headers;
494             my $form_ct;
495 0 0         if( keys %{$req->{body_params}}) {
  0            
496 0           require HTTP::Request::Common;
497             my $r = HTTP::Request::Common::POST( $req->{url},
498 0           [ %{ $req->{body_params} }],
  0            
499             );
500 0           $headers = HTTP::Headers->new( %{ $req->{headers} }, $r->headers->flatten );
  0            
501 0           $body = $r->content;
502 0           $form_ct = $r->content_type;
503             } else {
504 0           $headers = HTTP::Headers->new( %$headers );
505             };
506              
507             # Store metadata / generate "signature" for later inspection/isolation?
508 0           my $uri = _build_uri( $req );
509 0 0         $uri->query_param( %{ $req->{query_params} || {} });
  0            
510             my $res = HTTP::Request->new(
511 0           $req->{method} => $uri,
512             $headers,
513             $body,
514             );
515 0           $res
516             }
517              
518             =head2 C<< as_dancer >>
519              
520             generate_requests(
521             method => 'POST',
522             url => '/feedback/:item',
523             wrap => \&HTTP::Request::Generator::as_dancer,
524             )
525              
526             Converts the request data to a L object.
527              
528             During the creation of Dancer::Request objects, C<< %ENV >> will be empty except
529             for C<< $ENV{TMP} >> and C<< $ENV{TEMP} >>.
530              
531              
532             This function needs and dynamically loads the following modules:
533              
534             L
535              
536             L
537              
538             =cut
539              
540 0     0 1   sub as_dancer($req) {
  0            
  0            
541 0           require Dancer::Request;
542 0           require HTTP::Request;
543 0           HTTP::Request->VERSION(6); # ->flatten()
544              
545 0           my $body = '';
546 0           my $headers;
547             my $form_ct;
548 0 0         if( keys %{$req->{body_params}}) {
  0            
549 0           require HTTP::Request::Common;
550             my $r = HTTP::Request::Common::POST( $req->{url},
551 0           [ %{ $req->{body_params} }],
  0            
552             );
553 0           $headers = HTTP::Headers->new( %{ $req->{headers} }, $r->headers->flatten );
  0            
554 0           $body = $r->content;
555 0           $form_ct = $r->content_type;
556             } else {
557 0           $headers = HTTP::Headers->new( %$headers );
558             };
559              
560 0           my $uri = _build_uri( $req );
561              
562             # Store metadata / generate "signature" for later inspection/isolation?
563 0           my %old_ENV = %ENV;
564 0           local %ENV; # wipe out non-overridable default variables of Dancer::Request
565 0           my @keep = (qw(TMP TEMP));
566 0           @ENV{ @keep } = @old_ENV{ @keep };
567             my $res = Dancer::Request->new_for_request(
568             $req->{method} => $uri->path,
569             $req->{query_params},
570             $body,
571             $headers,
572             { CONTENT_LENGTH => length($body),
573             CONTENT_TYPE => $form_ct,
574             HTTP_HOST => (join ":", $req->{host}, $req->{port}),
575             SERVER_NAME => $req->{host},
576             SERVER_PORT => $req->{port},
577             REQUEST_METHOD => $req->{ method },
578 0           REQUEST_URI => $uri,
579             SCRIPT_NAME => $uri->path,
580              
581             },
582             );
583 0           $res->{_http_body}->add($body);
584             #use Data::Dumper; warn Dumper $res;
585 0           $res
586             }
587              
588             =head2 C<< as_plack >>
589              
590             generate_requests(
591             method => 'POST',
592             url => '/feedback/:item',
593             wrap => \&HTTP::Request::Generator::as_plack,
594             )
595              
596             Converts the request data to a L object.
597              
598             During the creation of Plack::Request objects, C<< %ENV >> will be empty except
599             for C<< $ENV{TMP} >> and C<< $ENV{TEMP} >>.
600              
601             This function needs and dynamically loads the following modules:
602              
603             L
604              
605             L
606              
607             L
608              
609             =cut
610              
611 0     0 1   sub as_plack($req) {
  0            
  0            
612 0           require Plack::Request;
613 0           Plack::Request->VERSION(1.0047);
614 0           require HTTP::Headers;
615 0           require Hash::MultiValue;
616              
617 0           my %env = %$req;
618 0           $env{ 'psgi.version' } = '1.0';
619 0           $env{ 'psgi.url_scheme' } = delete $env{ scheme };
620 0 0         $env{ 'plack.request.query_parameters' } = [%{delete $env{ query_params }||{}} ];
  0            
621 0 0         $env{ 'plack.request.body_parameters' } = [%{delete $env{ body_params }||{}} ];
  0            
622 0           $env{ 'plack.request.headers' } = HTTP::Headers->new( %{ delete $req->{headers} });
  0            
623 0           $env{ REQUEST_METHOD } = delete $env{ method };
624 0           $env{ REQUEST_URI } = _build_uri( $req );
625 0           $env{ SCRIPT_NAME } = $env{ REQUEST_URI }->path;
626 0           delete $env{ url };
627 0           $env{ QUERY_STRING } = ''; # not correct, but...
628 0           $env{ SERVER_NAME } = delete $env{ host };
629 0           $env{ SERVER_PORT } = delete $env{ port };
630             # need to convert the headers into %env HTTP_ keys here
631 0           $env{ CONTENT_TYPE } = undef;
632              
633             # Store metadata / generate "signature" for later inspection/isolation?
634 0           my %old_ENV = %ENV;
635 0           local %ENV; # wipe out non-overridable default variables of Dancer::Request
636 0           my @keep = (qw(TMP TEMP));
637 0           @ENV{ @keep } = @old_ENV{ @keep };
638 0           my $res = Plack::Request->new(\%env);
639 0           $res
640             }
641              
642             1;
643              
644             =head1 SEE ALSO
645              
646             L for the pattern syntax
647              
648             =head1 REPOSITORY
649              
650             The public repository of this module is
651             L.
652              
653             =head1 SUPPORT
654              
655             The public support forum of this module is L.
656              
657             =head1 BUG TRACKER
658              
659             Please report bugs in this module via the RT CPAN bug queue at
660             L
661             or via mail to L.
662              
663             =head1 AUTHOR
664              
665             Max Maischein C
666              
667             =head1 COPYRIGHT (c)
668              
669             Copyright 2017-2019 by Max Maischein C.
670              
671             =head1 LICENSE
672              
673             This module is released under the same terms as Perl itself.
674              
675             =cut