Defining Routes with state

9 views
Skip to first unread message

Vagif Verdi

unread,
Jun 22, 2009, 10:41:38 PM6/22/09
to Compojure
(GET "/counter"
(let [count (session :counter 0)]
[(session-assoc :counter (inc count))
(session-assoc :foo "bar")
(str " counter = " count)]))

Questions on this snippet and its "magical" behavior.

1. only second session-assoc :foo is evaluated. First one is ignored.
Counter is not incremented and is not saved to session. Why ?

2. Why do i need to put code into vector ? What's wrong with normal
block

(let [count (session :counter 0)]
(session-assoc :counter (inc count))
(session-assoc :foo "bar")
(str " counter = " count)))

Why in this case counter is not save into session ?

James Reeves

unread,
Jun 23, 2009, 3:17:00 PM6/23/09
to Compojure
On Jun 23, 3:41 am, Vagif Verdi <Vagif.Ve...@gmail.com> wrote:
> 1. only second session-assoc :foo is evaluated. First one is ignored.
> Counter is not incremented and is not saved to session. Why ?

That's an interesting corner case I hadn't thought of. You could
write:

(GET "/counter"
(let [count (session :counter 0)]
[(session-assoc :counter (inc count), :foo "bar")
(str " counter = " count)]))

This would work. However, putting two session-assocs in a vector will
currently cause one to overwrite the other. Solving this could be a
little tricky, but hopefully I should be able to come up with
something.

> 2. Why do i need to put code into vector ? What's wrong with normal
> block
>
>   (let [count (session :counter 0)]
> (session-assoc :counter (inc count))
>      (session-assoc :foo "bar")
>      (str " counter = " count)))
>
> Why in this case counter is not save into session ?

Session handling in Compojure is functional. When you call session-
assoc, you are not directly modifying the session. Instead, session-
assoc returns a handler function:

(let [request {:session {:a 0, :b 0}}]
((session-assoc :a 1) request)

=> {:session {:a 1, :b 0}}

The output of this handler is merged with the other items of the
vector to form the final response. This is why two session-assocs are
tricky to handle. Because session-assoc only knows about the request,
it can't merge its results with any existing session on the response.

Perhaps I need to change response handling a little.

- James

Vagif Verdi

unread,
Jun 23, 2009, 5:08:34 PM6/23/09
to Compojure


On Jun 23, 11:17 am, James Reeves <weavejes...@googlemail.com> wrote:
> That's an interesting corner case I hadn't thought of.

Interesting. How do you handle situations where you have to update
session state several times in the body of the handle ? It is not
always possible to group all session changes in one place.

What about middleware that can also update session state and then pass
execution to the handler ?

Vagif Verdi

unread,
Jun 23, 2009, 5:19:14 PM6/23/09
to Compojure
On Jun 23, 11:17 am, James Reeves <weavejes...@googlemail.com> wrote:
> Session handling in Compojure is functional.

I think the problem is that you are trying to get from one function
(handle), two results: session state and response output.

Probably the handler should return only the response output. The
session should be a ref with convenience function to update that ref
anywhere in the handler.

This way you also get rid of unconventional syntax (vector instead of
normal body of function)

Besides trying to hide state of the session in the handle that in most
cases will also be updating state in database and many other places,
just for the sake of being functional, does not make any sense.

Let's deal with the session state explicitly.

James Reeves

unread,
Jun 23, 2009, 5:25:29 PM6/23/09
to Compojure
On Jun 23, 10:08 pm, Vagif Verdi <Vagif.Ve...@gmail.com> wrote:
> Interesting. How do you handle situations where you have to update
> session state several times in the body of the handle ? It is not
> always possible to group all session changes in one place.
>
> What about middleware that can also update session state and then pass
> execution to the handler ?

The session won't be updated until the handler returns, so in a sense
all your session changes are grouped in one place. However, if you
wanted to incrementally build up the session response, you'd run into
some difficulties.

I think the solution is to make functions like session-assoc aware of
the response-in-progress. If I made this change, then session-assoc
would work as expected.

- James

Vagif Verdi

unread,
Jun 23, 2009, 9:45:28 PM6/23/09
to Compojure
I tried to change with-session myself, but hit the wall (i'm a clojure
newbee):

;;---------------
;; Thread local Var
(def *current-session*)

(defn with-session
"Wrap a handler in a session of the specified type. Session type
defaults to
:memory if not supplied."
([handler]
(with-session handler :memory))
([handler session-repo]
(fn [request]
(binding [*session-repo* session-repo]
(let [request (-> request assoc-cookies
assoc-request-session
assoc-request-flash)]
(binding [*current-session* (:session request)]
(println *current-session*)
(let [response (handler request)]
(when response
(save-handler-session request response *current-
session*)
(set-session-cookie request response *current-
session*)))))))))

(defn update-session [& keys]
(set! *current-session* (apply assoc *current-session* keys)))
;;---------------

(defroutes counter
(GET "/counter"
(let [count (or (*current-session* :count) 2)
bobo (or (*current-session* :bobo) 102)]

(update-session :count (inc count))
(update-session :bobo (inc bobo))
(str "count = " count "<br> second = " bobo))))


This gives me error: Var compojure/*current-session* is unbound.

But println right before executing handler in with-session actually
prints a session map. So it is obviously bound right before handler.

I'm doing something wrong, but do not know what.

James Reeves

unread,
Jun 24, 2009, 4:32:40 PM6/24/09
to Compojure
On Jun 24, 2:45 am, Vagif Verdi <Vagif.Ve...@gmail.com> wrote:
> I tried to change with-session myself, but hit the wall (i'm a clojure
> newbee):

We can probably leave with-session the way it is. I think these are
the two functions we need to change:

compojure.http.response/update-response
compojure.http.session/alter-session

Compojure uses the update-response method to update the HTTP response
with the return value from a route macro. The type of the return value
determines how the response is updated.

For instance, if the return value is a string, it is added to the body
and a default content type of "text/html" is set:

(GET "/" "Hello World")

=> (create-response request "Hello World")

=> (update-response request default-response "Hello World")

=> (update-response request {:status 200, :headers {}} "Hello
World")

=> {:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"}

You can also return a function from a route macro. When update-
response is called, it passes the request to the function and uses the
return value to update the response.

(GET "/" (fn [request] "Hi"))

=> (update-response request default-response (fn [request] "Hi"))

=> (update-response request default-response "Hi")

The advantage of this approach is that we can nest a Ring handler
within a route macro without requiring any special syntax:

(GET "/" (GET "/" "Nested!))

=> (GET "/" "Nested!")

However, the disadvantage is that we have no control over how the
response is updated. This is problematic for the alter-session
function:

(defn alter-session [func & args]
(fn [request]
(set-session
(apply func (request :session) args))))

Here alter-session uses a function to update the request session.
However, it doesn't know about the response, so this will overwrite
any existing :session key that has been put into the response.

I think we should probably pass both the session and the partially
generated response into the function. For example:

(GET "/" (fn [request response] "Hello World"))

This would mean we couldn't return a handler function directly anymore
(because handler functions only take one argument), but we could
always create a wrapper function like the one below for those cases:

(defn to-route [handler]
(fn [request response]
(handler request)))

The advantage of passing the response in is that we gain more control,
and that's part of what Compojure is about.

If you want to make this change, I can attempt to clarify of any part
of that explanation that's unclear. I really appreciate any effort
people make toward contributing to Compojure. But I also don't mind
making the change, if you happen to be short on time.

- James
Reply all
Reply to author
Forward
0 new messages