https://github.com/hoplon/castra.git
git clone 'https://github.com/hoplon/castra.git'
(ql:quickload :hoplon.castra)
Web application RPC library for Clojure/Script and Ring.
clojure
[hoplon/castra "3.0.0-alpha7"] ;; latest release
The purpose of Castra is to make async server calls feel like
expression evaluation, providing a more cohesive programming
experience across the front end and back end of your system. Instead
of thinking “I'm going to send this data bag in a POST to the /xyz
endpoint,” supports you in thinking thoughts like “I'm going to
evaluate the expression (update-record 123 {:x 1 :y 2})
on the
server.”
Castra's front-end and back-end libraries implement this RPC pattern.
RPC does come with baggage, though. Implementing an RPC framework that provides a seamless and transparent remote execution model is a very difficult distributed systems problem. Castra does not attempt this. Instead, Castra's RPC model embraces the asynchronous nature of client server communication.
Applicable buzzwords, if you're not into the whole brevity thing, are unidirectional dataflow and the CQRS pattern.
Castra spans the gap between server and client. This makes documentation a bit more difficult. This section will jump back and forth between client and server code — it should be understood that the server code is Clojure and client is ClojureScript.
Most of the magic happens in Castra's castra.middleware/wrap-castra
ring middleware. This middleware looks for an expression under the
:body
key of the ring request map. It looks for a request that looks
something like:
{:request-method :post
:body "(my.app/update-record 123 {:x 1 :y 2})"}
(my.app/update-record 123 {:x 1 :y 2})
by resolving and attempting to call the function my.app/update-record
.
(This function should be created with castra.core/defrpc
, explained below.):body
and a 200 status.You can think of the response as if it were this:
{:status 200
:body (pr-str (my.app/update-record 123 {:x 1 :y 2}))}
Obviously, we don't want the client to be able to evaluate arbitrary
expressions on the server (we already have nREPL for that). We
want to be able to mark certain functions as part of our application's
RPC interface. This is accomplished with castra.core/defrpc
:
(ns my.app
(:require
[castra.core :as c]
[some.database :as db]))
(c/defrpc get-record
[id]
(first (db/query "SELECT * FROM record WHERE id = ?" id)))
(c/defrpc update-record
[id {:keys [x y]}]
(db/execute "UPDATE IN record SET x = ?, y = ? WHERE id = ?" x y id)
(get-record id))
Castra provides a ClojureScript library for creating the RPC stub functions
the client will call. These are constructed by the castra.core/mkremote
function.
(ns my.app.client
(:require
[castra.core :as c]
[javelin.core :as j :include-macros true]))
(j/defc record nil)
(j/defc error nil)
(j/defc loading nil)
(def get-record (c/mkremote 'my.app/get-record record error loading))
(def update-record (c/mkremote 'my.app/update-record record error loading))
The mkremote
function takes four arguments, three of which are
Javelin cells:
defrpc
on the server.This defines the get-record
and update-record
functions in the client,
which can be called like any other ClojureScript function.
Using the server and client code above, we can make a little webapp that shows us the contents of a record.
Here is a simple Hoplon page that satisfies these requirements:
(page "index.html"
(:require
[my.app.client :as c]))
(defc= loading? (some-> c/loading seq count)) ; contains count of in-flight commands
(defc= error-message (some-> c/error .-message)) ; contains the last command's error message
(c/get-record 1) ; get record 1 when page first loads
(html
(head)
(body
(p :toggle loading? ; show when things are being processed
(text "Processing ~{loading?} requests..."))
(p :toggle error-message ; show when an operation fails
(text "Error: ~{error-message}"))
(p (text "Record: ~{c/record}")) ; display the current record
(let [id (cell nil)]
(form :submit #(c/get-record @id) ; edit and submit form to load record
(p (label "Record ID: ")
(input :value id :keyup #(reset! id @%)))
(p (button :type "submit" "submit"))))))
There are a few things to notice about this application:
form
above) does not need
to know or care which places will respond to the change (eg. the p
displaying the current record), and vice versa.The diagram to the right illustrates the flow of data through the application. There are actually two dataflow loops in the program:
keyup
DOM event fires, triggering a state transition (callback).reset!
on the id
cell, updating its value.value
property of the input element is bound to the id
cell,
so it is automatically updated when id
changes.submit
DOM event fires, triggering the callback.get-record
.c/record
cell is automatically updated
with the new current record.textContent
property of the text node displaying the current record
is bound to a formula cell that updates when c/record
changes.Notice how similar the two cases are. The main difference is at step 3. In
the local loop the callback directly updates the id
cell with the
synchronously with reset!
. In the remote loop the c/record
cell is
updated asynchronously with Castra.
The Hoplon Demos repo contains demo apps using Castra.
defrpc
and endpoints.Copyright © 2013 FIXME
Distributed under the Eclipse Public License, the same as Clojure.