t-cool

Node.js入門: 導入 - Node.jsでアプリケーションを書くための入門ガイド

Node.js入門: 導入

Node.jsでアプリケーションを書くための入門ガイド


本記事は、原著者の許諾のもと、翻訳・掲載しています。

Getting started with Node.js: Introduction / Nilesh Singh


1989年、Tim Berners-Lee氏が、今日、インターネットとして世に知られているビジョンを提案しました。当時、その提案は十分に受け入れられませんでしたが、彼はWebの初期バージョンを開発する時間を与えられることになり、その結果、HTML、URI、HTTP、ブラウザ(WorldWideWeb)が、初のWebサーバhttpdと共に公開されました。

それ以降、その提案は我々にとって必要不可欠になり、httpdサーバは、今日、Webサービスが書かれる際の中心的なアイデアになりました。

では、サーバとはどのようなもので、なぜWebの設計において必要なのでしょうか。Wikipediaでは、サーバについて、次のように書かれています:

サーバとは、「クライアント」と呼ばれる他のプログラムや機器に、機能を提供するコンピュータプログラムか機器である。

サーバとは、リモートコンピュータであり、ユーザのリクエストを受けて、その結果を返すものです。では、サーバにはどのようなリクエストが入ってきて、また、どのように受け入れるのでしょうか?この部分が、サーバサイドプログラミングが扱うところです。我々はこれまで、Java、Ruby on Rails、Django、PHP、ASP.NET、Apache HTTP Server等を、ビジネスにで用いてきました。それらは、期待にそぐわないと見なされるまでは、すごく良いものでした。

これらのフレームワークは、用いられる目的において優れており、産業界でも広くシェアを獲得してきました。しかし、インターネットが進化するにつれて、よりダイナミックなサーバが必要とされるようになりました。

クライアント側のアプリケーションが数百マイクロ秒ごとにコンテンツを更新するように求めるような、長いポーリングを扱うアプリケーションを作ると仮定しましょう。

先ほどのテクノロジーを用いると、クライアントがリクエストを停止するのを確認するまでスレッドが残るので、スレッドが作られるごとに機能は低下します。

これは、1秒間に数千の新しいクライアントを処理するシステムには対処できるようにみえますが、C10K問題が起こると、サーバは対処できなくなります。

image01.png

複数の機器を使うと、最近のコンテンツのために、サーバにポーリングをすることができます。

Node.jsやNginxのようなイベント駆動の設計では、C10K問題を扱うのにより効率的です。実際、どちらも、その問題を解決するために開発されました。では、Node.jsとはどういうものであり、何ができて、どのように実行しているのかに焦点をあてて見ていきましょう。Nginxについては、後の記事で紹介します。

結局、Node.jsとは何なのか?

Node.jsは、2009年はじめに、Ryan Dahlさんに書かれました。Ryanさんは当時、大量のリクエストを同時に処理できないこと、また、実行ブロックスタイル(逐次プログラミング)を用いる既存のWebサーバに対して批判的でした。Ryanさんは、並行性を念頭に、Google ChromeのオープンソースのJavaScriptエンジンV8とイベントループを用いてNode.jsを書きました。V8とイベントループを組み合わせたことで、Node.jsは、複数の同時リクエストを、シングルスレッド上でI/Oブロッキングなしで処理できるようになりました。しかし、どのようにして実現できたのでしょうか?

イベントループを理解する

Node.jsにおいて、全ては非同期のタスクです。入力スクリプトの初期化からシステムのコールバックにいたるまで、最初から全ては非同期であり、あるタスクが終了しても失敗しても発動(fire)します。

イベントループは、主に、シングルスレッドのJavaScript環境をシステムのカーネルに引き渡すために機能します。

最近の大半のOSは、非同期のタスクを渡すためのカーネルインターフェイスを提供していますが、提供していないOSもあります。そのような場合は、Node.jsにイベントループを提供するLibevというライブラリを使います。Libevは、非同期のI/Oタスクを渡すために、4つのスレッドを作ります。

Node.js内部の働きについて興味があるようでしたら、このチュートリアルは役にたつでしょう。イベントループとその内部の振る舞いについては、別の記事で紹介します。

Node.jsでサーバを起動する

簡単なNode.jsアプリケーションを書くには、すべきことが2つあります。1つ目は、Node.jsが提供するHTTPモジュールをインポートすることです。JavaScript共通のrequireメソッド、もしくは、ECMAScriptのimportを使ってモジュールをインポートした後、createServerメソッドでサーバを初期化する必要があります。createServerメソッドは、コールバック関数を、引数、リクエスト、レスポンスと一緒に登録します。

const http = require('http')
const server = http.createServer((req, res) => {
    res.setHeader('Content-Type', 'text/plain')
    res.end('Hello, this is a sample server!')
})
server.listen(8080)

先ほども言及した通り、Node.jsにおいては全てのコードは、非同期のタスクです。createServerメソッドがイベントループと一緒に登録したコールバック関数は、新しいリクエストがサーバにヒットする度に呼び出されます。そして、リクエストオブジェクトとレスポンスオブジェクトは、スレッドプールかOSカーネルの非同期のインターフェイスによって準備されます。

コールバックの後に続くことは単純明快です。リクエストオブジェクト(req)を受けとり、求められる詳細を引き出し、要求に応じて、レスポンスオブジェクト(res)をリクエストの送り主(クライアント)に返します。本当にそんなに単純なのでしょうか?では、これらのコールバックオブジェクトについて見ていきましょう。

リクエストオブジェクトを理解する

リクエストは、その名前が示すように、サーバにヒットする全てのリクエストを処理するハンドラです。reqは、ただのJavaScriptオブジェクトであり、サーバにリクエストとして認められるのに必要な情報(クライアントのIPアドレス、ホスト名、ヘッダ、ボディー部(JSON等の形式)、パラメータ、クエリ文字等)を含みます。

# リクエストのURLに含まれるパラメータを読む
const params = req.params

リクエストのプロパティを正しくパースして、適切にレスポンスを返すのは、サーバの責任です。Node.jsはリクエストをパースしますが、リクエストオブジェクトを読み、リクエスト-レスポンスのトランザクションを終わらせるのは、プログラマーの仕事です。

リスポンスオブジェクト

トランザクションを完了するために、有効なレスポンスをユーザに送る必要があります。しかし、何が有効で、何が有効でないのでしょうか?また、どのように、また何をレスポンスとして返すのでしょうか?

Node.jsは、そのような場面を対処するために、最低限のAPIを提供しています。リクエストのコールバックとして受け取るレスポンスオブジェクトは、クライアントに応答するために必要な全てのメソッドを保持します。レスポンスヘッダーを設定するためのsetHeader、データをレスポンスのボディー部にアタッチするために用いられるappend、また、レスポンスのプロセスを終わらせるためのend等のメソッドを含みます。

res.setHeader('Content-Type', 'text/plain')

事前に定義されたメソッドを用いることは必ずしも有効であるとは限りませんが、それは確かにエラーを最小限に留めてくれます。

結局、何を受け入れるのか?

最後のパズルのピースは、listenメソッドです。サーバのインスタンスを初期化した後、なぜlistenメソッドが必要なのでしょうか。

ネットワークの授業を受けたことがあれば、クライアントは任意である一方で、サーバーは全てのクライアントにIPアドレスが知られている1つのシステムだと知っているでしょう。全てのクライアントに知らせるために、サーバはlistenとacceptの仕組みを実装します。一度リクエストが確立されると、クエリに保持されて、サーバはアクセプトする準備ができます。

listenメソッドは Node.jsでlistenの部分を担い、createServercallbackは acceptの部分を処理します。listenは4つのパラメータをうけとります。1つ目はNode.jsが動いているポート番号です。2つ目はホスト名であり、サーバ内のプロセスを特定するためのポート番号を伴います。

const PORT = 8080,
      HOSTNAME = '127.0.0.1',
      BACKLOG = 10

server.listen(PORT, HOSTNAME, BACKLOG, () => {
    console.log('Server is now listening on', PORT)
})

3つ目とパラメータは、リスナーのクエリを保留するためのbacklogです。4つ目のパラメータは、リスナーが最初に成功したときに呼び出されるコールバック関数です。

全てのパラメータのlistenの受け入れはオプショナルですが、常にポート番号とホスト名、またbacklogも割り当てておくことが良いです。理由は、そのデフォルト値にあります。もしOSが独自のポートを見つけられない場合や、0に設定された場合、OSは使用されていないポート番号をプロセスに割り当てます。同様のことがホスト名にも当てはまります。もしサーバが独自のホスト名を見つけられない場合、IPv4では(0.0.0.0)、IPv6では(::)で接続のアクセプトを開始します。どちらの値も、プロキシサーバの後ろで動作させるケースや、80番ポートで起動させるといった運用環境では受け付けられません。

上のコードを用いると、どのようなリクエストに対しても、"Hello, this is a sample server!"という文字列を返すサーバを起動することができます。 上のコードを(server.js)という名前で保存して、次のように起動してください:

node server.js

起動すると、リスナーのコールバック関数で定義したように、"Server is now listening on 8080"というログが表示されるでしょう。Node.jsで優れている点は、listenするために、CPUのリソースが食いつぶされないことでしょう。新しいリクエストが処理されるまでの間、じっと休止しています。

では、ここまでのNode.js入門をまとめましょう。Node.jsとは何なのか、またNode.jsがスレッド対応に至った背景、イベントループについて、HTTPサーバの起動方法について見てきました。Node.jsでのリクエストとレスポンスのオブジェクトについても学びました。また、Node.jsが、イベントループの助けをかりて、新しいリクエストをlisten, acceptして、リクエスト-レスポンスんのトランザクションを完了しているかを見てきました。

