this includes view templates - they aren't visible to the user, but loaded by the javascript for rendering page snippets

The code in twitter-example.coffee is then loaded (as javascript) - this contains the client-side application logic

On page load, the TwitterExample constructor is called, which sets up view logic and calls check_status()

check_status() makes an ajax call to "/auth/check_status.json" on the server

The server URLs are usually handled by the defroutes matchers half way down core.clj -
but in this case, the user has not yet authorized, so the middleware function with-oauth-check kicks in

with-oauth-check determines there is no auth information in the session, so it starts the oauth interaction with twitter

It makes a call to Twitter to get a request token, based on the application credentials and our local URLs. The request token is stored in the user's session

It also determines the appropriate URL on twitter to which the user should be redirected

It then returns a HTTP 401 error, with a JSON body containing a message, and an authURL value with the redirect URL

The client catches the 401 error and calls auth-error() which renders a HTML view "noauth", showing the user the message "Please authorize in Twitter", and a link to the authURL returned from the server

When the user clicks on the "authorize" link, they are taken to Twitter, where they can authorize - in which case they are redirected to the app, with the URL "/twitter-oauth-response" with url parameters containing extra data to confirm the user is authorized

The server gets the "/twitter-oauth-response" request (it's actually stored in 'oauth-response-path', matching the line (GET oauth-response-path [oauth_token oauth_verifier] in core.clj

It fetches the request token from the session, and makes another API call to Twitter to convert the request token and verifier into an access token

The access token is stored in the session, so future calls to "/auth" URLs won't result in a 401 error. (note the request token is no longer needed, and should probably be thrown away at this stage)

The server then redirects the user back to "/", the starting page of the application

a more friendly application could try to store what the user originally requested, and after geting authorization, complete that request, but it's more than this little app can do

The client reloads the page again, as before, which calls check_status() which calls "/auth/check_status.json"

This time the user has an access token in the session, so the with-oauth-check middleware does nothing

The defroutes matcher for "/auth/check_status.json" matches the request, and returns a simple payload consisting of the user's twitter name

The client (in the check_status() function) renders the user's name, then calls fetch_tweets()

This results in another ajax call, this time to "/auth/tweets.json"

The server matches "/auth/tweets.json" and makes a Twitter API call to retrieve the user's home timeline

The JSON response from Twitter is filtered to remove unneeded information, and then returned to the client

The client renders the resulting twitter messages.

Phew! This may look complex, but it's a pretty common workflow for applications trying to do OAuth. It's actually somewhat simpler in clojure/coffeescript than other languages I've done this in!

Testing

Testing is somewhat rudimentary - there are a few tests included, but they only test a few simple things.

I'm still a bit unsure how far to TDD clojure - it's obviously possible, but there's a lot of functionality that probably doesn't gain much from (unit) tests. Unless I define the whole namespace as a unit, and only mock external interfaces. Hmm...

Other stuff

editing and building CoffeeScript / Sass files

CoffeeScript and Sass are precompilers - they take .coffee and .scss files, and build javascript and CSS respectively.

You can work with the resulting javascript and CSS files if you want - they are in resources/public/javascript and resources/public/stylesheets.
But if you want to make non-trivial changes, you'll want to be able to compile the original sources.

Dependencies

You also need to install sass, which is a ruby library - again, beyond the scope of this readme! See [http://sass-lang.com/] for more.

Compiling

You can manually compile the scss/coffee files with:
./precompile.sh
Or you can run a background script that compiles files whenever they change with:
./watch_all.sh
These only work on Unix/Mac systems - Windows users will have to work out a Windows equivalent.

To Do

See separate TODO.markdown file

Notes on Heroku deployment

Heroku deployment was a dream - the only hiccup I had (apart from a bunch of work to remove file-based configuration) was some extra dependencies.

For some reason I had to add
[ring/ring-core "0.3.8"]
[ring/ring-jetty-adapter "0.3.8"]
to the dependencies in project.clj - leiningen standalone seems to handle these automagically, but on heroku I had to be explicit.

It's that simple! heroku ps lists running processes, heroku logs shows the log. You can even run heroku run lein repl and get a REPL!

Note I've confirmed with Heroku, despite some slight lack of clarity on their site, this is free for the basic level of service (750 dynamo-hours per month).

License

Copyright (c) 2011 Kornelis Sietsma

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.