Categories
File::Serialize JSON Moo MooX::Options MooX::Options NewYorkCity Perl Zip Codes

Creating A Simple JSON NYC Zip Code Database File With Perl and MooX::Options

I found myself needing some New York City detailed Zip Code information for another script I was creating. The zip codes themselves are easy enough to find online. I needed to include more details about each zip code location.  I created a Perl script to merge two hard coded Perl data structures, which are printed out as a very basic JSON database file.

When creating Perl scripts with command line options, my go-to CPAN module is Getopt::Long. However for this script I will use MooX::Options, as I may extract some of the methods to be used in a future Moo module.

This will have three options, ‘create_zip_db’, ‘read_zip_db’  and ‘verbose’. The ‘doc’ attribute gives a brief description of each option. The ‘short’ attribute specifies any aliases that can be used for each option. The is ‘ro’ , means that the option value is immutable.

option create_zip_db => (
    is    => 'ro',
    short => 'new_zipdb|new_zip',
    doc   => q/Create a new NYC Zip, Borough, District, Town JSON file./,
);

option read_zip_db => (
    is    => 'ro',
    short => 'read_db',
    doc   => q/Read the NYC Zip file database./,
);

option verbose => ( is => 'ro', doc => 'Print details' );

There are three Moo attributes.  Some time in the future I can put these into a separate Moo module.

has db_dir => (
    is      => 'rw',
    isa     => Path,
    coerce  => 1,
    default => sub { "$Bin/../db" }
);

has zip_db_json_file => (
    is      => 'lazy',
    isa     => Path,
    builder => sub {
        $_[0]->db_dir->child("zip_db.json");
    }
);

has zip_hash => (
    is => 'lazy',
    isa =>
      sub { die "'zips_hash' must be a HASH" unless ( ref( $_[0] ) eq 'HASH' ) }
    ,
    builder => sub {
        deserialize_file $_[0]->zip_db_json_file;
    }
);

The first attribute ‘db_dir’ specifies the future location of the JSON file. It uses Types::Path Tiny   to enforce this directory path as a Path::Tiny  object. The ‘zip_db_json_file’ is also a Types::Path::Tiny Path.

The ‘zip_hash’ is the data structure what will store the NYC Zip code, borough, district, town information. The ‘isa’ for this attribute will ensure that it is a Perl hash.  The ‘deserialize_file’  function comes from the CPAN module, File::Serialize , which is very useful for dumping out Perl data structures to a JSON file, or in this case slurping in a JSON file to a Perl data structure. It also handles formats other than JSON.

Note that the ‘zip_hash’ attribute is ‘lazy’.  I’m not saying that zip codes are particularly adverse to work. This is just Moo’s way of saying, “please don’t make me do anything until I really have to”.  That way, resources are not nu-necessarily used creating a structure that isn’t being called for. 

# Main
sub run {
    my ($self) = @_;
    $self->create_new_zipdb_file if $self->create_zip_db;
    $self->read_and_dump_the_db  if $self->read_zip_db;
    say "All Done!"              if $self->verbose;
}
main->new_with_options()->run;

MooX::Options has it’s own particular style for creating a “Main” function that you won’t usually see in standard Perl scripts. It may be borrowed from brian d foy’s “Modulino” concept. Anyway, the script is invoked by:

main->new_with_options()->run;

The main ‘run’ function will call the methods as specified by the command line options.

To run this script from the command line.

# To get help
λ perl bin\create_zipdb.pl -h
USAGE: create_zipdb.pl [-h] [long options ...]

    --create_zip_db  Create a new NYC Zip, Borough, District, Town JSON
                     file.
    --read_zip_db    Read the NYC Zip file database.
    --verbose        Print details

    --usage          show a short help message
    -h               show a compact help message
    --help           show a long help message
    --man            show the manual

# Create a JSON file database
λ perl bin\create_zipdb.pl --create_zip_db --v

# Read the database and dump to the terminal
λ perl bin\create_zipdb.pl --read_zip_db

Most of the actual work of reading in the hard coded data structure and creating/reading the JSON database file is done here:

sub create_new_zipdb_file {
    my $self          = shift;
    my $zip_boro_dist = $self->get_raw_zip_data();
    serialize_file $self->zip_db_json_file => $zip_boro_dist;
    say "Created a new " . $self->zip_db_json_file if $self->verbose;
}

sub get_raw_zip_data {
    my $self         = shift;
    my %zips_to_city = %{ _get_zips_to_city() };
    my %bdz          = %{ _get_borough_district_zips() };
    my %zip_boro_dist;
    for my $borough ( sort keys %bdz ) {
        my %district = %{ $bdz{$borough} };
        for my $district_name ( sort keys %district ) {
            my @district_zips = @{ $district{$district_name} };
            for my $zip ( sort @district_zips ) {
                my ( $city, $county ) = split /,/, $zips_to_city{$zip};
                $county =
                    $borough eq 'Brooklyn' ? 'Kings'
                  : $borough eq 'Bronx'    ? 'Bronx'
                  : 'New York'
                  unless $county;

                $zip_boro_dist{$zip} = {
                    borough  => $borough,
                    district => $district_name,
                    city     => $city,
                    county   => $county,
                };
            }
        }
    }
    return \%zip_boro_dist;
}

sub read_and_dump_the_db {
    my $self         = shift;
    my $location_rec = $self->zip_hash;
    dump $location_rec;
}

Method ‘get_raw_zip_data’ grabs the two hard coded data structures and merges them. It makes a few little adjustments.  It is called by ‘create_new_zipdb_file which uses the ‘serialize_file’ function from  File::Serialize to dump the the Perl data structure in JSON format to the output JSON file.

Method ‘read_and_dump_the_db’ just reads this JSON file into the ‘zip_hash’ and dumps the contents to the console.

   "10022" : {
      "borough" : "Manhattan",
      "city" : "New York",
      "county" : "New York",
      "district" : "Gramercy Park and Murray Hill"
   },
   "10023" : {
      "borough" : "Manhattan",
      "city" : "New York",
      "county" : "New York",
      "district" : "Upper West Side"
   },
   ...
     "10314" : {
      "borough" : "Staten Island",
      "city" : "Staten Island",
      "county" : "Richmond",
      "district" : "Mid-Island"
   },
   "10451" : {
      "borough" : "Bronx",
      "city" : "Bronx",
      "county" : "Bronx",
      "district" : "High Bridge and Morrisania"
   },
   ...
  "11426" : {
      "borough" : "Queens",
      "city" : "Bellerose",
      "county" : "Queens",
      "district" : "Southeast Queens"
   },
   "11427" : {
      "borough" : "Queens",
      "city" : "Queens Village",
      "county" : "Queens",
      "district" : "Southeast Queens"
   },
   "11428" : {
      "borough" : "Queens",
      "city" : "Queens Village",
      "county" : "Queens",
      "district" : "Southeast Queens"
   },

The complete script can be found here create_zipdb.pl

Categories
Moo Perl

Using Moo (continued)

Continuing from my last post  Using Moo,  I will add a little more functionality to my File::Info class.  At first I was going to subclass it, but then I decided to use  Moo::Role instead. Moo::Role which is based on Role::Tiny. Roles are a really convenient way of adding extra functionality to your class.

package File::Info;
use Scalar::Util qw/blessed looks_like_number/;
use Moo;
use v5.16;
#-------------------------------------------------------------------------------
#  Base class to provide some useful information about a given file.
#-------------------------------------------------------------------------------
use Path::Tiny;
use Carp;
use File::stat;
use namespace::clean;    # clean imported functions from your namespace.
#-------------------------------------------------------------------------------
#  Attributes
#-------------------------------------------------------------------------------
has file => (
    is  => 'ro',
    isa => sub {
        #--- Because I like Path::Tiny
        Carp::croak(
            qq{'in_file' needs to be an existing file and a Path::Tiny Object!})
          unless ( ref $_[0] eq 'Path::Tiny' and $_[0]->exists );
    },
    coerce => sub {
        return $_[0]
          if ( ref $_[0] eq 'Path::Tiny' );
        return path( $_[0] );
    },
    required => 1
);

