Hatena::Grouperlang

檜山正幸のErlang未確認情報 RSSフィード

2009-04-21 (火)

YAWS for Windows:余計なことを

| 10:20

YAWS for Windowsは、インストーラが勝手にパスを追加する。まー、当たり前の動作だけど、僕はハマってしまった。

Argから取れるURLパスの情報

| 13:59

Argに何でも入っているが、まず、request内にpathがある。

  • request.path {abs_path,"/app/dump/foo?bar"}

abs_pathじゃないときがあるかどうかは不明。abs_pathじゃないリクエスト? イメージできない。

argの直下にあるpathinfoがけっこう使いやすい。

  • server_path "/app/dump/foo"
  • querydata "bar"
  • prepath "/app/"
  • pathinfo "/foo"

ローカルファイルシステムとの関係は次でわかる。

  • docroot "c:/cygwin/var/yaws_site"
  • docroot_mount "/"
  • fullpath "c:/cygwin/var/yaws_site/app/dump/foo"

2009-04-14 (火)

YAWSのクッキー処理 その2

| 08:46

YAWSには、session_serverがあるが、session_serverを使わなくても次のようなことはできる。

setcookie関数で作ったヘッダを毎回ブラウザに押し込むことによって訪問回数をカウントする例。サーバー側では何も記録してないし、セッションも使ってない。

%% -*- coding: utf-8 -*-

%% @doc クッキーのテスト
-module(amod_cookie).

-export([out/1]).
-compile(export_all). % for test

-include("../../yaws/include/yaws_api.hrl").

%% @doc 一意的な訪問者IDを生成する.
%% @spec () -> string()
generate_visitor_id() ->
  "Hiyama". % 俺しかおらんわ

%% @doc 回数を1増やす.
%% 入力も出力も整数を表す文字列.
%% @spec (string()) -> string()
increment(TimesStr) ->
  Next = 
    try
      list_to_integer(TimesStr) + 1
    catch
      _:_ ->
        0
    end,
  integer_to_list(Next).


%% @doc YAWSのコールバック関数
%% @spec (#arg{}) -> term()
out(A) ->
  H = A#arg.headers,
  ReceivedCookie = H#headers.cookie,
  case yaws_api:find_cookie_val("visitor", A) of
    [] ->
      CookieHeaderVisitor = yaws_api:setcookie("visitor","Hiyama","/"),
      CookieHeaderTimes = yaws_api:setcookie("times","1","/"),
      Ehtml = {ehtml,
              {html,[],
               [
                {body, [],
                 ["I just set your cookie."]
                 }
                ]
               }
              },
      [Ehtml, CookieHeaderVisitor, CookieHeaderTimes];
    Visitor ->
      CookieText = io_lib:format("~p~n", [ReceivedCookie]),
      Ehtml= {ehtml,
              {html,[],
               [
                {body, [],
                 [
                  "Hi, " ++ Visitor ++ ".",
                  {hr, ,},
                  "I have received: " ++ CookieText,
                  ""
                 ]
                }
               ]
              }
              },
      NewTimes = increment(yaws_api:find_cookie_val("times", A)),
      CookieHeaderTimes = yaws_api:setcookie("times", NewTimes, "/"),
      [Ehtml, CookieHeaderTimes]
  end.

クッキーヘッダの構文

| 09:31

YAWSソース中にRFCからの抜粋を発見。

%%  Parse a Set-Cookie header. 
%% 
%%  RFC (2109) ports are from RFC 2965
%% 
%%  "Cookie:" cookie-version 1*((";" | ",") cookie-value)
%%  "Set-Cookie:"  cookies
%%  "Set-Cookie2:" cookies
%%  cookie-value    =       NAME "=" VALUE [";" path] [";" domain] [";" port]
%%  cookie          =       NAME "=" VALUE *( ";" cookie-av )
%%  cookie-version  =       "$Version" "=" value
%%  NAME            =       attr
%%  VALUE           =       value
%%  path            =       "$Path" "=" value
%%  domain          =       "$Domain" "=" value
%%  port            =       "$Port" "=" <"> value <">
%% 
%%  cookie-av       = "Comment" "=" value
%%                  | "CommentURL" "=" <"> http_URL <">
%%                  | "Discard"
%%                  | "Domain" "=" value
%%                  | "Max-Age" "=" value
%%                  | "Path" "=" value
%%                  | "Port" [ "=" <"> portlist <"> ]
%%                  | "Secure"
%%                  | "Version" "=" 1*DIGIT
%% 

YAWSのクッキー処理 その3:セッションサーバー

| 11:16

YAWSのセッションサーバーは、次のAPIを経由して使える。

Create a new cookie based session, the yaws system will set the cookie. The new random generated cookie is returned from this function. The Opaque argument will typically contain user data such as user name and password.

As above, but allows to set a session specific time-out value, overriding the system specified time-out value.

  • new_cookie_session(Opaque, TTL, CleanupPid)

As above, but also sends a message {yaws_session_end, Reason, Cookie, Opaque} to the provided CleanuPid where Reason can be either of timeout or normal. The Cookie is the HTTP cookie as returned by new_session() and the Opaque is the user provided Opaque parameter to new_session(). The purpose of the feature is to cleanup resources assigned to the session.

  • cookieval_to_opaque(CookieVal)
  • print_cookie_sessions()
  • replace_cookie_session(Cookie, NewOpaque)
  • delete_cookie_session(Cookie)

クッキー、クッキーと言っているが、セッションサーバーHTTPクッキーとは一切何の関係もない。 単に、タイムアウトTTL)付きでkey/value pairを保存するストレージ。キーのほうがセッションIDだが、これを cookie string = cookie value と呼んでいる。非常に誤解を与えやすく困ったもんだ。

