IO へのアダプター層 (IO::Adapter クラス) IO::Adapter の概要 &fmldevel; でのメンバーリストの読み書き、例えばアドレスの登録/削除は IO::Adapter という抽象クラスを通じて行ないます。ようは Unix で vfs/vnode インターフェイスの導入した時のような感じですね。 使い方は次のようになります。 use IO::Adapter; $obj = new IO::Adapter $map, $map_params; $obj->open || croak("cannot open $map"); while ($x = $obj->get_next_key()) { ... } $obj->close; $map は「map:識別子」となります。file: は省略可能です。 現在のところ $map は次のようなものが使えます。 file:/var/spool/ml/elena/recipients unix.group:root nis.group:root mysql:id postgresql:id (未実装、実装予定) ldap:id (未実装、実装予定) file: は通常のファイルです。 unix.group:root は /etc/group の root のエントリにあるユーザのリストを、 nis.group:root は NIS の root のエントリにあるユーザのリストです。 mysql:id は MySQL へのアクセスです。 mysql:id で指定されたパラメータを用います。 このパラメータは new IO::Adapter を実行する際に、 指定する必要があります。 IO::Adapter のメソッド 現在、IO::Adapter の提供する公式なメソッドは次の通りです。 new() open() close() get_next_key() add(KEY) delete(KEY) getpos() setpos(NUM) eof() touch() find(REGEXP, $args) です。 KEY はデータベースのプライマリキーで、 通常メールアドレスです。 REGEXP は正規表現ですが、たいていはメールアドレスです。 多くのマップを同じように扱うために、 メソッドは極力最少限に、少数精鋭なものだけに限定する必要があります。 最低限のメソッド群の定義は実際に実装してみる実験を通じて選ばれました。 その際、ファイルの IO よりは、SQL との IO を想定しつつ、 メソッドの組を考えています。 そのため、&fml4; のファイルを前提にした処理とくらべると、 少し不自由に思えるでしょうが、 そのおかげで抽象化が高くなっています。 &fmldevel; で必要なメソッドの型 get_next_key() のように引数なしか STR を引数にして STR を返り値にする ものが多いです。これはファイルの一覧を見るとか、特定のアドレスに対する 何かの値を取り寄せるとかいう操作がよく使われるためです。 しかし、特定のアドレスに対する値が組になっているものがあります。 これは KEY_STR => [ VALUE_STR_1 VALUE_STR_2 VALUE_STR_3 ] のようなデータ構造が返ってくることを期待する場合です。 &fml4; の actives はメールアドレスに対していくつかのオプションの値を記 述するので、こういったデータ構造と考えられます。 例 rudo@nuinui.net s=skip m=xxx.yyy.z # commnet rudo@nuinui.net => [ s=skip m=xxx.yyy.z # comment ] まとめると引数は「ない」か」STR」なので STR とひとまとめにしても良いで しょう。一方、返り値は STR か ARRAY_REF になります。 引数 返り値 --------------------------------------- なし => STR STR => STR なし => [STR, STR, ... ] STR => [STR, STR, ... ] file マップ 『file:ファイル名』もしくは単なる『ファイル名』は ごく普通のテキストファイルへの IO を抽象化したものです。 unixgroup マップ /etc/group を抽象化します。 当然 READ ONLY です。 /etc/group の wheel:*:0:root,rudo,kenken は IO::Adapter では unixgroup:wheel としてアクセスできます。 $obj = new IO::Adapter "unixgroup:wheel"; というオブジェクトに対し get_next_key() すると グループのメンバーが順に返されます。 ちょうど root rudo kenken のようなファイルとみなされます。 つまり 3 行のファイルを読みだすようにみなされているわけですね。 NIS マップ /etc/group のケースとほぼ同様ですが、ソースが NIS/YP から group にあたるものを引っ張ってくるところだけが異なります。 MySQL マップ 保守性をあげるためには、一箇所に設定をまとめておきたい。つまり、一つの cf ファイルにいろいろ書けるようにしておきたい。たとえば &fmldevel; の 場合、config.cf の中だけに SQL の設定を書いておきたいわけです。 そこで、[mysql:members] (から終りまで、もしくは次の =head 文まで)の ブロックで、マップのタイプと識別子を表現しています。 config.cf の記述例 member_maps = mysql:members recipient_maps = mysql:recipients [mysql:members] sql_server = localhost sql_user = fml sql_password = uja sql_database = fml sql_table = ml sql_find = select * from ... ... IO::Adapter は new IO::Adapter "mysql:members", $config; のように呼び出します。 この $config は次のようなハッシュ・リファレンスになっていることが 期待されています。 $config => { [mysql:members] => { sql_sever => localhost ... } } FML::Config は .cf から上のようなハッシュを構成します。 よって IO::Adapter の new() の際には、 FML::Config のオブジェクトを引数で渡せば OK です。 議論: SQL 構文を config.cf に書くには? (参照: fml-devel 204) 関数が呼ばれた時に始めて決まる変数もあるので cf で全部定義することはできない。 そこで、lexical scope で取り扱われる変数の表現があれば解決だろうか? 現在では &varname 記法(下記)を使っている。 例: 以下は、受信者リストとメンバーリストが食い違う例なので、 アドレスの登録をする際に、fml_recepient という値が異なる。 その場合、member と recipient は少し異なる SQL 命令になるので 別々に定義する。 off/on 命令を無視するとか、 fml は sql からデータを読むだけで、 書き込むことはしないなら、 話は簡単になる。 member_maps = mysql:members recipient_maps = mysql:recipients [mysql:members] sql_server = localhost sql_user = fml sql_password = uja sql_database = fml sql_table = ml sql_get_next_key = select fml_address from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' sql_getline = select * from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' sql_add = insert into $sql_table values ('$ml_name', '$ml_domain', '&address', 1, 1) sql_delete = delete from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_address = '&address' sql_find = select * from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_address like '&regexp' [mysql:recipients] sql_server = localhost sql_user = fml sql_password = uja sql_database = fml sql_table = ml sql_get_next_key = select fml_address from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_recipient = '1' sql_getline = select * from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_recipient = '1' sql_add = update ml set recipient = 1 where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_address = '&address' sql_delete = update ml set recipient = 0 where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_address = '&address' sql_find = select * from $sql_table where fml_ml = '$ml_name' and fml_domain = '$ml_domain' and fml_recipient = '1' and fml_address like '&regexp'