The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::mkfeyorm;
{
  $App::mkfeyorm::VERSION = '0.010';
}
# ABSTRACT: Make skeleton code with Fey::ORM

use Moose;
use Moose::Util::TypeConstraints;
use MooseX::SemiAffordanceAccessor;
use MooseX::StrictConstructor;
use autodie;

use Data::Section -setup;
use File::Basename;
use File::Spec::Functions;
use Template;

has 'schema' => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

subtype 'TableRef',
    as 'HashRef';

sub _db_table_name {
    my $table = shift;

    $table =~ s/([A-Z]+)::/"_\L$1_"/ge;
    $table =~ s/([A-Z]+)([A-Z])/"_\L$1_$2"/ge;
    $table =~ s/([A-Z])/"_\L$1"/ge;
    $table =~ s/::/_/g;
    $table =~ s/_+/_/g;
    $table =~ s/^_//;

    return $table;
}

coerce 'TableRef',
    from 'ArrayRef',
    via {
        my %result = map { $_ => _db_table_name($_) } @$_;

        \%result;
    };

has 'tables' => (
    is       => 'rw',
    isa      => 'TableRef',
    required => 1,
    coerce   => 1,
);

has '_db_tables' => (
    is       => 'rw',
    isa      => 'ArrayRef',
);

has 'output_path' => (
    is      => 'rw',
    isa     => 'Str',
    default => 'lib',
);

after 'set_output_path' => sub {
    my ( $self, $path ) = @_;

    my $tt = Template->new({
        OUTPUT_PATH      => $self->output_path,
        DEFAULT_ENCODING => 'utf-8',
    }) || die "$Template::ERROR\n";

    $self->_set_template($tt);
};

has 'namespace' => (
    is      => 'rw',
    isa     => 'Str',
);

has 'table_namespace' => (
    is      => 'rw',
    isa     => 'Str',
);

has 'schema_namespace' => (
    is      => 'rw',
    isa     => 'Str',
);

has 'schema_template' => (
    is      => 'rw',
    isa     => 'Str',
    default => ${ __PACKAGE__->section_data('schema.tt') },
);

has 'table_template' => (
    is      => 'rw',
    isa     => 'Str',
    default => ${ __PACKAGE__->section_data('table.tt') },
);

has 'cache' => (
    is      => 'rw',
    isa     => 'Bool',
    default => 0,
);

has 'template_params' => (
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { {} },
);

has '_template' => (
    is         => 'rw',
    isa        => 'Template',
    lazy_build => 1,
);

sub _build__template {
    my $self = shift;

    my $tt = Template->new({
        OUTPUT_PATH      => $self->output_path,
        DEFAULT_ENCODING => 'utf-8',
    }) || die "$Template::ERROR\n";

    return $tt;
}

sub schema_module {
    my $self = shift;

    my $full_name = join(
        '::',
        grep { $_ } (
            $self->namespace,
            $self->schema_namespace,
            $self->schema
        )
    );

    return $full_name;
}

sub table_modules {
    my ( $self, @tables ) = @_;

    my @full_names;
    if (@tables) {
        for my $table (@tables) {
            my $full_name = join(
                '::',
                grep { $_ } (
                    $self->namespace,
                    $self->table_namespace,
                    $table,
                )
            );
            push @full_names, $full_name;
        }
    }
    else {
        for my $table ( sort keys %{ $self->tables } ) {
            my $full_name = join(
                '::',
                grep { $_ } (
                    $self->namespace,
                    $self->table_namespace,
                    $table,
                )
            );
            push @full_names, $full_name;
        }
    }

    return @full_names;
}

sub process {
    my $self = shift;

    $self->process_schema;
    $self->process_table($_, $self->tables->{$_}) for keys %{ $self->tables };
}

