(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|))
Everything. Caveman2 was written from scratch.
These are noteworthy points.
One of the most frequently asked questions was "Which should I use: ningle or Caveman? What are the differences?" I think these were asked so frequently because Caveman and ningle were too similar. Both of them are called "micro", and had no database support.
With Caveman2, Caveman is no longer a "micro" web application framework. It supports CL-DBI, and has database connection management by default. Caveman has started growing up.
Caveman is intended to be a collection of common parts of web applications. With Caveman2, I use three rules to make decisions:
You came here because you're interested in living like a caveman, right? This isn't Disneyland, but we can start here. Let's get into a cave!
Caveman2 is now available on 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
This is an example that assumes that the name of your application is "myapp". Before starting the server, you must first load your app.
(ql:quickload :myapp)
Your application has functions named start
and stop
to start/stop your web application.
(myapp:start :port 8080)
As Caveman is based on Clack/Lack, you can choose which server to run on -- Hunchentoot, Woo or Wookie, etc.
(myapp:start :server :hunchentoot :port 8080)
(myapp:start :server :fcgi :port 8080)
I recommend you use Hunchentoot on a local machine, and use Woo in a production environment.
You can also start your application by using clackup command.
$ ros install clack
$ which clackup
/Users/nitro_idiot/.roswell/bin/clackup
$ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp
Caveman2 provides 2 ways to define a route -- @route
and defroute
. You can use either.
@route
is an annotation macro, defined by using cl-annot. It takes a method, a URL-string, and a function.
@route GET "/"
(defun index ()
...)
;; A route with no name.
@route GET "/welcome"
(lambda (&key (|name| "Guest"))
(format nil "Welcome, ~A" |name|))
This is similar to Caveman1's @url
except for its argument list. You don't have to specify an argument when it is not required.
defroute
is just a macro. It provides the same functionality as @route
.
(defroute index "/" ()
...)
;; A route with no name.
(defroute "/welcome" (&key (|name| "Guest"))
(format nil "Welcome, ~A" |name|))
Since Caveman bases on ningle, Caveman also has the Sinatra-like routing system.
;; 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) () ...)
Route patterns may contain "keywords" to put the value into the argument.
(defroute "/hello/:name" (&key name)
(format nil "Hello, ~A" name))
The above controller will be invoked when you access "/hello/Eitaro" or "/hello/Tomohiro", and name
will be "Eitaro" or "Tomohiro", as appropriate.
(&key name)
is almost same as a lambda list of Common Lisp, except it always allows other keys.
(defroute "/hello/:name" (&rest params &key name)
;; ...
)
Route patterns may also contain "wildcard" parameters. They are accessible by using splat
.
(defroute "/say/*/to/*" (&key splat)
; matches /say/hello/to/world
(format nil "~A" splat))
;=> (hello world)
(defroute "/download/*.*" (&key splat)
; matches /download/path/to/file.xml
(format nil "~A" splat))
;=> (path/to/file xml)
If you'd like to write use a regular expression in a URL rule, :regexp t
should work.
(defroute ("/hello/([\\w]+)" :regexp t) (&key captures)
(format nil "Hello, ~A!" (first captures)))
Normally, routes are tested for a match in the order they are defined, and only the first route matched is invoked, with the following routes being ignored. However, a route can continue testing for matches in the list, by including next-route
.
(defroute "/guess/:who" (&key who)
(if (string= who "Eitaro")
"You got me!"
(next-route)))
(defroute "/guess/*" ()
"You missed!")
You can return following formats as the result of defroute
.
Redirect to another route with(redirect "url")
. A second optional argument is the status code, 302 by default.
When you defined routes with names, you can find the URL from a name with (url-for route-name &rest params)
.
The function will throw an error if no route is found.
See also:
add-query-parameters base-url params
Parameter keys containing square brackets ("[" & "]") will be parsed as structured parameters. You can access the parsed parameters as _parsed
in routers.
<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))))"
;; With assoc-utils
(ql:quickload :assoc-utils)
(import 'assoc-utils:aget)
(defroute "/edit" (&key _parsed)
(format nil "~S" (aget _parsed "person")))
Blank keys mean they have multiple values.
<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 uses Djula as its default templating engine.
{% extends "layouts/default.html" %}
{% block title %}Users | MyApp{% endblock %}
{% block content %}
<div id="main">
<ul>
{% for user in users %}
<li><a href="https://github.com/fukamachi/caveman/blob/master/{{ 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))
If you want to get something from a database or execute a function using Djula you must explicity call list
when passing the arguments to render so that the code executes.
(import 'myapp.view:render)
(render #P"users.html"
(list :users (get-users-from-db)))
This is an example of a 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
is a part of a skeleton project. You can find its code in "src/view.lisp".
Images, CSS, JS, favicon.ico and robot.txt in "static/" directory will be served by default.
/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
You can change these rules by rewriting "PROJECT_ROOT/app.lisp". See Clack.Middleware.Static for detail.
Caveman adopts Envy as a configuration switcher. This allows definition of multiple configurations and switching between them according to an environment variable.
This is a typical example:
(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|))
Every configuration is a property list. You can choose the configuration which to use by setting APP_ENV
.
To get a value from the current configuration, call myapp.config:config
with the key you want.
(import 'myapp.config:config)
(setf (osicat:environment-variable "APP_ENV") "development")
(config :debug)
;=> T
When you add :databases
to the configuration, Caveman enables database support. :databases
is an association list of database settings.
(defconfig |production|
'(:databases
((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234")
(:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234"))))
db
in a package myapp.db
is a function for connecting to each databases configured the above. Here is an example.
(use-package '(:myapp.db :sxql :datafly))
(defun search-adults ()
(with-connection (db)
(retrieve-all
(select :*
(from :person)
(where (:>= :age 20))))))
The connection is alive during the Lisp session, and will be reused in every HTTP request.
retrieve-all
and the query language came from datafly and SxQL. See those sets of documentation for more information.
There are several special variables available during a HTTP request. *request*
and *response*
represent a request and a response. If you are familiar with Clack, these are instances of subclasses of Clack.Request and Clack.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)
If you would like to set Content-Type "application/json" for all "*.json" requests, next-route
can be used.
(defroute "/*.json" ()
(setf (getf (response-headers *response*) :content-type) "application/json")
(next-route))
(defroute "/user.json" () ...)
(defroute "/search.json" () ...)
(defroute ("/new.json" :method :POST) () ...)
Session data is for memorizing user-specific data. *session*
is a hash table that stores session data.
This example increments :counter
in the session, and displays it for each visitor.
(defroute "/counter" ()
(format nil "You came here ~A times."
(incf (gethash :counter *session* 0))))
Caveman2 stores session data in-memory by default. To change this, specify :store
to :session
in "PROJECT_ROOT/app.lisp".
This example uses RDBMS to store session data.
'(: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)
NOTE: Don't forget to add :lack-session-store-dbi
as :depends-on
of your app. It is not a part of Clack/Lack.
See the source code of Lack.Session.Store.DBi for more information.
(import 'caveman2:throw-code)
(defroute ("/auth" :method :POST) (&key |name| |password|)
(unless (authorize |name| |password|)
(throw-code 403)))
To specify error pages for 404, 500 or such, define a method on-exception
of your app.
(defmethod on-exception ((app <web>) (code (eql 404)))
(declare (ignore app code))
(merge-pathnames #P"_errors/404.html"
*template-directory*))
Though Caveman doesn't have a feature for hot deployment, Server::Starter -- a Perl module -- makes it easy.
$ APP_ENV=production start_server --port 8080 -- clackup --server :fcgi app.lisp
NOTE: Server::Starter requires the server to support binding on a specific fd, which means only :fcgi
and :woo
are the ones work with start_server
command.
To restart the server, send HUP signal (kill -HUP <pid>
) to the start_server
process.
Caveman outputs error backtraces to a file which is specified at :error-log
in your configuration.
(defconfig |default|
`(:error-log #P"/var/log/apps/myapp_error.log"
:databases
((:maindb :sqlite3 :database-name ,(merge-pathnames #P"myapp.db"
*application-root*)))))
(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>"
(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>"
{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))
Licensed under the LLGPL License.