もし、この記事について、何か問題や質問があれば、下のコメントからお願いします。あなたからのクエリに喜んでリスポンドします。また、もしこの記事が役にたつと思ったら、星ボタンを押していただき、周りの人に勧めて共有してください。

ClojureScriptでコマンドライン・スクリプトを書く


本記事は、原著者の許諾のもと、翻訳・掲載しています。 Command line scripting with ClojureScript / akiroz


ClojureScriptコミュニティによる開発のおかげで、コマンドラインスクリプトClojureで書くのが楽しくなってきました。ClojureScriptの中心チームとlumoを開発しているanmonteiroさんには、心から敬意を表します。

Clojureは、データを処理するための短いスクリプトを書くのに良い言語だと思います。操作用の関数やイミュータブルな構造が言語に組み込まれているので、参照性やdeep-cloningについて心配する必要がありません。

Lumoでスクリプトを起動する

では、始めるために簡単な方法を紹介します。lumoをシステムにインストールして、Clojureのファイルを起動します。hello.cljsという名前でファイルを作り、次のように編集して保存してください。

(println "Hello World!")

では、次のように実行してみましょう。

$ npm i -g lumo-cljs # lumoをインストールする
$ lumo hello.cljs
Hello World!

簡単でしょう? では次は、より実用的なプログラムを書くために、Node.jsのAPIを利用する方法を紹介します。

;; 次のプログラムでは、https://randomuser.meで生成されたjsonのペイロードをパーズして、
;; データの一部を抽出します。予め、次のコマンドでデータをダウンロードしてください。
;; $ wget 'https://randomuser.me/api/?results=10' -O randomUsers.json

;; 注: シングルクォートをお忘れなく
(require
  '[clojure.string :refer [capitalize]]   ;; Clojureのライブラリをrequireします
  '[fs :refer [writeFileSync]]            ;; Node.JSのfilesystem関連のモジュールをrequireします
  '["./randomUsers.json" :as input-json]  ;; JSONファイルをJSのデータとしてrequireします
  )

(defn parse-user
  [{:keys [email name]
    {:keys [username password]} :login}]
  {:id         (str (random-uuid)) ;; built-in
   :username   username
   :password   password
   :email      email
   :full-name  (str (capitalize (:first name))
                    " "
                    (capitalize (:last name)))
   })

(let [results (:results (js->clj input-json :keywordize-keys true))]
  (writeFileSync
    "randomUsers.edn"
    (pr-str (mapv parse-user results))))

ご覧の通り、このrequire関数は、JSのrequire関数と同じように動作します。次のようにすることと同じです。

const { writeFileSync } = require('fs');
const inputJson = require('./randomUsers.json');

npmからインストールされるnode_modulesにあるモジュールとも上手く動作します(現状lumo1.8.0)。残念ながら、Clojure側の依存関係を管理する簡単な方法はありません。現状は、Clojureのライブラリが収められるjarファイルを手動で管理する必要があります。詳しくは、lumoのWikiを参考にしてください。

npmとの連携

少し大きなプロジェクトのためには、npmで依存関係を管理するために、package.jsonを使いたいでしょう。 もしlumoをグローバル環境にインストールしたくなければ、lumoをプロジェクト内だけにインストールすることもできます。

次の例では、上の例と同様に動作します。NPMにあるrequestのライブラリを使ってJSONhttps://randomuser.me から取得します。先ほどのコードを、適切に名前空間をつけて、2つのClojureScriptのファイルに分割します。

my-tool
|\_ package.json
 \_ src
     \_ my_tool
        |\_ core.cljs
         \_ user.cljs

Clojure名前空間のシステムは、ディレクトリ構造を反映するので、my-tool.coreは、my_tool/core.cljsでなければいけません。名前空間でハイフンで区切られた名前は、ファイルシステムではsnake_caseに変換されなければいけません。

core.cljs:

(ns my-tool.core
  (:require [my-tool.user :as user]
            [fs :refer [writeFileSync]]
            ;; request library from npm, since the imported name
            ;; is the same as the package name, :as isn't needed.
            ;; const request = require('request');
            [request]))

;; The -main function is called by lumo with CLI args
(defn -main [n]
  (request
    #js{:url (str "https://randomuser.me/api/?results=" n)
        :json true}
    (fn [_ _ body]
      (let [results (:results (js->clj body :keywordize-keys true))]
        (writeFileSync
          "randomUsers.edn"
          (pr-str (mapv user/parse results)))))))

user.cljs:

(ns my-tool.user
  (:require [clojure.string :refer [capitalize]]))

(defn parse
  [{:keys [email name]
    {:keys [username password]} :login
    }]
  {:id         (str (random-uuid))
   :username   username
   :password   password
   :email      email
   :full-name  (str (capitalize (:first name))
                    " "
                    (capitalize (:last name)))
   })

package.json:

{
  "name": "my-tool",
  "version": "1.0.0",
  "scripts": {
    "start": "lumo -c src -m my-tool.core"
  },
  "dependencies": {
    "lumo-cljs": "^1.8.0",
    "request": "^2.85.0"
  }
}

package.json内の"script"の行をご覧ください。

-cフラグは、lumoに対して、あなたのソースコードがどこにあるのかを知らせます。

-mフラグは、あなたの-main関数がどの名詞空間にあるのかを特定します。

ではこのツールをnpmを使って起動してみましょう。

$ npm install
$ npm start 12 ## 12人のユーザを取得して、randomUsers.ednを出力します

REPLでの開発

Clojureを経験するのに、REPLを元にしたインタラクティブな開発なしには終われません。 package.jsonscriptsセクションに次の行を足してください。

"repl": "lumo -c src -i src/my_tool/core.cljs -n 5777 -r"

-iフラグは、core.cljsを起点にREPLを初期化します。

-nフラグは、portを5777番でソケットのREPLを開始します。

-rフラグは、ターミナルでREPLを起動します。

このようにしてREPLを開始することで、状態(state)を失うことなく、任意のコードをあなたのランタイムで実行できます。

