Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fukamachi/caveman
Lightweight web application framework for Common Lisp.
https://github.com/fukamachi/caveman
common-lisp framework web
Last synced: 5 days ago
JSON representation
Lightweight web application framework for Common Lisp.
- Host: GitHub
- URL: https://github.com/fukamachi/caveman
- Owner: fukamachi
- Created: 2011-02-26T07:50:03.000Z (almost 14 years ago)
- Default Branch: master
- Last Pushed: 2024-05-26T14:19:07.000Z (7 months ago)
- Last Synced: 2024-10-31T11:45:53.869Z (about 2 months ago)
- Topics: common-lisp, framework, web
- Language: Common Lisp
- Homepage: http://8arrow.org/caveman/
- Size: 945 KB
- Stars: 783
- Watchers: 66
- Forks: 63
- Open Issues: 47
-
Metadata Files:
- Readme: README.markdown
Awesome Lists containing this project
README
# Caveman2 - Lightweight web application framework
[![Build Status](https://travis-ci.org/fukamachi/caveman.svg?branch=master)](https://travis-ci.org/fukamachi/caveman)
## Usage
```common-lisp
(defparameter *web* (make-instance '))@route GET "/"
(defun index ()
(render #P"index.tmpl"))@route GET "/hello"
(defun say-hello (&key (|name| "Guest"))
(format nil "Hello, ~A" |name|))
```## About Caveman2
### What's different from Caveman "1"?
Everything. Caveman2 was written from scratch.
These are noteworthy points.
* Is based on [ningle](http://8arrow.org/ningle/)
* Has database integration
* Uses new, separate configuration system ([Envy](https://github.com/fukamachi/envy))
* Has new routing macro### The reason I wrote it from scratch:
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.
## Design Goal
Caveman is intended to be a collection of common parts of web applications. With Caveman2, I use three rules to make decisions:
* Be extensible.
* Be practical.
* Don't force anything.## Quickstart
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!
### Installation
Caveman2 is now available on [Quicklisp](https://www.quicklisp.org/beta/).
```common-lisp
(ql:quickload :caveman2)
```### Generating a project skeleton
```common-lisp
(caveman2:make-project #P"/path/to/myapp/"
:author "")
;-> 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
```### Start a server
This is an example that assumes that the name of your application is "myapp".
Before starting the server, you must first load your app.```common-lisp
(ql:quickload :myapp)
```Your application has functions named `start` and `stop` to start/stop your web application.
```common-lisp
(myapp:start :port 8080)
```As Caveman is based on Clack/Lack, you can choose which server to run on -- Hunchentoot, Woo or Wookie, etc.
```common-lisp
(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](https://github.com/fukamachi/clack/blob/master/roswell/clackup.ros).
$ ros install clack
$ which clackup
/Users/nitro_idiot/.roswell/bin/clackup$ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp
### Routing
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](https://github.com/arielnetworks/cl-annot). It takes a method, a URL-string, and a function.
```common-lisp
@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`.
```common-lisp
(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](http://www.sinatrarb.com/)-like routing system.
```common-lisp
;; 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.
```common-lisp
(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.
```common-lisp
(defroute "/hello/:name" (&rest params &key name)
;; ...
)
```Route patterns may also contain "wildcard" parameters. They are accessible by using `splat`.
```common-lisp
(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.
```common-lisp
(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`.
```common-lisp
(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`.
* String
* Pathname
* Clack's response list (containing Status, Headers and Body)### Redirection
Redirect to another route with`(redirect "url")`. A second optional argument is the status code, 302 by default.
### Reverse URLs
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.
### More helper functions
See also:
- `add-query-parameters base-url params`
### Structured query/post parameters
Parameter keys containing square brackets ("[" & "]") will be parsed as structured parameters. You can access the parsed parameters as `_parsed` in routers.
```html
```
```common-lisp
(defroute "/edit" (&key _parsed)
(format nil "~S" (cdr (assoc "person" _parsed :test #'string=))))
;=> "((\"name\" . \"Eitaro\") (\"email\" . \"[email protected]\") (\"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.
```html
```
```common-lisp
(defroute "/add" (&key _parsed)
(format nil "~S" (assoc "items" _parsed :test #'string=)))
;=> "(((\"name\" . \"WiiU\") (\"price\" . \"30000\")) ((\"name\" . \"PS4\") (\"price\" . \"69000\")))"
```### Templates
Caveman uses [Djula](http://mmontone.github.io/djula/djula/) as its default templating engine.
```html
{% extends "layouts/default.html" %}
{% block title %}Users | MyApp{% endblock %}
{% block content %}
{% endblock %}
``````common-lisp
(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](http://mmontone.github.io/djula/) you must explicity call `list` when passing the arguments to render so that the code executes.
```common-lisp
(import 'myapp.view:render)(render #P"users.html"
(list :users (get-users-from-db)))
```### JSON API
This is an example of a JSON API.
```common-lisp
(defroute "/user.json" (&key |id|)
(let ((person (find-person-from-db |id|)))
;; person => (:|name| "Eitaro Fukamachi" :|email| "[email protected]")
(render-json person)));=> {"name":"Eitaro Fukamachi","email":"[email protected]"}
````render-json` is a part of a skeleton project. You can find its code in "src/view.lisp".
### Static file
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](http://quickdocs.org/clack/api#package-CLACK.MIDDLEWARE.STATIC) for detail.
### Configuration
Caveman adopts [Envy](https://github.com/fukamachi/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:
```common-lisp
(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.
```common-lisp
(import 'myapp.config:config)(setf (osicat:environment-variable "APP_ENV") "development")
(config :debug)
;=> T
```### Database
When you add `:databases` to the configuration, Caveman enables database support. `:databases` is an association list of database settings.
```common-lisp
(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.
```common-lisp
(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](https://github.com/fukamachi/datafly) and [SxQL](https://github.com/fukamachi/sxql). See those sets of documentation for more information.
### Set HTTP headers or HTTP status
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](http://clacklisp.org/), these are instances of subclasses of [Clack.Request](http://quickdocs.org/clack/api#package-CLACK.REQUEST) and [Clack.Response](http://quickdocs.org/clack/api#package-CLACK.RESPONSE).
```common-lisp
(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.
```common-lisp
(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 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.
```common-lisp
(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.
```diff
'(: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.
- [Lack.Session.Store.Dbi](https://github.com/fukamachi/lack/blob/master/src/middleware/session/store/dbi.lisp)
### Throw an HTTP status code
```common-lisp
(import 'caveman2:throw-code)(defroute ("/auth" :method :POST) (&key |name| |password|)
(unless (authorize |name| |password|)
(throw-code 403)))
```### Specify error pages
To specify error pages for 404, 500 or such, define a method `on-exception` of your app.
```common-lisp
(defmethod on-exception ((app ) (code (eql 404)))
(declare (ignore app code))
(merge-pathnames #P"_errors/404.html"
*template-directory*))
```### Hot Deployment
Though Caveman doesn't have a feature for hot deployment, [Server::Starter](http://search.cpan.org/~kazuho/Server-Starter-0.15/lib/Server/Starter.pm) -- 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 `) to the `start_server` process.
### Error Log
Caveman outputs error backtraces to a file which is specified at `:error-log` in your configuration.
```common-lisp
(defconfig |default|
`(:error-log #P"/var/log/apps/myapp_error.log"
:databases
((:maindb :sqlite3 :database-name ,(merge-pathnames #P"myapp.db"
*application-root*)))))
```## Use another templating library
### CL-WHO
```common-lisp
(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."))))
;=> "
; Welcome to Caveman!Blah blah blah."
```* [CL-WHO Website](http://weitz.de/cl-who/)
### CL-Markup
```common-lisp
(import 'cl-markup:xhtml)(defroute "/" ()
(xhtml
(:head (:title "Welcome to Caveman!"))
(:body "Blah blah blah.")))
;=> "Welcome to Caveman!Blah blah blah."
```* [CL-Markup repository](https://github.com/arielnetworks/cl-markup)
### cl-closure-template
```html
{namespace myapp.view}{template renderIndex}
"Welcome to Caveman!
Blah blah blah.
{/template}
``````common-lisp
(import 'myapp.config:*template-directory*)(closure-template:compile-cl-templates (merge-pathnames #P"index.tmpl"
*template-directory*))(defroute "/" ()
(myapp.view:render-index))
```* [cl-closure-template](http://quickdocs.org/cl-closure-template/)
* [Closure Templates Documentation](https://developers.google.com/closure/templates/docs/overview)## See Also
* [Clack](http://clacklisp.org/) - Web application environment.
* [Lack](https://github.com/fukamachi/lack) - The core of Clack.
* [ningle](http://8arrow.org/ningle/) - Super micro web application framework that Caveman is based on.
* [Djula](http://mmontone.github.io/djula/) - HTML Templating engine.
* [CL-DBI](http://8arrow.org/cl-dbi/) - Database-independent interface library.
* [SxQL](http://8arrow.org/sxql/) - SQL builder library.
* [Envy](https://github.com/fukamachi/envy) - Configuration switcher.
* [Roswell](https://github.com/snmsts/roswell) - Common Lisp implementation manager.## Author
* Eitaro Fukamachi ([email protected])
# License
Licensed under the LLGPL License.