key/value pairのvalueのほうはopaque data、まったく任意。cookie session とは、「cookie value とopaque data の対」のことだと思えばいい、それ以上の意味は何もない。セッションサーバーは内部でETSを操作しているだけ。

CRUD API関数 ETS操作
Create new_cookie_session/{1,2, 3} insert
Read (Query) cookieval_to_opaqu/1 lookup
Update replace_cookie_session/2 lookup, insert
Delete delete_cookie_session/1 delete

サンプルは後で。

2009-04-13 (月)

YAWSのクッキー処理 その1

| 18:47

「YAWSのクッキーセッション」にて:

yaws_apiにあるいくつかの関数を使う。説明をそのうち書くだろう、たぶん。

今日は、次の2つの関数だけ。

  • yaws_api:setcookie/[2-6]
  • yaws_api:find_cookie_val/2

クッキーについての記述は次が原典(つっても詳しくない)。

setcookie関数

  • setcookie(Name, Value, [Path, [ Expire, [Domain , [Secure]]]])

Sets a cookie to the browser.

クッキーのデータは、名前/値ペアの集合なので、基本は NameとValueを指定する。

引数 説明 データ型
Name セットするクッキー項目の名前 アトムまたは文字列
Value 名前に対応する値(内容) アトムまたは文字列
Path そのドメインのどのパスから下で有効になるか文字列
Expire 有効期限(無効になる時刻)文字列、秒単位までの時刻文字列
Domain どのドメインに送るか 文字列
Secure 暗号化するときだけ送るかどうか アトム on, off

このsetcookie関数は、{header,{set_cookie, "foo=bar;"} } のようなSet-Cookie:ヘッダを作って返すだけ。戻り値であるヘッダ指定をそのままYAWSに出力すればよい。関数名は make_cookie_headerとかが適切だったろう。

ブラウザにSet-Cookie:ヘッダが入ったレスポンスが届くと、クッキーの名前/値ペアがローカルストアに押し込まれる。

幸いに、次にクッキーの詳しい解説がある。

YAWSのsetcookie関数は、引数から次のようにしてSet-Cookie:の文字列を作っている。

  • " Domain="++Domain++";"
  • " Expires="++Expire++";"
  • Secureがonなら、" secure;"
  • 最後に "~s=~s;~s~s~s Path=~s", [Name,Value,SetDomain,SetExpire, SetSecure, SetPath]

EShellで実験できる。

305> {header, {set_cookie, X}} = yaws_api:setcookie("User", "hiyama"), io:format("~n~s~n", [X]).

User=hiyama;
ok
306> 

find_cookie_val関数

  • find_cookie_val(Cookie, Header)

