File Coverage

blib/lib/IO/Iron/IronMQ/Queue.pm
Criterion Covered Total %
statement 29 236 12.2
branch 0 36 0.0
condition n/a
subroutine 12 29 41.3
pod 15 15 100.0
total 56 316 17.7


line stmt bran cond sub pod time code
1             package IO::Iron::IronMQ::Queue;
2              
3             ## no critic (Documentation::RequirePodAtEnd)
4             ## no critic (Documentation::RequirePodSections)
5             ## no critic (Subroutines::RequireArgUnpacking)
6             ## no critic (ControlStructures::ProhibitPostfixControls)
7              
8 3     3   59 use 5.010_000;
  3         12  
9 3     3   17 use strict;
  3         5  
  3         66  
10 3     3   14 use warnings;
  3         6  
  3         67  
11              
12             # Global creator
13       3     BEGIN {
14             # Export Nothing
15             }
16              
17             # Global destructor
18       3     END {
19             }
20              
21             # ABSTRACT: IronMQ (Online Message Queue) Client (Queue).
22              
23             our $VERSION = '0.14'; # VERSION: generated by DZP::OurPkgVersion
24              
25 3     3   16 use Log::Any qw($log);
  3         5  
  3         33  
26 3     3   626 use Hash::Util 0.06 qw{lock_keys unlock_keys};
  3         88  
  3         18  
27 3     3   197 use Carp::Assert::More;
  3         19  
  3         654  
28 3     3   29 use English '-no_match_vars';
  3         6  
  3         17  
29 3     3   1170 use Params::Validate qw(:all);
  3         7  
  3         461  
30              
31 3     3   21 use IO::Iron::Common;
  3         6  
  3         73  
32 3     3   19 use IO::Iron::IronMQ::Api;
  3         6  
  3         8965  
33             require IO::Iron::IronMQ::Message;
34              
35             sub new {
36 0     0 1   my $class = shift;
37 0           my %params = validate(
38             @_,
39             {
40             'name' => { type => SCALAR, }, # queue name.
41             'ironmq_client' => { type => OBJECT, }, # Reference to IronMQ client
42             'connection' => { type => OBJECT, }, # Reference to REST client
43             }
44             );
45 0           $log->tracef( 'Entering new(%s, %s)', $class, \%params );
46 0           my $self;
47 0           my @self_keys = ( ## no critic (CodeLayout::ProhibitQuotedWordLists)
48             'ironmq_client', # Reference to IronMQ client
49             'name', # Queue name
50             'connection', # Reference to REST client
51             'last_http_status_code', # After successfull network operation, the return value is here.
52             );
53 0           lock_keys( %{$self}, @self_keys );
  0            
54 0           $self->{'ironmq_client'} = $params{'ironmq_client'};
55 0           $self->{'name'} = $params{'name'};
56 0           $self->{'connection'} = $params{'connection'};
57 0           assert_isa( $self->{'connection'}, 'IO::Iron::Connection', 'self->{\'connection\'} is IO::Iron::Connection.' );
58 0           assert_isa( $self->{'ironmq_client'}, 'IO::Iron::IronMQ::Client', 'self->{\'ironmq_client\'} is IO::Iron::IronMQ::Client.' );
59 0           assert_nonblank( $self->{'name'}, 'self->{\'name\'} is defined and is not blank.' );
60              
61 0           unlock_keys( %{$self} );
  0            
62 0           my $blessed_ref = bless $self, $class;
63 0           lock_keys( %{$self}, @self_keys );
  0            
64              
65 0           $log->tracef( 'Exiting new: %s', $blessed_ref );
66 0           return $blessed_ref;
67             }
68              
69             sub size {
70 0     0 1   my $self = shift;
71 0           my %params = validate(
72             @_,
73             {
74             # No parameters
75             }
76             );
77 0           $log->tracef('Entering size().');
78              
79 0           my $queue_name = $self->name();
80 0           my $connection = $self->{'connection'};
81 0           my ( $http_status_code, $response_message ) =
82             $connection->perform_iron_action( IO::Iron::IronMQ::Api::IRONMQ_GET_QUEUE_INFO(), { '{Queue Name}' => $queue_name, } );
83 0           $self->{'last_http_status_code'} = $http_status_code;
84 0           my $size = $response_message->{'queue'}->{'size'};
85 0           $log->debugf( 'Queue size is %s.', $size );
86              
87 0           $log->tracef( 'Exiting size(): %s', $size );
88 0           return $size;
89             }
90              
91             sub post_messages {
92              
93             # TODO Limit the total size!
94 0     0 1   my $self = shift;
95             my %params = validate(
96             @_,
97             {
98             'messages' => {
99             type => ARRAYREF,
100             callbacks => {
101             'assert_class' => sub {
102 0     0     foreach my $message ( @{ $_[0] } ) {
  0            
103 0           assert_isa( $message, 'IO::Iron::IronMQ::Message', 'Message is IO::Iron::IronMQ::Message.' );
104              
105             # FIXME Do this better!
106             }
107 0           return 1;
108             }
109             }
110             }, # one or more objects of class IO::Iron::IronMQ::Message.
111             }
112 0           );
113 0           my @messages = @{ $params{'messages'} };
  0            
114 0           $log->tracef( 'Entering post_messages(%s)', @messages );
115              
116 0           my $queue_name = $self->name();
117 0           my $connection = $self->{'connection'};
118 0           my @message_contents;
119 0           foreach my $message (@messages) {
120 0           my ( $msg_body, $msg_delay, $msg_push_headers, ) = ( $message->body(), $message->delay(), $message->push_headers(), );
121 0           my $message_content = {};
122 0           $message_content->{'body'} = $msg_body;
123 0 0         $message_content->{'delay'} = $msg_delay if defined $msg_delay;
124 0 0         $message_content->{'push_headers'} = $msg_push_headers if defined $msg_push_headers;
125              
126             # Gimmick to ensure the proper jsonization of numbers
127             # Otherwise numbers might end up as strings.
128 0           $message_content->{'delay'} += 0;
129              
130 0           CORE::push @message_contents, $message_content;
131             }
132 0           my %item_body = ( 'messages' => \@message_contents );
133              
134 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
135             IO::Iron::IronMQ::Api::IRONMQ_POST_MESSAGES(),
136             {
137             '{Queue Name}' => $queue_name,
138             'body' => \%item_body,
139             }
140             );
141 0           $self->{'last_http_status_code'} = $http_status_code;
142              
143 0           my ( @ids, $msg );
144 0           @ids = ( @{ $response_message->{'ids'} } ); # message ids.
  0            
