t-cool

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.