has file_stat => (
    is  => 'rwp',
    isa => sub {
        Carp::croak(qq{$_[0] is not a File::stat::stat!})
          unless ( $_[0] and ( ref $_[0] ) );
    },
);

has size_bytes => (
    is  => 'rwp',
    isa => sub {
        Carp::croak(qq{'bytes' value $_[0] is not numeric!})
          unless ( looks_like_number( $_[0] ) );
    },
    lazy    => 1,
    builder => sub {
        $_[0]->file_stat->size;
    },
);

has mod_time_seconds => (
    is  => 'ro',
    isa => sub {
        Carp::croak(qq{$_[0] is not a number!})
          unless ( looks_like_number( $_[0] ) );
    },
    lazy    => 1,
    builder => sub {
        $_[0]->file_stat->mtime;
    },
);

#--- Moo roles to print bytes/seconds in a more readable form
with qw{MakeBytesPretty MakeSecondsPretty};

#-------------------------------------------------------------------------------
#  Builders and Triggers
#-------------------------------------------------------------------------------
sub BUILD {
    my ($self) = @_;
    $self->_set_file_stat( File::stat::stat( $self->file ) )
      or Carp::croak( qq{File stat failed to get the file stat for, }
          . $self->file
          . qq{!: $!} );
}
1;

And the Roles…

package MakeBytesPretty;
use Moo::Role; #--- Makes this a role, and not a class. 
use v5.16;
use namespace::clean;

requires 'size_bytes'; #-- Make sure that this attribute exists somewhare. 
#-------------------------------------------------------------------------------
#  Moo role to prints bytes a little more pretty.
#-------------------------------------------------------------------------------
sub make_file_size_pretty {
    my ($self) = @_;
    my $bytes = $self->size_bytes;

    return ( not $bytes )
      ? 0
      : ( $bytes <= 1024 ) ? sprintf( "%0.02f", $bytes )
      . " Bytes"

      : ( $bytes <= 1048576 ) ? sprintf( "%0.02f", ( $bytes / 1024 ) ) . " KB"

      : ( $bytes <= 1073741824 )
      ? sprintf( "%0.02f", ( $bytes / 1048576 ) ) . " MB"

      : ( $bytes <= 1099511627776 )
      ? sprintf( "%0.02f", ( $bytes / 1073741824 ) ) . " GB"

      : ( $bytes <= 1125899906842624 )
      ? sprintf( "%0.02f", ( $bytes / 1099511627776 ) ) . " TB"

      : ( $bytes <= 1152921504606846976 )

      ? sprintf( "%0.02f", ( $bytes / 1125899906842624 ) ) . " PB"    # Petabyte

      : sprintf( "%0.02f", ( $bytes / 1152921504606846976 ) ) . " EB" # Exabyte
}
#-------------------------------------------------------------------------------
#  End
#-------------------------------------------------------------------------------
1;
package MakeSecondsPretty;
use Moo::Role;
use v5.16;
use namespace::clean;

requires 'mod_time_seconds';
#-------------------------------------------------------------------------------
#  Moo role to print time 'Seconds' in a more pretty way.
#-------------------------------------------------------------------------------
sub make_mod_time_pretty {
    my ( $self ) = @_;
    my $seconds = $self->mod_time_seconds;
    return ( not $seconds )
      ? 0
      : ( $seconds <= 60 ) ? sprintf( "%0.02f", $seconds )
      . " Seconds"

      : ( $seconds <= 3600 )
      ? sprintf( "%0.02f", ( $seconds / 60 ) ) . " Minutes"

      : ( $seconds <= 3600 )
      ? sprintf( "%0.02f", ( $seconds / 3600 ) ) . " Hours"

      : ( $seconds <= 86400 )
      ? sprintf( "%0.02f", ( $seconds / 86400 ) ) . " Days"

      : sprintf( "%0.02f", ( $seconds / 604800 ) ) . " Weeks";
}
#-------------------------------------------------------------------------------
#  End
#-------------------------------------------------------------------------------
1;

