Blame tools/lib/ModuleGenerator.pm

Packit 4ed398
package    # hide from PAUSE
Packit 4ed398
    ModuleGenerator;
Packit 4ed398
Packit 4ed398
use v5.22;
Packit 4ed398
Packit 4ed398
use strict;
Packit 4ed398
use warnings;
Packit 4ed398
use feature qw( postderef signatures );
Packit 4ed398
use namespace::autoclean;
Packit 4ed398
use autodie;
Packit 4ed398
Packit 4ed398
use Data::Dumper::Concise qw( Dumper );
Packit 4ed398
use JSON::MaybeXS qw( decode_json );
Packit 4ed398
use List::AllUtils qw( max uniq );
Packit 4ed398
use Locale::Codes::Language
Packit 4ed398
    qw( language_code2code LOCALE_LANG_ALPHA_2 LOCALE_LANG_ALPHA_3 );
Packit 4ed398
use ModuleGenerator::Locale;
Packit 4ed398
use Parse::PMFile;
Packit 4ed398
use Path::Iterator::Rule;
Packit 4ed398
use Path::Tiny qw( path );
Packit 4ed398
use Scalar::Util qw( reftype );
Packit 4ed398
use Specio::Declare;
Packit 4ed398
use Specio::Library::Builtins;
Packit 4ed398
use Specio::Library::Path::Tiny;
Packit 4ed398
use Specio::Subs qw( Specio::Library::Builtins );
Packit 4ed398
use Text::Template;
Packit 4ed398
Packit 4ed398
use Moose;
Packit 4ed398
Packit 4ed398
## no critic (TestingAndDebugging::ProhibitNoWarnings)
Packit 4ed398
no warnings qw( experimental::postderef experimental::signatures );
Packit 4ed398
## use critic
Packit 4ed398
Packit 4ed398
with 'MooseX::Getopt::Dashes';
Packit 4ed398
Packit 4ed398
our $VERSION = '0.10';
Packit 4ed398
Packit 4ed398
has _only_locales => (
Packit 4ed398
    traits   => ['Array'],
Packit 4ed398
    is       => 'ro',
Packit 4ed398
    isa      => t( 'ArrayRef', of => t('Str') ),
Packit 4ed398
    init_arg => 'locales',
Packit 4ed398
    default => sub { [] },
Packit 4ed398
    handles => {
Packit 4ed398
        _has_only_locales => 'count',
Packit 4ed398
    },
Packit 4ed398
    documentation => 'If specified, only these locales will be built.',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
has _autogen_warning => (
Packit 4ed398
    is      => 'ro',
Packit 4ed398
    isa     => t('Str'),
Packit 4ed398
    lazy    => 1,
Packit 4ed398
    builder => '_build_autogen_warning',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
has _generator_script => (
Packit 4ed398
    is      => 'ro',
Packit 4ed398
    isa     => t('File'),
Packit 4ed398
    lazy    => 1,
Packit 4ed398
    builder => '_build_generator_script',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
has _source_data_root => (
Packit 4ed398
    is      => 'ro',
Packit 4ed398
    isa     => t('Dir'),
Packit 4ed398
    lazy    => 1,
Packit 4ed398
    builder => '_build_source_data_root',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
has _locale_codes => (
Packit 4ed398
    is      => 'ro',
Packit 4ed398
    isa     => t( 'ArrayRef', of => t('Str') ),
Packit 4ed398
    lazy    => 1,
Packit 4ed398
    builder => '_build_locale_codes',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
has _locales => (
Packit 4ed398
    is   => 'ro',
Packit 4ed398
    isa  => t( 'ArrayRef', of => object_isa_type('ModuleGenerator::Locale') ),
Packit 4ed398
    lazy => 1,
Packit 4ed398
    builder => '_build_locales',
Packit 4ed398
);
Packit 4ed398
Packit 4ed398
sub run ($self) {
Packit 4ed398
    $self->_clean_old_data;
Packit 4ed398
    $self->_locales;
Packit 4ed398
    $self->_write_data_files;
Packit 4ed398
    $self->_write_catalog_pm;
Packit 4ed398
    $self->_write_pod_files;
Packit 4ed398
Packit 4ed398
    return 0;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _clean_old_data ($self) {
Packit 4ed398
    my $pir  = Path::Iterator::Rule->new;
Packit 4ed398
    my $iter = $pir->file->name(qr/\.pod$/)->iter('lib');
Packit 4ed398
    while ( my $path = $iter->() ) {
Packit 4ed398
        $path = path($path);
Packit 4ed398
        ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
        say 'Removing ', $path->basename;
Packit 4ed398
        $path->remove;
Packit 4ed398
    }
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _build_locales ($self) {
Packit 4ed398
    my @locales;
Packit 4ed398
    for my $code ( $self->_locale_codes->@* ) {
Packit 4ed398
        my $locale = ModuleGenerator::Locale->instance(
Packit 4ed398
            code             => $code,
Packit 4ed398
            source_data_root => $self->_source_data_root,
Packit 4ed398
        );
Packit 4ed398
Packit 4ed398
        ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
        say $locale->code . q{ - } . $locale->en_name;
Packit 4ed398
        say $_ for $locale->source_files;
Packit 4ed398
        print "\n";
Packit 4ed398
        ## use critic
Packit 4ed398
Packit 4ed398
        push @locales, $locale;
Packit 4ed398
    }
Packit 4ed398
Packit 4ed398
    return \@locales;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _write_data_files ($self) {
Packit 4ed398
    my %raw_locales = $self->_write_data_pm;
Packit 4ed398
Packit 4ed398
    for my $code ( sort keys %raw_locales ) {
Packit 4ed398
        my $dumped = $self->_dump_with_unicode( $raw_locales{$code} );
Packit 4ed398
        my $file = path( 'share', $code . '.pl' );
Packit 4ed398
        ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
        say "Generating $file";
Packit 4ed398
        $file->spew($dumped);
Packit 4ed398
    }
Packit 4ed398
Packit 4ed398
    return;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _write_data_pm ($self) {
Packit 4ed398
    my %codes;
Packit 4ed398
    my %names;
Packit 4ed398
    my %native_names;
Packit 4ed398
    my %raw_locales;
Packit 4ed398
    for my $locale ( $self->_locales->@* ) {
Packit 4ed398
        $codes{ $locale->code }               = 1;
Packit 4ed398
        $names{ $locale->en_name }            = $locale->code;
Packit 4ed398
        $native_names{ $locale->native_name } = $locale->code;
Packit 4ed398
        $raw_locales{ $locale->code }         = $locale->data_hash;
Packit 4ed398
    }
Packit 4ed398
Packit 4ed398
    my $data_pm_file = path(qw( lib DateTime Locale Data.pm ));
Packit 4ed398
    ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
    say "Generating $data_pm_file";
Packit 4ed398
    ## use critic
Packit 4ed398
    my $data_pm = $data_pm_file->slurp_utf8;
Packit 4ed398
Packit 4ed398
    $self->_insert_autogen_warning( \$data_pm );
Packit 4ed398
Packit 4ed398
    $self->_insert_var_in_code(
Packit 4ed398
        'CLDRVersion',
Packit 4ed398
        $self->_locales->[0]->version, 1, \$data_pm
Packit 4ed398
    );
Packit 4ed398
Packit 4ed398
    $self->_insert_var_in_code( 'Codes',       \%codes,        1, \$data_pm );
Packit 4ed398
    $self->_insert_var_in_code( 'Names',       \%names,        1, \$data_pm );
Packit 4ed398
    $self->_insert_var_in_code( 'NativeNames', \%native_names, 1, \$data_pm );
Packit 4ed398
Packit 4ed398
    $self->_insert_var_in_code(
Packit 4ed398
        'ISO639Aliases',
Packit 4ed398
        $self->_iso_639_aliases, 1, \$data_pm
Packit 4ed398
    );
Packit 4ed398
Packit 4ed398
    # These are some of the world's top languages by speakers plus a few
Packit 4ed398
    # locales where I think there are lots of Perl people.
Packit 4ed398
    my %preload = map { $_ => delete $raw_locales{$_} }
Packit 4ed398
        qw( ar en en-CA en-US es fr-FR hi ja-JP pt-BR zh-Hans-CN zh-Hant-TW );
Packit 4ed398
Packit 4ed398
    $self->_insert_var_in_code( 'LocaleData', \%preload, 0, \$data_pm );
Packit 4ed398
Packit 4ed398
    $data_pm_file->spew_utf8($data_pm);
Packit 4ed398
Packit 4ed398
    return %raw_locales;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _iso_639_aliases ($self) {
Packit 4ed398
    my %aliases;
Packit 4ed398
    for my $locale ( $self->_locales->@* ) {
Packit 4ed398
        next if length $locale->language_code > 2;
Packit 4ed398
Packit 4ed398
        my $three = language_code2code(
Packit 4ed398
            $locale->language_code,
Packit 4ed398
            LOCALE_LANG_ALPHA_2, LOCALE_LANG_ALPHA_3
Packit 4ed398
        );
Packit 4ed398
Packit 4ed398
        my $full_three_code = join '-',
Packit 4ed398
            grep {defined} (
Packit 4ed398
            $three,
Packit 4ed398
            $locale->script_code,
Packit 4ed398
            $locale->territory_code,
Packit 4ed398
            $locale->variant_code
Packit 4ed398
            );
Packit 4ed398
Packit 4ed398
        $aliases{$full_three_code} = $locale->code;
Packit 4ed398
    }
Packit 4ed398
    return \%aliases;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _write_catalog_pm ($self) {
Packit 4ed398
    my $catalog_pm_file = path(qw( lib DateTime Locale Catalog.pm ));
Packit 4ed398
    ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
    say "Generating $catalog_pm_file";
Packit 4ed398
    ## use critic
Packit 4ed398
    my $catalog_pm = $catalog_pm_file->slurp_utf8;
Packit 4ed398
Packit 4ed398
    my $max_code = max map { length $_->code } $self->_locales->@*;
Packit 4ed398
    $max_code += 3;
Packit 4ed398
    my $max_en_name = max map { length $_->en_name } $self->_locales->@*;
Packit 4ed398
    $max_en_name += 3;
Packit 4ed398
    my $max_native_name
Packit 4ed398
        = max map { length $_->native_name } $self->_locales->@*;
Packit 4ed398
    $max_native_name += 3;
Packit 4ed398
Packit 4ed398
    my $locale_list = sprintf(
Packit 4ed398
        " %-${max_code}s%-${max_en_name}s%-${max_native_name}s\n",
Packit 4ed398
        'Locale code', 'Locale name (in English)', 'Native locale name'
Packit 4ed398
    );
Packit 4ed398
    $locale_list
Packit 4ed398
        .= q{ } . '=' x ( $max_code + $max_en_name + $max_native_name );
Packit 4ed398
    $locale_list .= "\n";
Packit 4ed398
Packit 4ed398
    for my $locale ( sort { $a->code cmp $b->code } $self->_locales->@* ) {
Packit 4ed398
        $locale_list .= sprintf(
Packit 4ed398
            " %-${max_code}s%-${max_en_name}s%-${max_native_name}s\n",
Packit 4ed398
            $locale->code, $locale->en_name, $locale->native_name,
Packit 4ed398
        );
Packit 4ed398
    }
Packit 4ed398
    $locale_list .= "\n";
Packit 4ed398
Packit 4ed398
    $locale_list =~ s/ +$//mg;
Packit 4ed398
Packit 4ed398
    $catalog_pm =~ s/(^=for :locales\n\n).+^(?==)/$1$locale_list/ms
Packit 4ed398
        or die 'locale list subst failed';
Packit 4ed398
Packit 4ed398
    $catalog_pm_file->spew_utf8($catalog_pm);
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _insert_var_in_code ( $self, $name, $value, $public, $code ) {
Packit 4ed398
    my $sigil
Packit 4ed398
        = !ref $value              ? '$'
Packit 4ed398
        : reftype $value eq 'HASH' ? '%'
Packit 4ed398
        :                            '@';
Packit 4ed398
Packit 4ed398
    my $safe;
Packit 4ed398
    if ( ref $value ) {
Packit 4ed398
        $safe = $self->_dump_with_unicode($value);
Packit 4ed398
        $safe =~ s/^[\{\[]/(/;
Packit 4ed398
        $safe =~ s/[\}\]]\n$/)/;
Packit 4ed398
    }
Packit 4ed398
    else {
Packit 4ed398
        $safe = $value = is_Num($value) ? $value : B::perlstring($value);
Packit 4ed398
    }
Packit 4ed398
Packit 4ed398
    my $declarator = $public ? 'our' : 'my';
Packit 4ed398
    ${$code} =~ s/
Packit 4ed398
        (\#<<<\n
Packit 4ed398
         \#\#\#\Q :start $name:\E\n)
Packit 4ed398
        .*
Packit 4ed398
        (\#\#\#\Q :end $name:\E\n
Packit 4ed398
         \#>>>\n)
Packit 4ed398
    /$1$declarator $sigil$name = $safe;\n$2/xs
Packit 4ed398
        or die "inserting $name failed";
Packit 4ed398
Packit 4ed398
    return;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
# Data::Dumper dumps all Unicode characters using Perl's \x{feedad0g}
Packit 4ed398
# syntax. If the character is in the 0x80-0xFF range, then Perl will not treat
Packit 4ed398
# this as a UTF-8 char when it sees it (either at compile or eval time). We
Packit 4ed398
# force it to use UTF-8 by replacing \x{feedad0g} with \N{U+feedad0g}, which
Packit 4ed398
# is always interpreted as UTF-8.
Packit 4ed398
sub _dump_with_unicode ( $self, $val ) {
Packit 4ed398
    my $dumped = Dumper($val);
Packit 4ed398
    $dumped =~ s/\\x\{([^}]+)\}/$self->_unicode_char_for($1)/eg;
Packit 4ed398
    return $dumped;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _unicode_char_for ( $, $hex ) {
Packit 4ed398
    ## no critic (BuiltinFunctions::ProhibitStringyEval)
Packit 4ed398
    my $num = eval '0x' . $hex;
Packit 4ed398
    die $@ if $@;
Packit 4ed398
    return '\N{U+' . sprintf( '%04x', $num ) . '}';
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _insert_autogen_warning ( $self, $code ) {
Packit 4ed398
    ${$code} =~ s/(?:^###+$).+(?:^###+$)\n+//ms;
Packit 4ed398
    ${$code} =~ s/^/$self->_autogen_warning/e;
Packit 4ed398
    return;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _build_autogen_warning ($self) {
Packit 4ed398
    my $script = $self->_generator_script->basename;
Packit 4ed398
Packit 4ed398
    return <<"EOF";
Packit 4ed398
###########################################################################
Packit 4ed398
#
Packit 4ed398
# This file is partially auto-generated by the DateTime::Locale generator
Packit 4ed398
# tools (v$VERSION). This code generator comes with the DateTime::Locale
Packit 4ed398
# distribution in the tools/ directory, and is called $script.
Packit 4ed398
#
Packit 4ed398
# This file was generated from the CLDR JSON locale data. See the LICENSE.cldr
Packit 4ed398
# file included in this distribution for license details.
Packit 4ed398
#
Packit 4ed398
# Do not edit this file directly unless you are sure the part you are editing
Packit 4ed398
# is not created by the generator.
Packit 4ed398
#
Packit 4ed398
###########################################################################
Packit 4ed398
Packit 4ed398
EOF
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _write_pod_files ($self) {
Packit 4ed398
    my $template = Text::Template->new(
Packit 4ed398
        TYPE   => 'FILE',
Packit 4ed398
        SOURCE => path(qw( tools templates locale.pod ))->stringify,
Packit 4ed398
    ) or die $Text::Template::ERROR;
Packit 4ed398
Packit 4ed398
    use lib 'lib';
Packit 4ed398
    require Test::File::ShareDir::Dist;
Packit 4ed398
    Test::File::ShareDir::Dist->import( { 'DateTime-Locale' => 'share' } );
Packit 4ed398
    require DateTime;
Packit 4ed398
    require DateTime::Locale;
Packit 4ed398
Packit 4ed398
    my @example_dts = (
Packit 4ed398
        DateTime->new(
Packit 4ed398
            year      => 2008,
Packit 4ed398
            month     => 2,
Packit 4ed398
            day       => 5,
Packit 4ed398
            hour      => 18,
Packit 4ed398
            minute    => 30,
Packit 4ed398
            second    => 30,
Packit 4ed398
            time_zone => 'UTC',
Packit 4ed398
        ),
Packit 4ed398
        DateTime->new(
Packit 4ed398
            year      => 1995,
Packit 4ed398
            month     => 12,
Packit 4ed398
            day       => 22,
Packit 4ed398
            hour      => 9,
Packit 4ed398
            minute    => 5,
Packit 4ed398
            second    => 2,
Packit 4ed398
            time_zone => 'UTC',
Packit 4ed398
        ),
Packit 4ed398
        DateTime->new(
Packit 4ed398
            year      => -10,
Packit 4ed398
            month     => 9,
Packit 4ed398
            day       => 15,
Packit 4ed398
            hour      => 4,
Packit 4ed398
            minute    => 44,
Packit 4ed398
            second    => 23,
Packit 4ed398
            time_zone => 'UTC',
Packit 4ed398
        ),
Packit 4ed398
    );
Packit 4ed398
Packit 4ed398
    for my $code ( DateTime::Locale->codes ) {
Packit 4ed398
        my $underscore = $code =~ s/-/_/gr;
Packit 4ed398
Packit 4ed398
        my $pod_file
Packit 4ed398
            = path( qw( lib DateTime Locale ), $underscore . '.pod' );
Packit 4ed398
        ## no critic (InputOutput::RequireCheckedSyscalls)
Packit 4ed398
        say "Generating $pod_file";
Packit 4ed398
        ## use critic
Packit 4ed398
Packit 4ed398
        my $locale = DateTime::Locale->load($code)
Packit 4ed398
            or die "Cannot load $code";
Packit 4ed398
Packit 4ed398
        my $name   = $locale->name;
Packit 4ed398
        my $filled = $template->fill_in(
Packit 4ed398
            HASH => {
Packit 4ed398
                autogen_warning => $self->_autogen_warning,
Packit 4ed398
                name            => 'DateTime::Locale::' . $underscore,
Packit 4ed398
                description =>
Packit 4ed398
                    "Locale data examples for the $name ($code) locale",
Packit 4ed398
                example_dts => \@example_dts,
Packit 4ed398
                locale      => \$locale,
Packit 4ed398
            },
Packit 4ed398
        ) or die $Text::Template::ERROR;
Packit 4ed398
Packit 4ed398
        $pod_file->spew_utf8($filled);
Packit 4ed398
    }
Packit 4ed398
Packit 4ed398
    return;
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _build_generator_script {
Packit 4ed398
    return path($0);
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _build_source_data_root ($self) {
Packit 4ed398
    return $self->_generator_script->parent->parent->child('source-data');
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
sub _build_locale_codes ($self) {
Packit 4ed398
Packit 4ed398
    # We need to have en-US available so we can build a DateTime.pm object.
Packit 4ed398
    return [ uniq( 'en-US', @{ $self->_only_locales } ) ]
Packit 4ed398
        if $self->_has_only_locales;
Packit 4ed398
Packit 4ed398
    my $avail = decode_json(
Packit 4ed398
        $self->_source_data_root->child(
Packit 4ed398
            qw( cldr-core availableLocales.json ))->slurp_raw
Packit 4ed398
    );
Packit 4ed398
Packit 4ed398
    my $default
Packit 4ed398
        = decode_json(
Packit 4ed398
        $self->_source_data_root->child(qw( cldr-core defaultContent.json ))
Packit 4ed398
            ->slurp_raw );
Packit 4ed398
Packit 4ed398
    return [
Packit 4ed398
        $avail->{availableLocales}{full}->@*,
Packit 4ed398
        $default->{defaultContent}->@*
Packit 4ed398
    ];
Packit 4ed398
}
Packit 4ed398
Packit 4ed398
__PACKAGE__->meta->make_immutable;
Packit 4ed398
Packit 4ed398
1;