This function can be used to search for a cookie that was previously set by setcookie/2-6. For example if we set a cookie as yaws_api:setcookie("sid",SomeRandomSid), then on subsequent requests from the browser we can call: find_cookie("sid",(Arg#arg.headers)#headers.cookie)

The function returns [] if no cookie was found, otherwise the actual cookie is returned as a string.

上記のhttp://www.studyinghttp.net/cookiesによると、クッキー文字列はこんな感じの文字列らしい。

Customer="Tarou_YAMADA"; Part_Number="IBMPC_01";

エスケープすると、 "Customer=\"Tarou_YAMADA\"; Part_Number=\"IBMPC_01\";" ですな。

値がダブルクォートされているのは仕様(構文)ではない気がする。イコールとセミコロンがデリミタで、イコールからセミコロンのあいだが問答無用で値とみなされるようだ。空白は無視されるようだが、名前とイコールの間の空白は許されない。セミコロンをエスケープする方法はないみたい(みたいみたい)。

find_cookie_val(Cookie, A) when record(A, arg) ->
    find_cookie_val(Cookie,  (A#arg.headers)#headers.cookie);

となっているので、第二引数にArgを入れても大丈夫。

find_cookie_val関数は単なる文字列処理関数で、第1引数の名前文字列をキーとする値を探して返すだけ。値が見つからなければ空文字列("" = 空リスト)を返す。ただし、テストするときは、第二引数にArgじゃなくて、文字列のリストを入れることができる。僕は、シェルでテストしてクッキー文字列の構文を推測したのだった。

Erlang文字列処理ってコンナだよ、ってサンプルとしてソースを貼っておく。


%% 第2引数文字列のリスト!

find_cookie_val(_Cookie, []) ->
    [];
find_cookie_val(Cookie, [FullCookie | FullCookieList]) ->
    case eat_cookie(Cookie, FullCookie) of
        [] ->
            find_cookie_val(Cookie, FullCookieList);
        Val ->
            Val
    end.

%% Remove leading spaces before eating.
eat_cookie([], _)           -> [];
eat_cookie([$\s|T], Str)    -> eat_cookie(T, Str);
eat_cookie(_, [])           -> [];
eat_cookie(Cookie, [$\s|T]) -> eat_cookie(Cookie, T);
eat_cookie(Cookie, Str) when list(Cookie),list(Str) ->
    try
        eat_cookie2(Cookie++"=", Str, Cookie)
    catch
        _:_ -> []
    end.

%% Look for the Cookie and extract its value.
eat_cookie2(_, [], _)    -> 
    throw("not found");
eat_cookie2([H|T], [H|R], C) -> 
    eat_cookie2(T, R, C);
eat_cookie2([H|_], [X|R], C) when H =/= X ->
    {_,Rest} = eat_until(R, $;),
    eat_cookie(C, Rest);
eat_cookie2([], L, _) -> 
    {Meat,_} = eat_until(L, $;),
    Meat.

eat_until(L, U) ->
    eat_until(L, U, []).

eat_until([H|T], H, Acc)              -> {lists:reverse(Acc), T};
eat_until([H|T], U, Acc) when H =/= U -> eat_until(T, U, [H|Acc]);
eat_until([], _, Acc)                 -> {lists:reverse(Acc), []}.

2009-04-08 (水)

YAWSのクッキーセッション

| 13:47

yaws_apiにあるいくつかの関数を使う。説明をそのうち書くだろう、たぶん。

-record(myopaque, {udata,
                   times = 0,
                   foobar}).

out(A) ->
    H = A#arg.headers,
    C = H#headers.cookie,
    case yaws_api:find_cookie_val("baz", C) of
        [] ->
            M = #myopaque{},
            Cookie = yaws_api:new_cookie_session(M),
            Data = {ehtml,
                    {html,[],
                     ["I just set your cookie to ", Cookie, "Click ",
                      {a, [{href,"session1.yaws"}], " here "},
                      "to revisit"]}},
            CO = yaws_api:setcookie("baz",Cookie,"/"),
            [Data, CO];
        Cookie ->
            {ok, OP} = yaws_api:cookieval_to_opaque(Cookie),
            OP2 = OP#myopaque{times = OP#myopaque.times + 1},
            yaws_api:replace_cookie_session(Cookie, OP2),
            Data = {ehtml,
                    {html,[],
                     [
                      "Click ",
                      {a, [{href,"session1.yaws"}], " here "},
                      "to revisit",
                      {p, [], f("You have been here ~p times", [OP2#myopaque.times])},
                      {p, [], f("Your cookie is ~s", [Cookie])}]}},
            Data
    end.

2009-04-07 (火)

YAWS for Windows

| 16:18

バージョン 1.80 からインストラー付きの win32 version of yaws が付いたようだ。Yaws-1.81-windows-installer.exe をダウンロードして実行してみたが、確かに何もしないでインストールできた。

が、起動ファイルが yaws.exeなのが不便。バッチファイルは無理だったのかな? ちょいと変更するにもCのビルド環境が必要なのは困るな。Windowsだとビルド環境が面倒だもん。[追記]自分でバッチファイル書けばいいだけのハナシだわな。[/追記]

2009-01-05 (月)

headersレコード、otherフィールド、http_headerレコード

| 17:59

#arg{}にheadersというフィールドがあり、その型は#headers{}、めぼしいHTTPヘッダはフィールドとして最初から定義されているが、その他のヘッダはotherフィールドにリストとしてまとめて入る。そのotherリストの要素がいまいちわからない。

yaws_soap_lib.erl に次のようなことが書いてあった。

%% record http_header is not defined?? 
findHeader(Label, Headers) ->
    findHeader0(yaws:to_lower(Label), Headers).

findHeader0(_Label, []) ->
  undefined;
findHeader0(Label, [{_,_,Hdr,_,Val}|T]) ->
    case {Label, yaws:to_lower(Hdr)} of
	{X,X} -> Val;
	_     -> findHeader0(Label, T)
    end;
findHeader0(_Label, undefined) ->
  undefined.

注目すべきは、関数じゃなくて %% record http_header is not defined?? というコメント。僕も、http_header というレコードを探したがどうも見つからない。推測するに、

#http_header{
 num = integer(),
 name = atom(), 
 unused = undefined,
 value = string()
}

かなり重要なヘッダがotherに入ってしまっている。nameとvalueだけ取り出してみると、次のようなヘッダがotherに入る。

  1. {'Accept-Charset', "Shift_JIS,utf-8;q=0.7,*;q=0.7"}
  2. {'Accept-Encoding', "gzip,deflate"}
  3. {'Accept-Language', "ja,en-us;q=0.7,en;q=0.3"}

yaws_apiには、上のfindHeaderのような関数が見あたらない。ユーティリティを自分で書くしかないだろう(findHeaderは借用できるが)。

2008-12-30 (火)

appmodの使い方とか

| 08:57

appmodとは、YAWSに対するのコールバックモジュールで、out/1 というコールバック関数を持つモジュールをappmodとして指定する(その指定方法は後述)と、outの引数にYAWSが持っているリクエスト情報をすべて詰め混んだレコードを渡してくれる。適当に処理して戻せばそれがレスポンスになるという仕掛け。

out/1の引数に関しては、http://yaws.hyber.org/yman.yaws?page=yaws_api

The out(Arg) function is supplied one argument, an #arg{} structure.

We have the following relevant record definitions:

の下に書いてあるが、あてになるかどうかはわからない。使うバージョンのyaws_api.hrlを見た方が確実。(マニュアルなんてそんなものよ。)「argのダンプとか」も参照。

out/1の戻り値規約は、同じページのRETURN VALUES from out/1に書いてある。よく使うのは、html, ehtml, content, header, redirectあたりだろう。その他のAPI関数http://yaws.hyber.org/yman.yaws?page=yaws_api に書いてある。このページと http://yaws.hyber.org/yman.yaws?page=yaws.conf(設定ファイル)の2つを見ればだいたい間に合う。

さて、amod_helloとamod_dumpというモジュールをappmodとして指定するには、yaws.confに次のように書く。

<server localhost>
        port = 8888
        listen = 0.0.0.0
        docroot = /tmp
        dir_listings = true
        appmods = <hello, amod_hello> <dump, amod_dump>
</server>

appmods=の後に、<パス, モジュール名>を単にズラズラと並べればよい。パスがルートなら、</, amod_hello> のように書く(気持ち悪いが)。<hello, amod_hello> は、</hello, amod_hello> でもよい(スラッシュをチャンと書いたほうが明確かもしれない)。<"/hello", amod_hello> <"/dump arg", amod_dump> のような書き方もできる。アクセスするときは、空白を %20 にすればいい。

amod_helloは例えば次のようだとする(参照「ehtmlの基本」)。

%% -*- coding: utf-8 -*-

%% @doc helloを出力するYAWSのappmod
-module(amod_hello).
-export([out/1]).

-include("../../yaws/include/yaws_api.hrl").

%% @spec (#arg{}) -> {ehtml, term()}
out(_Arg)->
  {ehtml, 
   {h1, [],
    [
     "Hello"
    ]
   }
  }.

これをコンパイルしてamod_hello.beamを作るが、ERTSにこのbeamの在処<ありか>を認識させるためには、次の方法がある。

  1. シェルスクリプトyawsの -pa、--pa オプションを使う。これはerlに-paをそのまま渡す。
  2. ~/.erlang, ./.erlang に code:add_patha/1, code:add_pathz/1 を書いておく。
  3. 環境変数ERL_LIBSを使う。

などの方法があるが、yaws.confのebin_dir項目に書いておくのが一番お手軽だろう(お手軽なだけでお奨めとは言ってない)。

ebin_dir = "c:/Documents and Settings/Hiyama/Work/yaws_ebin"

Windowsの場合、ダブルクォートすれば空白入りのパスもOK。上の指定ならば、コンパイル済みamod_hello.beamを"c:/Documents and Settings/Hiyama/Work/yaws_ebin"の下にコピーしておく。

YAWSを yaws -i または yaws -w で起動。シェルからlists:reverse(code:get_path()). としてみると、yaws.confで指定したパスが確かに追加されていることを確認できる。さらに、code:which(amod_hello)でamod_helloが見えているかどうかを確認できる。

ブラウザから http://localhost:8888/hello にアクセスすると amod_hello:out/1 の(YEWSが変換した)実行結果が表示される。パスが hello, hello/, hello?foo=bar, hello/foo/bar などなら、リクエストは全てamod_helloに渡る。「HTTP GETで取ってきてファイルに書く」関数を使って inets_util:http_dump("http://localhost:8888/hello"). とすれば、ヘッダとかも確認できる。

{"HTTP/1.1",200,"OK"}
[
  {"date","Mon, 29 Dec 2008 23:47:28 GMT"},
  {"server","Yaws/1.77 Yet Another Web Server"},
  {"content-length","15"},
  {"content-type","text/html"}
]

2008-12-24 (水)

argのダンプとか

| 12:37

YAWSを使うにはargについて知っている必要がある。学習、テスト、デバッグに、argのダンプがあると便利だろう。YAWSバージョンによってargが多少違うようだが、yaws-1.77 ベース。

書いただけで全然テストしてないが(苦笑):

%% -*- coding: utf-8 -*-

%% @doc YAWSを利用する際に便利な関数群.
-module(yaws_util).

-export([ % プロパティリストの生成
          full_arg_plist/1, std_arg_plist/1, 
          full_headers_plist/1, std_headers_plist/1,
          request_plist/1
         ]).
-export([ % プロパティリストからテーブルの生成
          full_arg_table/2, std_arg_table/2,
          full_headers_table/2, std_headers_table/2,
          request_table/2,

          full_arg_table/1, std_arg_table/1,
          full_headers_table/1, std_headers_table/1,
          request_table/1
         ]).
-export([ % ダンプ関数
          dump/1, dump_full_arg/1, dump_full_headers/1
         ]).
-export([ % テストに使うダミーデータ
          dummy_arg/0, dummy_headers/0, dummy_request/0
         ]).

-include("../../yaws/include/yaws_api.hrl").

-define(DEFAULT_TABLE_ATTRS, [{border, 2}]).

%% ==================================================

%% @doc #arg{}から、項目名/値を要素とするプロパティリストを作る.
%% @spec (#arg{}) -> [{atom(), term()}]
full_arg_plist(Arg) when is_record(Arg, arg) ->
  [
   {clisock, Arg#arg.clisock}, 
   {client_ip_port, Arg#arg.client_ip_port}, 
   {headers, Arg#arg.headers}, 
   {req, Arg#arg.req}, 
   {clidata, Arg#arg.clidata}, 
   {server_path, Arg#arg.server_path}, 
   {querydata, Arg#arg.querydata}, 
   {docroot, Arg#arg.docroot}, 
   {docroot_mount, Arg#arg.docroot_mount}, 
   {fullpath, Arg#arg.fullpath}, 
   {cont, Arg#arg.cont}, 
   {state, Arg#arg.state}, 
   {pid, Arg#arg.pid}, 
   {opaque, Arg#arg.opaque}, 
   {prepath, Arg#arg.prepath}, 
   {pathinfo, Arg#arg.pathinfo}
  ].

%% @doc #arg{}から、headersとreqを除き、項目名/値を要素とするプロパティリストを作る.
%% @spec (#arg{}) -> [{atom(), term()}]
std_arg_plist(Arg) when is_record(Arg, arg) ->
  [
   {clisock, Arg#arg.clisock}, 
   {client_ip_port, Arg#arg.client_ip_port}, 
%   {headers, Arg#arg.headers}, 
%   {req, Arg#arg.req}, 
   {clidata, Arg#arg.clidata}, 
   {server_path, Arg#arg.server_path}, 
   {querydata, Arg#arg.querydata}, 
   {docroot, Arg#arg.docroot}, 
   {docroot_mount, Arg#arg.docroot_mount}, 
   {fullpath, Arg#arg.fullpath}, 
   {cont, Arg#arg.cont}, 
   {state, Arg#arg.state}, 
   {pid, Arg#arg.pid}, 
   {opaque, Arg#arg.opaque}, 
   {prepath, Arg#arg.prepath}, 
   {pathinfo, Arg#arg.pathinfo}
  ].

%% @doc #headers{}から、項目名/値を要素とするプロパティリストを作る.
%% @spec (#headers{}) -> [{atom(), term()}]
full_headers_plist(Headers) when is_record(Headers, headers) ->
  [
   {connection, Headers#headers.connection}, 
   {accept, Headers#headers.accept}, 
   {host, Headers#headers.host}, 
   {if_modified_since, Headers#headers.if_modified_since}, 
   {if_match, Headers#headers.if_match}, 
   {if_none_match, Headers#headers.if_none_match}, 
   {if_range, Headers#headers.if_range}, 
   {if_unmodified_since, Headers#headers.if_unmodified_since}, 
   {range, Headers#headers.range}, 
   {referer, Headers#headers.referer}, 
   {user_agent, Headers#headers.user_agent}, 
   {accept_ranges, Headers#headers.accept_ranges}, 
   {cookie, Headers#headers.cookie}, 
   {keep_alive, Headers#headers.keep_alive}, 
   {location, Headers#headers.location}, 
   {content_length, Headers#headers.content_length}, 
   {content_type, Headers#headers.content_type}, 
   {content_encoding, Headers#headers.content_encoding}, 
   {authorization, Headers#headers.authorization}, 
   {transfer_encoding, Headers#headers.transfer_encoding}, 
   {other, Headers#headers.other}
  ].

%% @doc #headers{}から、otherを除いて、項目名/値を要素とするプロパティリストを作る.
%% @spec (#headers{}) -> [{atom(), term()}]
std_headers_plist(Headers) when is_record(Headers, headers) ->
  [
   {connection, Headers#headers.connection}, 
   {accept, Headers#headers.accept}, 
   {host, Headers#headers.host}, 
   {if_modified_since, Headers#headers.if_modified_since}, 
   {if_match, Headers#headers.if_match}, 
   {if_none_match, Headers#headers.if_none_match}, 
   {if_range, Headers#headers.if_range}, 
   {if_unmodified_since, Headers#headers.if_unmodified_since}, 
   {range, Headers#headers.range}, 
   {referer, Headers#headers.referer}, 
   {user_agent, Headers#headers.user_agent}, 
   {accept_ranges, Headers#headers.accept_ranges}, 
   {cookie, Headers#headers.cookie}, 
   {keep_alive, Headers#headers.keep_alive}, 
   {location, Headers#headers.location}, 
   {content_length, Headers#headers.content_length}, 
   {content_type, Headers#headers.content_type}, 
   {content_encoding, Headers#headers.content_encoding}, 
   {authorization, Headers#headers.authorization}, 
   {transfer_encoding, Headers#headers.transfer_encoding} %,
%   {other, Headers#headers.other}
  ].

%% @doc #http_request{}から、項目名/値を要素とするプロパティリストを作る.
%% @spec (#http_request{}) -> [{atom(), term()}]
request_plist(Request) when is_record(Request, http_request) ->
  [
   {method, Request#http_request.method},
   {path, Request#http_request.path},
   {version, Request#http_request.version}
  ].


%% ==================================================
%%  tables
%% ==================================================

%% @spec ({atom(), term()}) -> term()
tr({Name, Value}) ->
  % 注意:属性リストを省略できない!
  Title = if
            is_atom(Name) -> atom_to_list(Name);
            true -> Name
          end,
  Item = io_lib:format("~p", [Value]),
  {tr, [],
   [
    {td, [], [Title]},
    {td, [], [Item]}
   ]
  }.

%% @spec (PList, Attrs) -> Table
%% where
%%   PList = [{atom(), term()}]
%%   Attrs = [{atom(), term()}]
%%   Table = term()
make_table(PList, Attrs) ->
  TableContent = lists:map(fun tr/1, PList),
  {table, Attrs, TableContent}.

%% @doc #arg{}から、EHTMLテーブルを作る.
%% 引数Attrsは、tableタグに付加する属性リスト.
%% @spec (#arg{}, Attrs) -> Table
%% where
%%   Attrs = [{atom(), term()}]
%%   Table = term()
full_arg_table(Arg, Attrs) when is_record(Arg, arg) ->
  make_table(full_arg_plist(Arg), Attrs).

%% @doc #arg{}から、reqとheadersを除いたEHTMLテーブルを作る.
%% 引数Attrsは、tableタグに付加する属性リスト.
%% @spec (#arg{}, Attrs) -> Table
%% where
%%   Attrs = [{atom(), term()}]
%%   Table = term()
std_arg_table(Arg, Attrs) when is_record(Arg, arg) ->
  make_table(std_arg_plist(Arg), Attrs).

%% @doc #headers{}から、EHTMLテーブルを作る.
%% 引数Attrsは、tableタグに付加する属性リスト.
%% @spec (#headers{}, Attrs) -> Table
%% where
%%   Attrs = [{atom(), term()}]
%%   Table = term()
full_headers_table(Headers, Attrs) when is_record(Headers, headers) ->
  make_table(full_headers_plist(Headers), Attrs).

%% @doc #headers{}から、otherを除いて、EHTMLテーブルを作る.
%% 引数Attrsは、tableタグに付加する属性リスト.
%% @spec (#headers{}, Attrs) -> Table
%% where
%%   Attrs = [{atom(), term()}]
%%   Table = term()
std_headers_table(Headers, Attrs) when is_record(Headers, headers) ->
  make_table(std_headers_plist(Headers), Attrs).

%% @doc #http_request{}から、EHTMLテーブルを作る.
%% 引数Attrsは、tableタグに付加する属性リスト.
%% @spec (#http_request{}, Attrs) -> Table
%% where
%%   Attrs = [{atom(), term()}]
%%   Table = term()
request_table(Request, Attrs) when is_record(Request, http_request) ->
  make_table(request_plist(Request), Attrs).

%% @equiv full_arg_table(Arg, DEFAULT_TABLE_ATTRS)
%% @spec (#arg{}) -> Table
%% where
%%   Table = term()
full_arg_table(Arg) when is_record(Arg, arg) ->
  make_table(full_arg_plist(Arg), ?DEFAULT_TABLE_ATTRS).

%% @equiv std_arg_table(Arg, DEFAULT_TABLE_ATTRS)
%% @spec (#arg{}) -> Table
%% where
%%   Table = term()
std_arg_table(Arg) when is_record(Arg, arg) ->
  make_table(std_arg_plist(Arg), ?DEFAULT_TABLE_ATTRS).

%% @equiv full_header_table(Headers, DEFAULT_TABLE_ATTRS)
%% @spec (#headers{}) -> Table
%% where
%%   Table = term()
full_headers_table(Headers) when is_record(Headers, headers) ->
  make_table(full_headers_plist(Headers), ?DEFAULT_TABLE_ATTRS).

%% @equiv std_header_table(Headers, DEFAULT_TABLE_ATTRS)
%% @spec (#headers{}) -> Table
%% where
%%   Table = term()
std_headers_table(Headers) when is_record(Headers, headers) ->
  make_table(std_headers_plist(Headers), ?DEFAULT_TABLE_ATTRS).

%% @equiv request_table(Request, DEFAULT_TABLE_ATTRS)
%% @spec (#http_request{}) -> Table
%% where
%%   Table = term()
request_table(Request) when is_record(Request, http_request) ->
  make_table(request_plist(Request), ?DEFAULT_TABLE_ATTRS).
 
%% ==================================================
%%  dumpers
%% ==================================================

%% @doc #arg{}に含まれるすべての情報を多少整理変形して表示する.
%% @spec (#arg{}) -> EHTML
%%  where
%%    EHTML = {ehtml, term()}
dump(Arg) ->
  {ehtml,
   {html, [],
    [
     {body, [],
      [
       request_table(Arg#arg.req),
       full_headers_table(Arg#arg.headers),
       std_arg_table(Arg)
      ]
     }
    ]
   } % /html
  }. % /ehtml

%% @doc #arg{}に含まれるすべての情報を表示する.
%% @spec (#arg{}) -> EHTML
%%  where
%%    EHTML = {ehtml, term()}
dump_full_arg(Arg) ->
  {ehtml,
   {html, [],
    [
     {body, [],
      [
       full_arg_table(Arg)
      ]
     }
    ]
   } % /html
  }. % /ehtml

%% @doc #headers{}に含まれるすべての情報を表示する.
%% ただし、引数は#arg{}である点に注意
%% @spec (#arg{}) -> EHTML
%%  where
%%    EHTML = {ehtml, term()}
dump_full_headers(Arg) ->
  Headers = Arg#arg.headers,
  Table = full_headers_table(Headers),
  {ehtml,
   {html, [],
    [
     {body, [],
      [
       Table
      ]
     }
    ]
   } % /html
  }. % /ehtml

%% ==================================================
%%  dummy data
%% ==================================================

%% @doc テストに使うダミー#arg{}.
%% @spec () -> #arg{}
dummy_arg() ->
  #arg {
           clisock = "clisock", 
           client_ip_port = "client_ip_port", 
           headers = dummy_headers(), 
           req = dummy_request(), 
           clidata = "clidata", 
           server_path = "server_path", 
           querydata = "querydata", 
           docroot = "docroot", 
           docroot_mount = "docroot_mount", 
           fullpath = "fullpath", 
           cont = "cont", 
           state = "state", 
           pid = "pid", 
           opaque = "opaque", 
           prepath = "prepath", 
           pathinfo = "pathinfo"
          }.

%% @doc テストに使うダミー#headers{}.
%% @spec () -> #headers{}
dummy_headers() ->
  #headers {
               connection = "connection", 
               accept = "accept", 
               host = "host", 
               if_modified_since = "if_modified_since", 
               if_match = "if_match", 
               if_none_match = "if_none_match", 
               if_range = "if_range", 
               if_unmodified_since = "if_unmodified_since", 
               range = "range", 
               referer = "referer", 
               user_agent = "user_agent", 
               accept_ranges = "accept_ranges", 
               cookie = "cookie", 
               keep_alive = "keep_alive", 
               location = "location", 
               content_length = "content_length", 
               content_type = "content_type", 
               content_encoding = "content_encoding", 
               authorization = "authorization", 
               transfer_encoding = "transfer_encoding", 
               other = ["other1", "other2"]
              }.

%% @doc テストに使うダミー#http_request{}.
%% @spec () -> #http_request{}
dummy_request() ->
  #http_request{
               method = "method",
               path = "path",
               version = "version"
               }.

[追記]動かしてみたら、テーブルのレイアウトがひどすぎるのが判明。少しは直そう、そのうち。[/追記]

2008-12-22 (月)

ehtmlの基本

| 08:46

要素を表す構文の基本は3項タプル:

  • {タグ名, 属性リスト, 内容リスト}

{h1, [],
  [
     "Hello, EHTML."
  ]
}

意味は自明だろう。ehtmlとして認識させるには、目印ehtmlを付けて

  • {ehtml, EHTMLターム}

とする。

{ehtml, 
   {h1, [],
     [
       "Hello, EHTML."
     ]
   }
}.

http://yaws.hyber.org/yman.yaws?page=yaws_api に次の記述があっった。

{ehtml, Term}
  This will transform the erlang term Term into a stream  of  HTML
  content. The basic syntax of Term is

  EHTML = [EHTML] | {Tag, Attrs, Body} | {Tag, Attrs} | {Tag} |
          binary() | character()
  Tag   = atom()
  Attrs = [{Key, Value}]  or {EventTag, {jscall, FunName, [Args]}}
  Key   = atom()
  Value = string()
  Body  = EHTML

つまり、

  1. 内容リストは省略できる。
  2. 内容リストと属性リストの両方を省略できる。
  3. だが、属性リストだけの省略は不可能。
  4. アトムだけのtagはさすがにダメか。構文的には、やろうと思えばできるけどね。
  5. テキストノードの表現には、やはりbinary()が使える。
  6. X = [X] | binary() | character() | ... であるから、iodata() 型を包含する。
  7. 属性値にはバイナリが使えない。少し残念。
  8. 用語的に言えば、bodyはcontentの間違い。
  9. 結局EHTMLは要素とは限らず、内容(バランストテキスト、フラグメント)の表現となる。
  10. {EventTag, {jscall, FunName, [Args]}} はonloadとかのことだろうが、なぜ特別扱いしているか不明。

XMerlのショートハンドと互換性はないような気がするが、XMerl構文のほうを忘れてしまったので、にわかに断定はできない。

まー、常識的な感覚(?)で書いていればそうは間違えないだろう。

ChrisChris2012/10/08 18:35I have exalcty what info I want. Check, please. Wait, it's free? Awesome!

ehiuwhjehiuwhj2012/10/09 05:41dRVMpk <a href="http://mwqynuzrielz.com/">mwqynuzrielz</a>

cikndmzkucikndmzku2012/10/11 13:543HjQHE <a href="http://esjngyqtffkz.com/">esjngyqtffkz</a>

xuremzxuremz2012/10/12 03:1521avPK , [url=http://alsrcycxgrye.com/]alsrcycxgrye[/url], [link=http://mftdjxvbmeto.com/]mftdjxvbmeto[/link], http://cqcupvbysolc.com/