This is the jingle Reference Manual, version 0.1.0, generated automatically by Declt version 4.0 beta 2 "William Riker" on Sun Sep 15 04:07:16 2024 GMT+0.
The main system appears first, followed by any subsystem dependency.
jingle
jingle – ningle with bells and whistles
jingle
Marin Atanasov Nikolov <dnaeon@gmail.com>
Marin Atanasov Nikolov <dnaeon@gmail.com>
BSD 2-Clause
* jingle
=jingle= is [[https://github.com/fukamachi/ningle][ningle]], but with bells and whistles.
* Requirements
- [[https://www.quicklisp.org/beta/][Quicklisp]]
* Installation
=jingle= is not yet in Quicklisp, so in order to install it you will need to
clone the repo and add it to your [[https://www.quicklisp.org/beta/faq.html][Quicklisp local-projects]].
#+begin_src shell
cd ~/quicklisp/local-projects
git clone https://github.com/dnaeon/cl-jingle.git
#+end_src
* Demo
The =JINGLE.DEMO= system provides a ready-to-run example REST API,
which comes with an [[https://swagger.io/specification/][OpenAPI 3.x spec]], [[https://swagger.io/tools/swagger-ui/][Swagger UI]], and a command-line
interface app built with [[https://github.com/dnaeon/clingon][clingon]].
In order to build the demo application, simply execute this command.
#+begin_src shell
make demo
#+end_src
This will build the =jingle-demo= app, which you can find in the
=bin/= directory.
In order to start the HTTP server, execute the following command.
#+begin_src shell
bin/jingle-demo serve
#+end_src
Once the HTTP server is up and running navigate to
[[http://localhost:5000/api/docs/][http://localhost:5000/api/docs/]] which should take you to the Swagger
UI.
[[./images/jingle-swagger-ui.png]]
Make sure to check the various sub-commands provided by =jingle-demo=,
which allow you to interface with the REST APIs.
You can also run the demo app in Docker. First, build the image.
#+begin_src shell
make demo-docker
#+end_src
Once the image is built you can start up the service by executing the
following command.
#+begin_src shell
docker run -p 5000:5000 cl-jingle:latest serve –address 0.0.0.0
#+end_src
You can also view a short demo of the command-line interface
application, which interfaces with the REST API here.
[[./images/jingle-demo.gif]]
* Usage
Start up your REPL and load the =JINGLE= system.
#+begin_src lisp
CL-USER> (ql:quickload :jingle)
To load "jingle":
Load 1 ASDF system:
jingle
; Loading "jingle"
..................................................
[package jingle.core].............................
[package jingle]
(:JINGLE)
#+end_src
First thing we need to do is to create a new =JINGLE:APP= instance.
#+begin_src lisp
CL-USER> (defparameter *app* (jingle:make-app))
*APP*
#+end_src
When creating a new instance of =JINGLE:APP= you can provide
additional keyword args, which specify what HTTP server to use,
address to bind to, the port to listen on, middlewares, etc..
A very simple HTTP handler, which returns =Hello, World!= looks like
this.
#+begin_src lisp
(defun hello-handler (params)
(declare (ignore params))
"Hello, World!")
#+end_src
The following is an example of an HTTP handler which echoes back the
payload you send to it.
#+begin_src lisp
(defun echo-handler (params)
"A simple handler which echoes back the payload you send to it"
(declare (ignore params))
(jingle:set-response-header :content-type (jingle:request-content-type jingle:*request*))
(jingle:set-response-header :content-length (jingle:request-content-length jingle:*request*))
(maphash (lambda (k v)
(jingle:set-response-header k v))
(jingle:request-headers jingle:*request*))
(jingle:request-content jingle:*request*))
#+end_src
Next thing we need to do is register our handlers.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/hello") #’hello-handler)
CL-USER> (setf (jingle:route *app* "/echo" :method :post) #’echo-handler)
#+end_src
And now we can start the app.
#+begin_src lisp
CL-USER> (jingle:start *app*)
#+end_src
Trying out our endpoints using =curl(1)= gives us this result.
#+begin_src shell
$ curl -vvv –get http://localhost:5000/hello
* Trying 127.0.0.1:5000...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /hello HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.86.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 09 Dec 2022 09:46:13 GMT
< Server: Hunchentoot 1.3.0
< Transfer-Encoding: chunked
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello, World!
#+end_src
And this is our echo handler.
#+begin_src shell
$ curl -v -s –data ’{"foo": "bar", "baz": "42"}’ -H "My-Header: SomeValue" -H "Content-Type: application/json" -X POST http://localhost:5000/echo
* Trying 127.0.0.1:5000...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> POST /echo HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.86.0
> Accept: */*
> My-Header: SomeValue
> Content-Type: application/json
> Content-Length: 27
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 09 Dec 2022 13:57:30 GMT
< Server: Hunchentoot 1.3.0
< My-Header: SomeValue
< Accept: */*
< User-Agent: curl/7.86.0
< Host: localhost:5000
< Content-Length: 27
< Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"foo": "bar", "baz": "42"}
#+end_src
In order to stop the application, evaluate the following expression.
#+begin_src lisp
CL-USER> (jingle:stop *app*)
#+end_src
** Handlers
Handlers are regular [[https://github.com/fukamachi/ningle][ningle]] routes, which accept a single argument,
representing the request parameters.
** Environment
=jingle= exports the special variable =JINGLE:*ENV*= which is
dynamically bound to the request environment of [[https://github.com/fukamachi/lack][Lack]]. You can query
the environment directly from =jingle= and don’t have to worry about
where the environment is coming from.
** Headers
=jingle= provides the =JINGLE:SET-RESPONSE-HEADER= function for
setting up HTTP response headers.
A simple handler which sets the =Content-Type= header to =text/plain=
looks like this.
#+begin_src lisp
(defun hello (params)
(declare (ignore params))
(jingle:set-response-header :content-type "text/plain")
"Hello, World!")
#+end_src
Other useful functions which operate on HTTP headers are
=JINGLE:GET-REQUEST-HEADER= and =JINGLE:GET-RESPONSE-HEADER=, which
retrieve the value of the HTTP header associated with the request and
response respectively.
** Status Codes
The =JINGLE:SET-RESPONSE-STATUS= function sets the Status Code for the
HTTP Response.
#+begin_src lisp
(defun foo-handler (params)
(declare (ignore params))
(jingle:set-response-status :accepted)
"Task accepted")
#+end_src
Arguments passed to =JINGLE:SET-RESPONSE-STATUS= may be a number
(e.g. =400=), a keyword (e.g. =:bad-request=), or a string (e.g. =Bad
Request=) of the status code. The following three expressions are
equivalent, and they all set the HTTP Status Code to =400 (Bad
Request)=.
#+begin_src lisp
(jingle:set-response-status 400)
(jingle:set-response-status :bad-request)
(jingle:set-response-status "Bad Request")
#+end_src
Another useful function operating on HTTP Status Codes is
=JINGLE:EXPLAIN-STATUS-CODE=.
#+begin_src lisp
CL-USER> (jingle:explain-status-code 400)
"Bad Request"
CL-USER> (jingle:explain-status-code :bad-request)
"Bad Request"
#+end_src
=JINGLE:STATUS-CODE-KIND= returns the kind of the HTTP Status Code as
classified by [[https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml][IANA]], e.g.
#+begin_src lisp
CL-USER> (jingle:status-code-kind 400)
:CLIENT-ERROR
CL-USER> (jingle:status-code-kind :unauthorized)
:CLIENT-ERROR
CL-USER> (jingle:status-code-kind :internal-server-error)
:SERVER-ERROR
CL-USER> (jingle:status-code-kind :moved-permanently)
:REDIRECTION
CL-USER> (jingle:status-code-kind 100)
:INFORMATIONAL
CL-USER> (jingle:status-code-kind "Accepted")
:SUCCESS
#+end_src
Other HTTP status code predicates you may find useful are
=JINGLE:INFORMATIONAL-CODE-P=, =JINGLE:SUCCESS-CODE-P=,
=JINGLE:REDIRECTION-CODE-P=, =JINGLE:CLIENT-ERROR-CODE-P= and
=JINGLE:SERVER-ERROR-CODE-P=.
** Static Resources
Static resources can be served by adding them using
=JINGLE:STATIC-PATH= method, e.g.
#+begin_src lisp
(jingle:static-path *app* "/static/" "~/public_html/")
#+end_src
You can serve static resources from multiple directories as well. In
order to do that simply install them, before you start up the app.
#+begin_src lisp
(jingle:static-path *app* "/static-1/" "/path/to/static-1/")
(jingle:static-path *app* "/static-2/" "/path/to/static-2/")
(jingle:static-path *app* "/static-3/" "/path/to/static-3/")
#+end_src
** Directory Browser
The =JINGLE:SERVE-DIRECTORY= method installs a middleware which allows
you to browse the contents of a given path. For example the following
code exposes the =~/Documents= and =~/Projects= directories.
#+begin_src lisp
(jingle:serve-directory *app* "/docs" "~/Documents")
(jingle:serve-directory *app* "/projects" "~/Projects")
#+end_src
When accessing the directories from the browser make sure to add a
slash at the end of the paths. For example the above directories will
have to accessed at http://localhost:5000/docs/ and
http://localhost:5000/projects/ respectively, if you are using the
default HTTP port when starting up the app.
** Middlewares
You can use regular [[https://github.com/fukamachi/lack#middlewares][Lack middlewares]] with =jingle= as well. Simply
install them using the =JINGLE:INSTALL-MIDDLEWARE= method.
The following simple middleware pushes a new property to the request
environment, which can be queried by the HTTP handlers.
First, implement the middleware.
#+begin_src lisp
(defun my-middleware (app)
"A custom middleware which pushes a new property to the request
environment and exposes it to HTTP handlers."
(lambda (env)
(setf (getf env :my-middleware/message) "my middleware message")
(funcall app env)))
#+end_src
Then we create a =JINGLE:APP= and install it.
#+begin_src lisp
CL-USER> (defparameter *app* (jingle:make-app))
CL-USER> (jingle:install-middleware *app* #’my-middleware)
#+end_src
An example handler which uses the message placed by our middleware may
look like this.
#+begin_src lisp
(defun my-handler (params)
(declare (ignore params))
(jingle:set-response-status :ok)
(jingle:set-response-header :content-type "text/plain")
(getf jingle:*env* :my-middleware/message))
#+end_src
Finally we have to register our handler and start the app.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/my-middleware") #’my-handler)
CL-USER> (jingle:start *app*)
#+end_src
Trying it out using =curl(1)= returns the following response.
#+begin_src shell
$ curl -vvv –get http://localhost:5000/my-middleware
* Trying 127.0.0.1:5000...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /my-middleware HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.86.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 09 Dec 2022 11:42:17 GMT
< Server: Hunchentoot 1.3.0
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
* Connection #0 to host localhost left intact
my middleware message
#+end_src
Here’s an example which uses Lack’s =accesslog= middleware and how to
use it with =jingle=. First, load the respective system, which
provides the middleware, and then simply install it into the =jingle=
app.
#+begin_src lisp
CL-USER> (ql:quickload :lack-middleware-accesslog)
CL-USER> (jingle:install-middleware *app* lack.middleware.accesslog:*lack-middleware-accesslog*)
#+end_src
Search for other middlewares you can already use in Quicklisp, e.g.
#+begin_src lisp
CL-USER> (ql:system-apropos "lack-middleware")
#+end_src
You can use middlewares to push metadata into the environment for HTTP
handlers to use. For example, if your HTTP handlers need to read from
and write to a database, you may want to create a middleware, which
pushes a =CL-DBI= connection into the environment, so that HTTP
handlers can use it, when needed.
In order to clear out all installed middlewares you can use the
=JINGLE:CLEAR-MIDDLEWARES= method, e.g.
#+begin_src lisp
CL-USER> (jingle:clear-middlewares *app*)
#+end_src
** Redirects
Redirects in =jingle= are handled by the =JINGLE:REDIRECT= function.
An example HTTP handler which redirects to [[https://lispcookbook.github.io/cl-cookbook/][The Common Lisp Cookbook]]
looks like this.
#+begin_src lisp
(defun to-the-cookbook (params)
(declare (ignore params))
(jingle:redirect "https://lispcookbook.github.io/cl-cookbook/"))
#+end_src
Register the HTTP handler and start the app.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/cookbook") #’to-the-cookbook)
CL-USER> (jingle:start *app*)
#+end_src
Navigate to http://localhost:5000/cookbook and you will be
automatically redirected.
There is also another way for defining redirects using
=JINGLE:REDIRECT-ROUTE=. The following example shows how to install
two redirect routes to your =jingle= app, without having to
explicitely define the HTTP handlers in advance.
#+begin_src lisp
CL-USER> (jingle:redirect-route *app* "/sbcl" "https://sbcl.org/")
CL-USER> (jingle:redirect-route *app* "/ecl" "https://ecl.common-lisp.dev/")
#+end_src
** Request Parameters
The =JINGLE:GET-REQUEST-PARAM= function may be used within HTTP
handlers to get the value associated with a given parameter.
Suppose we have the following example HTTP handler, which returns
information about supported products and is exposed via the
=/api/v1/product/:name= endpoint.
#+begin_src lisp
(defparameter *products*
’((:|id| 1 :|name| "foo")
(:|id| 2 :|name| "bar")
(:|id| 3 :|name| "baz")
(:|id| 4 :|name| "qux")
(:|id| 5 :|name| "foo v2")
(:|id| 6 :|name| "bar v3")
(:|id| 7 :|name| "baz v4")
(:|id| 8 :|name| "qux v5"))
"The list of our supported products")
(defun find-product-by-name (name)
"Finds a product by name"
(find name
*products*
:key (lambda (item) (getf item :|name|))
:test #’string=))
(defun product-handler (params)
"Handles requests for /api/v1/product/:name endpoint"
(jingle:set-response-status :ok)
(jingle:set-response-header :content-type "application/json")
(let* ((name (jingle:get-request-param params :name))
(product (find-product-by-name name)))
(if product
(jonathan:to-json product)
(progn
(jingle:set-response-status :not-found)
(jonathan:to-json ’(:|error| "Product not found"))))))
#+end_src
Register the HTTP handler and start the app.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/api/v1/product/:name") #’product-handler)
CL-USER> (jingle:start *app*)
#+end_src
Testing it out with different product names using =curl(1)=.
#+begin_src shell
$ curl -s –get http://localhost:5000/api/v1/product/foo | jq ’.’
{
"id": 1,
"name": "foo"
}
$ curl -s –get http://localhost:5000/api/v1/product/bar | jq ’.’
{
"id": 2,
"name": "bar"
}
$ curl -s –get http://localhost:5000/api/v1/product/unknown | jq ’.’
{
"error": "Product not found"
}
#+end_src
Another example HTTP handler which returns a list of products in a
paginated way, exposed via the =/api/v1/products= endpoint.
#+begin_src lisp
(defun take (items from to)
"A helper function to return the ITEMS between FROM and TO range"
(let* ((len (length items))
(to (if (>= to len) len to)))
(if (>= from len)
nil
(subseq items from to))))
(defun products-handler (params)
"Handles requests for /api/v1/product and returns a page of products"
(jingle:set-response-status :ok)
(jingle:set-response-header :content-type "application/json")
;; Parse the ‘FROM’ and ‘TO’ query parameters. Use default values of
;; 0 and 5 for the params.
(let ((from (parse-integer (jingle:get-request-param params "from" "0") :junk-allowed t))
(to (parse-integer (jingle:get-request-param params "to" "5") :junk-allowed t)))
(cond
((or (null from) (null to)) (jingle:set-response-status :bad-request) nil) ;; NIL added here for the response body
((or (minusp from) (minusp to)) (jingle:set-response-status :bad-request) nil) ;; NIL added here for the response body
(t (jonathan:to-json (take *products* from to))))))
#+end_src
Register the new API endpoint.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/api/v1/products") #’products-handler)
#+end_src
Testing it out using =curl(1)= with different values for =from= and
=to= query params.
#+begin_src shell
$ curl -s –get ’http://localhost:5000/api/v1/products?from=0&to=2’ | jq ’.’
[
{
"id": 1,
"name": "foo"
},
{
"id": 2,
"name": "bar"
}
]
$ curl -s –get ’http://localhost:5000/api/v1/products?from=2&to=4’ | jq ’.’
[
{
"id": 3,
"name": "baz"
},
{
"id": 4,
"name": "qux"
}
]
#+end_src
Another way to retrieve request parameter values is to use the
=JINGLE:WITH-REQUEST-PARAMS= macro. The previous example handler can
be rewritten this way.
#+begin_src lisp
(defun products-handler (params)
(jingle:with-json-response
(jingle:with-request-params ((from-param "from" "0") (to-param "to" "5")) params
;; Parse the query parameters and make sure we’ve got good values
(let ((from (parse-integer from-param :junk-allowed t))
(to (parse-integer to-param :junk-allowed t)))
(cond
((or (null from) (null to))
(jingle:set-response-status :bad-request)
nil) ;; NIL added here for the response body
((or (minusp from) (minusp to))
(jingle:set-response-status :bad-request)
nil) ;; NIL added here for the response body
(t (take *products* from to)))))))
#+end_src
** Macros
The following helper macros are available in =jingle=.
- =JINGLE:WITH-JSON-RESPONSE=
- =JINGLE:WITH-REQUEST-PARAMS=
- =JINGLE:WITH-HTML-RESPONSE=
The =JINGLE:WITH-JSON-RESPONSE= macro sets up various HTTP headers
such as =Content-Type= to =application/json= for you and evaluates the
body. The last evaluated expression from the body is encoded as a JSON
object using =JONATHAN:TO-JSON=.
The following example uses =LOCAL-TIME= and =JONATHAN= systems, so
make sure you have them loaded already.
#+begin_src lisp
(defclass ping-response ()
((message
:initarg :message
:initform "pong"
:reader ping-response-message
:documentation "Message to send as part of the response")
(timestamp
:initarg :timestamp
:initform (local-time:now)
:reader ping-response-timestamp))
(:documentation "A response sent as part of a PING request"))
(defmethod jonathan:%to-json ((object ping-response))
(jonathan:with-object
(jonathan:write-key-value "message" (ping-response-message object))
(jonathan:write-key-value "timestamp" (ping-response-timestamp object))))
(defun ping-handler (params)
(declare (ignore params))
(jingle:with-json-response
(make-instance ’ping-response)))
#+end_src
Register the HTTP handler and start the app.
#+begin_src lisp
CL-USER> (setf (jingle:route *app* "/api/v1/ping") #’ping-handler)
CL-USER> (jingle:start *app*)
#+end_src
Trying it you should see results similar to the ones below.
#+begin_src shell
$ curl -s –get http://localhost:5000/api/v1/ping | jq ’.’
{
"message": "pong",
"timestamp": 1670593969
}
$ curl -s –get http://localhost:5000/api/v1/ping | jq ’.’
{
"message": "pong",
"timestamp": 1670593974
}
$ curl -s –get http://localhost:5000/api/v1/ping | jq ’.’
{
"message": "pong",
"timestamp": 1670593976
}
#+end_src
The =JINGLE:WITH-REQUEST-PARAMS= macro provides an easy way to bind
symbols to request params from within HTTP handlers.
#+begin_src lisp
(defun foo-handler (params)
(jingle:with-request-params ((foo "foo") (bar "bar")) params
;; Use FOO and BAR params in order to ...
...))
#+end_src
The =JINGLE:WITH-HTML-RESPONSE= is similar to
=JINGLE:WITH-JSON-RESPONSE=, but sets up the response with a
=Content-Type: text/html; charset=utf-8= header.
** Error Handling
The =JINGLE:BASE-HTTP-ERROR= condition may be used as the base for
user-defined conditions.
If a condition is signalled from within HTTP handlers and the
condition is a sub-class of =JINGLE:BASE-HTTP-ERROR=, then the
=JINGLE:HANDLE-ERROR= method will be invoked.
The purpose of =JINGLE:HANDLE-ERROR= is to handle the error and set up
an appropriate HTTP response, which will be returned to the client.
The rest of this section describes how to create and use custom errors
for a very simple REST API. The API we will develop provides the
following endpoints.
#+begin_src shell
GET /api/v1/product => Returns a list of products (supports ‘from‘ and ‘to‘ query params)
GET /api/v1/product/:name => Returns a product by name, if found
#+end_src
The error responses which we will return to clients would look like this.
#+begin_src javascript
{
"error": "<Reason for the error response>"
}
#+end_src
First we will define our =API-ERROR= condition, and then define the
=JINGLE:HANDLE-ERROR= method on it, so that we return consistent error
responses to our API clients.
#+begin_src lisp
(define-condition api-error (jingle:base-http-error)
()
(:documentation "Represents a condition which will be signalled on API errors"))
(defmethod jingle:handle-error ((error api-error))
"Handles the error and sets up the HTTP error response to be sent to clients"
(with-accessors ((code jingle:http-error-code)
(body jingle:http-error-body)) error
(jingle:set-response-status code)
(jingle:set-response-header :content-type "application/json")
(jonathan:to-json (list :|error| body))))
#+end_src
Next, we will implement some helper functions that signal common
client-error HTTP responses.
#+begin_src lisp
(defun throw-not-found-error (message)
"Throws a 404 (Not Found) HTTP response"
(error ’api-error :code :not-found :body message))
(defun throw-bad-request-error (message)
"Throws a 400 (Bad Request) HTTP response"
(error ’api-error :code :bad-request :body message))
#+end_src
Having our conditions and error-related functions we will also define
another helper function, which will be responsible for parsing HTTP
query parameters as integers, which we will use in our handlers.
#+begin_src lisp
(defun get-int-param (params name &optional default)
"Gets the NAME parameter from PARAMS and parses it as an integer.
In case of invalid input it will signal a 400 (Bad Request) error"
(let ((raw (jingle:get-request-param params name default)))
(typecase raw
(number raw)
(null (throw-bad-request-error (format nil "missing value for ‘~A‘ param" name)))
(string (let ((parsed (parse-integer raw :junk-allowed t)))
(unless parsed
(throw-bad-request-error (format nil "invalid value for ‘~A‘ param" name)))
parsed))
(t (throw-bad-request-error (format nil "unsupported value for ‘~A‘ param" name))))))
#+end_src
We will be building on top of the /products/ API, which was shown in a
previous section. The =*PRODUCTS*= var will be our "database" in this
simple API.
#+begin_src lisp
(defparameter *products*
’((:|id| 1 :|name| "foo")
(:|id| 2 :|name| "bar")
(:|id| 3 :|name| "baz")
(:|id| 4 :|name| "qux")
(:|id| 5 :|name| "foo v2")
(:|id| 6 :|name| "bar v3")
(:|id| 7 :|name| "baz v4")
(:|id| 8 :|name| "qux v5"))
"The list of our supported products")
(defun find-product-by-name (name)
"Finds a product by name"
(find name
*products*
:key (lambda (item) (getf item :|name|))
:test #’string=))
(defun take (items from to)
"A helper function to return the ITEMS between FROM and TO range"
(let* ((len (length items))
(to (if (>= to len) len to)))
(if (>= from len)
nil
(subseq items from to))))
#+end_src
And these are the actual HTTP handlers, which will accept and handle
client requests.
#+begin_src lisp
(defun get-product-handler (params)
"Handles requests for the /api/v1/product/:name endpoint"
(jingle:with-json-response
(let* ((name (jingle:get-request-param params :name))
(product (find-product-by-name name)))
(unless product
(throw-not-found-error "product not found"))
product)))
(defun get-products-page-handler (params)
"Handles requests for the /api/v1/product endpoint"
(jingle:with-json-response
(let ((from (get-int-param params "from" 0))
(to (get-int-param params "to" 2)))
(when (or (minusp from) (minusp to))
(throw-bad-request-error "‘from‘ and ‘to‘ must be positive"))
(take *products* from to))))
#+end_src
Finally, we will create our =JINGLE:APP=, register our handlers and
start serving HTTP requests.
#+begin_src lisp
CL-USER> (defparameter *app* (jingle:make-app))
CL-USER> (setf (jingle:route *app* "/api/v1/product") #’get-products-page-handler)
CL-USER> (setf (jingle:route *app* "/api/v1/product/:name") #’get-product-handler)
CL-USER> (jingle:start *app*)
#+end_src
Time to test things out.
#+begin_src shell
# Getting a page of products using default ‘from‘ and ‘to‘ params
$ curl -s –get ’http://localhost:5000/api/v1/product’ | jq ’.’
[
{
"id": 1,
"name": "foo"
},
{
"id": 2,
"name": "bar"
}
]
# Getting next page of products
$ curl -s –get ’http://localhost:5000/api/v1/product?from=2&to=4’ | jq ’.’
[
{
"id": 3,
"name": "baz"
},
{
"id": 4,
"name": "qux"
}
]
# Passing invalid query params
$ curl -s –get ’http://localhost:5000/api/v1/product?from=bad-value’ | jq ’.’
{
"error": "invalid value for ‘from‘ param"
}
# Passing negative values
$ curl -s –get ’http://localhost:5000/api/v1/product?to=-42’ | jq ’.’
{
"error": "‘from‘ and ‘to‘ must be positive"
}
# Getting a product by name
$ curl -s –get ’http://localhost:5000/api/v1/product/foo’ | jq ’.’
{
"id": 1,
"name": "foo"
}
# Getting a non-existing product
$ curl -s –get ’http://localhost:5000/api/v1/product/unknown’ | jq ’.’
{
"error": "product not found"
}
#+end_src
Great, things work as expected and our API clients will receive
consistent error responses with the proper HTTP status codes set.
** Reverse URLs
When you register routes in your app with names, you can then refer to
these routes by their names. This is useful in situations where you
need to get the URL for a particular route.
In order to get the URL for a route with a particular name use the
=JINGLE:URL-FOR= generic function.
Consider the HTTP handlers we have shown in the previous section of
this document.
#+begin_src lisp
(defun get-product-handler (params)
"Handles requests for the /api/v1/product/:name endpoint"
(jingle:with-json-response
(let* ((name (jingle:get-request-param params :name))
(product (find-product-by-name name)))
(unless product
(throw-not-found-error "product not found"))
product)))
(defun get-products-page-handler (params)
"Handles requests for the /api/v1/product endpoint"
(jingle:with-json-response
(let ((from (get-int-param params "from" 0))
(to (get-int-param params "to" 2)))
(when (or (minusp from) (minusp to))
(throw-bad-request-error "‘from‘ and ‘to‘ must be positive"))
(take *products* from to))))
#+end_src
We can register these handlers and associate them with a name, which we
can later refer to.
#+begin_src lisp
CL-USER> (defparameter *app* (jingle:make-app))
CL-USER> (setf (jingle:route *app* "/api/v1/product" :method :get :identifier "get-products-page")
#’get-products-page-handler)
CL-USER> (setf (jingle:route *app* "/api/v1/product/:name" :method :get :identifier "get-product-by-name")
#’get-product-handler)
#+end_src
Now, we can get the actual URLs for our HTTP handlers by using their
names.
#+begin_src lisp
CL-USER> (jingle:url-for *app* "get-product-by-name" :name "foo")
#<QURI.URI:URI /api/v1/product/foo>
CL-USER> (jingle:url-for *app* "get-products-page" :|from| 0 :|to| 100)
#<QURI.URI:URI /api/v1/product?from=0&to=100>
#+end_src
Resolving URLs using =JINGLE:URL-FOR= is also useful when you are
creating test cases for your HTTP handlers. Within your test cases
instead of manually constructing the URLs to the respective HTTP
handlers you may refer to them by using their names.
Make sure to also check the =JINGLE.DEMO= system, which uses a
/handler registry/, which is used for registering the HTTP handlers
for a =JINGLE:APP=.
Once you resolve the URL for a particular handler you can construct
the final URL, which will contain scheme, hostname, etc.
#+begin_src lisp
CL-USER> (jingle:url-for *app* "get-product-by-name" :name "foo")
#<QURI.URI:URI /api/v1/product/foo>
CL-USER> (quri:merge-uris * (quri:make-uri :host "example.org" :scheme "https"))
#<QURI.URI.HTTP:URI-HTTPS https://example.org/api/v1/product/foo>
#+end_src
** Testing HTTP Handlers
The =JINGLE:TEST-APP= is a test app meant to be used for test
cases. The difference between =JINGLE:TEST-APP= and =JINGLE:APP= is
that the test app always binds on =127.0.0.1= and listens on a random
port within a given range.
Also, when using =JINGLE:URL-FOR= generic function with a
=JINGLE:TEST-APP= the result is a full URL, which contains the scheme,
the hostname and the port of the running test HTTP server.
Make sure to check the =JINGLE.DEMO.TEST= system for some examples,
which provides the test suite for the demo application.
* Contributing
=jingle= is hosted on [[https://github.com/dnaeon/cl-jingle][Github]]. Please contribute by reporting issues,
suggesting features or by sending patches using pull requests.
* License
This project is Open Source and licensed under the [[http://opensource.org/licenses/BSD-2-Clause][BSD License]].
* Authors
- Marin Atanasov Nikolov <dnaeon@gmail.com>
0.1.0
str
(system).
quri
(system).
myway
(system).
lack
(system).
lack-middleware-static
(system).
lack-middleware-mount
(system).
lack-app-directory
(system).
clack
(system).
ningle
(system).
cl-reexport
(system).
local-time
(system).
jonathan
(system).
find-port
(system).
jingle
(module).
Modules are listed depth-first from the system components tree.
jingle/jingle
jingle
(system).
codes.lisp
(file).
utils.lisp
(file).
core.lisp
(file).
package.lisp
(file).
Files are sorted by type and then listed depth-first from the systems components trees.
jingle/jingle.asd
jingle/jingle/codes.lisp
jingle/jingle/utils.lisp
jingle/jingle/core.lisp
jingle/jingle/package.lisp
jingle/jingle/codes.lisp
jingle
(module).
*status-codes*
(special variable).
client-error-code-p
(function).
explain-status-code
(function).
informational-code-p
(function).
lookup-status-code
(function).
lookup-status-code-or-lose
(function).
redirection-code-p
(function).
server-error-code-p
(function).
status-code-kind
(function).
status-code-number
(function).
success-code-p
(function).
jingle/jingle/utils.lisp
jingle
(module).
get-request-header
(function).
get-request-param
(function).
get-response-header
(function).
redirect
(function).
request-header-is-set-p
(function).
response-header-is-set-p
(function).
set-response-body
(function).
set-response-header
(function).
set-response-status
(function).
with-html-response
(macro).
with-json-response
(macro).
with-request-params
(macro).
%get-response-header-values
(function).
jingle/jingle/core.lisp
codes.lisp
(file).
utils.lisp
(file).
jingle
(module).
*env*
(special variable).
address
(reader method).
(setf address)
(writer method).
app
(class).
base-http-error
(condition).
call
(method).
clear-middlewares
(generic function).
configure
(generic function).
debug-mode
(reader method).
(setf debug-mode)
(writer method).
find-route
(generic function).
handle-error
(generic function).
http-error-body
(reader method).
(setf http-error-body)
(writer method).
http-error-code
(reader method).
(setf http-error-code)
(writer method).
http-server
(reader method).
(setf http-server)
(writer method).
http-server-kind
(reader method).
(setf http-server-kind)
(writer method).
install-middleware
(generic function).
make-app
(function).
make-test-app
(function).
middlewares
(reader method).
(setf middlewares)
(writer method).
port
(reader method).
(setf port)
(writer method).
redirect-route
(generic function).
serve-directory
(generic function).
silent-mode
(reader method).
(setf silent-mode)
(writer method).
start
(generic function).
static-path
(generic function).
stop
(generic function).
test-app
(class).
url-for
(generic function).
use-thread
(reader method).
(setf use-thread)
(writer method).
jingle/jingle/package.lisp
codes.lisp
(file).
utils.lisp
(file).
core.lisp
(file).
jingle
(module).
Packages are listed by definition order.
jingle.codes
common-lisp
.
*status-codes*
(special variable).
client-error-code-p
(function).
explain-status-code
(function).
informational-code-p
(function).
lookup-status-code
(function).
lookup-status-code-or-lose
(function).
redirection-code-p
(function).
server-error-code-p
(function).
status-code-kind
(function).
status-code-number
(function).
success-code-p
(function).
jingle.utils
common-lisp
.
get-request-header
(function).
get-request-param
(function).
get-response-header
(function).
redirect
(function).
request-header-is-set-p
(function).
response-header-is-set-p
(function).
set-response-body
(function).
set-response-header
(function).
set-response-status
(function).
with-html-response
(macro).
with-json-response
(macro).
with-request-params
(macro).
%get-response-header-values
(function).
jingle.core
common-lisp
.
*env*
(special variable).
address
(generic reader).
(setf address)
(generic writer).
app
(class).
base-http-error
(condition).
clear-middlewares
(generic function).
configure
(generic function).
debug-mode
(generic reader).
(setf debug-mode)
(generic writer).
find-route
(generic function).
handle-error
(generic function).
http-error-body
(generic reader).
(setf http-error-body)
(generic writer).
http-error-code
(generic reader).
(setf http-error-code)
(generic writer).
http-server
(generic reader).
(setf http-server)
(generic writer).
http-server-kind
(generic reader).
(setf http-server-kind)
(generic writer).
install-middleware
(generic function).
make-app
(function).
make-test-app
(function).
middlewares
(generic reader).
(setf middlewares)
(generic writer).
port
(generic reader).
(setf port)
(generic writer).
redirect-route
(generic function).
serve-directory
(generic function).
silent-mode
(generic reader).
(setf silent-mode)
(generic writer).
start
(generic function).
static-path
(generic function).
stop
(generic function).
test-app
(class).
url-for
(generic function).
use-thread
(generic reader).
(setf use-thread)
(generic writer).
Definitions are sorted by export status, category, package, and then by lexicographic order.
*ENV* will be dynamically bound to the Lack environment. It can be used to query the environment from within the HTTP handlers.
HTTP Status Code Registry from IANA
A helper macro to be used from within HTTP handlers, which sets the Content-Type to text/html. On error, it will return a response with status code 500 (Internal Server Error) and a short HTML body describing the condition, which was signalled.
A helper macro to be used from within HTTP handlers, which sets the Content-Type to application/json, and encodes the last evaluated expression of BODY as a JSON object.
A helper macro which binds symbols to values from the request
parameters, and evalutes BODY.
PARAM-ITEMS is a list of (SYMBOL PARAM-NAME DEFAULT-VALUE) lists, which represent the symbols to be bound while evaluating the BODY.
PARAMS-ALIST is the alist passed to the jingle handler.
Returns T, if the HTTP Status code is classified as ‘Client Error’
Returns the text notes for the HTTP Status Code by looking up the registry
Returns two values, first one is a list representing the values of the requested header NAME, and the second is T or NIL, depending on whether the header was set.
Returns the value associated with the NAME param. If NAME param is not set, returns DEFAULT.
Returns two values, first one is a list representing the values of the requested response header NAME, and the second one is T or NIL, depending on whether the header was set.
Returns T, if the HTTP Status code is classified as ‘Informational’
Returns the HTTP Status Code associated with VALUE by looking up the registry
Creates a new jingle app
Creates a new jingle app for testing purposes only. This app is useful only when you are creating test cases for your HTTP handlers, as it binds on 127.0.0.1 and listens on a random port within the PORT-MIN:PORT-MAX range.
Sets the HTTP status code to CODE and Location header to LOCATION
Returns T, if the HTTP Status code is classified as ‘Redirection’
Predicate for testing whether a given request header is set
Predicate for testing whether a given response header is set
Returns T, if the HTTP Status code is classified as ‘Server Error’
Sets the body for the HTTP response to BODY. Internally ningle’s response is an instance of LACK.RESPONSE:RESPONSE. This function is meant to be used from within handlers.
Sets the HTTP header with the given NAME to VALUE to be sent to the client as part of the HTTP response. Internally ningle’s response is an instance of LACK.RESPONSE:RESPONSE. This function is meant to be used from within handlers.
Sets the status code for the HTTP response to CODE. Internally ningle’s response is an instance of LACK.RESPONSE:RESPONSE. This function is meant to be used from within handlers. VALUE may be a number, a keyword or a string representing the HTTP Status Code
Returns the kind of the HTTP Status Code
Returns the HTTP Status Code number associated with VALUE. If VALUE is a number, then return VALUE. If VALUE is a string or keyword then lookup the status code registry for the code.
Returns T, if the HTTP Status code is classified as ‘Success’
Removes all middlewares from the app
Performs any steps needed to configure the application, before starting it up
Finds and returns the route with the given name
Handles the CONDITION by setting up appropriate HTTP response to send to the client
base-http-error
)) ¶base-http-error
)) ¶base-http-error
)) ¶base-http-error
)) ¶base-http-error
)) ¶Adds a new middleware to the app
Creates a redirection route for the APP at the given PATH to LOCATION
Serves the files from the given root directory
Starts the jingle application and serves requests
Adds a static path to serve files from
Stops the jingle application and the underlying HTTP server
Returns the URL for the given route NAME with all PARAMS applied to it
app
) env) ¶Dynamically binds *ENV* to the Lack environment before it hits the HTTP handlers. This way the HTTP handlers can interact with the surrounding environment exposed by Lack by using JINGLE.CORE:*ENV*.
If a condition is signalled and the condition is a sub-class of JINGLE:BASE-HTTP-ERROR, then invoke JINGLE:HANDLE-ERROR for setting up an appropriate HTTP response for the client.
lack.component
.
Base condition class for HTTP errors
simple-error
.
The HTTP Status Code
(quote (error "must specify the http status code"))
:code
The body to send as part of the HTTP response
(quote nil)
:body
The jingle app
app
.
(setf address)
.
address
.
call
.
clear-middlewares
.
configure
.
(setf debug-mode)
.
debug-mode
.
find-route
.
(setf http-server)
.
http-server
.
(setf http-server-kind)
.
http-server-kind
.
install-middleware
.
(setf middlewares)
.
middlewares
.
(setf port)
.
port
.
redirect-route
.
serve-directory
.
(setf silent-mode)
.
silent-mode
.
start
.
static-path
.
stop
.
url-for
.
(setf use-thread)
.
use-thread
.
The underlying HTTP server of the app. Do not set this slot directly.
:http-server
The HTTP server to use, e.g. :hunchentoot, :woo, etc.
:hunchentoot
:http-server-kind
The list of middlewares for the app
:middlewares
The address on which the server will listen on
"127.0.0.1"
:address
The port on which the server will listen to
5000
:port
port
.
If set to T, will start the app in debug mode
t
:debug-mode
Do not output informational messages from Clack, if set to T
:silent-mode
Start server in a separate thread, if set to T
t
:use-thread
A jingle application used for testing purposes only
app
.
Collects all values for the response header with the given NAME
Jump to: | %
(
A C D E F G H I L M P R S U W |
---|
Jump to: | %
(
A C D E F G H I L M P R S U W |
---|
Jump to: | *
A B C D H M P S U |
---|
Jump to: | *
A B C D H M P S U |
---|
Jump to: | A B C F J M P S T U |
---|
Jump to: | A B C F J M P S T U |
---|