dnaeon / cl-jingle

Common Lisp web framework with bells and whistles (based on ningle)
Other
48 stars 5 forks source link
common-lisp lisp web webdev webframework

=jingle= is [[https://github.com/fukamachi/ningle][ningle]], but with bells and whistles.

=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

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]]

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

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

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

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' andTO' 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=.

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": "" }

+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.

=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.

This project is Open Source and licensed under the [[http://opensource.org/licenses/BSD-2-Clause][BSD License]].