$ npm run repl
...
cljs.user=> (in-ns 'my-tool.core) ;; switch to our core namespace
my-tool.core=> (user/parse {:name {:first "john" :last "smith"}})
{:id "c1b61773-133e-434c-afbd-d82b95b814d3",
 :username nil,
 :password nil,
 :email nil,
:full-name "John Smith"}

さいごに

現状でのツールには荒い点がいくつかありますが、Clojureコマンドラインスクリプトを書き始めるのも、選択肢の1つにあげてもいい時期にきています。Lumoの起動時間は、Clojureが起動する時間と比べてはるかに早いので、新鮮な感じがすると思います。

もしClojureが好きならば、この記事の内容を試してみてください。

Radiance - Common Lisp Webアプリケーション開発環境


原著者の許諾のもと、翻訳・掲載しています。

Radiance: A Common Lisp web application environment , written by Shirakumo


Radianceについて

Radianceは、Webアプリケーションの開発環境です。Webフレームワークの一種ですが、汎用的で、変更も容易です。 特別な変更を加える必要なく、WebサイトやWebアプリを簡単に書くことができます。

入手するには

Radiance、また関連するモジュールやアプリケーションは、Quicklispを通して配布されています。Radianceをインストールするには次のようにしてください。

(ql-dist:install-dist "http://dist.tymoon.eu/shirakumo.txt")
(ql:quickload :radiance)

以上で、Quicklispのquickloadコマンドで、Purplish等のRadianceモジュールを利用できるようになります。

チュートリアル

Radianceのチュートリアルはこちらです。Radianceの重要コンセプトや、Webアプリの書き方を紹介しています。チュートリアルを通して、Radianceの使い方に慣れることができます。また、特定の機能が必要なときに、どこを調べるべきかが分かります。チュートリアルの最後では、本番環境でのRadianceのインストールとデプロイ方法について説明しています。

簡単な例

あなたが一番したいことは、ユーザにHTMLを提供することでしょう。その方向に進めながら、少しずつ拡張していきます。まずは、Radianceの準備をします。

(ql:quickload :radiance)
(radiance:startup)

初めてRadianceを使う場合には、r-welcomeモジュールを使ってください。r-welcomeモジュールを使うと、ブラウザーでの最初のページにリンクさせることができます。まずは、小さなページにリンクをはりましょう。

(in-package :rad-user)

(define-page example "/example" ()
  (setf (content-type *response*) "text/plain") 
  "Hi!")

localhost:8080/exampleにアクセスすると、"Hi"と表示されるはずです。これでは、かなり退屈ですね。では、cl-whoを用いてHTMLを出してみましょう。

(ql:quickload :cl-who)
(define-page example "/example" ()
  (cl-who:with-html-output-to-string (o)
    (cl-who:htm
     (:html
      (:head (:title "Example Page"))
      (:body (:header (:h1 "Couldn't Be Simpler."))
             (:main (:p "Trust me on this one.")))))))

普通であれば、フォントの書式を変えたり、CSSファイルを追加しながら、ページのスタイルを作っていきます。CSSファイルをサーブすることは可能ですが、長い目でみれば、最適の方法とはいえません。

ここでは、モジュール(module)を作る方法をご紹介します。モジュールを使うことで、Webページを適切に整理することができます。モジュールに必要なファイルは手動でも作れますが、ここでは雛形でモジュールを自動生成します。

(create-module "example")

モジュールが自動生成されたあと、モジュールへのパスが返されます。生成されるものは、ASDFのシステムファイル、lispのmainファイル、2つのフォルダ(statictemplate)です。

staticフォルダには、静的にサーブされるファイルが入ります。templateには、テンプレートエンジン関連のファイルが入ります。

example.lispを開いて、先ほどのコードを元にページを定義しましょう。

(define-page example "/example" ()
  (cl-who:with-html-output-to-string (o)
    (cl-who:htm
     (:html
      (:head (:title "Example Page"))
      (:body (:header (:h1 "Couldn't Be Simpler."))
             (:main (:p "Trust me on this one.")))))))

ページは、シンボル名によって特定されます。自分のモジュールを作ると、パッケージが生成されます。前の例のシンボルは、一度も使われていないシンボルです。名前衝突を避けるために、rad-userパッケージでページを消す必要があるかもしれません。

(remove-page 'rad-user::example)

次に、簡単なCSSファイルを作りましょう。staticフォルダに、example.cssという名前で作成してください。自分でCSSを書くのが面倒な方は、次のCSSコードをお使いください。

body{
    font-family: sans-serif;
    font-size: 12pt;
    background: #EEE;
}

header{
    text-align: center;
}

main{
    width: 800px;
    margin: 0 auto 0 auto;
    background: #FFF;
    padding: 10px;
    border: 1px solid #BBB;
    border-radius: 5px;
}

では、CSSファイルにリンクするように、HTMLを修正しましょう。アドレスがスタイルシートにたどり着くために、後の章ではルーティングシステムを使いますが、今はその必要はありません。焦らずに、進めていきましょう。

(define-page example "/example" ()
  (cl-who:with-html-output-to-string (o)
    (cl-who:htm
     (:html
      (:head (:title "Example Page")
             (:link :rel "stylesheet" :type "text/css" 
                    :href (uri-to-url "/static/example/example.css" :representation :external)))
      (:body (:header (:h1 "Couldn't Be Simpler."))
             (:main (:p "Trust me on this one.")))))))

ページを再度読み込むと、スタイルが適用されているはずです。uri-to-urlが動作する原理については、後ほど詳しく説明します。大切なことは、どのようにセットアップしても、静的ファイルへのリンクは適切に解決されるということです。

1. Radianceのコンセプトと部品

1.1 URI

Radianceの中心的なコンセプトはURIです。URIはオブジェクトであり、ドメインポート番号(オプション)パスを含むリストで構成されます。

RadianceのURIは、一般的なURIから要素を抽出したもので、スキーマ、クエリ、フラグメント等は含みません。また重要な違いとして、domainsのURIは、フレームワークにおいて、複数の場所で使われます。例えば、locationを捕捉するときや、dispatch matchingを処理するために使われます。

URIは変更可能です。URIの修正は、クリティカルパスがある場所で行われるので、パフォーマンス上、重要です。想定外の方法でURIを修正した場合、予期しない動作が発生する可能性があります。

URIは、単一の文字列で表現されます。文字列にシリアライズされて、完全なURIオブジェクトにパーズして戻すことも可能です。URIはFASLファイルにリテラルで書き出されるので、マクロから吐き出してもいいです。URIのシンタックスは、次の通りです。

URI     ::= DOMAINS? (':' PORT)? '/' PATH?
DOMAINS ::= DOMAIN ('.' DOMAIN)*
DOMAIN  ::= ('a'..'Z' | '0'..'9' | '-')
PORT    ::= ('0'..'9'){1, 5}
PATH    ::= .*

URIをURLに変換するために、uri-to-urlを使います。uri-to-urlを使うと、反転(reversal)、エンコーディング、フォーマットの修正は自動で行われます。

uri, domains, port, path, matcher, uri-string, make-uri, make-url, ensure-uri, copy-uri, parse-uri, uri<, uri>, uri=, uri-matches, merge-uris, represent-uri, uri-to-urlを参考にしてください。

1.2 リクエストとレスポンス

行き来するデータを格納しておくために、requestオブジェクトとresponseオブジェクトがあります。requestオブジェクトは、リクエストがどこに向かうのかを表すURI、POST、GET、ヘッダ、クッキーなどのHTTP通信のペイロードデータを全て保持します。responseオブジェクトは、リターンコード、ヘッダー、クッキー、BODYデータを保持します。

リクエストが行われている間、これらの2つのオブジェクトが必ず存在し、*request**response*に束縛されなければいけません。それらは、動的なページを生成するために必要な情報を多く保持しています。さらに、リクエストはdataテーブルを含んでおり、任意のデータを保持することができます。これは、システム内の各々の部品の間で、リクエストの実行中に取得されるような情報をやりとりするときに役立ちます。

リクエストは、必ずしもHTTPサーバから来る必要はありません。動作をテストするために、プログラムからリクエストを送ることも可能です。どのような場合であっても、リクエストをディスパッチするインターフェイスはrequestと呼ばれます。この仕組みは、リクエストとレスポンスを構築して、URIを適切に処理します。もし自分自身でリクエストオブジェクトを送りたいのであれば、execute-requestを使うこともできます。

リクエスト処理に関する詳しい情報は、dispatcher、pages、API endpointをご参照ください。

*request*, *response*, *default-external-format*, *default-content-type* , request, uri, http-method, headers, post-data, get-data, cookies, user-agent, referer, domain, remote, data, issue-time, response, data, return-code, content-type, external-format, headers, cookies, cookie, name, value, domain, path, expires, http-only, secure, cookie-header, cookie, get-var, post-var, post/get, header, file, redirect, serve-file, request-run-time, *debugger*, handle-condition, render-error-page, execute-request, set-data, requestも参考にしてしてください。

1.3 ルーティング

リクエストがディスパッチされる前には、ルーティングシステムを通過します。他のフレームワークでは'routes'はどのハンドラがリクエストを処理するかを指定しますが、Radianceではその方式とは異なります。Radianceにおいてルート(route)とは、URIを変換するフォームです。この部分は、2つの世界を作成して保持します。内部の世界外部の世界です。

内部の世界は、実際にWebアプリケーションが住む世界です。外部の世界は、HTTPサーバとWebサイトを利用するユーザが住む世界です。この区別は、サーバの潜在的な罠を避けてWebアプリケーションを書くために必要不可欠です。あなたのアプリケーションを動作させるために、どのドメインやポート、パスが必要になるかを心配する必要がありません。一方で、アプリが壊れないように、Webの管理者として、システムをあなたの思う通りにカスタマイズして動作させる必要があります。

そのためには、ルーティングが役に立ちます。ルーティングは、MappingReversalの2種類があります。

Mappingは、外部の世界からくるURIを内部の世界のURIに変換します。普通は、トップレベルのドメインを切り取り、サブドメインにマッピングします。

Reversalは逆のことをします。内部の世界から外部の世界へといきます。このことは、あなたが提供するWebページが、実際に外部からアクセス可能なリソースを参照できるようにするために必要です。

ルーティングは、任意の処理を行うことができます。基本的なレベルでは、何らかの方法でURIを修正する関数です。ルーティングを使うと、強力で柔軟なシステムを構築することができます。アプリケーションの開発者として、external-uriuri-to-urlを、ページ内の全てのリンクに使うようにしてください。

route, name, direction, priority, translator, route, remove-route, list-routes, define-route, define-matching-route, define-target-route, define-string-route, internal-uri, external-uriも参考にしてください。

1.4 URIディスパッチャー

ついに、リクエストに対してコンテンツを生成する段階まできました。URIディスパッチャーはURIのサブクラスであり、名前、関数、優先順位を運びます。

優先順位に基づいたリストは、いつリクエストがきても実行されます。リクエストのURIは、それぞれのディスパッチャーに対応して、最初に対応する最初のディスパッチャー関数が実行されます。たったこれだけです。

ディスパッチャー関数は、ページの内容を提供するために、必要な値を、レスポンスオブジェクトに設定する責任があります。そのためには、レスポンスオブジェクトのdataのフィールドに直接設定するか、関数から適切な値を返します。Radianceは、4つの型のデータ(streampathnamestring(array (unsigned-byte 8)))を受けとります。

もしURIディスパッチャーが明示的な優先順位の番号を持っていない場合、優先順位はURIの特異性によって決まります。どのように計算がされるかについて詳しく知りたい場合は、URIソーティング関数であるuri>をみてください。

uri-dispatcher, name, dispatch-function, priority, uri-dispatcher, remove-uri-dispatcher, list-uri-dispatchers, uri-dispatcher>, define-uri-dispatcher, dispatchもご参照ください。

1.5 Page

ページは、実際にコンテンツを提供する関数を定義するために用いられます。しかし、ページはuriディスパッチャーであり、物事を簡単にするためのマクロをいくつか含んでいます。注目してもらいたいのは、拡張可能なオプションです。では、詳しくみていきましょう。

Radianceには、デフォルトでセットアップされるページがいくつかあります。faviconrobotsのページは、Radianceのstatic/ディレクトリでサーブされます。本番環境のサーバでも、自分のサイトでファイルを提供したり更新したりしたいはずです。

そのような目的を満たすために、staticページの仕組みがあります。staticページは、静的コンテンツをWebアプリケーションとモジュールにサーブします。staticページは、そのドメインでも/static/...のパスで有効であり、最初のディレクトリがモジュールの名前である形式である必要があります。残りは、モジュールのstatic/ディレクトリの範囲にあるパスです。この仕組みにより、CSS、JavaScript、画像などの静的ファイルを参照することができます。

最後に、apiページですが、APIエンドポイントのディスパッチを処理する役割があります。これについては次の章で説明します。ページは、静的ファイルの場合と同様に、どのドメインにおいても/api/...のパスで捕捉して動作します。

page, remove-page, define-pageをご参照ください。

1.6 APIエンドポイント

Radianceは、REST APIとの連携もサポートしています。これは、取ってつけたような機能ではありません。多くの現代のアプリケーションは、そのようなAPIを提供することが多く、Radianceは、APIエンドポイントを含むアプリケーションを書く方法を提案します。

コンセプトとしては、APIエンドポイントとは、ブラウザーのリクエストに応じて呼び出される関数です。そのレスポンスは、リクエスト主が読める形式にシリアライズされます。重要な点は、APIエンドポイントは、ユーザからも、プログラムからも利用可能であるべきということです。APIを通してプログラムから実行されるアクションは全て、ユーザによって実行されるものであるので、Radianceでは、両方から利用可能にするよう推奨しています。重複を避けるために、これら2つは、1つとして扱われます。

データの修正作業は全て、APIエンドポイントを通して提供されます。リクエストをしたのがユーザプログラムのどちらなのかを区別せずに処理されます。ユーザがリクエストを行う場合は、適切なページにリダイレクトを行います。プログラムからリクエストが行われた場合は、読み込めるフォーマットで、データのペイロードが提供されます。

全てのパートのうち、APIフォーマットのシステムですが、データを特別なフォーマットにシリアライズすることを担当します。デフォルトでは、S式を基本とするフォーマットが提供されていますが、JSON形式の出力も簡単にロードできます。

次に、browserのPOST/GETパラメータの仕様をみましょう。そのパラメータが"true"という文字列を含む場合、APIリクエストはユーザからであると扱われて、データのペイロードはされずにリダイレクトされます。

あなたのアプロケーションは、統一されたAPIを提供するために、これらをうまく使う必要があります。今、実際のエンドポイントの定義は、名前、関数、引数を記述するラムダリスト、リクエストのパーズ関数で構成されます。引数の典型としては、必須の引数かオプショナル引数が理にかなっています。結局、HTTPリクエストは、キーワード引数しかもつことができません。キーワード引数は、あってもなくてもいいです。

APIエンドポイントの名前は、どこにリーチできるかを示す識別名として機能します。APIエンドポイントは、/api/パスに存在し、エンドポイントの名前もそれに従います。エンドポイントを修正する際には、あなたのモジュールやアプリケーションが、他のエンドポイントをうっかり踏み外さないように気をつけなければいけません。これはURIディスパッチのときとは違いますが、理由は、APIエンドポイントは、曖昧さやパスのプロセスを許可しないからです。なので、全てのエンドポイントは、唯一のパスをもなければいけません。唯一のパスは、直接、名前としてサーブします。

生の(raw)関数は、APIがインターフェイスを提供する関数です。生の関数は、リクエストされたアクションを行い、適切なデータを上で述べた通りに返す役割を果たします。フォーマットされたAPIのデータを返すためには、api-outputを使います。ブラウザリクエストからリクエストを受けてリダイレクトをするには、redirectを使います。

最後に、リクエストのパーズ関数は、リクエストオブジェクトを受け取り、関数が必要な実引数を抽出して、最終的に、可能であれば、適切な引数を適用させて関数を呼び出します。パーズ関数は、もし必要な引数が見当たらない場合、api-argument-missingエラーの信号を送ります。無駄な実引数は無視されます。

call-apiを用いると、プログラムからAPIエンドポイントを呼び出すこともできます。call-api-requestを使うと、Requestをシュミレーションすることができます。どちらもURIディスパッチの仕組みを通る必要はありません。

ページと似ていますが、APIエンドポイントの定義は、定義を簡単にするために、拡張オプションも受け付けます。オプションについては、次で説明します。

api, *default-api-format*, *serialize-fallback*, api-format, remove-api-format, list-api-formats, define-api-format, api-output, api-serialize, api-endpoint, remove-api-endpoint, list-api-endpoints, api-endpoint, name, handler, argslist, request-handler, call-api-request, call-api, define-apiをご参照ください。

1.7 オプション

オプションは、拡張可能な定義のマクロを提供します。これは、フレームワークが何か定義する際に、共通のオペレーションをより短しようと拡張しようとするときに役に立ちます。例えば、アクセス権があるユーザに対して、ページかAPIエンドポイントを制限するような共通するタスクがあるとします。

このような実装を簡単にするために、Radianceは一般的なオプションに仕組みを提供します。オプションは、定義のマクロが属するオプションの型によって分けられます。Radianceは、apipageのオプションを提供します。

それぞれのオプションは、オプションの型に応じて、名前と多くの引数を受け入れる関数のために、キーワードをもっています。定義名、ボディー部、最終にオプションに渡される値を含んだリストが、いつも引数として与えられます。

この拡張用(expansion)の関数は、定義マクロのボディーの式を変換します。環境の設定を許可するために、定義自体では含まれない式を吐き出すこともできます。

option, option-type, name, expander, option, remove-option, list-options, define-option, expand-optionsをご参照ください。

1.8 モジュール

モジュールの概念は、Radianceにおいて必要不可欠です。全体を構成する部品として働きます。技術レベルでいうと、モジュールは、特別なメタデータが与えられたパッケージです。モジュールは、modularizeしステmyによって提供され、フック、トリガー、インターフェイス等を使いやすくして、他の情報をトラッキングするために使われます。

defpackageを使う代わりに、define-moduleを使うようにしてください。シンタックスはdefpackageですが、:domainのような特別なオプションを含んでいるので、モジュールが機能するプライマリ・ドメインを特定することができます。

モジュールのシステムでは、ASDFのシステムをモジュールに記すこともできます。もし記せば、そのASDFシステムは仮想のモジュールになります。このようにするためには、システムの定義に3つのオプションを追加する必要があります:

:defsystem-depends-on (:radiance)
:class "radiance:virtual-module"
:module-name "MY-MODULE"

こうすることで、Radianceは、ASDFのシステム情報を特定して、あなたのモジュールに関連づけることができます。 新しいモジュールのために、必須のシステムとモジュールの定義を自動で行うには、create-moduleをみてください。

virtual-module, virtual-module-name, define-module, define-module-extension, delete-module, module, module-p, module-storage, module-storage-remove, module-identifier, module-name, current-module, module-domain, module-permissions, module-dependencies, module-required-interfaces, module-required-systems, module-pages, module-api-endpoints, describe-module, find-modules-directory, *modules-directory*, create-moduleをご参照ください。

1.9 フック

Radianceには、互いのモジュールを連携させるために、フック(hook)の仕組みがあります。フックを使うと、ある種のイベントに反応して、任意の関数を実行することができます。例えば、意見交換をするようなソフトウェアでは、新しい投稿が作成されるごとにトリガーされるフックを設定することがでいます。拡張では、追加のタスクを実行するためにトリガーをフックに対して定義できます。

フックは、任意の数のトリガーをもつことができますが、トリガーは長すぎないようにしてください。フックをトリガーすることは、全てのトリガーが終了するまでブロックされる動作だからです。そのようなわけで、長い間続くようなトリガーの実行は、リクエストへのレスポンスを遅らせてしまう可能性があります。 long.

フックは、長い間はonであり、後にoffになるスイッチにような関数であるべきです。もし新しいトリガーが実行中に定義された場合は、自動で呼び出されるべきです。これは、define-hook-switchが容易にすることです。define-hook-switchは2つのフックを作ります。1つ目がトリガーされると、それに定義されているトリガーは、後に2つ目のフックがトリガーされたときに、自動で呼び出されます。このおかげで、仮にトリガーがサーバの起動後に定義されたとしても、server-startが適切に動作します。

list-hooks, define-hook, remove-hook, define-trigger, remove-trigger, trigger, define-hook-switchをご参照ください。

1.10 インターフェイス

システムが一枚岩になることを避けるために、バックエンドは拡張できるようにするために、Radianceはインターフェイスの仕組みを含んでいます。一般的な意味では、インターフェイスとは、関数やマクロ、変数がどのように動作するかについて、約束事を決めますが、実際に実装するのはインターフェイスではありません。インターフェイスが動作する全てを作っている実際の機能性は、実装の外側にあります。このことにより、ユーザはインターフェイスに対してコードを書くことができ、特定のバックエンドに結びつけることなく、与えられた機能を利用することができます。

具体例として、データベースのためのインターフェイスをみていきましょう。データベースには多くの種類があり、全ては違う方法でやりとりをしますが、データの保存、検索、修正等、どれにも共通する操作があるので、このインターフェイスは実用的です。

では、共通する操作を提供するインターフェイスを作ります。これは、特定の種類のデータベースが実際に動かすためにすることは、実装次第です。アプリケーションの作者として、データベース・インターフェイスを活用することができます。このインターフェイスを使えば、様々なデータベースに対して自動的にうまく動作するようにできます。

これは、アプリケーション作者に有利なだけでなく、インターフェイスで分離することにより、システム管理者は、比較的容易に、実装に存在しない特殊な機能を自分で実装することができることです。インターフェイスが不透明であるおかげで、実装はLispのプロセスで動くものと、外部で動くプロセスとの橋渡しをすることができます。 これは、Productionシステムの管理者が必要な情報を選び出すために、開かれた選択肢を多く与えます。

実際、インターフェイスは特別な種類のモジュールであり、特別な種類のパッケージです。定義の一部として、関数や変数などの束縛のために、一連の定義を含みます。インターフェイスはパッケージなので、あなたがコンポーテントで使えるユーザは、他のパケージにあるものも何でも使えます。違いはありません。実装の作者として、あなたはインターフェイスが示す定義を再定義することができます。

実際にインターフェイスを利用するモジュールを読み込むためには、インターフェイスの実装は、事前に読み込まれる必要があります。そうでなければ、マクロは適切に動作しません。あなたのASDFシステムの定義において、特定の実装を参照する必要なく、インターフェイスに依存することを許可するためには、Radianceは拡張されたASDFを提供します。この拡張を使うと、(:interface :foo)のようなリストを:depends-onに追加することができます。モジュールがロードされたときに、Radianceはインターフェイスを具体的な処理に分解します。

Radianceは、標準のインターフェイスを提供します。それぞれのインターフェイスは、radiance-contribsによって提供される標準実装を1つ以上もちます。インターフェイスは次の通りです:

  • admin 拡張可能な管理者ページを提供します。

  • auth 認証とログインに関する全てを扱います。

  • ban IPアドレスによってユーザがサイトにアクセスすることを禁止します。

  • cache キャッシュのための仕組みを提供します。

  • database 柔軟なデータベースのインターフェイスです。オブジェクト保存、リレーショナルデータベースをバックエンドとして利用できます。

  • logger ログ出力のためのインターフェイスです。デバッグやメッセージを出力できます。

  • mail メールを送るための最小限のインターフェイスです。

  • profile ユーザのプロフィールや属性を拡張するために使います。

  • rate 特定のリソースにrate limitationを許可します。

  • server Radianceを外部の世界と結びつける架け橋の役割を果たすインターフェイスです。

  • session トラッキングするために、持続するセッションを保証します。

  • user ユーザアカウントとパーミションの機能を提供します。

それぞれのインターフェイスについては、次の章で説明します。

interface, interface-p, implementation, implements, reset-interface, define-interface-extension, find-implementation, load-implementation, define-interface, define-implement-triggerをご参照ください。

1.11 環境

複数のRadianceインスタンスに対して、同じマシン内で異なる設定ができるように、Radianceでは環境(Environment)という仕組みを用意しています。環境とは、基本的には、Radianceとロードされるモジュールのための設定ファイルの一式です。Radianceの設定は、インターフェイスを選択される実装にマッピングすることで、もしインターフェイスが求められたときに選択されるように決定します。

startupが呼び出された時、どれだけ遅くとも、特定の環境が選択されます。早ければ、モジュールがロードされたときに選択されます。後者の場合は、環境を選ぶために、インタラクティブな再起動が可能です。これは必須の機能ですが、理由は、そうでなければ、Radianceがインターフェイスのマッピングを解決できないからです。

環境のシステムの一部として、Radianceは、あなたのアプリケーションで使える(おそらく、使うべき)設定システムを提供します。設定システムを使うと、それぞれの環境ごとに、適切に設定することができます。その設定は、いつでも持続性があるうえ、可読性にも優れたフォーマットで保存されるので、特別なツールで読み込んだり、修正したりする必要はありません。

実際に設定手順でどのように保存処理がされているかを知りたい方は、ubiquitousを参考にしてください。 value関数の代わりに、Radianceではconfig関数を使えます。

environment-change, environment, check-environment, mconfig-pathname, mconfig-storage, mconfig, defaulted-mconfig, config, defaulted-configをご参照ください。

1.12 インスタンスの管理

最後に、Radianceは、起動からシャットダウンまでのシーケンスを提供しています。このおかげで、ソフトは適切に起動して利用可能になり、その後、綺麗に片付けて終了することを確かにします。

そのシーケンスの大部分は、正しい順番で、適切な回数、特定のフックが呼び出されることによって実現されています。

インターフェイスの関数を適切に使うことで、サーバを手動で起動することは可能ですが、その方法で、アプリケーションが正しく動作すると想定するのはやめてください。多くは、特定のフックが適切な順番で呼び出されることを要求しています。このような理由で、startupshutdownでRadianceインスタンスを管理する必要があります。startupshutdownのドキュメントには、どのフックが、どの順番で呼び出されているかが説明されています。実装では、シンボルがexportされていない限り、追加で特定されない定義をインターフェオスのパッケージのシンボルに加えることができます。

*startup-time*, uptime, server-start, server-ready, server-stop, server-shutdown, startup, startup-done, shutdown, shutdown-done, started-pをご参照ください。

2. 標準のインターフェイス

標準のインターフェイスは、Radianceとcore packageと一緒に配布されています。ライブラリは、追加のインターフェイスを提供することも可能です。インターフェイスの実装ですが、インターフェイス定義では、次の制限の緩和が許可されています:

&key引数を含むラムダリストは、実装依存のキーワード引数を使って拡張できます。&optional引数を含み、&key&restを含まないラムダリストは、オプショナル引数で拡張できます。必須の引数しか含まないラムダリストは、オプショナル引数かキーワード引数で拡張できます。

2.1 管理

管理・インターフェイスは、管理者ページを作成するためのものです。ユーザ構成の設定やシステム情報の表示のために使えます。"管理(administration)"という名前ですが、システムの管理者だけに限ったものではありません。どのユーザにも利用できることができます。

管理者ページは、カテゴリー分けされたメニュやパネルを表示できなければいけません。パネル群は、他のモジュールによって提供されており、admin:define-panelで追加できます。秘密情報を含むようなパネルにアクセスするパネルは、:accessオプションでアクセスを禁止させるようにしてください。

パーミッションについては、 userインターフェイスを参照してください。

管理者ページや特定のパネルにリンクをはるには、pageリソースを使ってください。

admin:list-panels, admin:remove-panel, admin:define-panel, admin:panelをご覧ください。

2.2 認証

認証・インターフェイスは、ユーザをリクエストと結びつけます。そのために、ユーザがシステムに対して自分自身を認証させる方法を提供しなければいけません。どのように実現するかは、実装次第です。実装は、認証のプロセスが初期化するためのページを提供しなければいけません。pageのソースを通してURIを認証プロセスに渡して、"login"を引数として渡します。

現在リクエストに結び付けられているユーザをauth:currentコマンドで調べることができます。ユーザが"anonymous"と解釈される場合は、NILを返します。詳しくはuserインターフェイスを参照ください。

auth:*login-timeout*, auth:page, auth:current, auth:associateもご覧ください。

2.3 ban

ban・インターフェイスは、IPによるアクセス制限を提供します。IP BANされたクライアントのIPアドレスからは、リクエストするページに対してアクセス出来なくなります。BANは、タイムアウトの後、手動・自動いずれでも、離す(lift)することができます。実装としては、ユーザのIPを監視するために、追加で労力を割くことは想定していません。

ban:jail, ban:list, ban:jail-time, ban:releaseをご参照ください。

2.4 キャッシュ

キャッシュ・インターフェイスは、一般的なキャッシュの仕組みを提供します。カスタマイズ可能で無効化のテストをもっています。キャッシュを明示的に新しくするには、cache:renewをつかいます。cache:with-cacheを使うと、キャッシュの部品を構築することができ、テストのformがtrueと評価されたときに、BODYのキャッシュ値が返されるようにできます。

キャッシュ値が保存される方法は実装によります。 cache:getcache:with-cacheを使うと、キャッシュ値は文字列かバイト列に強制変換されます。実装では、キャッシュに格納できる typeには制限がありませんが、少なくとも、文字列とバイト列はサポートします。

キャッシュ値の名前は、名前とパッケージ名が、次の文字を含まないシンボルでなければいけません:<>:"/\|?*. キャッシュ値の変形は、princによって表示される表現とは区別されるオブジェクトである必要があります。その際には、先ほどと同じ文字の制限が適用されます。

cache:get, cache:renew, cache:with-cacheをご参照ください。

2.5 データベース

データベース・インターフェイスは、データを持続させるためのレイヤーを提供します。通常は、データベースと呼ばれるレイヤーです。リレーショナル型のデータベースである必要はありませんが、そうであってもいいです。実装の変数を保持するために、基本的なデータベースの機能しかサポートされません。(joinsやtriggers等はありません)データ型も、整数、float、文字列に限定されます。これらの制限にも関わらず、多くのアプリケーションにおいて、データベースインターフェイスはとても役に立ちます。

伝統的なRDMBの用語と区別するために、特別な用語が使われます: schemaは"structure"、tableは"collection"、rowは"record"とします。

データベースに接続する前に、データベース関連の命令を実行すると、未定義の動作を招きます。Radianceでは、Radianceが動作している間はデータベースが接続されることを保障しているので、どのページ、どのAPI、どのURIディスパッチャーの定義において、データベースインターフェイスを問題なく使えます。

実際にデータ保存を行うための関数は、db:insertdb:removedb:updatedb:selectdb:iterateです。 それらは、あなたが期待するように動作するはずです。

詳しくは、それぞれの関数に書かれているコメントを読んでください。コレクションの作り方、どのような制限があるかを知りたい方は、db:createのコメントも参考にしてください。

データベースは、データ操作が完了すれば、Radianceを再起動したり、Lispイメージ、マシンがクラッシュしたとしても、データの変更は存続されなければいけません。

database:condition, database:connection-failed, database:connection-already-open, database:collection-condition, database:invalid-collection, database:collection-already-exists, database:invalid-field, database:id, database:ensure-id, database:connect, database:disconnect, database:connected-p, database:collections, database:collection-exists-p, database:create, database:structure, database:empty, database:drop, database:iterate, database:select, database:count, database:insert, database:remove, database:update, database:with-transaction, database:query, database:connected, database:disconnectedを参照してください。

2.6 logger

logger・インターフェースは、ログの関数を提供します。システムの中で関連して起きていることについて、ログのメッセージを出すことができます。ログの出力内容と出力方法に関しては、実装とシステムの管理者次第です。

logger:log, logger:trace, logger:debug, logger:info, logger:warn, logger:error, logger:severe, logger:fatalをご覧ください。

2.7 メール

メール・インターフェースは、メールを送る仕組みを組み込むことができます。様々なコンポーネントが、Webサイトの外からユーザとつながるために、メールのアクセスが必要になるかもしれません。リモートサーバ、ローカル環境でのメール送信等、メールの送信方法の設定は、処理系依存です。mail:sendのフックを使うと、メールが送られる前に、メールに反応することができます。

mail:sendをご覧ください。

2.8 プロフィール

プロフィール・インターフェイスは、userにある種のpresenceをもたせたいアプリケーションにおいて、共通で使用されるuserインターフェイスを拡張できるようにします。そのインターフェイスは、機能の一部として、ユーザのプロフィールが表示されるページを提供する必要があります。そのプロフィールは、数種類のパネルを表示しなければいけません。パネルは、他のモジュールによって提供されており、profile:define-panelで追加できます。

pageのリソースの型を通して、URIをプロファイルのページに移動することができます。

そのインターフェイスは、視覚的にユーザを特定させるためにprofile:avatarアバター画像にアクセスさせることもできます。また、profile:nameを使うと、ユーザがユーザ名をカスタマイズできます。さらに、profile:fieldsprofile:add-fieldprofile:remove-fieldを使うと、どのようなデータをユーザの属性に含むか、それを公(public)に表示させるかどうかを指定できます。

profile:page, profile:avatar, profile:name, profile:fields, profile:add-field, profile:remove-field, profile:list-panels, profile:remove-panel, profile:define-panel, profile:panelをご参照ください。

2.9 rate

rate・インターフェースは、Rate limitationの仕組みを提供します。秘密情報やコストが高いリソースへの負荷の高いアクセスを防ぐことができます。2つの段階があります。第1段階は、rate:define-limitにより、特定のリソースに対して、Rate limitationの動作を定義します。第1段階は、リソースがrate:with-limitationにより保護されます。

もし、特定のユーザからのblockへのアクセスが頻繁すぎる場合は、blockは呼び出されません。制限の定義があるコードが、代わりに実行されます。

Rate limitationは、クライアント、ユーザ、セッションごとですが、グローバルではないことに注意してください。

rate:define-limit, rate:left, rate:with-limitationをご参照ください。

2.10 サーバ

serverインターフェイスとloggerインターフェイスは、Radianceが起動時に順番通りに読み込まれます。HTTPリクエストに応答する責任があります。実装では、リクエストを受け入れて、Radianceのrequest関数に渡す必要があります。その後、responseはリクエスト主に戻されます。

リスナーの動作を特定する引数は実装によることに注意してください。しかし、実装は、(mconfig :radiance :port)で設定されたlocalhostとポートからでアクセスできる標準のリスナーを提供して、radiance:startupで起動できるようにする必要があります。

server:start, server:stop, server:listeners, server:started, server:stoppedをご参照ください。

2.11 セッション

セッション・インターフェースは、あるクライアントが行う複数のリクエストを追跡します。クライアントによっては情報を隠蔽したり偽装している場合があるので、完全にはクライアントを追跡できるとはいえません。しかし、多くのユーザに対しては、うまく動作するはずです。セッション・インターフェイスは、他のインターフェイスや低レイヤーのライブラリの中で使われます。ユーザ認証にような一貫性を保つために使われます。

session:*default-timeout*, session:session, session:=, session:start, session:get, session:list, session:id, session:field, session:timeout, session:end, session:active-p, session:createをご参照ください。

2.12 ユーザ

ユーザ・インターフェースは、ユーザオブジェクトを永続させ、パーミションの仕組みを組み込めます。ユーザ認証、ユーザの特定、トラッキング等は扱いません。このインターフェイスでは、ユーザオブジェクトを提供するのみであり、パーミション情報が管理されます。

パーミションに関する詳細は、user:userを参照してください。

user:condition, user:not-found, user:user, user:=, user:list, user:get, user:username, user:fields, user:field, user:remove-field, user:remove, user:check, user:grant, user:revoke, user:add-default-permissions, user:create, user:remove, user:action, user:ready, user:unreadyもご覧ください。

参考

Common Lisp – destructuring-bind

元の記事はこちらです:

Common Lisp – destructuring-bind / Kyle Burton著

Common Lisp – destructuring-bind

リストの分解

;; これが定番の使い方です。リストを分解します。
(destructuring-bind
      (first second)
    '(1 2)
  (format t "first:~A second:~A ~&" first second))
;;; => first:1 second:2

ドット表記

;; ドット表記のリストも分解できます。
(destructuring-bind
      (first . second)
    '(1 . 2)
  (format t "first:~A second:~A ~&" first second))
;;; => first:1 second:2

ドット表記で残りの引数を全て捕捉

;; destructuring-bindの第一引数はラムダリストですが、
;; ドット記法を使うと、残り全ての引数を捕捉することができます。
(destructuring-bind
      (first second . stuff)
    '(1 2 3 4 5)
  (format t "first:~A second:~A rest:~A ~&" first second stuff))
;;; => first:1 second:2 rest:(3 4 5)

&restで残りの引数を全て捕捉

;; また、レスト引数を使って残り全ての引数を捕捉することもできます。
(destructuring-bind
      (first second &rest stuff)
    '(1 2 3 4 5)
  (format t "first:~A second:~A rest:~A ~&" first second stuff))
;;; => first:1 second:2 rest:(3 4 5)

&optionalでデフォルト値を設定

;; オプショナル引数を使って、引数にデフォルト値を設定することもできます。
(destructuring-bind
      (first second &optional (third 'default))
    '(1 2)
  (format t "first:~A second:~A third:~A ~&" first second third))
;;; => first:1 second:2 third:DEFAULT

(destructuring-bind
      (first second &optional (third 'default))
    '(1 2 3)
  (format t "first:~A second:~A third:~A ~&" first second third))
;;; => first:1 second:2 third:3

キーワード引数の利用

;; また、キーワード引数を使うこともできます。
(destructuring-bind
      (first second &key third)
    '(1 2 :third 3)
  (format t "first:~A second:~A third:~A ~&" first second third))
;;; => first:1 second:2 third:3

木構造を逆パース

;; 最後に、木構造を逆パース(unparse)するためにも使うことができます。
;; 分解したいデータ構造に対して、あなた自身の変数宣言を適用することができます。
;; このテクニックは、XMLをS式に変換した後、データを処理する際に役立ちます。
(destructuring-bind
      (a (b (c d e (f g) h i j)) &rest remainder)
    '(1 (2 (3 4 5 (6 7) 8 9 10)) 11 12 13 14 15)
  (format t
          "a:~A b:~A c:~A d:~A e:~A f:~A g:~A h:~A i:~A j:~A remainder:~A ~&"
          a b c d e f g h i j remainder))
;;; => a:1 b:2 c:3 d:4 e:5 f:6 g:7 h:8 i:9 j:10 remainder:(11 12 13 14 15)

Caveman2 - README 訳


Caveman(2018年8月14日時点)のREADMEの和訳です。READMEの更新に合わせて、こちらの和訳も更新します。


Caveman2 - 軽量なWebアプリケーションフレームワーク

Build Status

利用方法

(defparameter *web* (make-instance '<app>))

@route GET "/"
(defun index ()
  (render #P"index.tmpl"))

@route GET "/hello"
(defun say-hello (&key (|name| "Guest"))
  (format nil "Hello, ~A" |name|))

Caveman2とは?

Caveman1との相違点

Caveman2は、ゼロから書き直しました。

重要な点は、次の通りです:

  • ningleを元にしていること
  • データベースとの連携ができること
  • 開発環境の切り替えができること(Envyの利用)
  • 新たなルーティングマクロ

ゼロから書き直した理由

「ningleとCaveman、どちらを使うべきですか?」

「違いは何ですか?」

と聞かれることがよくありました。

両者の役割が似ていることが原因だったと思います。

どちらも小さなフレームワークであり、データベースをサポートしています。

Caveman2は、小さなWebフレームワークではありません。CL-DBIをサポートし、デフォルトでデータベース接続を管理します。

デザインの目標

Cavemanは、Webアプリケーションの開発で共通する部品を集めたコレクションです。

Cavemanは、3つのルールのもと、開発されました。

  • 拡張できること
  • 実用的であること
  • 何も強要しないこと

はじめに

あなたがここにきたということは、Caveman(洞窟男)のように暮らすことに、興味があるのでしょうか?

洞窟にはディズニーランドはありませんが、始めるには良い場所です。

さて、洞窟に入りましょう。

インストール

現在、Caveman2はQuicklispで入手できます。

(ql:quickload :caveman2)

プロジェクトのテンプレートを生成する

(caveman2:make-project #P"/path/to/myapp/"
                       :author "<Your full name>")
;-> writing /path/to/myapp/.gitignore
;   writing /path/to/myapp/README.markdown
;   writing /path/to/myapp/app.lisp
;   writing /path/to/myapp/db/schema.sql
;   writing /path/to/myapp/shlyfile.lisp
;   writing /path/to/myapp/myapp-test.asd
;   writing /path/to/myapp/myapp.asd
;   writing /path/to/myapp/src/config.lisp
;   writing /path/to/myapp/src/db.lisp
;   writing /path/to/myapp/src/main.lisp
;   writing /path/to/myapp/src/view.lisp
;   writing /path/to/myapp/src/web.lisp
;   writing /path/to/myapp/static/css/main.css
;   writing /path/to/myapp/t/myapp.lisp
;   writing /path/to/myapp/templates/_errors/404.html
;   writing /path/to/myapp/templates/index.tmpl
;   writing /path/to/myapp/templates/layout/default.tmpl

ルーティング

Caveman2は、ルーティングを定義するために、2通りの方法を提供しています。

@routedefroute、どちらを使うかは、あなた次第です。

@routeは、アノテーションのマクロであり、cl-annotを用いています。

「メソッド」 「URL文字列」 「関数」 を受け取ります。

@route GET "/"
(defun index ()
  ...)

;; 無名のroute
@route GET "/welcome"
(lambda (&key (|name| "Guest"))
  (format nil "Welcome, ~A" |name|))

@routeは、引数のリスト以外は、Caveman1の@urlと似ています。

必要がない場合は、引数を指定する必要はありません。

defrouteは単なるマクロであり、@routeと同じ機能を提供します。

(defroute index "/" ()
  ...)

;; 無名のroute
(defroute "/welcome" (&key (|name| "Guest"))
  (format nil "Welcome, ~A" |name|))

Cavemanは、ningleを元に作られているので、Sinatraのようなルーティングも使えます。

;; GET request (default)
@route GET "/" (lambda () ...)
(defroute ("/" :method :GET) () ...)

;; POST request
@route POST "/" (lambda () ...)
(defroute ("/" :method :POST) () ...)

;; PUT request
@route PUT "/" (lambda () ...)
(defroute ("/" :method :PUT) () ...)

;; DELETE request
@route DELETE "/" (lambda () ...)
(defroute ("/" :method :DELETE) () ...)

;; OPTIONS request
@route OPTIONS "/" (lambda () ...)
(defroute ("/" :method :OPTIONS) () ...)

;; For all methods
@route ANY "/" (lambda () ...)
(defroute ("/" :method :ANY) () ...)

ルートパターンには、値を引数に入れるために、キーワードを含むことができます。

(defroute "/hello/:name" (&key name)
  (format nil "Hello, ~A" name))

上のコントローラーでは、 "/hello/Eitaro"にアクセスがきたときnameは"Eitaro"になり、"/hello/Tomohiro"にアクセスがきたときnameは"Tomohiro"になります。

(&key name)はCommon Lispのラムダリストとほぼ同じですが、他のkeyを含むこともできます。

(defroute "/hello/:name" (&rest params &key name)
  ;; ...
  )

ルートパターンには、ワイルドカード引数を含めることもできます。splatでアクセス可能です。

(defroute "/say/*/to/*" (&key splat)
  ; /say/hello/to/world にマッチします
  splat ;=> ("hello" "world")
  ))

(defroute "/download/*.*" (&key splat)
  ; /download/path/to/file.xml にマッチします
  splat ;=> ("path/to/file" "xml")
  ))

URLのルールに正規表現を使うときには、:regexp tを明記してください。

(defroute ("/hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~A!" (first captures)))

通常、ルート(routes)は、定義された順番通りにマッチします。

はじめにマッチしたルートが呼びだされ、残りは無視されます。

next-routeを使うことで、次にマッチするルートに処理を渡すことができます。

(defroute "/guess/:who" (&key who)
  (if (string= who "Eitaro")
      "You got me!"
      (next-route)))

(defroute "/guess/*" ()
  "You missed!")

defrouteの結果として、次のフォーマットを返すことができます。

  • 文字列
  • パス名
  • Clackのレスポンスのリスト(Status、Headers、Bodyを含みます)

クエリー問い合わせ/POSTパラメータ

角括弧"[" & "]"を含むパラメータのキーは、クエリー問い合わせとしてパースされます。

ルータ(routers)で_parsedとしてパースされたパラメータにアクセスすることができます。

<form action="/edit">
  <input type="name" name="person[name]" />
  <input type="name" name="person[email]" />
  <input type="name" name="person[birth][year]" />
  <input type="name" name="person[birth][month]" />
  <input type="name" name="person[birth][day]" />
</form>
(defroute "/edit" (&key _parsed)
  (format nil "~S" 
        (cdr (assoc "person" _parsed :test #'string=))))

;=> "((\"name\" . \"Eitaro\") (\"email\" . \"e.arrows@gmail.com\") (\"birth\" . ((\"year\" . 2000) (\"month\" . 1) (\"day\" . 1))))"
;=> "((\"name\" . \"Eitaro\") (\"email\" . \"e.arrows@gmail.com\") (\"birth\" . ((\"year\" . 2000) (\"month\" . 1) (\"day\" . 1))))"

空白のキーは、多値を含むことを表しています。

<form action="/add">
  <input type="text" name="items[][name]" />
  <input type="text" name="items[][price]" />

  <input type="text" name="items[][name]" />
  <input type="text" name="items[][price]" />

  <input type="submit" value="Add" />
</form>
(defroute "/add" (&key _parsed)
  (format nil "~S" (assoc "items" _parsed :test #'string=)))
;=> "(((\"name\" . \"WiiU\") (\"price\" . \"30000\")) ((\"name\" . \"PS4\") (\"price\" . \"69000\")))"

テンプレート

Cavemanは、デフォルトのテンプレートエンジンとして、Djulaを採用しています。

{% extends "layouts/default.html" %}
{% block title %}Users | MyApp{% endblock %}
{% block content %}
<div id="main">
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.name }}</a></li>
  {% endfor %}
  </ul>
</div>
{% endblock %}
(import 'myapp.view:render)

(render #P"users.html"
        '(:users ((:url "/id/1"
                   :name "nitro_idiot")
                  (:url "/id/2"
                   :name "meymao"))
          :has-next-page T))

Djulaを用いて、データベースから何か取得したり、関数を実行するには、どうしたらいいでしょうか。

レンダー(render)に、実行された引数を渡すときには、明示的にlist関数を使う必要があります。

(import 'myapp.view:render)

(render #P"users.html"
        (list :users (get-users-from-db)))

JSON API

次は、JSON APIの使用例です。

(defroute "/user.json" (&key |id|)
  (let ((person (find-person-from-db |id|)))

    ;; person => (:|name| "Eitaro Fukamachi" :|email| "e.arrows@gmail.com")

    (render-json person)))

;=> {"name":"Eitaro Fukamachi","email":"e.arrows@gmail.com"}

render-jsonは、スケルトンプロジェクトの一部です。 コードは、"src/view.lisp"にあります。

静的なファイル

"static/"フォルダーにある画像、css、js、favicon.ico、robot.txtは、デフォルトでサーブされます。

/images/logo.png => {PROJECT_ROOT}/static/images/logo.png
/css/main.css    => {PROJECT_ROOT}/static/css/main.css
/js/app/index.js => {PROJECT_ROOT}/static/js/app/index.js
/robot.txt       => {PROJECT_ROOT}/static/robot.txt
/favicon.ico     => {PROJECT_ROOT}/static/favicon.ico

"PROJECT_ROOT/app.lisp"を書き直すことで、このルールを変更することができます。

詳細は、Clack.Middleware.Staticを参照してください。

環境設定

Cavemanは、環境設定を切り替えるために、Envyを採用しています。

Envyを使うと、複数の環境を定義して、環境設定を環境変数で切り替えることができます。

次は、典型的な利用方法です。

(defpackage :myapp.config
  (:use :cl
        :envy))
(in-package :myapp.config)

(setf (config-env-var) "APP_ENV")

(defconfig :common
  `(:application-root ,(asdf:component-pathname (asdf:find-system :myapp))))

(defconfig |development|
  '(:debug T
    :databases
    ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"test.db"
                                                        *application-root*)))))

(defconfig |production|
  '(:databases
    ((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234")
     (:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234"))))

(defconfig |staging|
  `(:debug T
    ,@|production|))

全ての環境設定は、plistです。APP_ENVを設定することで、どの開発環境を使うかを選ぶことができます。

現在の環境設定から値を得るには、myapp.config:configに入手したいキーと一緒に呼び出します。

(import 'myapp.config:config)

(setf (osicat:environment-variable "APP_ENV") "development")
(config :debug)
;=> T

データベース

:databasesを環境設定に加えると、Cavemanはデータベースのサポートを有効化します。

:databasesは、データベースの設定を含んだalistです。

(defconfig |production|
  '(:databases
    ((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234")
     (:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234"))))

myapp.dbにあるdbは、上で設定された各々のデータベースに接続するための関数です。

次のように使えます。

(use-package '(:myapp.db :sxql :datafly))

(defun search-adults ()
  (with-connection (db)
    (retrieve-all
      (select :*
        (from :person)
        (where (:>= :age 20))))))

接続は、Lispセッションの間は有効であり、それぞれのHTTPリクエストで再利用されます。

retrieve-allとクエリ言語は、dataflySxQLからきています。

詳細は、それぞれのドキュメントをご参照ください。

HTTPヘッダー/HTTPステータスを設定する

HTTPリクエストの間、特別な変数を利用できます。

*request**response*は、リクエストとレスポンスを表します。

Clackを使い慣れている場合は、Clack.RequestClack.Responseのサブクラスのインスタンスもあります。

(use-package :caveman2)

;; Get a value of Referer header.
(http-referer *request*)

;; Set Content-Type header.
(setf (getf (response-headers *response* :content-type) "application/json")

;; Set HTTP status.
(setf (status *response*) 304)

全ての"*.json"へのリクエストに対して、Content-Typeとして"application/json"を設定するには、next-routeが便利です。

(defroute "/*.json" ()
  (setf (getf (response-headers *response*) :content-type) "application/json")
  (next-route))

(defroute "/user.json" () ...)
(defroute "/search.json" () ...)
(defroute ("/new.json" :method :POST) () ...)

Using session

セッションデータとは、ユーザー固有のデータを記憶するためのものです。

*session*は、セッションデータを表すハッシュテーブルです。

次の例では、セッションでの:counterを増やして、それぞれの訪問者に表示しています。

(defroute "/counter" ()
  (format nil "You came here ~A times."
          (incf (gethash :counter *session* 0))))

Caveman2は、デフォルトで、セッションデータをメモリに保存します。

変更するには、"PROJECT_ROOT/app.lisp"にある:store:sessionに指定します。 To change it, specify :store to :session in .

次の例では、保存するためにRDBMSを使っています。

      '(:backtrace
        :output (getf (config) :error-log))
      nil)
- :session
+ (:session
+  :store (make-dbi-store :connector (lambda ()
+                                      (apply #'dbi:connect
+                                             (myapp.db:connection-settings)))))
  (if (productionp)
      nil
      (lambda (app)

注: あなたのアプリの:depends-onとして、:lack-session-store-dbiを追加するのを忘れないように気をつけてください。

それはClack/Lackの一部ではありません。

詳しい情報は、Lack.Session.Store.DBiのコードをご覧ください。

HTTPステータスコードを投げる

(import 'caveman2:throw-code)

(defroute ("/auth" :method :POST) (&key |name| |password|)
  (unless (authorize |name| |password|)
    (throw-code 403)))

エラーページを特定する

404, 500等のエラーページを特定するには、アプリ内でon-exceptionメソッドを定義してください。

(defmethod on-exception ((app <web>) (code (eql 404)))
  (declare (ignore app code))
  (merge-pathnames #P"_errors/404.html"
                   *template-directory*))

サーバを起動する

あなたのアプリは、起動と停止のために、startstopという名前の関数をもっています。

あなたのアプリが"myapp"という名前だとすると、次のようになります。

(myapp:start :port 8080)

Cavemanは、Clack/Lackを元にしているので、Hunchentoot、mod_lisp、FastCGIのどのサーバを使うかを選択することができます。

(myapp:start :server :hunchentoot :port 8080)
(myapp:start :server :fcgi :port 8080)

ローカル環境ではHunchentoot、本番環境ではFastCGIかWooを使うことをおすすめします。

clackupコマンドを使って、アプリを起動することもできます。 clackup command.

$ ros install clack
$ which clackup
/Users/nitro_idiot/.roswell/bin/clackup

$ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp

ホットデプロイ

Cavemanにはホットデプロイの機能はありませんが、PerlモジュールのServer::Starterを使うと簡単にできます。

$ APP_ENV=production start_server --port 8080 -- clackup --server :fcgi app.lisp

注: Server::Starterは、サーバが特定のFDにバインドできる必要があるので、start_serverコマンドで動作するのは、:fcgi:wooだけです。

サーバを再起動するには、HUPシグナル(kill -HUP <pid>)を、start_serverプロセスに送ってください。

エラーログ

Cavemanは、エラーのバックトレースを、:error-logで特定したファイルに書き出します。

(defconfig |default|
  `(:error-log #P"/var/log/apps/myapp_error.log"
    :databases
    ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"myapp.db"
                                                        *application-root*)))))

他のテンプレートエンジンを使う

CL-WHO

(import 'cl-who:with-html-output-to-string)

(defroute "/" ()
  (with-html-output-to-string (output nil :prologue t)
    (:html
      (:head (:title "Welcome to Caveman!"))
      (:body "Blah blah blah."))))
;=> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">
;   <html><head><title>Welcome to Caveman!</title></head><body>Blah blah blah.</body></html>"

CL-Markup

(import 'cl-markup:xhtml)

(defroute "/" ()
  (xhtml
    (:head (:title "Welcome to Caveman!"))
    (:body "Blah blah blah.")))
;=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><html><head><title>Welcome to Caveman!</title></head><body>Blah blah blah.</body></html>"

cl-closure-template

{namespace myapp.view}

{template renderIndex}
<!DOCTYPE html>
<html>
<head>
  <title>"Welcome to Caveman!</title>
</head>
<body>
  Blah blah blah.
</body>
</html>
{/template}
(import 'myapp.config:*template-directory*)

(closure-template:compile-cl-templates (merge-pathnames #P"index.tmpl"
                                                        *template-directory*))

(defroute "/" ()
  (myapp.view:render-index))
(ql:quickload :clack-middleware-clsql)
(import 'clack.middleware.clsql:<clack-middleware-clsql>)

(builder
 (<clack-middleware-clsql>
  :database-type :mysql
  :connection-spec '("localhost" "db" "fukamachi" "password"))
 *web*)
* [Clack.Middleware.Clsql](http://quickdocs.org/clack/api#system-clack-middleware-clsql) * [CLSQL: Common Lisp SQL Interface](http://clsql.b9.com/) ### Postmodern ClackアプリでPostmodernを使うためには、`Clack.Middleware.Postmodern`を使ってください。 Cavemanでは、"PROJECT_ROOT/app.lisp"の`builder`にミドルウェアを追加してください。
(ql:quickload :clack-middleware-postmodern)
(import 'clack.middleware.postmodern:<clack-middleware-postmodern>)

(builder
 (<clack-middleware-postmodern>
  :database "database-name"
  :user "database-user"
  :password "database-password"
  :host "remote-address")
 *web*)
* [Clack.Middleware.Postmodern](http://quickdocs.org/clack/api#system-clack-middleware-postmodern) * [Postmodern](http://marijnhaverbeke.nl/postmodern/) -->

参考

  • Clack - Web application environment.
  • Lack - The core of Clack.
  • ningle - Super micro web application framework Caveman bases on.
  • Djula - HTML Templating engine.
  • CL-DBI - Database independent interface library.
  • SxQL - SQL builder library.
  • Envy - Configuration switcher.
  • Roswell - Common Lisp implementation manager.

作者

  • Eitaro Fukamachi (e.arrows@gmail.com)

ライセンス

Licensed under the LLGPL License.

ningle - README 訳


ningle(2018年8月14日時点)のREADMEの和訳です。READMEの更新に合わせて、こちらの和訳も更新します。


ningle

Build Status

ningleは、Common Lisp製の軽量Webアプリケーションフレームワークです。

使い方

(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/")
      "Welcome to ningle!")

(setf (ningle:route *app* "/login" :method :POST)
      #'(lambda (params)
          (if (authorize (cdr (assoc "username" params :test #'string=))
                         (cdr (assoc "password" params :test #'string=)))
              "Authorized!"
              "Failed...Try again.")))

(clack:clackup *app*)

ここまで進めてブラウザで http://localhost:5000/ にアクセスすると、ningleが"Welcome to ningle!"と表示してくれます。

インストール

(ql:quickload :ningle)

ningleについて

ningleは、Cavemanのフォークプロジェクトです。Cavemanではプロジェクトの雛形を生成しますが、ningleでは生成しません。

ningleは軽量のフレームワークなので、Clackについて少し知識が必要です。Clackは、ningleの元になっているサーバ・インターフェイスです。

はじめに

ルーティング

ningleはSinatraのようなルーティングシステムをもっています。

;; GET request (デフォルト)
(setf (ningle:route *app* "/" :method :GET) ...)

;; POST request
(setf (ningle:route *app* "/" :method :POST) ...)

;; PUT request
(setf (ningle:route *app* "/" :method :PUT) ...)

;; DELETE request
(setf (ningle:route *app* "/" :method :DELETE) ...)

;; OPTIONS request
(setf (ningle:route *app* "/" :method :OPTIONS) ...)

ルーティングのパターンには、引数に値を設定するために、キーワードを含むことができます。

(setf (ningle:route *app* "/hello/:name")
      #'(lambda (params)
          (format nil "Hello, ~A" (cdr (assoc :name params)))))

上のコントローラでは、"/hello/Eitaro"や"/hello/Tomohiro"にアクセスしたときに呼び出されます。(cdr (assoc :name params))`は、"Eitaro"と"Tomohiro"になります。

ワイルドカードを含めることも可能です。(assoc :splat params)でアクセスできます。

(setf (ningle:route *app* "/say/*/to/*")
      #'(lambda (params)
          ; matches /say/hello/to/world
          (cdr (assoc :splat params)) ;=> ("hello" "world")
          ))

(setf (ningle:route *app* "/download/*.*")
      #'(lambda (params)
          ; matches /download/path/to/file.xml
          (cdr (assoc :splat params)) ;=> ("path/to/file" "xml")
          ))

正規表現を使うことも可能です:

(setf (ningle:route *app* "/hello/([\\w]+)" :regexp t)
      #'(lambda (params)
          (format nil "Hello, ~A!" (first (cdr (assoc :captures params))))))

必要条件

Routeは、様々な整合条件を含むことができます。例えば、Acceptの場合は次のようになります:

(setf (ningle:route *app* "/" :accept '("text/html" "text/xml"))
      #'(lambda (params)
          (declare (ignore params))
          "<html><body>Hello, World!</body></html>"))

(setf (ningle:route *app* "/" :accept "text/plain")
      #'(lambda (params)
          (declare (ignore params))
          "Hello, World!"))

独自に条件を定義することも簡単にできます。

(setf (ningle:requirement *app* :probability)
      #'(lambda (value)
          (<= (random 100) value)))

(setf (ningle:route *app* "/win_a_car" :probability 10)
      #'(lambda (params)
          (declare (ignore params))
          "You won!"))

(setf (ningle:route *app* "/win_a_car")
      #'(lambda (params)
          (declare (ignore params))
          "Sorry, you lost."))

リクエストとレスポンス

ningleには特別な変数が2つあります。*request**response*です。これらは、毎回のリクエストの度に、Lack.RequestLack.Responseに束縛されます。

例えば、これらを使うことで、それぞれのコントローラにおいて、レスポンスのステータスコードやContent-Type等を変更できます。

(setf (lack.response:response-headers *response*)
      (append (lack.response:response-headers *response*)
              (list :content-type "application/json")))

(setf (lack.response:response-headers *response*)
      (append (lack.response:response-headers *response*)
              (list :access-control-allow-origin "*")))

(setf (lack.response:response-status *response*) 201)

Context

ningleには、contextという便利な機能があります。contextは、内部のハッシュテーブルにアクセスするために使います。

(setf (context :database)
      (dbi:connect :mysql
                   :database-name "test-db"
                   :username "nobody"
                   :password "nobody"))

(context :database)
;;=> #<DBD.MYSQL:<DBD-MYSQL-CONNECTION> #x3020013D1C6D>

セッションを利用する

ningle自体にセッションの仕組みはありませんが、Lack.Builderと一緒にLack.Middleware.Sessionを使うことをおすすめします。

(import 'lack.builder:builder)

(clack:clackup
  (builder
    :session
    *app*))

もちろん、ningleとあわせて、他のLackミドルウェアを使うこともできます。

参考

作者

  • Eitaro Fukamachi (e.arrows@gmail.com)

著作権

Copyright (c) 2012-2014 Eitaro Fukamachi (e.arrows@gmail.com)

ライセンス

Licensed under the LLGPL License.