145 0           $msg = $response_message->{'msg'}; # Should be "Messages put on queue."
146 0           $log->debugf( 'Pushed IronMQ Message(s) (queue name=%s; message id(s)=%s).', $self->{'name'}, ( join q{,}, @ids ) );
147 0 0         if (wantarray) {
148 0           $log->tracef( 'Exiting post_messages: %s', ( join q{:}, @ids ) );
149 0           return @ids;
150             }
151             else {
152 0 0         if ( scalar @messages == 1 ) {
153 0           $log->tracef( 'Exiting post_messages: %s', $ids[0] );
154 0           return $ids[0];
155             }
156             else {
157 0           $log->tracef( 'Exiting post_messages: %s', scalar @ids );
158 0           return scalar @ids;
159             }
160             }
161             }
162              
163             sub reserve_messages {
164 0     0 1   my $self = shift;
165 0           my %params = validate(
166             @_,
167             {
168             'n' => { type => SCALAR, optional => 1, }, # Number of messages to pull.
169             'timeout' => { type => SCALAR, optional => 1, }
170             , # When reading from queue, after timeout (in seconds), item will be placed back onto queue.
171             'wait' => { type => SCALAR, optional => 1, }, # Seconds to long poll the queue.
172             'delete' => { type => SCALAR, optional => 1, } # Do not put each message back on to the queue after reserving.
173             }
174             );
175 0           assert_positive( wantarray, 'Method reserve_messages() only works in LIST context!' );
176 0           $log->tracef( 'Entering reserve_messages(%s)', \%params );
177              
178 0           my $queue_name = $self->name();
179 0           my $connection = $self->{'connection'};
180 0           my %item_body;
181 0 0         $item_body{'n'} = $params{'n'} + 0 if $params{'n'};
182 0 0         $item_body{'timeout'} = $params{'timeout'} + 0 if $params{'timeout'};
183 0 0         $item_body{'wait'} = $params{'wait'} + 0 if $params{'wait'};
184 0 0         $item_body{'delete'} = $params{'delete'} if $params{'delete'};
185 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
186             IO::Iron::IronMQ::Api::IRONMQ_RESERVE_MESSAGES(),
187             {
188             '{Queue Name}' => $queue_name,
189             'body' => \%item_body,
190             }
191             );
192 0           $self->{'last_http_status_code'} = $http_status_code;
193              
194 0           my @pulled_messages;
195 0           my $messages = $response_message->{'messages'}; # messages.
196 0           foreach ( @{$messages} ) {
  0            
197 0           my $msg = $_;
198 0           $log->debugf( 'Pulled IronMQ Message (queue name=%s; message id=%s).', $self->{'name'}, $msg->{'id'} );
199             my $message = IO::Iron::IronMQ::Message->new(
200             'body' => $msg->{'body'},
201             'id' => $msg->{'id'},
202             'reserved_count' => $msg->{'reserved_count'},
203 0           'reservation_id' => $msg->{'reservation_id'},
204             );
205 0           CORE::push @pulled_messages, $message; # using CORE routine, not this class' method.
206             }
207 0           $log->debugf( 'Reserved %d IronMQ Messages (queue name=%s).', scalar @pulled_messages, $self->{'name'} );
208 0 0         $log->tracef( 'Exiting reserve_messages(): %s', @pulled_messages ? @pulled_messages : '[NONE]' );
209 0           return @pulled_messages;
210             }
211              
212             sub peek_messages {
213 0     0 1   my $self = shift;
214 0           my %params = validate(
215             @_,
216             {
217             'n' => { type => SCALAR, optional => 1, }, # Number of messages to read.
218             }
219             );
220 0           assert_positive( wantarray, 'Method peek_messages() only works in LIST context!' );
221 0           $log->tracef( 'Entering peek_messages(%s)', \%params );
222              
223 0           my $queue_name = $self->name();
224 0           my $connection = $self->{'connection'};
225 0           my %query_params;
226 0 0         $query_params{'{n}'} = $params{'n'} if $params{'n'};
227 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
228             IO::Iron::IronMQ::Api::IRONMQ_PEEK_MESSAGES(),
229             {
230             '{Queue Name}' => $queue_name,
231             %query_params
232             }
233             );
234 0           $self->{'last_http_status_code'} = $http_status_code;
235              
236 0           my @peeked_messages;
237 0           my $messages = $response_message->{'messages'}; # messages.
238 0           foreach ( @{$messages} ) {
  0            
239 0           my $msg = $_;
240 0           $log->debugf( 'peeked IronMQ Message (queue name=%s; message id=%s.', $self->{'name'}, $msg->{'id'} );
241             my $message = IO::Iron::IronMQ::Message->new(
242             'body' => $msg->{'body'},
243 0           'id' => $msg->{'id'},
244             );
245 0 0         $message->reserved_count( $msg->{'reserved_count'} ) if $msg->{'reserved_count'};
246              
247             # When peeking, timeout is not returned
248             # (it is irrelevent, because peeking does not reserve the message).
249 0           push @peeked_messages, $message;
250             }
251 0 0         $log->tracef( 'Exiting peek_messages(): %s', @peeked_messages ? @peeked_messages : '[NONE]' );
252 0           return @peeked_messages;
253             }
254              
255             sub delete_message {
256 0     0 1   my $self = shift;
257 0           my %params = validate(
258             @_,
259             {
260             'message' => {
261             type => OBJECT,
262             isa => 'IO::Iron::IronMQ::Message',
263             optional => 0,
264             },
265             'subscriber_name' => {
266             type => SCALAR,
267             optional => 1,
268             },
269             }
270             );
271 0           $log->tracef( 'Entering delete(%s)', \%params );
272              
273 0           my $queue_name = $self->name();
274 0           my $connection = $self->{'connection'};
275 0           my $message = $params{'message'};
276 0           my %item_body = ( 'reservation_id' => $message->reservation_id(), );
277 0 0         $item_body{'subscriber_name'} = $params{'subscriber_name'} if $params{'subscriber_name'};
278 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
279             IO::Iron::IronMQ::Api::IRONMQ_DELETE_MESSAGE(),
280             {
281             '{Queue Name}' => $queue_name,
282             '{Message ID}' => $message->id(),
283             'body' => \%item_body,
284             }
285             );
286 0           $self->{'last_http_status_code'} = $http_status_code;
287              
288 0           my $msg = $response_message->{'msg'}; # Should be 'Deleted'
289 0           $log->debugf( 'Deleted IronMQ Message (queue name=%s; message id=%s.', $queue_name, $params{'message'}->id() );
290 0           $log->tracef( 'Exiting delete_message(): %s', 'undef' );
291 0           return;
292             }
293              
294             sub delete_messages {
295 0     0 1   my $self = shift;
296              
297             # my %params = validate(
298             # @_, {
299             # 'ids' => {
300             # type => ARRAYREF,
301             # }, # one or more id strings (alphanum text string).
302             # }
303             # );
304 0           my @messages = validate_pos( @_, ( { type => OBJECT, isa => 'IO::Iron::IronMQ::Message', } ) x scalar @_ );
305              
306             # my @message_ids = @{$params{'ids'}};
307 0           assert_positive( scalar @messages, 'There is one or more messages.' );
308 0           $log->tracef( 'Entering delete_messages(%s)', \@messages );
309              
310 0           my $queue_name = $self->name();
311 0           my $connection = $self->{'connection'};
312 0           my %item_body = ( 'ids' => [], );
313 0           my @message_ids;
314 0           foreach my $msg (@messages) {
315 0           CORE::push @{ $item_body{'ids'} }, { 'id' => $msg->id(), 'reservation_id' => $msg->reservation_id(), };
  0            
316 0           CORE::push @message_ids, $msg->id();
317             }
318              
319 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
320             IO::Iron::IronMQ::Api::IRONMQ_DELETE_MESSAGES(),
321             {
322             '{Queue Name}' => $queue_name,
323             'body' => \%item_body,
324             }
325             );
326 0           $self->{'last_http_status_code'} = $http_status_code;
327              
328 0           my $msg = $response_message->{'msg'}; # Should be 'Deleted'
329 0           $log->debugf( 'Deleted IronMQ Message(s) (queue name=%s; message id(s)=%s.', $queue_name, ( join q{,}, @message_ids ) );
330 0           $log->tracef( 'Exiting delete_messages: %s', 'undef' );
331 0           return;
332             }
333              
334             sub touch_message {
335 0     0 1   my $self = shift;
336 0           my %params = validate(
337             @_,
338             {
339             'message' => {
340             type => OBJECT,
341             isa => 'IO::Iron::IronMQ::Message',
342             optional => 0,
343             },
344             'timeout' => {
345             type => SCALAR,
346             optional => 1,
347             },
348             }
349             );
350 0           $log->tracef( 'Entering touch_message(%s)', \%params );
351              
352 0           my $queue_name = $self->name();
353 0           my $connection = $self->{'connection'};
354 0           my $message = $params{'message'};
355 0           my %item_body = ( 'reservation_id' => $message->reservation_id(), );
356 0 0         $item_body{'timeout'} = $params{'timeout'} if $params{'timeout'};
357 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
358             IO::Iron::IronMQ::Api::IRONMQ_TOUCH_MESSAGE(),
359             {
360             '{Queue Name}' => $queue_name,
361             '{Message ID}' => $message->id(),
362             'body' => \%item_body,
363             }
364             );
365 0           $self->{'last_http_status_code'} = $http_status_code;
366 0           $message->reservation_id( $response_message->{'reservation_id'} );
367 0           $log->debugf( 'Touched IronMQ Message (queue name=%s; message id=%s.', $queue_name, $message->id() );
368              
369 0           $log->tracef( 'Exiting touch_message(): %s', 'undef' );
370 0           return;
371             }
372              
373             sub release_message {
374 0     0 1   my $self = shift;
375 0           my %params = validate(
376             @_,
377             {
378             'message' => {
379             type => OBJECT,
380             isa => 'IO::Iron::IronMQ::Message',
381             optional => 0,
382             },
383             'delay' => { type => SCALAR, optional => 1, }, # Delay before releasing.
384             }
385             );
386 0 0         assert_nonnegative_integer( $params{'delay'} ? $params{'delay'} : 0, 'Parameter delay is a non negative integer.' );
387 0           $log->tracef( 'Entering release_message(%s)', \%params );
388              
389 0           my $queue_name = $self->name();
390 0           my $connection = $self->{'connection'};
391 0           my $message = $params{'message'};
392 0           my %item_body = ( 'reservation_id' => $message->reservation_id(), );
393 0 0         $item_body{'delay'} = $params{'delay'} if $params{'delay'};
394              
395             # We do not give delay a default value (0); we let IronMQ use internal default values!
396 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
397             IO::Iron::IronMQ::Api::IRONMQ_RELEASE_MESSAGE(),
398             {
399             '{Queue Name}' => $queue_name,
400             '{Message ID}' => $message->id(),
401             'body' => \%item_body,
402             }
403             );
404 0           $self->{'last_http_status_code'} = $http_status_code;
405             $log->debugf( 'Released IronMQ Message(s) (queue name=%s; message id=%s; delay=%d)',
406 0 0         $queue_name, $params{'id'}, $params{'delay'} ? $params{'delay'} : 0 );
407              
408 0           $log->tracef( 'Exiting release_message: %s', 1 );
409 0           return 1;
410             }
411              
412             sub clear_messages {
413 0     0 1   my $self = shift;
414 0           my %params = validate(
415             @_,
416             {
417             # No parameters
418             }
419             );
420 0           $log->tracef('Entering clear_messages()');
421              
422 0           my $queue_name = $self->name();
423 0           my $connection = $self->{'connection'};
424 0           my %item_body;
425 0           my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
426             IO::Iron::IronMQ::Api::IRONMQ_CLEAR_MESSAGES(),
427             {
428             '{Queue Name}' => $queue_name,
429             'body' => \%item_body, # Empty body.
430             }
431             );
432 0           $self->{'last_http_status_code'} = $http_status_code;
433 0           my $msg = $response_message->{'msg'}; # Should be 'Cleared'
434 0           $log->debugf( 'Cleared IronMQ Message queue %s.', $queue_name );
435 0           $log->tracef( 'Exiting clear_messages: %s', 'undef' );
436 0           return;
437             }
438              
439             sub get_push_statuses {
440 0     0 1   my $self = shift;
441 0           my %params = validate(
442             @_,
443             {
444             'id' => { type => SCALAR, }, # message id.
445             }
446             );
447 0           assert_positive( wantarray == 0, 'Method get_push_statuses() only works in SCALAR context!' );
448 0           assert_nonblank( $params{'id'}, 'Parameter id is a non null string.' );
449 0           $log->tracef( 'Entering get_push_statuses(%s)', \%params );
450              
451 0           my $queue_name = $self->name();
452 0           my $connection = $self->{'connection'};
453             my ( $http_status_code, $response_message ) = $connection->perform_iron_action(
454             IO::Iron::IronMQ::Api::IRONMQ_GET_PUSH_STATUSES_FOR_A_MESSAGE(),
455             {
456             '{Queue Name}' => $queue_name,
457 0           '{Message ID}' => $params{'id'},
458             }
459             );
460 0           $self->{'last_http_status_code'} = $http_status_code;
461 0           my $info = $response_message;
462 0           $log->debugf( 'Returned push status for message %s.', $params{'id'} );
463              
464 0           $log->tracef( 'Exiting get_push_statuses: %s', $info );
465 0           return $info;
466             }
467              
468 0     0 1   sub ironmq_client { return $_[0]->_access_internal( 'ironmq_client', $_[1] ); }
469 0     0 1   sub name { return $_[0]->_access_internal( 'name', $_[1] ); }
470 0     0 1   sub connection { return $_[0]->_access_internal( 'connection', $_[1] ); }
471 0     0 1   sub last_http_status_code { return $_[0]->_access_internal( 'last_http_status_code', $_[1] ); }
472              
473             # TODO Move _access_internal() to IO::Iron::Common.
474              
475             sub _access_internal {
476 0     0     my ( $self, $var_name, $var_value ) = @_;
477 0           $log->tracef( '_access_internal(%s, %s)', $var_name, $var_value );
478 0 0         if ( defined $var_value ) {
479 0           $self->{$var_name} = $var_value;
480 0           return $self;
481             }
482             else {
483 0           return $self->{$var_name};
484             }
485             }
486              
487             1;
488              
489             __END__
490              
491             =pod
492              
493             =encoding UTF-8
494              
495             =head1 NAME
496              
497             IO::Iron::IronMQ::Queue - IronMQ (Online Message Queue) Client (Queue).
498              
499             =head1 VERSION
500              
501             version 0.14
502              
503             =head1 SYNOPSIS
504              
505             Please see IO::Iron::IronMQ::Queue for usage.
506              
507             =for stopwords IronMQ Params subitem io Mikko Koivunalho perldoc CPAN
508              
509             =for stopwords AnnoCPAN tradename licensable MERCHANTABILITY Iron.io
510              
511             =head1 REQUIREMENTS
512              
513             =head1 SUBROUTINES/METHODS
514              
515             =head2 new
516              
517             =over
518              
519             =item Creator function.
520              
521             =back
522              
523             =head2 size
524              
525             =over
526              
527             =item Params: [none]
528              
529             =item Return: queue size (integer).
530              
531             =back
532              
533             =head2 post_messages
534              
535             =over
536              
537             =item Params: one or more IO::Iron::IronMQ::Message objects.
538              
539             =item Return: message id(s) returned from IronMQ (if in list context),
540             or number of messages.
541              
542             =back
543              
544             =head2 reserve_messages
545              
546             =over
547              
548             =item Params: n (number of messages). default 1,
549             timeout (timeout for message processing in the user program, default: queue value),
550             wait (Time to long poll for messages, in seconds. Max is 30 seconds. Default 0.),
551             delete (If true, do not put each message back on to the queue after reserving. Default false)
552              
553             =item Return: list of IO::Iron::IronMQ::Message objects,
554             empty list if no messages available.
555              
556             =back
557              
558             =head2 peek_messages
559              
560             =over
561              
562             =item Params: n, number of messages to read
563              
564             =item Return: list of IO::Iron::IronMQ::Message objects,
565             empty list if no messages available.
566              
567             =back
568              
569             =head2 delete_message
570              
571             =over
572              
573             =item Params: one IO::Iron::IronMQ::Message object.
574              
575             =item Return: [NONE]
576              
577             =back
578              
579             =head2 delete_messages
580              
581             =over
582              
583             =item Params: one or more messages (IO::Iron::IronMQ::Message).
584              
585             =item Return: undefined.
586              
587             =back
588              
589             =head2 touch_message
590              
591             Changes the reservation_id of the parameter IO::Iron::IronMQ::Message object.
592              
593             =over
594              
595             =item Params: IO::Iron::IronMQ::Message object.
596              
597             =item Return: undefined
598              
599             =back
600              
601             =head2 release_message
602              
603             =over
604              
605             =item Params: IO::Iron::IronMQ::Message.
606              
607             =item Return: undefined
608              
609             =back
610              
611             =head2 clear_messages
612              
613             =over
614              
615             =item Params: [None].
616              
617             =item Return: undefined.
618              
619             =back
620              
621             =head2 get_push_statuses
622              
623             =over 8
624              
625             =item Params: id (message id).
626              
627             =item Return: a hash containing info, subitem 'subscribers' is an array.
628              
629             =back
630              
631             =head2 Getters/setters
632              
633             Set or get a property.
634             When setting, returns the reference to the object.
635              
636             =over 8
637              
638             =item name Message queue name.
639              
640             =item ironmq_client Reference to the IO::Iron::IronMQ::Client object which instantiated this object.
641              
642             =item connection Reference to the Connection object.
643              
644             =item last_http_status_code HTTP status code returned by the last call to Iron.io services.
645              
646             =back
647              
648             =head1 AUTHOR
649              
650             Mikko Koivunalho <mikko.koivunalho@iki.fi>
651              
652             =head1 BUGS
653              
654             Please report any bugs or feature requests to bug-io-iron@rt.cpan.org or through the web interface at:
655             http://rt.cpan.org/Public/Dist/Display.html?Name=IO-Iron
656              
657             =head1 COPYRIGHT AND LICENSE
658              
659             This software is copyright (c) 2023 by Mikko Koivunalho.
660              
661             This is free software; you can redistribute it and/or modify it under
662             the same terms as the Perl 5 programming language system itself.
663              
664             The full text of the license can be found in the
665             F<LICENSE> file included with this distribution.
666              
667             =cut