This is an early port of Ring, the Clojure web application library, to CFML.
Ring is based around the concepts of Handlers, Requests, Responses, and Middleware. It relies heavily on functional composition. You might want to read the Ring Concepts wiki page for background at this point. Ring for CFML is intended to be "as close to Ring (for Clojure) as possible, but no closer" (to paraphrase Andrew Koenig, describing C++ in terms of C). This README outlines the CFML-specific differences, but you should otherwise assume that Ring for CFML behaves just like Ring for Clojure and should be programmed the same way.
To explore the examples in this repository, I recommend using CommandBox. That will allow you to just git clone [email protected]:seancorfield/ring-cfml.git
, cd ring-cfml
, and run box start
.
Ring for CFML has been tested on:
- Adobe ColdFusion 11 and 2016 (it will not run on earlier versions due to reliance on built-in member functions).
- Lucee 4.5, 5.0, and 5.1
Jump into the ring-cfml Gitter room for support or to ask questions!
The basic concepts of Handlers, Requests, Responses, and Middleware are covered below, with CFML-Specific differences highlighted.
The basic building block in Ring is a handler -- a function that accepts a request (structure) and returns a response (structure). See below for definitions of those structures. A handler runs all of the business logic associated with a request and may return a response. For example:
// Application.cfc
component extends=ring.core {
handler = function( req ) {
resp = new ring.util.response();
return resp.response( "The time is #now()#." );
};
}
This handler returns a simple HTML response with the given text. No .cfm
page will be executed as part of any request since the handler produces a response.
A more interesting handler would render a CFML template:
// Application.cfc
component extends=ring.core {
handler = function( req ) {
resp = new ring.util.response();
return resp.file( req, "/path/to/some.cfm" );
};
}
The req
is passed into the file()
call and is available as a local variable in some.cfm
. ring.util.response/file()
is a CFML extension to Ring, made possible because CFML has its own builtin template engine -- how convenient!
In general, in your Application.cfc
, you would define handler
to be a stack of middleware wrapped around a custom handler function -- see below.
CFML-Specific: If your handler (and/or middleware) does not return a response (as defined below), Ring for CFML will automatically run the requested template, based on the URL. At the end of processing a request, Ring for CFML will default the HTTP status to 200
(OK), the Content-Type
to text/html; charset=utf-8
, and the HTTP body to ""
(a empty string). By contrast, Ring for Clojure has no such default behavior and a request that does not yield a proper response will usually lead to an error.
Ring abstracts an HTTP request as a simple structure. Most of the entries in the initial request structure are CGI variables, with the addition of:
uri
- equivalent toCGI.PATH_INFO
in Ring for CFML (and in fact it is an alias forpath_info
in the request),headers
- a structure containing all of the HTTP headers that came in with the request, with their names adjusted for CFML (-
in the header name becomes_
in the CFML structure).
This request structure is passed through the middleware and handler stack and those functions made read elements from it or add new elements to it. In general, middleware adds to the request structure before the handler is called, and adds to the response structure returned by the handler.
In particular, the wrap_params
middleware will add query_params
(if not already present) containing values from the query_string
, form_params
(if not already present) containing values from the HTTP body (POST/PUT/PATCH), and will add all those values to params
(creating it, if not already present).
CFML-Specific: The request structure is mutable and is implemented on top of the request
scope. This is an acknowledgement that CFML is not a functional programming language and has no immutable data structures. This may lead to slight differences in behavior compared to Ring for Clojure but they should be intuitive and in keeping with the CFML language.
Ring also abstracts an HTTP response as a simple structure. Any structure that contains the keys status
, body
, and a structure called headers
is considered an HTTP response. It may include additional keys but it must contain at least those three, otherwise it will be treated as an HTTP request in most contexts within Ring.
Any headers in the response structure are sent back to the client (with _
in the header name replaced by -
, reversing the mapping that added headers
to the request structure above), along with the status
and body
values. In addition, middleware may process other fields in the response and use them to send additional data back to the client (such as cookies).
The ring.util.response
namespace (component) provides convenience functions for creating a simple HTTP response, setting the Content-Type
header for the response, setting the HTTP status, adding headers and so on.
CFML-Specific: ring.util.response/file()
-- shown above under Handlers is a Ring for CFML extension -- and the Ring for Clojure predicate response?
becomes is_response()
in Ring for CFML.
Each piece of middleware is a function that accepts a handler and returns a new handler. In general, that new handler will perform call the original handler, performing some work either before or after (or both). The general form of a middleware function is:
function middleware( handler ) {
return function( req ) {
var new_req = preprocess( req );
if ( proceed( new_req ) ) {
var resp = handler( new_req );
return postprocess( resp );
} else {
return some_response();
}
};
}
The ring.middleware
namespace (component) contains some common, basic middleware:
wrap_cookies
- addsreq.cookies
as a structure containing all the cookies passed into a request; ifcookies
is added to a response, those cookies will be sent back to the client (along with the original cookies received); implemented on top of thecookie
scope,wrap_cors
- adds CORS support to the request and response (not yet implemented),wrap_exception
- calls the suppliedhandler
inside atry
/catch
and, if an exception occurs, logs it to the console and returns a 400 HTTP status with the exception message as the body; CFML-Specific,wrap_json_params
- addsreq.json_params
and expandsreq.params
with the values obtained by decoding the HTTP request body (as JSON or form-urlencoded data),wrap_json_response
- if the responsebody
is not a simple value, serializes it as JSON and sets theContent-Type
of the response to"application/json; charset=utf-8"
,wrap_params
- addsreq.query_params
,req.form_params
, and expandsreq.params
with the values from URL scope, form scope, and both scopes respectively,wrap_session
- addsreq.session
as a structure containing the contents of the session scope when a request starts; ifsession
is added to a response, those session values will be added back to the session scope before the response is sent to the client.
In addition, two convenience functions are providing for constructing stacks of middleware:
default_stack
- wraps the suppliedhandler
in a default set of middleware: JSON response & params, params, session, cookies, CORS, exception,stack
- wraps the suppliedhandler
in the specified sequence of middleware, in order.
Copyright (c) 2016-2017 Sean Corfield.
Distributed under the Apache Source License 2.0.