As before, it’s a good idea to write a test script to make sure all is ok. This test script isn’t overly thorough, but it’s good enough for this demonstration.

use Test::Modern;    #-- I wonder will it be modern in 10 years time??
use FindBin qw($Bin);
use lib qq{$Bin/../lib};    #-- Where File::Info is located.
use File::Info;
use Scalar::Util qw/blessed looks_like_number/;

my $fi = object_ok(
    sub { File::Info->new( file => qq{IMAG0029.jpg} ) },
    '$fi',
    isa => [qw(Moo::Object )],
    can => [
        qw(  file file_stat size_bytes mod_time_seconds make_file_size_pretty
          make_mod_time_pretty )
    ],
    clean => 1,
    more  => sub {
        my $file_info_obj = shift;
        isa_ok( $file_info_obj->file, 'Path::Tiny',
            q{File::Info 'file' is a Path::Tiny object.} );
        is( $file_info_obj->file->basename,
            qq{IMAG0029.jpg},
            q{File::Info 'file' has the correct file basename.} );
        isa_ok( $file_info_obj->file_stat, q{File::stat},
            qq{File::Info 'file_stat' is a 'File::stat'.} );
        like( $file_info_obj->file_stat->size,
            qr/^[0-9]+$/,
            q{File::Info 'file_stat->size' returns a numeric file size.} );
        like( $file_info_obj->size_bytes,
            qr/^[0-9]+$/, qq{File::Info 'size_bytes' is numeric.} );
        like(
            $file_info_obj->make_file_size_pretty,
            qr/^d+?.d+ (bytes|KB|MB|GB|TB|PB|EB)$/, qq{
            File::Info bytes 'make_file_size_pretty' 
            prints a file size followed by the size Unit.}
        );
        like( $file_info_obj->mod_time_seconds, qr/^d+$/,
            qq{File::Info 'mod_time_seconds' prints the file modification time.}
        );
        like(
            $file_info_obj->make_mod_time_pretty,
            qr/^d+?.d+ (Seconds|Minutes|Hours|Days|Weeks)$/, qq{
            File::Info bytes 'make_mod_timepretty' 
            prints the file mod time followed by the time Unit.}
        );
    },
);

done_testing();

Run the test script …

Moo > prove -v t/test_file.t 
t/test_file.t .. 
    # Subtest: $fi ok
    ok 1 - instantiate $fi
    ok 2 - $fi is blessed
    ok 3 - '$fi' isa 'Moo::Object'
    ok 4 - File::Info->can(...)
    ok 5 - File::Info contains no imported functions
        # Subtest: more tests for $fi
        ok 1 - 'File::Info 'file' is a Path::Tiny object.' isa 'Path::Tiny'
        ok 2 - File::Info 'file' has the correct file basename.
        ok 3 - 'File::Info 'file_stat' is a 'File::stat'.' isa 'File::stat'
        ok 4 - File::Info 'file_stat->size' returns a numeric file size.
        ok 5 - File::Info 'size_bytes' is numeric.
        ok 6 - 
        #             File::Info bytes 'make_file_size_pretty' 
        #             prints a file size followed by the size Unit.
        ok 7 - File::Info 'mod_time_seconds' prints the file modification time.
        ok 8 - 
        #             File::Info bytes 'make_mod_timepretty' 
        #             prints the file mod time followed by the time Unit.
        ok 9 - no exception thrown by additional tests
        1..9
    ok 6 - more tests for $fi
    1..6
ok 1 - $fi ok
ok 2 - no (unexpected) warnings (via done_testing)
1..2
ok
All tests successful.
Files=1, Tests=2,  1 wallclock secs ( 0.03 usr  0.00 sys +  0.08 cusr  0.02 csys =  0.13 CPU)
Result: PASS
[17:10 - 1.06]

Ok. Looks fine for now.

However, I would like to change the module around a little. I want to be able to print the size and age of a given file in a somewhat readable fashion.

I removed the ‘mod_time_seconds’ attribute and replaced it with ‘mod_time_moment’ . This attribute will store the file’s modification time as a Time::Moment object. This will add a little more versatility to the module. Time::Moment is a handy and fast time manipulation module that’s relatively new on the block. Other modules that I could have used would be, Time::Piece which I use a lot also, and  the venerable DateTime module, which is excellent for serious date calculations involving time zones, durations etc.

