$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

Re:Re:Teng::Schema::DeclareにPlugin機構があったらいいのにって妄想してた

nekokakさんに返答してもらえたので、もう一度考えなおしてみました。

Schema定義時以外に差し込む方法

 なるほど、SchemaAPIがあるので、Tableオブジェクトが作られていれば、後から挿し込むことも可能ということなんですね。勉強になりました。まだちゃんと理解していないですが、動的にinflateを追加するときとかに使えるといった感じでしょうか。
 ただ、いまいち今回やりたかったことを実現できるのかがわかりませんでした(自分の理解が足りないだけの可能性が大いにありますが...)

やりたかったことを再確認

 正直自分でも何がやりたかったのか、不明確なので、もう一度再確認してみます。下のように、Schema定義DSLを外側から拡張したいというのが、今回やってみたかったことです。例えばdatetime_columnsって指定しておくと、勝手にDateTimeにinflate/deflateしてくれるみたいな感じです。

 下が例です。

# datetime_columnsって指定したカラムを自動的にDateTimeに
# inflate/deflateしてくれるようなSchema定義DSLを外から拡張したい
table {
    name "sample";
    pk "id";
    columns qw( name created_at updated_at );
    datetime_columns qw(created_at updated_at);
};

もう一度他の方法も検討してみた

 さてちょっと他の方法も検討したところ、こんな感じで出来るんじゃないかという方法を思いついたので、まとめてみます。

前提

 前提としてTeng::Schema::Declareのtableメソッドを知る必要があります。

our @EXPORT = qw(
    schema
    name
    table
    pk
    columns
    row_class
    inflate
    deflate
);

# ....
# ....

sub pk(@);
sub columns(@);
sub name ($);
sub row_class ($);
sub inflate_rule ($@);
sub table(&) {
    my $code = shift;
    my $current = _current_schema();

    my (
        $table_name,
        @table_pk,
        @table_columns,
        @inflate,
        @deflate,
        $row_class,
    );
    no warnings 'redefine';
    
    my $dest_class = caller();
    no strict 'refs';
    no warnings 'once';
    local *{"$dest_class\::name"}      = sub ($) { 
        $table_name = shift;
        $row_class  = row_namespace($table_name);
    };
    local *{"$dest_class\::pk"}        = sub (@) { @table_pk = @_ };
    local *{"$dest_class\::columns"}   = sub (@) { @table_columns = @_ };
    local *{"$dest_class\::row_class"} = sub (@) { $row_class = shift };
    local *{"$dest_class\::inflate"} = sub ($&) {
        my ($rule, $code) = @_;
        if (ref $rule ne 'Regexp') {
            $rule = qr/^\Q$rule\E$/;
        }
        push @inflate, ($rule, $code);
    };
    local *{"$dest_class\::deflate"} = sub ($&) {
        my ($rule, $code) = @_;
        if (ref $rule ne 'Regexp') {
            $rule = qr/^\Q$rule\E$/;
        }
        push @deflate, ($rule, $code);
    };

    $code->();

    my @col_names;
    my %sql_types;
    while ( @table_columns ) {
        my $col_name = shift @table_columns;
        if (ref $col_name) {
            my $sql_type = $col_name->{type};
            $col_name = $col_name->{name};
            $sql_types{$col_name} = $sql_type;
        }
        push @col_names, $col_name;
    }

    $current->add_table(
        Teng::Schema::Table->new(
            columns      => \@col_names,
            name         => $table_name,
            primary_keys => \@table_pk,
            sql_types    => \%sql_types,
            inflators    => \@inflate,
            deflators    => \@deflate,
            row_class    => $row_class,
        )
    ); 
}

 これを見ると分かるとおり、DeclareをuseするとSchema定義用の関数がEXPORTされ、さらにtable内でlocalとしてメソッドをredefineすることによって、各テーブルごとに定義を行えるようになっています。
 で、これを見て気づいたことがありました。すべてのSchema定義用関数はuseされたpackageにEXPORTされます。じゃあ、そのEXPORTされた関数を外側から利用したら、拡張できるんじゃないかということです。

拡張側

 拡張側は前回でいう、Plugin側です。EXPORTされた関数を使うことによって、datetime_columnsというSchema定義を追加してみています。

package Teng::Schema::Declare::Columns::DateTime;

use strict;
use warnings;
use Carp;

our $VERSION = '0.0.1';

use Teng::Schema::Declare;

use Exporter::Lite;
our @EXPORT = qw(datetime_columns);

sub datetime_columns {
    my (@columns) = @_;

    my $columns_regexp = join('|', @columns);
    my $regexp = qr{^(?:$columns_regexp)$};
    my ($pkg) = caller;
    my $inflate = \&{$pkg . '::inflate'};
    my $deflate = \&{$pkg . '::deflate'};
    $inflate->($regexp => \&inflate_datetime);
    $deflate->($regexp => \&deflate_datetime);
}

sub inflate_datetime {
    my ($col_value) = @_;
    return DateTime::Format::MySQL->parse_datetime($col_value);
}

sub deflate_datetime {
    my ($col_value) = @_;
    return DateTime::Format::MySQL->format_datetime($col_value);
}

1;
利用側

Schema定義側はこんな感じで使います。

package Sample::Schema;
use strict;
use warnings;

use Teng::Schema::Declare;
use Teng::Schema::Declare::Columns::DateTime;

table {
    name "sample";
    pk "id";
    columns qw( name created_at updated_at );
    datetime_columns qw(created_at updated_at);
};

1;

まとめ

 というわけで、もう一度考えなおしてみて、別の方法を使ってやってみました。ただ、正直ハックっぽい感じになってしまったので、このようなやり方で本当に大丈夫かということが、まだわからない感じです。
 もし、もうちょっといい方法などあれば、教えて頂きたいです。