sub process_tables {
    my ( $self, @tables ) = @_;

    if (@tables) {
        $self->process_table($_, $self->tables->{$_}) for @tables;
    }
    else {
        $self->process_table($_, $self->tables->{$_}) for keys %{ $self->tables };
    }
}

sub process_schema {
    my ( $self, $content ) = @_;

    my $schema = $self->schema_module;
    my @tables = $self->table_modules;

    my $vars = {
        SCHEMA => $schema,
        TABLES => \@tables,
        CACHE  => $self->cache,
        PARAMS => $self->template_params,
    };

    $self->_template->process(
        \$self->schema_template,
        $vars,
        ref $content eq 'SCALAR' ? $content : $self->module_path($schema),
    ) or die $self->_template->error, "\n";
}

sub process_table {
    my ( $self, $orig_table, $db_table, $content ) = @_;

    $db_table = _db_table_name($orig_table) unless $db_table;

    my $schema     = $self->schema_module;
    my ( $table )  = $self->table_modules($orig_table);

    my $vars = {
        SCHEMA   => $schema,
        TABLE    => $table,
        CACHE    => $self->cache,
        DB_TABLE => $db_table,
        PARAMS   => $self->template_params,
    };

    $self->_template->process(
        \$self->table_template,
        $vars,
        ref $content eq 'SCALAR' ? $content : $self->module_path($table),
    ) or die $self->_template->error, "\n";
}

sub module_path {
    my ( $self, $module ) = @_;

    return catfile( split(/::/, $module) ) . '.pm';
}

__PACKAGE__->meta->make_immutable;
1;




=pod

=encoding utf-8

=head1 NAME

App::mkfeyorm - Make skeleton code with Fey::ORM

=head1 VERSION

version 0.010

=head1 SYNOPSIS

    use App::mkfeyorm;
    
    my $app = App::mkfeyorm->new(
        output_path      => 'somewhere/lib',
        schema           => 'Schema',
        tables           => [qw(
            MC::User
            MC::Role
            MC::UserRole
            AE::Source
            AE::Task
            CM::Source
            CM::Task
        )],
        namespace        => 'MedicalCoding',
        table_namespace  => 'Model',
    );
    
    $app->process;

=head1 DESCRIPTION

This module generates L<Fey::ORM> based module on the fly.
At least C<schema> and C<tables> attributes are needed.

=head1 ATTRIBUTES

=head2 schema

Schema module name (required)

    my $schema_module_name = $self->schema;
    $self->set_schema($schema_module_name);

=head2 tables

Table module name list (required)

    my $table_module_names_ref = $self->tables;
    $self->set_tables(\@table_module_names);
    $self->set_tables(\%table_module_names);

=head2 output_path

Output path for generated modules.
Default output directory is C<lib>.

    my $output_path = $self->output_path;
    $self->set_output_path($output_path);

=head2 namespace

Namespace for schema and table module

=head2 table_namespace

Namespace for table module

=head2 schema_namespace

Namespace for schema module

=head2 schema_template

Schema template string.
If you want to use your own template file then use this attribute.

=head2 table_template

Table template string.
If you want to use your own template file then use this attribute.

=head2 cache

Use cache feature or not. Default is false.
It uses L<Storable> to save and load cache file.

=head2 template_params

Hash reference for templating.
Set this attribute if you want to use additional parameters
in your own templates.

=head1 METHODS

=head2 process

Generate the schema module & table module

    my $app = App::mkfeyorm->new(
        schema          => 'Schema',
        tables          => {
            User     => 'user',
            Role     => 'role',
            UserRole => 'user_role',
        },
        namespace       => 'Web::Blog',
        table_namespace => 'Model',
    );
    $app->process;

=head2 process_schema

Generate the schema module.

    $app->process_schema;

=head2 process_table

Generate the talbe module.

    $app->process_table;

=head2 process_tables

Generate the table module.

    $app->process_tables;                  # generates all tables
    $app->process_tables(qw/ User Role /); # generates User and Role tables