I also added two new methods to get the time since the last file modification. One will return the elapsed time in seconds and the other will return the elapsed time in a more readable form.

Here is the updated File::Info module…

package File::Info;
use Scalar::Util qw/blessed looks_like_number/;
use Moo;
use v5.16;

#-------------------------------------------------------------------------------
#  Base class to provide some useful information about a given file.
#-------------------------------------------------------------------------------
use Path::Tiny;
use Carp;
use File::stat;
use Time::Moment;
use Time::Piece;
use namespace::clean;    # clean imported functions from your namespace.

#-------------------------------------------------------------------------------
#  Attributes
#-------------------------------------------------------------------------------
has file => (
    is  => 'ro',
    isa => sub {

        #--- Because I like Path::Tiny
        Carp::croak(
            qq{'in_file' needs to be an existing file and a Path::Tiny Object!})
          unless ( ref $_[0] eq 'Path::Tiny' and $_[0]->exists );
    },
    coerce => sub {
        return $_[0]
          if ( ref $_[0] eq 'Path::Tiny' );
        return path( $_[1] );
    },
    required => 1
);

has file_stat => (
    is  => 'rwp',
    isa => sub {
        Carp::croak(qq{$_[0] is not a File::stat::stat!})
          unless ( $_[0] and ( ref $_[0] ) );
    },
);

has size_bytes => (
    is  => 'rwp',
    isa => sub {
        Carp::croak(qq{'bytes' value $_[0] is not numeric!})
          unless ( looks_like_number( $_[0] ) );
    },
    lazy    => 1,
    builder => sub {
        $_[0]->file_stat->size;
    },
);

#--- The file's last modification time
#    as a Time::Moment Object
has mod_time_moment => (
    is  => 'ro',
    isa => sub {
        Carp::croak(
            qq{Bad 'mod_time_moment' because $_[0] is not a Time::Moment!})
          unless ( ref $_[0] eq q{Time::Moment} );
    },
    lazy    => 1,
    builder => sub {
        #--- Time::Piece To keep it in the local time zone
   Time::Moment->from_object( scalar Time::Piece::localtime($_[0]->file_stat->mtime ) 
        );
    },
);

#--- Moo roles to print bytes/seconds in a more readable form
with qw{MakeBytesPretty MakeSecondsPretty};

#-------------------------------------------------------------------------------
#  Builders and Triggers
#-------------------------------------------------------------------------------
sub BUILD {
    my ($self) = @_;
    $self->_set_file_stat( File::stat::stat( $self->file ) )
      or Carp::croak( qq{File stat failed to get the file stat for, }
          . $self->file
          . qq{!: $!} );
}

#-------------------------------------------------------------------------------
#  Methods
#-------------------------------------------------------------------------------
#--- The file age since its last modification expressed in seconds.
sub seconds_since_mod {
    my ($self) = @_;
    return ( Time::Moment->now()->epoch - $self->mod_time_moment->epoch );
}

sub time_since_mod_pretty {
    my ($self) = @_;
    return $self->make_seconds_pretty( $self->seconds_since_mod );
}

#-------------------------------------------------------------------------------
1;

I also made some changes to the role MakeSecondsPretty.pm. I removed the ‘requires’  statement and renamed the only method to ‘make_seconds_pretty’ . I also changed this method slightly.

sub make_seconds_pretty {
    my ( $self , $seconds ) = @_;
    return ( not $seconds )
      ? 0
      : ( $seconds <= 60 ) ? sprintf( "%0.02f", $seconds )
      . " Seconds"

      : ( $seconds <= 3600 )
      ? sprintf( "%0.02f", ( $seconds / 60 ) ) . " Minutes"

      : ( $seconds <= 86400 )
      ? sprintf( "%0.02f", ( $seconds / 3600 ) ) . " Hours"

      : ( $seconds <= 604800 )
      ? sprintf( "%0.02f", ( $seconds / 86400 ) ) . " Days"

      : sprintf( "%0.02f", ( $seconds / 604800 ) ) . " Weeks";
}

