NAME

Aion::Annotation::Reader - reads annotations, comments and module update times

VERSION

0.1.0

SYNOPSIS

File etc/annotation/todo.ann:

For::Test#abc,5=add1
For::Test#xyz,11=add2

File etc/annotation/remarks.ini:

For::Test#,4=The package for testing
For::Test#abc,9=Is property\n  readonly

File var/cache/modules.mtime.ini:

For::Test=2025-01-02 03:04:05



use Aion::Annotation::Reader;
use Time::Local qw/timelocal/;

my $reader = Aion::Annotation::Reader->new('todo');

my @ann; push @ann, $_ while <$reader>;

my $ann = [
	{pkg => 'For::Test', name => 'abc', line => '5',  annotation => 'add1'},
	{pkg => 'For::Test', name => 'xyz', line => '11', annotation => 'add2'},
];

\@ann # --> $ann

my $reader_remarks = Aion::Annotation::Reader->new(Aion::Annotation::Reader::READ_REMARKS);

my $remarks = [
	{pkg => 'For::Test', name => '',    line => '4', remark => 'The package for testing'},
	{pkg => 'For::Test', name => 'abc', line => '9', remark => "Is property\n  readonly"},
];

\@{$reader_remarks} # --> $remarks

my $reader_mtime = Aion::Annotation::Reader->new(Aion::Annotation::Reader::READ_MTIME);

# Время хранится как unixtime, поэтому ожидаемое значение не зависит от часового пояса
my $mtime = [{pkg => 'For::Test', mtime => timelocal(5, 4, 3, 2, 0, 2025)}];

\@{$reader_mtime} # --> $mtime

DESCRIPTION

Aion::Annotation::Reader reads the files that Aion::Annotation creates and returns their contents line by line as hashes.

Three types of data are available:

1. Annotations from files name.ann (the name is passed to the constructor).
2. Comments from remarks.ini.
3. Module update times from modules.mtime.ini.

The reader is designed as an iterator on top of a file: the constructor opens the file, and the next method returns the next recognized element. The object overloads a number of operations so that the reader can be used as a file or function directly in expressions.

Recognized strings are represented by hashes:

1. Annotation (file etc/annotation/*.ann) – keys pkg, name, line, annotation.
2. Comment (file etc/annotation/remarks.ini) – keys pkg, name, line, remark.
3. Time (file var/cache/modules.mtime.ini) – keys pkg, mtime.

Directories are configured by the environment variables AION_ANNOTATION_INI (default etc/annotation) and AION_ANNOTATION_CACHE (default var/cache).

OVERLOAD

Aion::Annotation::Reader overloads the operations:

<>

The reader's challenge.

my $reader = Aion::Annotation::Reader->new('todo');

my @ann; push @ann, $_ while <$reader>;
0+@ann  # -> 2

@{}

List of all elements.

my $reader = Aion::Annotation::Reader->new('todo');

my $ann = [
	{pkg => 'For::Test', name => 'abc', line => '5',  annotation => 'add1'},
	{pkg => 'For::Test', name => 'xyz', line => '11', annotation => 'add2'},
];

\@{$reader} # --> $ann

&{}

Call as functions.

Each call returns the following element or undef at the end:

my $reader = Aion::Annotation::Reader->new('todo');

&$reader # --> {pkg => 'For::Test', name => 'abc', line => '5', annotation => 'add1'}
&$reader # --> {pkg => 'For::Test', name => 'xyz', line => '11', annotation => 'add2'}
&$reader # -> undef

*{}

File descriptor.

my $reader = Aion::Annotation::Reader->new('todo');
my $fh = *$reader;
readline $fh  # ~> ^For::Test#abc,5=add1$

-X

File operations.

Operations like -e, -s, -f are performed on the reader file. The result of the operation is cached:

my $reader = Aion::Annotation::Reader->new('todo');
-e $reader # -> 1
-s $reader # -> 43
-f $reader # -> 1

""

String representation.

my $reader = Aion::Annotation::Reader->new('todo');
"$reader" # => Aion::Annotation::Reader<ann,etc/annotation/todo.ann>

CONSTANTS

READ_REMARKS

Special name with which the comment file is read:

Aion::Annotation::Reader::READ_REMARKS  # -> "-remarks"

READ_MTIME

Special name with which the update times file is read:

Aion::Annotation::Reader::READ_MTIME  # -> "-mtime"

MODULES_MTIME_FILE

Update times file name:

Aion::Annotation::Reader::MODULES_MTIME_FILE  # -> "modules.mtime.ini"

REMARKS_FILE

Comment file name:

Aion::Annotation::Reader::REMARKS_FILE  # -> "remarks.ini"

LINE_REGEX

Annotation or comment string regular expression:

my $line = 'For::Test#abc,5=add1';
my ($pkg, $name, $lineno, $text) = $line =~ Aion::Annotation::Reader::LINE_REGEX;
"$pkg|$name|$lineno|$text"  # -> "For::Test|abc|5|add1"

MTIME_REGEX

Update time string regular expression:

my $line = 'For::Test=2025-01-02 03:04:05';
$line =~ Aion::Annotation::Reader::MTIME_REGEX;
"$+{module}|$+{year}-$+{mon}-$+{mday} $+{hour}:$+{min}:$+{sec}"  # -> "For::Test|2025-01-02 03:04:05"

SUBROUTINES

new ($cls, $ann)

Constructor. $ann is passed the name of the annotation (then reads etc/annotation/$ann.ann), or the constants READ_REMARKS or READ_MTIME. The file opens immediately in the constructor:

my $reader = Aion::Annotation::Reader->new('todo');
ref $reader       # -> "Aion::Annotation::Reader"
$reader->{type}   # -> "ann"
$reader->path     # -> "etc/annotation/todo.ann"

The call is also valid on an instance:

my $reader = Aion::Annotation::Reader->new('todo');
$reader->new('todo')->path  # -> "etc/annotation/todo.ann"

If the file does not exist, an exception is thrown:

eval { Aion::Annotation::Reader->new('no_such_annotation') };
$@ # ~> ^etc/annotation/no_such_annotation\.ann: 

path ()

Path to open file:

my $reader = Aion::Annotation::Reader->new(Aion::Annotation::Reader::READ_REMARKS);
$reader->path  # -> "etc/annotation/remarks.ini"

next ()

Returns the next element recognized. In a scalar context - one element or undef, in a list context - all remaining elements:

my $reader = Aion::Annotation::Reader->new('todo');

scalar $reader->next # --> {pkg => 'For::Test', name => 'abc', line => '5', annotation => 'add1'}
scalar $reader->next # --> {pkg => 'For::Test', name => 'xyz', line => '11', annotation => 'add2'}
scalar $reader->next # -> undef

DESTROY ()

Closes a file when an object is destroyed. Usually called automatically:

my $f;
{
	my $reader = Aion::Annotation::Reader->new('todo');
	$f = $reader->{f};
	defined fileno $f # -> 1
} # $reader->DESTROY closes the file

fileno $f  # -> undef

detect_annotation ($self, $line)

Recognizes the annotation string. Returns a hash with the keys pkg, name, line, annotation:

my $test = {
	pkg => 'For::Test', name => 'xyz', line => '11', annotation => 'add2'
};

my $reader = Aion::Annotation::Reader->new('todo');
Aion::Annotation::Reader::detect_annotation($reader, "For::Test#xyz,11=add2\n") # --> $test

detect_remark ($self, $line)

Recognizes a comment line. Escaped sequences \n are turned into newlines, and \<character> into the character itself:

my $test = {
	pkg => 'For::Test', name => 'abc', line => '9', remark => "Is property\n  readonly"
};

my $reader = Aion::Annotation::Reader->new('todo');
Aion::Annotation::Reader::detect_remark($reader, "For::Test#abc,9=Is property\\n  readonly\n") # --> $test

detect_mtime ($self, $line)

Recognizes the update time string. The mtime field is unixtime (in the local time zone):

use Time::Local qw/timelocal/;

my $test = {
	pkg => 'For::Test', mtime => timelocal(5, 4, 3, 2, 0, 2025)
};

my $reader = Aion::Annotation::Reader->new('todo');
Aion::Annotation::Reader::detect_mtime($reader, "For::Test=2025-01-02 03:04:05\n") # --> $test

CORRUPT LINES

If the string does not match the format, a warn is issued, but the element is still returned - with undefined fields. This allows you not to interrupt reading on a damaged file:

use Aion::Annotation::Reader;

my @warn;
local $SIG{__WARN__} = sub { push @warn, @_ };

my $reader = Aion::Annotation::Reader->new('todo');

Aion::Annotation::Reader::detect_annotation($reader, "corrupt line\n") # --> {pkg => undef, name => undef, line => undef, annotation => undef}
Aion::Annotation::Reader::detect_remark($reader, "corrupt line\n") # --> {pkg => undef, name => undef, line => undef, remark => ''}
Aion::Annotation::Reader::detect_mtime($reader, "corrupt line\n") # --> {pkg => undef, mtime => undef}

0+@warn  # -> 3
$warn[0] # ~> ^etc/annotation/todo\.ann corrupt on line 

AUTHOR

Yaroslav O. Kosmina mailto:dart@cpan.org

LICENSE

⚖ GPLv3

COPYRIGHT

The Aion::Annotation::Reader module is copyright © 2026 Yaroslav O. Kosmina. Rusland. All rights reserved.