# -*- cperl -*- package POE::Component::IRC::Plugin::NoPaste::Pager; use strict; use warnings; use Carp; use UNIVERSAL qw(isa); use Error qw(:try); use Hash::Util qw(lock_keys); our $VERSION = '0.1'; =pod my $pager = Pager->new( # 目的のデータを取り出す方法。 # SQL文を使用する場合: fetch => { sql => q{SELECT foo, bar FROM foo WHERE bar=? AND baz=?}, placeholder => [100, 200], dbh => $dbh, }, # CODEを使用する場合: fetch => { code => sub { # $begin: 表示を開始する項目の番號(0オリジン) # $length: 表示する項目の個數 # $code_show: 表面的には、show_contentで指定された關數(内部的には違ふ) my ($begin, $length, $code_show) = @_; for ($begin .. ($begin + $length - 1)) { $code_show->("Item [$_]"); } }, }, # 全項目數を知る方法。 # fetchがSQL文であるなら、countを省略すれば # 自動的にSELECT COUNT(*)文をfetchのSQLから生成する。 count => { sql => q{SELECT COUNT(*) FROM foo WHERE bar=? AND baz=?}, placeholder => [100, 200], dbh => $dbh, }, # 若しくは count => { code => sub { return 100; }, }, page_to_show => 2, # 表示したいページ(1オリジン); デフォルトは1 pagenums_limit => 10, # 各ページへのリンクの最大表示個數; デフォルトは10 items_per_page => 20, # ページ毎の項目の最大表示數; デフォルトは20 # メタ情報が得られた時に呼ばれる關數。 show_meta => sub { # 番號は全て1オリジン。 # total: 総件數 # page: 現在のページ番號 # last_page: 最終ページ番號 # pagelink_begin: 各ページへのリンクの開始ページ番號 # pagelink_end: 各ページへのリンクの終了ページ番號(この番號も含む) my %meta = @_; print "page: $meta{page}/$meta{last_page}; total: $meta{total}\n"; }, # 項目が得られた時に呼ばれる關數。 show_content => sub { # fetchがSQL文なら、$_[0]はfetchrow_hashrefで得られたハッシュ。 # CODEなら、そのCODEの中から明示的に呼ばれなければならない。 print "item: $_[0]\n"; }, # 項目が一つも無い時に呼ばれる關數。省略可。 no_content => sub { # do what you want to }, ); $pager->execute; =cut sub new { my ($class, %args) = @_; my $fetch = $args{fetch}; if (!isa $fetch, 'HASH') { croak "Arg{fetch} is not a hashref"; } if (defined $fetch->{sql}) { $fetch->{dbh} or croak "Arg{fetch} has a sql, but not dbh"; $fetch->{code} and croak "Arg{fetch} has both of sql and code"; # FIXME: LIMITが付いてゐたらcroak } elsif (defined $fetch->{code}) { (isa $fetch->{code}, 'CODE') or croak "Arg{fetch}{code} is not a coderef: ".$fetch->{code}; $fetch->{sql} and croak "Arg{fetch} has both of sql and code"; } my $count = $args{count}; if (!defined $count) { if (defined $fetch->{sql}) { # fetchのSQL文を改造して作る。 (my $sql = $fetch->{sql}) =~ s/^\s*select\s+(.+?)\s+from/select count(*) from/i; $count = $args{count} = {}; $count->{sql} = $sql; $count->{placeholder} = $fetch->{placeholder} if $fetch->{placeholder}; $count->{dbh} = $fetch->{dbh}; } else { croak "Arg{count} cannot be omitted when the Arg{fetch} is not a SQL"; } } elsif (!isa $count, 'HASH') { croak "Arg{count} is not a hashref"; } # FIXME: $args{count}のsanity checkを(もっと) # FIXME: 以下の三つの變數をsanity check $args{page_to_show} ||= 1; $args{pagenums_limit} ||= 10; $args{items_per_page} ||= 20; foreach my $key (qw/show_meta show_content/) { if (!defined $args{$key}) { croak "Missing Arg{$key}"; } elsif (!isa $args{$key}, 'CODE') { croak "Arg{$key} is not a coderef: $args{$key}"; } } if (defined $args{no_content} and !isa $args{no_content}, 'CODE') { croak "Arg{no_content} is not a coderef: $args{no_content}"; } $args{no_content} ||= undef; my $this = bless \%args => $class; lock_keys %$this; $this; } sub ceil { my $val = $_[0]; my $floor = int($val); $floor == $val ? $floor : $floor + 1; } sub min { my $min = $_[0]; foreach (@_) { $min = $_ if $min > $_; } $min; } sub max { my $max = $_[0]; foreach (@_) { $max = $_ if $max < $_; } $max; } sub execute { my $this = shift; my $count; if (defined $this->{count}{sql}) { try { $count = $this->{count}{dbh}->selectrow_array( $this->{count}{sql}, undef, @{$this->{count}{placeholder} || []}); } otherwise { throw Error::Simple "Failed to do count by sql: $_[0]"; }; } else { try { $count = $this->{count}{code}->(); } otherwise { throw Error::Simple "Failed to do count by code: $_[0]"; }; } # この情報を基に、メタデータを生成 my $last_page = max(1, ceil($count / $this->{items_per_page})); my $page = min($this->{page_to_show}, $last_page); my $pagelink_begin = max(1, int($page - $this->{pagenums_limit} / 2)); my $pagelink_end = min($last_page, $pagelink_begin + $this->{pagenums_limit} - 1); # show_metaを呼ぶ $this->{show_meta}->( count => $count, page => $page, last_page => $last_page, pagelink_begin => $pagelink_begin, pagelink_end => $pagelink_end); # fetch實行 my $fetch_begin = ($page - 1) * $this->{items_per_page}; my $fetch_length = min($count - $fetch_begin, $this->{items_per_page}); if (defined $this->{fetch}{sql}) { try { # FIXME: これより増しなLIMIT生成方法を my $sql = $this->{fetch}{sql} . " LIMIT $fetch_begin, $fetch_length"; my $sth = $this->{fetch}{dbh}->prepare($sql); $sth->execute(@{$this->{fetch}{placeholder} || []}); while ($_ = $sth->fetchrow_hashref) { $this->{show_content}->($_); } if ($sth->rows == 0) { $this->{no_content} and $this->{no_content}->(); } } otherwise { throw Error::Simple "Failed to do fetch by sql: $_[0]"; }; } else { # $fetch_length囘呼ばれたかどうかをチェックする my $called_count = 0; my $checked_show = sub { $called_count++; goto &{$this->{show_content}}; }; try { $this->{fetch}{code}->( $fetch_begin, $fetch_length, $checked_show); } otherwise { throw Error::Simple "Failed to do fetch by code: $_[0]"; }; if ($called_count != $fetch_length) { carp "Fetch coderef didn't call show_content exactly $fetch_length times (call \$length($fetch_length) times)"; } if ($fetch_length == 0) { $this->{no_content} and $this->{no_content}->(); } } $this; } 1;