The testing script also has to be changed to reflect changes in the module.

my $fi = object_ok(
    sub { File::Info->new( file => qq{IMAG0029.jpg} ) },
    '$fi',
    isa => [qw(Moo::Object )],
    can => [
        qw(  file file_stat size_bytes mod_time_moment make_seconds_pretty
          seconds_since_mod time_since_mod_pretty )
    ],
    clean => 1,
    more  => sub {
        my $file_info_obj = shift;
        isa_ok( $file_info_obj->file, 'Path::Tiny',
            q{File::Info 'file' is a Path::Tiny object.} );
        is( $file_info_obj->file->basename,
            qq{IMAG0029.jpg},
            q{File::Info 'file' has the correct file basename.} );
        isa_ok( $file_info_obj->file_stat, q{File::stat},
            qq{File::Info 'file_stat' is a 'File::stat'.} );
        like( $file_info_obj->file_stat->size,
            qr/^[0-9]+$/,
            q{File::Info 'file_stat->size' returns a numeric file size.} );
        like( $file_info_obj->size_bytes,
            qr/^[0-9]+$/, qq{File::Info 'size_bytes' is numeric.} );
        like(
            $file_info_obj->make_file_size_pretty,
            qr/^d+?.d+ (bytes|KB|MB|GB|TB|PB|EB)$/, qq{
            File::Info bytes 'make_file_size_pretty' 
            prints a file size followed by the size Unit.}
        );
        isa_ok( $file_info_obj->mod_time_moment, q{Time::Moment},
            qq{File::Info 'mod_time_moment' returns a Time::Moment object.}
        );
        like( $file_info_obj->seconds_since_mod, qr/^d+$/,
            qq{File::Info 'seconds_since_mod' returns a numeric.}
        );
        like(
           $file_info_obj->time_since_mod_pretty(),
            qr/^d+?.d+ (Seconds|Minutes|Hours|Days|Weeks)$/, qq{
            File::Info 'time_since_mod_pretty' uses 'make_seconds_pretty' 
            to print the 'seconds_since_mod' as time followed by the time Unit.}
        );
    },
);

Run the test again…

Moo > prove -v t/test_file.t 
t/test_file.t .. 
    # Subtest: $fi ok
    ok 1 - instantiate $fi
    ok 2 - $fi is blessed
    ok 3 - '$fi' isa 'Moo::Object'
    ok 4 - File::Info->can(...)
    ok 5 - File::Info contains no imported functions
        # Subtest: more tests for $fi
        ok 1 - 'File::Info 'file' is a Path::Tiny object.' isa 'Path::Tiny'
        ok 2 - File::Info 'file' has the correct file basename.
        ok 3 - 'File::Info 'file_stat' is a 'File::stat'.' isa 'File::stat'
        ok 4 - File::Info 'file_stat->size' returns a numeric file size.
        ok 5 - File::Info 'size_bytes' is numeric.
        ok 6 - 
        #             File::Info bytes 'make_file_size_pretty' 
        #             prints a file size followed by the size Unit.
        ok 7 - 'File::Info 'mod_time_moment' returns a Time::Moment object.' isa 'Time::Moment'
        ok 8 - File::Info 'seconds_since_mod' returns a numeric.
        ok 9 - 
        #             File::Info 'time_since_mod_pretty' uses 'make_seconds_pretty' 
        #             to print the 'seconds_since_mod' as time followed by the time Unit.
        ok 10 - no exception thrown by additional tests
        1..10
    ok 6 - more tests for $fi
    1..6
ok 1 - $fi ok
ok 2 - no (unexpected) warnings (via done_testing)
1..2
ok
All tests successful.
Files=1, Tests=2,  1 wallclock secs ( 0.03 usr  0.01 sys +  0.10 cusr  0.02 csys =  0.16 CPU)
Result: PASS
[22:28 - 0.34]

Ok!

For my next post. I will use my module in a script with the help of  MooX::Options.

 

Categories
Moo Perl

Using Moo

