HTTPリクエスト(fetch)
rontolisp パッケージはJavaScriptの fetch APIをモデルにした外向きHTTPと、
それと自然に組み合わせられるJSON関数を提供します。いずれも Common Lisp の
一部ではないため、rontolisp: 修飾子で参照します
(パッケージを参照)。rontolisp:fetch は
リクエストを開始して即座に プロミス を返します。汎用のプロミス操作が
それを解決・変換し、rontolisp:json-parse / rontolisp:json-stringify が
JSONドキュメントとLispの値を相互変換します。
| 関数 | 用途 |
|---|---|
rontolisp:fetch | HTTPリクエストを開始する: (rontolisp:fetch url &optional options) |
rontolisp:await | プロミスが確定するまでブロックして値を返す |
rontolisp:then | 確定値にコールバックを適用する新しいプロミスを導出する |
rontolisp:promisep | 値がプロミスなら t |
rontolisp:json-parse | JSON文字列をLispの値にパースする |
rontolisp:json-stringify | Lispの値をJSON文字列にシリアライズする |
バックエンドのサポート。 インタプリタとJVMコンパイル済みクラスはJDKの
java.net.http.HttpClientを使い、fetchが返った瞬間からリクエストは バックグラウンドスレッドで走ります。WASMバックエンドは componentモード 専用 です (--component。wasi:http@0.2をimportするハイブリッド):fetchはPreview 1 (コアモジュール) モードではコンパイルエラーになり、 fetchを使うcomponentは非同期フラグに加えて-S http=yを付けて実行する 必要があります。ブラウザプレイグラウンド では本物のブラウザのfetch()が実行され (CORSの制約を受けます)、その間プログラムは続行 します。プロミス操作 (await/then/promisep) とJSON関数は すべての バックエンド・すべてのWASMモードで動作します — 制限が あるのはfetch自体だけです。
最初のリクエスト
fetch はリクエストが飛び始めたらすぐに返ります。プロミスを
rontolisp:await に渡すとレスポンスの到着までブロックし、結果の
プロパティリスト (:status <integer> :body <string> :headers <alist>) が
得られます:
個々のフィールドの読み取り:
(let ((res (rontolisp:await (rontolisp:fetch "http://example.com/"))))
(print (getf res :status)) ; => 200
(print (getf res :body)) ; => "<html>...</html>"
(print (getf res :headers))) ; => (("content-type" . "text/html") ...)
リクエストのオプション
省略可能な第2引数はオプションのプロパティリストで、:method
(文字列、デフォルト "GET")、:headers ((name . value) の文字列ペアの
alist)、:body (文字列) を指定できます:
;; GET with request headers (an alist of (name . value) string pairs)
(rontolisp:fetch "http://example.com/api"
(list :headers (list (cons "Accept" "application/json"))))
;; POST with a request body
(rontolisp:fetch "http://example.com/api"
(list :method "POST"
:headers (list (cons "Content-Type" "application/json"))
:body "{\"name\":\"rontolisp\"}"))
サポートされるメソッドは GET、HEAD、POST、PUT、DELETE、OPTIONS、
PATCH です。バックエンドごとのバリデーションのタイミングとエラー時の
挙動 (リクエストの失敗は fetch 時ではなく await 時に表面化します —
インタプリタとJVMはエラーをシグナルし、WASMは nil を返します) は
fetch
のリファレンスページを参照してください。
プロミス
fetch が返った時点でリクエストは既に走っているので、複数のリクエストは
オーバーラップします — 全部開始してからそれぞれをawaitします:
(let ((p1 (rontolisp:fetch "http://example.com/a"))
(p2 (rontolisp:fetch "http://example.com/b"))) ; both requests running
(list (rontolisp:await p1) (rontolisp:await p2)))
rontolisp:then は確定値を変換する新しいプロミスを導出します。JavaScriptの
Promise.prototype.then と同様に呼び出しはチェーンでき、プロミスを返す
コールバックは平坦化されます:
どちらの操作も汎用です: await はプロミス以外の値をそのまま通し、then も
プロミス以外を受け付けるので、プロミスかもしれない値を一様に扱えます。
rontolisp:promisep で両者を見分けられます:
then のコールバックは最初の await 時に遅延実行され、結果はメモ化され
ます (確定したプロミスは何度でもawaitできます)。正確なタイミングは
then
のリファレンスページを参照してください。
JSONの扱い
rontolisp:json-parse はJSONドキュメントをLispの値に変換します。デフォルト
ではJSONオブジェクトはキーワードをキーとするプロパティリストになるので、
結果は getf で読めます。配列はリストに、true/false/null は
t/nil になります:
:hash-table を渡すと、代わりに文字列キーのハッシュテーブルが返ります —
キーが任意の文字列のときや、空オブジェクトを nil と区別し続けたいときに
使ってください:
rontolisp:json-stringify はその逆です: キーワードのプロパティリストと
ハッシュテーブルはオブジェクトに、その他のリストは配列にシリアライズ
されます:
どちらの関数もrontolisp自身で書かれていて、使われたときにプログラムに
コンパイルされ、すべてのバックエンドで動作します。値の対応表と
エッジケース (整数の桁数、nil の曖昧さ、キーの順序) の全体は
json-parse と
json-stringify
のリファレンスページにあります。
完全なプログラム
これらの部品を組み合わせると、JSON APIの典型的な往復になります:
json-stringify でリクエストボディを作り、POSTし、レスポンスをawaitして
ボディを json-parse でパースします。以下を fetch-post.lisp として保存
してください:
(let* ((payload (rontolisp:json-stringify (list :name "rontolisp" :stars 1)))
(res (rontolisp:await
(rontolisp:fetch "https://httpbin.org/post"
(list :method "POST"
:headers (list (cons "Content-Type" "application/json"))
:body payload))))
(json (rontolisp:json-parse (getf res :body))))
(print (getf res :status))
(write-line (getf json :data)))
200
{"name":"rontolisp","stars":1}
実行方法
インタプリタで:
rontolisp fetch-post.lisp
JVMクラスにコンパイルして (クラス名は出力ファイル名から付きます):
rontolisp fetch-post.lisp -o FetchPost.class
java FetchPost
WASM componentにコンパイルして (wasmtime 46+。外向きHTTPを許可する
-S http=y に注意 — これがないと wasi:http のimportが提供されず、
インスタンス化に失敗します):
rontolisp fetch-post.lisp -o fetch-post.wasm --component
wasmtime run -W gc=y -W component-model-more-async-builtins=y -S http=y fetch-post.wasm
HTTPではなく素のTCPを使う場合 — あるいは サーバー 側を実装する場合 — は TCPソケットガイドを参照してください。