IO へのアダプター層 (IO::Adapter クラス) IO::Adapter の概要 &fml8; のメンバーリストはファイル、 Unix のグループ、 RDBMS (リレーショナルデータベースシステム)、 LDAP など、さまざまな形式で管理できます。 もちろん、デフォルトはファイルです。 &fml8; でのメンバーリストの読み書き、 たとえばメールアドレスの登録/削除は IO::Adapter という抽象クラスを通じて行ないます。 ようは Unix における vfs/vnode インターフェイスの導入です。 使い方は次のようになります。 use IO::Adapter; $io = new IO::Adapter $map, $map_params; $io->open || croak("cannot open $map"); while ($x = $io->get_next_key()) { ... } $io->close; $map は「map:識別子」形式です。 現在のところ $map は次のようなものが指定可能です。 なお file: は省略可能です。 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、 postgresql:id は PostgreSQL へのアクセスです。 ldap:id は LDAP へのアクセスです。 設定ファイル中では、(なにかとまぎらわしいので)、 RDBMS と LDAP を別の名前空間で定義する仕様です。 たとえば MySQL は config.cf の mysql:id で指定されたパラメータを用います。 [mysql:id] sql_database = fml sql_password = uja ... 略 ... このパラメータは 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; のファイルを前提にした処理とくらべると、 少し不自由に思えるでしょうが、 そのぶん抽象度合いは高くなっています。 そして、 これを実装し、長年運用できているので、 現在の仕様で必要十分のようです。 &fml8; で必要なメソッドの型 メソッドの型は、 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 としてアクセスできます。 $io = new IO::Adapter "unixgroup:wheel"; というオブジェクトに対し get_next_key() すると グループのメンバーが順に返されます。 ちょうど root rudo kenken のようなファイルとみなしています。 つまり /etc/group (上述の行)を、 合計3行のファイルを読みだす処理と同じと見なしています。 NIS マップ /etc/group のケースとほぼ同様ですが、 ソースが NIS/YP から group にあたる情報を出してきている点だけが異なります。 MySQL マップ 運用部隊としては 「保守性をあげるためには、一箇所に設定をまとめておきたい」 つまり 「一つの cf ファイルにいろいろ書けるようにしておきたい」 と考えます(あとで diff を取るとか考えると、このほうが便利です)。 そこで、 &fml8; の場合、 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 ... ... config.cf の中に別の名前空間を定義していると考えてもらってかまいません。 ここは Unix より MS-DOS の config.ini 形式風ですが、 Unix では該当するものが見つからないので .ini 風となりました。 RDBMS や LDAP に対し $config の情報が必要なため、 IO::Adapter は new IO::Adapter "mysql:members", $config; のように呼び出す必要があります。 よって、各ソフトウエアでは、つねに new IO::Adapter $map, $config; と書いておく必要があります。 この $config は、 次のようなハッシュ・リファレンスになっていることが期待されています。 通常 $config は FML::Config オブジェクトそのものです。 $config => { [mysql:members] => { sql_sever => localhost ... } } FML::Config は .cf から上のようなハッシュ・リファレンスを構成します。 よって IO::Adapter で new() を呼び出す際には、 つねに FML::Config オブジェクトを引数で渡すようにしてください。 議論: SQL 構文を config.cf に書くには? (参照: fml-devel 204) 関数が呼ばれた際に、 初めて決まる変数もあるので config.cf で定義は完結しません。 lexical scope で取り扱われる変数の表現が必要になります。 現在は、この目的のために &varname 記法(下記)を使っています。 ただ、どこで呼ばれるか?を考えないと分からないので、 利用可能なローカルスコープの変数一覧表を作るのは困難です。 どうしたものかというところですが… 例: 以下は、 受信者リストとメンバーリストが食い違う例です。 メールアドレスの登録をする際に、fml_address という値が異なります。 この場合、 member と recipient は少し異なる 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' off/on 命令を無視するとか、 fml が SQL からデータを読むだけで書き込むことはしないのであれば、 話は簡単になるわけですが… 避けるのは難しいですね。