Moo simplifies OO in Perl.
Here is a Moo class that will provide some information about a file. I will use D.A Golden’s Path::Tiny, because I use this module in almost every script that I write.

package File::Info;
use Moo;
use v5.16;
#-------------------------------------------------------------------------------
#  Base class to provide information about a given file.
#-------------------------------------------------------------------------------
use Path::Tiny;
use File::stat;
use Carp;
use namespace::clean; #--- Dont export unintentionally
#-------------------------------------------------------------------------------
#  Attributes
#-------------------------------------------------------------------------------
has file => (
    is  => 'ro',
    isa => sub {
        Carp::croak(qq{'in_file' needs to be an existing file and a Path::Tiny Object!})
          unless (ref $_[0] eq 'Path::Tiny' and $_[0]->exists);
    },
    coerce => sub {
        return $_[0]
          if ( ref $_[0] eq 'Path::Tiny' );
        return path( $_[0] );
    },
    required => 1
);

has file_stat => (
    is  => 'rwp',
    isa => sub {
        Carp::croak(qq{$_[0] is not a File::stat::stat!})
          unless ( $_[0] and ( ref $_[0] ) );
    },
);

#-------------------------------------------------------------------------------
#  Builders and Triggers
#-------------------------------------------------------------------------------
sub BUILD {
    my ($self) = @_;
    $self->_set_file_stat( File::stat::stat( $self->file ) )
      or Carp::croak( qq{File stat failed to get the file stat for, }
          . $self->file
          . qq{!: $!} );
}
1;

It would be a good idea to test this module before we proceed further.
I found a this Test::Modern  module by Toby Inkster(who always does good stuff). It’s a testing module that includes many other testing modules(Also see Test::Most).

Here’s a test to make sure that all’s ok with my File::Info module.

use Test::Modern;    #-- I wonder will it be modern in 10 years time??
use FindBin qw($Bin);
use lib qq{$Bin/../lib};    #-- Where File::Info is located.
use File::Info;

my $fi = object_ok(
    sub { File::Info->new( file => qq{IMAG0029.jpg} ) },
    '$fi',
    isa   => [qw(Moo::Object )],
    can   => [qw(  file file_stat )],
    clean => 1,
    more  => sub {
        my $file_info_obj = shift;

        isa_ok( $file_info_obj->file, 'Path::Tiny',
            q{File::Info 'file' is a Path::Tiny object.} );

        is( $file_info_obj->file->basename,
            qq{IMAG0029.jpg},
            q{File::Info 'file' has the correct file basename.} );

        isa_ok( $file_info_obj->file_stat, q{File::stat},
            qq{File::Info 'file_stat' is a 'File::stat'.} );

        like( $file_info_obj->file_stat->size,
            qr/^[0-9]+$/,
            q{File::Info 'file_stat->size' returns a numeric file size.} );
    },
);

done_testing();

I ran this test script and got the following results ( on the first attempt of course :)).

Moo > prove -v t/test_file.t 
t/test_file.t .. 
    # Subtest: $fi ok
    ok 1 - instantiate $fi
    ok 2 - $fi is blessed
    ok 3 - '$fi' isa 'Moo::Object'
    ok 4 - File::Info->can(...)
    ok 5 - File::Info contains no imported functions
        # Subtest: more tests for $fi
        ok 1 - 'File::Info 'file' is a Path::Tiny object.' isa 'Path::Tiny'
        ok 2 - File::Info 'file' has the correct file basename.
        ok 3 - 'File::Info 'file_stat' is a 'File::stat'.' isa 'File::stat'
        ok 4 - File::Info 'file_stat->size' returns a numeric file size.
        ok 5 - no exception thrown by additional tests
        1..5
    ok 6 - more tests for $fi
    1..6
ok 1 - $fi ok
ok 2 - no (unexpected) warnings (via done_testing)
1..2
ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.09 cusr  0.00 csys =  0.13 CPU)
Result: PASS
[21:42 - 0.37]
[austin@the-general-II 64] Moo >

 

Test::Modern seems pretty cool.  My File::Info module seems to be ok so far.  Now to add some more useful functionality.