=head2 schema_module

Get full name of schema module

=head2 table_modules

Get full names of table modules

=head2 module_path

Get module path from module names

=head1 SEE ALSO

L<Fey::ORM>

=head1 AUTHOR

Keedi Kim - κΉ€λ„ν˜• <keedi@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Keedi Kim.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


__DATA__

__[ schema.tt ]__
package [% SCHEMA %];

use Fey::DBIManager::Source;
use Fey::Loader;
use Fey::ORM::Schema;
[% FOREACH TABLE = TABLES -%]
use [% TABLE %];
[% END -%]

[% IF CACHE -%]
use Storable;
use File::Basename;
use File::Path qw(make_path);
[% END -%]

sub load {
    my ( $class, %params ) = @_;

    return unless $class;
    return if     $class->Schema;

    _load_schema(%params) or die "cannot load schema\n";
    _load_tables(qw/
[% FOREACH TABLE = TABLES -%]
        [% TABLE %]
[% END -%]
    /) or die "cannot load tables\n";
}

sub _load_schema {
    my %params = @_;

    my %source_params = map {
        defined $params{$_} ? ( $_ => $params{$_} ) : ();
    } qw(
        name
        dbh
        dsn
        username
        password
        attributes
        post_connect
        auto_refresh
        ping_interval
    );

    my $source = Fey::DBIManager::Source->new( %source_params );
[% IF CACHE -%]
    my $schema;
    if ($params{cache_file} && -f $params{cache_file}) {
        $schema = retrieve($params{cache_file});
    }
    else {
        $schema = Fey::Loader->new( dbh => $source->dbh )->make_schema;
    }
[% ELSE -%]
    my $schema = Fey::Loader->new( dbh => $source->dbh )->make_schema;
[% END -%]
    return if ref($schema) ne 'Fey::Schema';

[% IF CACHE -%]
    my $updated;
[% END -%]
    if ($params{fk_relations}) {
[% IF CACHE -%]
        ++$updated;
[% END -%]
        for my $relation ( @{ $params{fk_relations} } ) {
            my $source_table  = $relation->{source_table};
            my $source_column = $relation->{source_column};
            my $target_table  = $relation->{target_table};
            my $target_column = $relation->{target_column};

            my $fk = Fey::FK->new(
                source_columns => $schema->table($source_table)->column($source_column),
                target_columns => $schema->table($target_table)->column($target_column),
            );
            $schema->add_foreign_key($fk);
        }
    }

    #
    # Add foreign key if it is needed or remove it
    #
    #my $fk;
    #
    #$fk = Fey::FK->new(
    #    source_columns => $schema->table('src_table')->column('col_id'),
    #    target_columns => $schema->table('dest_table')->column('col_id'),
    #);
    #$schema->add_foreign_key($fk);

[% IF CACHE -%]
    if ($params{cache_file}) {
        if (!-e $params{cache_file} || $updated) {
            my $dirname = dirname($params{cache_file});
            make_path($dirname) unless -e $dirname;
            store($schema, $params{cache_file});
        }
    }

[% END -%]
    has_schema $schema;

    __PACKAGE__->DBIManager->add_source($source);

    return 1;
}

sub _load_tables {
    my @tables = @_;

    $_->load for @tables;

    return 1;
}

1;
__[ table.tt ]__
package [% TABLE %];
use Fey::ORM::Table;
use [% SCHEMA %];

use Moose;
use MooseX::SemiAffordanceAccessor;
use MooseX::StrictConstructor;
use namespace::autoclean;

sub load {
    my $class = shift;

    return unless $class;
    return if     $class->Table;

    my $schema = [% SCHEMA %]->Schema;
    my $table  = $schema->table('[% DB_TABLE %]');

    has_table( $table );

    #
    # Add another relationships like has_one, has_many or etc.
    #
    #has_many items => ( table => $schema->table('item') );
}

1;