]> gitweb @ CieloNegro.org - pci-nopaste.git/blob - lib/POE/Component/IRC/Plugin/NoPaste/Pager.pm
migrate from GNU arch to Git
[pci-nopaste.git] / lib / POE / Component / IRC / Plugin / NoPaste / Pager.pm
1 # -*- cperl -*-
2 package POE::Component::IRC::Plugin::NoPaste::Pager;
3 use strict;
4 use warnings;
5 use Carp;
6 use UNIVERSAL qw(isa);
7 use Error qw(:try);
8 use Hash::Util qw(lock_keys);
9 our $VERSION = '0.1';
10
11 =pod
12
13  my $pager = Pager->new(
14      # 目的のデータを取り出す方法。
15      # SQL文を使用する場合:
16      fetch => {
17          sql         => q{SELECT foo, bar FROM foo WHERE bar=? AND baz=?},
18          placeholder => [100, 200],
19          dbh         => $dbh,
20      },
21      # CODEを使用する場合:
22      fetch => {
23          code => sub {
24              # $begin: 表示を開始する項目の番號(0オリジン)
25              # $length: 表示する項目の個數
26              # $code_show: 表面的には、show_contentで指定された關數(内部的には違ふ)
27              my ($begin, $length, $code_show) = @_;
28              for ($begin .. ($begin + $length - 1)) {
29                  $code_show->("Item [$_]");
30              }
31          },
32      },
33      # 全項目數を知る方法。
34      # fetchがSQL文であるなら、countを省略すれば
35      # 自動的にSELECT COUNT(*)文をfetchのSQLから生成する。
36      count => {
37          sql         => q{SELECT COUNT(*) FROM foo WHERE bar=? AND baz=?},
38          placeholder => [100, 200],
39          dbh         => $dbh,
40      },
41      # 若しくは
42      count => {
43          code => sub {
44              return 100;
45          },
46      },
47      page_to_show   => 2,  # 表示したいページ(1オリジン); デフォルトは1
48      pagenums_limit => 10, # 各ページへのリンクの最大表示個數; デフォルトは10
49      items_per_page => 20, # ページ毎の項目の最大表示數; デフォルトは20
50      # メタ情報が得られた時に呼ばれる關數。
51      show_meta => sub {
52          # 番號は全て1オリジン。
53          # total: 総件數
54          # page: 現在のページ番號
55          # last_page: 最終ページ番號
56          # pagelink_begin: 各ページへのリンクの開始ページ番號
57          # pagelink_end: 各ページへのリンクの終了ページ番號(この番號も含む)
58          my %meta = @_;
59          print "page: $meta{page}/$meta{last_page}; total: $meta{total}\n";
60      },
61      # 項目が得られた時に呼ばれる關數。
62      show_content => sub {
63          # fetchがSQL文なら、$_[0]はfetchrow_hashrefで得られたハッシュ。
64          # CODEなら、そのCODEの中から明示的に呼ばれなければならない。
65          print "item: $_[0]\n";
66      },
67      # 項目が一つも無い時に呼ばれる關數。省略可。
68      no_content => sub {
69          # do what you want to
70      },
71  );
72  $pager->execute;
73
74 =cut
75
76 sub new {
77     my ($class, %args) = @_;
78
79     my $fetch = $args{fetch};
80     if (!isa $fetch, 'HASH') {
81         croak "Arg{fetch} is not a hashref";
82     }
83     if (defined $fetch->{sql}) {
84         $fetch->{dbh} or croak "Arg{fetch} has a sql, but not dbh";
85         $fetch->{code} and croak "Arg{fetch} has both of sql and code";
86         # FIXME: LIMITが付いてゐたらcroak
87     }
88     elsif (defined $fetch->{code}) {
89         (isa $fetch->{code}, 'CODE') or croak "Arg{fetch}{code} is not a coderef: ".$fetch->{code};
90         $fetch->{sql} and croak "Arg{fetch} has both of sql and code";
91     }
92
93     my $count = $args{count};
94     if (!defined $count) {
95         if (defined $fetch->{sql}) {
96             # fetchのSQL文を改造して作る。
97             (my $sql = $fetch->{sql}) =~ s/^\s*select\s+(.+?)\s+from/select count(*) from/i;
98             $count = $args{count} = {};
99             $count->{sql} = $sql;
100             $count->{placeholder} = $fetch->{placeholder} if $fetch->{placeholder};
101             $count->{dbh} = $fetch->{dbh};
102         }
103         else {
104             croak "Arg{count} cannot be omitted when the Arg{fetch} is not a SQL";
105         }
106     }
107     elsif (!isa $count, 'HASH') {
108         croak "Arg{count} is not a hashref";
109     }
110     # FIXME: $args{count}のsanity checkを(もっと)
111
112     # FIXME: 以下の三つの變數をsanity check
113     $args{page_to_show} ||= 1;
114     $args{pagenums_limit} ||= 10;
115     $args{items_per_page} ||= 20;
116
117     foreach my $key (qw/show_meta show_content/) {
118         if (!defined $args{$key}) {
119             croak "Missing Arg{$key}";
120         }
121         elsif (!isa $args{$key}, 'CODE') {
122             croak "Arg{$key} is not a coderef: $args{$key}";
123         }
124     }
125
126     if (defined $args{no_content} and
127           !isa $args{no_content}, 'CODE') {
128         croak "Arg{no_content} is not a coderef: $args{no_content}";
129     }
130     $args{no_content} ||= undef;
131     
132     my $this = bless \%args => $class;
133     lock_keys %$this;
134     $this;
135 }
136
137 sub ceil {
138     my $val = $_[0];
139     my $floor = int($val);
140     $floor == $val ? $floor : $floor + 1;
141 }
142
143 sub min {
144     my $min = $_[0];
145     foreach (@_) {
146         $min = $_ if $min > $_;
147     }
148     $min;
149 }
150
151 sub max {
152     my $max = $_[0];
153     foreach (@_) {
154         $max = $_ if $max < $_;
155     }
156     $max;
157 }
158
159 sub execute {
160     my $this = shift;
161
162     my $count;
163     if (defined $this->{count}{sql}) {
164         try {
165             $count = $this->{count}{dbh}->selectrow_array(
166                 $this->{count}{sql}, undef, @{$this->{count}{placeholder} || []});
167         } otherwise {
168             throw Error::Simple "Failed to do count by sql: $_[0]";
169         };
170     }
171     else {
172         try {
173             $count = $this->{count}{code}->();
174         } otherwise {
175             throw Error::Simple "Failed to do count by code: $_[0]";
176         };
177     }
178
179     # この情報を基に、メタデータを生成
180     my $last_page = max(1, ceil($count / $this->{items_per_page}));
181     my $page = min($this->{page_to_show}, $last_page);
182     my $pagelink_begin = max(1, int($page - $this->{pagenums_limit} / 2));
183     my $pagelink_end = min($last_page, $pagelink_begin + $this->{pagenums_limit} - 1);
184
185     # show_metaを呼ぶ
186     $this->{show_meta}->(
187         count          => $count,
188         page           => $page,
189         last_page      => $last_page,
190         pagelink_begin => $pagelink_begin,
191         pagelink_end   => $pagelink_end);
192
193     # fetch實行
194     my $fetch_begin = ($page - 1) * $this->{items_per_page};
195     my $fetch_length = min($count - $fetch_begin,
196                            $this->{items_per_page});
197     if (defined $this->{fetch}{sql}) {
198         try {
199             # FIXME: これより増しなLIMIT生成方法を
200             my $sql = $this->{fetch}{sql} . " LIMIT $fetch_begin, $fetch_length";
201             
202             my $sth = $this->{fetch}{dbh}->prepare($sql);
203             $sth->execute(@{$this->{fetch}{placeholder} || []});
204             while ($_ = $sth->fetchrow_hashref) {
205                 $this->{show_content}->($_);
206             }
207             if ($sth->rows == 0) {
208                 $this->{no_content} and
209                   $this->{no_content}->();
210             }
211         } otherwise {
212             throw Error::Simple "Failed to do fetch by sql: $_[0]";
213         };
214     }
215     else {
216         # $fetch_length囘呼ばれたかどうかをチェックする
217         my $called_count = 0;
218         my $checked_show = sub {
219             $called_count++;
220             goto &{$this->{show_content}};
221         };
222         try {
223             $this->{fetch}{code}->(
224                 $fetch_begin, $fetch_length, $checked_show);
225         } otherwise {
226             throw Error::Simple "Failed to do fetch by code: $_[0]";
227         };
228         if ($called_count != $fetch_length) {
229             carp "Fetch coderef didn't call show_content exactly $fetch_length times (call \$length($fetch_length) times)";
230         }
231         if ($fetch_length == 0) {
232             $this->{no_content} and
233               $this->{no_content}->();
234         }
235     }
236
237     $this;
238 }
239
240 1;