adventures in making stuff with Daniel Higginbotham

Building a Forum with Clojure, Datomic, Angular, and Ansible

27 July 2013

After many long months I've finished re-writing Grateful Place. The site now uses Clojure as an API server, with Datomic for the database, Angular for the front end, and Vagrant and Ansible for provisioning and deployment. This setup is awesome, the best of every world, and I love it.

Below we'll dive into the code base, covering the most important parts of each component and how everything works together. We'll cover:

  • Clojure API Server
    • Liberator for Easy API'ing
    • Going on a Spirit Journey with Friend
    • Testing a Clojure Web App is More Fun than Testing Rails
    • Serving files generated by Grunt/Angular
    • The Uberjar
  • Datomic
    • Why Oh Why Did I Do This
    • The Poopy Code I Wrote to Make Basic Things "Easier"
    • The Good Code I Ripped Off to do Migrations
    • Mapification with Cartographer, My Very Own Clojure Library!!!
  • Angular
    • Peeking and Secondary Controllers
    • Directives to the Rescue
  • Infrastructure
    • Creating a Local Sandbox with Vagrant
    • Provisioning with Ansible
    • Building and Deploying with a Janky Bash Script and Ansible
  • Development Workflow
    • Emacs Bookmarks and Keybindings
    • tmuxinator Config
    • Actually doing development

All source is available on github. This article isn't meant to be a tutorial. However, if you have any questions about how things work or about how to work on the codebase, please leave a comment and I'll do my best to clarify.

About the Site

Grateful Place is my attempt at creating the kind of online community that I'd like to belong to. It's still in its infancy, but I'd like for it to become a site where people consciously help lift each other up. One way to do this is by expressing gratitude on a daily basis, which science says increases happiness.

Some of the features include watching forum threads, liking posts, and creating a basic profile. I have a lot more planned, like tags and "summary" views, and I think the combination of Clojure and Angular will make it fun and easy for me to continue development :)

If you want to have a look without diving head-first into hippified waters (what, do you have something against happiness?), you can use the demo site with username/password test101/test101. Be warned, though, that that server might have some bugs.

Now, on to the code!

Clojure API Server

I'm very happy with using Clojure as an API server. The libraries involved are lightweight and transparent, with no magic, and that makes it so easy to fully understand what's going on. That was my experience with Liberator:

Liberator for Easy API'ing

Liberator provided me with a much better abstraction for handling logic which I was repeating in all of my controllers. For example, my create functions all basically looked like this before I moved to Liberator:

(defn create!
  [params auth]
  (protect
   (:id auth)
   (if-valid
    params (:create validations/post) errors
    (let [post-map (create-post params)]
      {:body post-map})
    (invalid errors))))

The above code implements a decision tree:

  1. First, the protect macro is used to ensure you're authorized to do whatever you're trying to do. The first argument is a boolean expression, in this case (:id auth), which just checks whether you're logged in. If the boolean expression is true, run everything that follows. Otherwise return an error status and error messages (see implementation).
  2. Check whether params is valid using the specified validation, in this case (:create validations/post). If it's valid, run the let statement, otherwise make the validation errors available in errors and run (invalid errors).

There are a couple things that I didn't like about this approach. First, there was too much distance between the logical branches. For example, protect is basically an if statement, but the else is hidden. Also, the actual code I wrote in if-valid is a bit long, which makes it difficult to visually understand how (invalid errors) relates.

Second, this approach required me to introduce more nesting in order to add more steps or checks in the workflow. This would make it even harder to understand as I'd mentally have to descend and ascend a few conditionals in order to understand what's going on. I'd end up with something like:

- Decision one
  - Decision one first branch: Decision two
    - Decision two first branch: Decision three
      - Decision three first branch
        ...
        Lots of code here physically creating distance between
        branches
      - Decision three second branch
        ...
        More code causing more distance
    - Decision two second branch
  - Decision one second branch... what was the decision even?
    I can't remember and now it's hard for me to visually
    associate this branch with its parent decision

So essentially, I'd have to keep an ever-growing decision tree in my head. The physical representation of the tree, the code, would help to obscure the logic flow as I added more code.

Here's how the same function looks when rewritten using Liberator:

(defresource create! [params auth]
  :allowed-methods [:post]
  :available-media-types ["application/json"]
  :authorized? (logged-in? auth)

  :malformed? (validator params (:create validations/post))
  :handle-malformed errors-in-ctx

  :post! (create-content ts/create-post params auth record)
  :handle-created record-in-ctx)

Holy shnikes! That's so much clearer!

Liberator improved my code by providing a pre-defined, HTTP-compliant decision tree, providing sensible default logic for nodes, and by allowing me to easily associate my own logic with the nodes.

This allows me to concentrate on one node at a time, instead of having to keep an increasingly complicated tree structure in my head. For example, I can physically place the logic for malformed? next to the code that I want to run if the request is malformed, specified by handle-malformed.

Liberator has excellent documentation and using it is a big win. It lets me just plug my own bits of logic into a carefully-coded, useful HTTP framework. I definitely recommend it.

Going on a Spirit Journey with Friend

Friend still kinda makes my head hurt. It's a useful library that gets the job done, but I feel like using it requires poring over the source code until you attain that brief flash of enlightenment that allows you to pound out the code you need for as long as some dark, atavistic, pre-conscious part of your brain can hold everything together. After that you pray that you won't need to change anything because, Jesus, that trip can take a lot out of you. I don't know, maybe that's why peyote was invented.

Anyway, that's a testament to my own need to learn (and perhaps a need for slightly clearer documentation) and not to the quality or value of the library itself. Everything hangs together, working with Ring in a stateless way, which I really appreciate.

OK enough of my blathering. We want code! The actual tricky bits were:

  • Getting Friend to return errors instead of redirecting
  • Creating an authenticated session as soon as a user registers instead of requiring login

It turned out that the first wasn't all that difficult. I think. Here's the code on github. It's also listed below, in the next code block.

The key bit is :login-failure-handler, which simple returns a map for Ring. I also have :redirect-on-auth? listed twice. I'm not sure if this is necessary but every once in awhile I like to do some shaman programming, throwing chicken bones and listening to the wind in hopes that everything turns out OK. Things are working and I'm not going to mess with them.

Creating the authenticated session is a different story. There are a lot of things going on. In order, they are:

  1. User submits registration form
  2. Request goes through a bunch of Ring middlewares that wrap the request, adding keyword params and handling json and whatnot
  3. Request hits the middleware created by Friend
  4. The request "hits" the users/attempt-registration Friend workflow
  5. If the registration is valid, return a friend authentication map. Friend "knows" that this is not meant to be a response sent to the browser, so the authentication map gets added to the Ring request map and the resulting map gets sent to the next Ring middleware
  6. The next ring middleware is routes
  7. The users/registration-success-response route matches
  8. users/registration-success-response returns a Ring map, providing a body. The response is a map like {:id 1234 :username "flyingmachine"}. This then gets used by Angular.

Here's all the relevant code. Steps are indicated in brackets, like [1] or [2] or [3]. Step 1 is omitted as that's not code, you silly goose.

;; The ring app, https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/server.clj#L29
(defn wrap
  [to-wrap]
  (-> to-wrap
      (wrap-session {:cookie-name "gratefulplace-session" :store (db-session-store {})})
      (wrap-restful-format :formats [:json-kw])
      wrap-exception
      wrap-keyword-params
      wrap-nested-params
      wrap-params))

; The ring app
(def app
  (-> routes ;; [6] after a successful registration the routes
             ;; middleware is called
      auth ;; [3] after request is wrapped, send it to friend
      wrap ;; [2]
      ))

;; Friend middlware
(defn auth
  [ring-app]
  (friend/authenticate
   ring-app
   {:credential-fn (partial creds/bcrypt-credential-fn credential-fn)
    :workflows [(workflows/interactive-form
                 :redirect-on-auth? false
                 :login-failure-handler (fn [req] {:body {:errors {:username ["invalid username or password"]}} :status 401}))
                users/attempt-registration ;; [4]
                session-store-authorize]
    :redirect-on-auth? false
    :login-uri "/login"
    :unauthorized-redirect-uri "/login"}))

;; [4] Friend runs this workflow function. If the workflow function
;; returns falsey, then friend tries the next workflow function. In
;; this case, when a user submits a registration form then the `when`
;; boolean expression is true and the function will not return falsey.
;; If the registration is successful it will return an authentication
;; map and continue to step 5. If the registration is unsuccessful it
;; will return a Ring response map, which is basically a map that has
;; the keys :body or :status.
;; https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/controllers/users.clj#L20
(defn attempt-registration
  [req]
  (let [{:keys [uri request-method params session]} req]
    (when (and (= uri "/users")
               (= request-method :post))
      (if-valid
       params (:create validations/user) errors
       ;; [5] Here's where we return the authentication map, which
       ;; Friend appends to the request map, sending the result to the
       ;; next middleware
       (cemerick.friend.workflows/make-auth
        (mapify-tx-result (ts/create-user params) record)
        {:cemerick.friend/redirect-on-auth? false})
       (invalid errors)))))


;; [7] The compojure route, https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/middleware/routes.clj#L67
(authroute POST "/users" users/registration-success-response)

;; [8] the final step in our journey
(defn registration-success-response
  [params auth]
  "If the request gets this far, it means that user registration was successful."
  (if auth {:body auth}))

I'm both proud and appalled that I wrote all that code.

Testing a Clojure Web App is More Fun than Testing Rails

For testing I decided to try out Midje. Midje is easy to get used to, and @marick has articulated a clear and compelling philosophy for it.

But before we get into some code let me explain the heading, "testing a Clojure web app is more fun than testing Rails." This has to do with Clojure itself and not with any testing library.

There's no real magic in any of the code I wrote. Everything is just a function. You give it an input and it returns an output. You give your application a Ring request and it goes through all the layers and returns a Ring response. You don't have to do any crazy setup hijinks or create special environments like you do in Rails - especially like you have to do when testing controllers. This makes testing so much easier and more fun.

So, that said, I feel like there's not much remarkable with my tests. There's a lot of room for improvement.

I ended up creating a lot of helper functions to DRY up my controller tests, and those might prove helpful to someone else. I also ended up writing a crazy-ass macro for creating functions with default positional arguments:

(defmacro defnpd
  ;; defn with default positional arguments
  [name args & body]
  (let [unpack-defaults
        (fn [args]
          (let [[undefaulted defaulted] (split-with (comp not vector?) args)
                argcount (count args)]
            (loop [defaulted defaulted
                   argset {:argnames (into [] undefaulted)
                           :application (into [] (concat undefaulted (map second defaulted)))}
                   unpacked-args [argset]
                   position (count undefaulted)]
              (if (empty? defaulted)
                unpacked-args
                (let [argname (ffirst defaulted)
                      new-argset {:argnames (conj (:argnames argset) argname)
                                  :application (assoc (:application argset) position argname)}]
                  (recur (rest defaulted) new-argset (conj unpacked-args new-argset) (inc position)))))))
        unpacked-args (unpack-defaults args)]

    `(defn ~name
       (~(:argnames (last unpacked-args))
        ~@body)
       ~@(map #(list (:argnames %)
                     `(~name ~@(:application %)))
              (drop-last unpacked-args)))))

;; Examples
(defnpd response-data
  [method path [params nil] [auth nil]]
  (data (res method path params auth)))

(defnpd res
  [method path [params nil] [auth nil]]
  (let [params (json/write-str params)]
       (server/app (req method path params auth))))

The next big step for me with testing is to get off my butt and figure out how to run some kind of autotest process with Midje.

If you're new to Clojure and are wondering what testing library, I think clojure.test works just fine. It's easier to understand than Midje, but Midje seems more powerful.

Serving files generated by Grunt/Angular

While developing, the frontend files are located completely outside of the Clojure application. The directory structure looks like:

/server
  /src
    /gratefulplace
      - server.clj
    /resources
    ...
/html-app
  /app
    - index.html
  /.tmp
    /scripts
      - app.js
      /controllers
        - topics.js
        ...

So I needed some way to get the Clojure app to actually serve up these files. I also needed to be able to serve the files when they're packaged as resources in the final uberjar. This turned out to be really easy:

;; https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/config.clj
;; Example config
(def conf
  (merge-with
   merge
   {:html-paths ["html-app"
                 "../html-app/app"
                 "../html-app/.tmp"]}))
(defn config
  [& keys]
  (get-in conf keys))               

;; https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/middleware/routes.clj#L33
;; Serve up angular app
(apply compojure.core/routes
         (map #(compojure.core/routes
                (compojure.route/files "/" {:root %})
                (compojure.route/resources "/" {:root %}))
              (reverse (config :html-paths))))
;; Route "/" to "/index.html"
(apply compojure.core/routes
       (map (fn [response-fn]
              (GET "/" [] (response-fn "index.html" {:root "html-app"})))
            [resp/file-response resp/resource-response]))

We're just iterating over each possible path for the front end files and creating both a file route and a resource route for them. This is a lazy way to do things, resulting in a few unnecessary routes. In the future, it would be nice to make the app "know" whether to use the single resource route, html-app, or whether it needs to use the file routes, ../html-app/app and ../html-app/.tmp.

The Uberjar

As I started to deploy the forum I found that I needed and easy way to run database-related tasks. Here's what I came up with:

(ns gratefulplace.app
  (:gen-class)
  (:require [gratefulplace.server :as server]
            [gratefulplace.db.manage :as db]))

(defn -main
  [cmd]
  (cond 
   (= cmd "server") (server/-main)
   ;; I know there's repetition here please don't hate me :'(
   (= cmd "db/reload") (do (println (db/reload)) (System/exit 0))
   (= cmd "db/migrate") (do (println (db/migrate)) (System/exit 0))))

So you can run java -jar gp2.jar server and get a server running, or reload the database or run migrations. I could also have used lein on the server, and I'll probably do that eventually. For now I'm just creating uberjars and copying them over.

Holy cow, the Clojure section is over! Let's talk about Datomic now!

Datomic

Why Oh Why Did I Do This

When I set about re-writing the site it felt risky to use Datomic because a) I didn't know how to use it and b) it didn't seem like it would add much value over postgres or mysql for my tiny side project.

But those were also compelling reasons to go with it: a) it's exciting to learn a completely new way of working with databases, designed by some really freaking smart people who know which side of the bread is buttered and b) it's just a tiny side project and I can do whatever I want.

Ultimately I'm happy with the decision. I've learned a lot by researching Datomic (see "Datomic for Five-Year-Olds") and using it has afforded the same simple, lightweight experience as using Clojure.

You won't find any mind-blowing code here – I'm still trying to learn how to use Datomic well – but hopefully you'll find it useful or interesting.

The Poopy Code I Wrote to Make Things "Easier"

I wrote a number of wrapper functions in the misleadingly-name gratefulplace.db.query namespace:

(ns gratefulplace.db.query
  (:require [datomic.api :as d])
  (:use gratefulplace.config))

;; This is dynamic so I can re-bind it for tests
(def ^:dynamic *db-uri* (config :datomic :db-uri))
(defn conn
  []
  (d/connect *db-uri*))
(defn db
  []
  (d/db (conn)))

;; Don't make me pass in the value of the database that gets boring
(def q
  #(d/q % (db)))

;; I'll give you an id, you give me a datomic entity or nil
(defn ent
  [id]
  (if-let [exists (ffirst (d/q '[:find ?eid :in $ ?eid :where [?eid]] (db) id))]
    (d/entity (db) exists)
    nil))

;; Is this an entity?! Tell me!
(defmulti ent? class)
(defmethod ent? datomic.query.EntityMap [x] x)
(defmethod ent? :default [x] false)

;; I'll give you some conditions, you'll give me an entity id
(defn eid
  [& conditions]
  (let [conditions (map #(concat ['?c] %) conditions)]
    (-> {:find ['?c]
         :where conditions}
        q
        ffirst)))

;; I want one thing please
(defn one
  [& conditions]
  (if-let [id (apply eid conditions)]
    (ent id)))

;; I want all the things please
(defn all
  [common-attribute & conditions]
  (let [conditions (concat [['?c common-attribute]]
                           (map #(concat ['?c] %) conditions))]
    (map #(ent (first %)) (q {:find ['?c]
                              :where conditions}))))

;; Passing the connection all the time is boring
(def t
  #(d/transact (conn) %))

(defn resolve-tempid
  [tempids tempid]
  (d/resolve-tempid (db) tempids tempid))

;; I make a lot of mistakes so please make it easy for me to retract them
(defn retract-entity
  [eid]
  (t [[:db.fn/retractEntity eid]]))

Some of these functions simply reduce the code I write by a tiny bit, for example by allowing me to not pass a connection or database value into every single database-related function, which would make no sense for me as I only have one database.

Others, like one and all provide me with an "easier" way of performing common queries but at the expense of sometimes writing queries in roundabout ways or taking away some of my flexibility.

For example, in the all function I'm limited to only one data source. The result is that I sometimes have to use the datomic.api functions in places where I'd prefer not to, and the codebase doesn't quite feel cohesive. One example of this is the query function in the watches controller:

(defresource query [params auth]
  :available-media-types ["application/json"]
  :handle-ok (fn [ctx]
               (map (comp record first)
                    (d/q '[:find ?watch
                           :in $ ?userid
                           :where [?watch :watch/user ?userid]
                           [?watch :watch/topic ?topic]
                           [?topic :content/deleted false]]
                         (db/db)
                         (:id auth)))))

I have to call datomic.api/q directly because I want to pass in ?userid.

I'm not sure whether I should drop these functions entirely and just use the datomic api or whether I should continue tweaking them to meet my needs.

The Good Code I Ripped Off to do Migrations

The gratefulplace.db.manage namespace has some code I stole and modified from Day of Datomic. It's a really cool, simple way of ensuring that migrations get run. The basic idea is that you keep track of schema names which have been installed, then install any schemas that haven't been installed. It's a simple, logical approach and the code that implements it is pretty neat, as you would expect from Stu Halloway.

Mapification with Cartographer, My Very Own Clojure Library!!!

Cartographer is the result of my attempt to easily do some processing and pull in relationships when converting a Datomic entity to a map. I think the README explains it all so you can learn more about it there. Here are some of the maprules used in GP2:

;; https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/db/maprules.clj
(defmaprules ent->topic
  (attr :id :db/id)
  (attr :title :topic/title)
  (attr :post-count (ref-count :post/topic))
  (attr :author-id (comp :db/id :content/author))
  (attr :last-posted-to-at (comp format-date :topic/last-posted-to-at))
  (has-one :first-post
           :rules gratefulplace.db.maprules/ent->post
           :retriever :topic/first-post)
  (has-one :author
           :rules gratefulplace.db.maprules/ent->user
           :retriever :content/author)
  (has-many :posts
            :rules gratefulplace.db.maprules/ent->post
            :retriever #(sort-by :post/created-at
                                 (:post/_topic %)))
  (has-many :watches
            :rules gratefulplace.db.maprules/ent->watch
            :retriever #(:watch/_topic %)))

(defmaprules ent->post
  (attr :id :db/id)
  (attr :content (mask-deleted :post/content))
  (attr :formatted-content (mask-deleted #(md-content (:post/content %))))
  (attr :deleted :content/deleted)
  (attr :created-at (comp format-date :post/created-at))
  (attr :topic-id (comp :db/id :post/topic))
  (attr :author-id (comp :db/id :content/author))
  (attr :likers #(map (comp :db/id :like/user) (:like/_post %)))
  (has-one :author
           :rules gratefulplace.db.maprules/ent->user
           :retriever :content/author)
  (has-one :topic
           :rules gratefulplace.db.maprules/ent->topic
           :retriever :post/topic))

There are definitely some edge cases where this approach gets strained but overall it's served me well.

I ended up creating a macro which allows you to easily create a function that, when applied to a datomic entity, returns a map using maprules created with Cartographer:

;; https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/server/src/gratefulplace/db/mapification.clj
(defmacro defmapifier
  [fn-name rules & mapify-opts]
  (let [fn-name fn-name]
    `(defn- ~fn-name
       ([id#]
          (~fn-name id# {}))
       ([id# addtl-mapify-args#]
          (if-let [ent# (or (db/ent? id#) (db/ent id#))]
            (let [mapify-opts# (merge-with (fn [_# x#] x#) ~@mapify-opts addtl-mapify-args#)]
              (fyingmachine.cartographer/mapify
               ent#
               ~rules
               mapify-opts#))
            nil)))))

Angular

I've been learning Angular since last November and I love it. Using it, I feel like I finally have the right tools for creating web apps.

Peeking and Secondary Controllers

I wanted to implement the idea of "peeking" at things on the forum. For example, if you click on a user link you'll just view a summary of his info in the right column instead of completely leaving the page you're on.

The idea is that, while reading a thread, you might find a response interesting. You want to know a little more about the author but don't want to lose your place. So you "peek" at him, which shows you some info and preserves your place in the thread. It was just something fun I wanted to try.

However, as far as I know Angular doesn't make this very easy for you. The approach I took was to have a Foundation controller which places the Support service on the scope. Since all other controllers are nested under Foundation, they'll have access to $scope.support.

The purpose of Support is define a way to show views in the right column and make data accessible to the view. For example, the author directive has the following:

https://github.com/flyingmachine/gratefulplace2/blob/v1.0.0/html-app/app/scripts/directives/author.coffee#L8
$scope.peekAtAuthor = (author)->
  User.get id: author.id, (data)->
    _(data.posts).reverse()
    data.posts = _.take(data.posts, 3)
    Support.peek.show("user", data)

The base view has the following:

<div id="more">
  <nav class="secondary">
    <ng-include src="support.secondaryNav.include()"></ng-include>
  </nav>
  <ng-include src="support.peek.include()"></ng-include>
</div>

And the user peek looks like this:

<div class="peek">
  <div class="user">
    <h3 class="username">{{support.peek.data.username}}</h3>
    <div class="about" ng-bind-html-unsafe="support.peek.data['formatted-about']"></div>
  </div>
  <div class="recent-posts">
    <h4>Recent Posts</h4>
    <div class="post" ng-repeat="post in support.peek.data.posts">
      <date data="post['created-at']"></date>
      <div>
        <a href="#/topics/{{post.topic.id}}">{{post.topic.title || 'view topic'}}</a>
      </div>
      <div class="content" ng-bind-html-unsafe="post.content">
      </div>
    </div>
  </div>
</div>

So, ultimately, what's happenins is that when you call Support.peek.show("user", data), it sets some variables so that the view associated with the "user" peek is shown. That view then accesses the data you passed to Support.peek.show with, e.g., support.peek.data.username.

I know this isn't a super-detailed explanation of what's going on, but I hope some investigation of the code will answer any questions you might have.

Directives to the Rescue

Angular directives are as powerful as everyone says they are, and I think I'm finally utilizing them well. You can see all my directives on github.

This article is already 500 times to long so I won't go into any details, but if you're looking to understand Angular better, read this excellent SO response to How do I “think in AngularJS/EmberJS(or other client MVC framework)” if I have a jQuery background?.

Infrastructure

Because GP2 uses Datomic Free, I couldn't deploy to Heroku. This meant having to actually handle provisioning a server myself and deploying without following a tutorial. In the end things are working well. The site's residing on a [Digital Ocean][https://www.digitalocean.com/] server, which has been very easy to work with.

Creating a Local Sandbox with Vagrant

Creating a local sandbox lets you make all your provisioning mistakes more quickly. If you're creating a new provisioning script of tweaking your existing one, you should do it in a virtual machine.

Vagrant makes this process as easy as an old shoe. Once you've installed virtualbox and vagrant all you have to do is run vagrant up from the infrastructure directory and you'll have a virtual machine ready to go. The Vagrant web site has excellent tutorials so check it out if you want to learn more.

Provisioning with Ansible

Ansible's supposed to be super simple compared to Puppet and Chef. I found it easy to learn. It's also simple enough to easily modify scripts and powerful enough to do exactly what I want it to, which is provision a server with Java and Datomic and deploy my app to it. You can check out my setup in infrastructure/ansible. If you're using Datomic free please do use it as a starting point.

provision.yml has just about everything you need to get a server up and running, with the exception of uploading SSH keys. deploy.yml is used by the janky bash script below to upload an uberjar, run migrations, and restart the server.

Building and Deploying with a Janky Bash Script and Ansible

Here's my janky Bash scripts which first build the app and then deploys it with Ansible:

# build.sh
#!/bin/bash
cd html-app
grunt build
rm -Rf ../server/resources/html-app
cp -R targets/public ../server/resources/html-app
cd ../server
lein uberjar
cd ..
cp  server/target/gratefulplace-0.1.0-SNAPSHOT-standalone.jar infrastructure/ansible/files/gp2.jar

# deploy.sh
#!/bin/bash
die () {
  echo >&2 "$@"
  exit 1
}

if [ "$#" -eq 0 ]
then INVENTORY="dev"
else INVENTORY=$1
fi

[ -e infrastructure/ansible/$INVENTORY ] || die "Inventory file $INVENTORY not found"

./build.sh
cd infrastructure/ansible/
ansible-playbook -i $INVENTORY deploy.yml

Workflow

OMG this article is almost over! Listen, I know you don't need to know this and it makes no difference to you but I am out here in the North Carolina heat sweating my ass off trying to finish this article so I can get on with my day. So it's pretty exciting that we're almost done.

Anyway - here are workflow improvements I developed over the course of this project. You might also want to check out this My Clojure Workflow, Reloaded.

Emacs Bookmarks, Snippets, and Keybindings

I created a bookmark to open my server/src/gratefulplace/server.clj file with just a few keystrokes instead of having to navigate to it. I recommend doing this for any project which you'll be toiling over for months on end!

Keybindings

Behold, my very first keybinding! This starts the Jetty server:

(defun nrepl-start-http-server ()
  (interactive)
  (nrepl-load-current-buffer)
  (nrepl-set-ns (nrepl-current-ns))
  ;; (with-current-buffer (nrepl-current-repl-buffer)
  ;;   (nrepl-send-string "(def server (-main)) (println server)"))
  (nrepl-interactive-eval (format "(println '(def server (%s/-main))) (println 'server)" (nrepl-current-ns)))
  (nrepl-interactive-eval (format "(def server (%s/-main)) (println server)" (nrepl-current-ns))))

(eval-after-load 'nrepl
  '(define-key clojure-mode-map (kbd "C-c C-v") 'nrepl-start-http-server))

So, once you have server.clj open and you've run nrepl-jack-in you can hit C-c C-v to start the server. Also check out the nrepl keybindings for some great workflow helpers.

tmuxinator config

In order to do development you need to have Datomic and Grunt running. Instead of having to open up a bunch of terminal tabs and handle all that manually every time I want to start working, I use tmuxinator so that I can get my environment set up in one comand. Here's my config:

# ~/.tmuxinator/nicu.yml
# you can make as many tabs as you wish...

project_name: gp2
project_root: ~/projects/web_sites/gp2
rvm: 1.9.3
tabs:
  - angular_server: git pull && cd html-app && grunt server
  - datomic: datomic
  - shell: 

I also have these nice little bash aliases:

alias "tmk"="tmux kill-session -t"
alias "datomic"="~/src/datomic/bin/transactor ~/src/datomic/config/samples/free-transactor-template.properties"

Actually Doing Development

So, in order to get to the point where you can actually start writing code and seeing the results, do the following:

  1. Install datomic and set up your own datomic alias
  2. Run mux gp2 to start tmux with your tmuxinator conf
  3. Open emacs
  4. Hit C-x r l to open your list of bookmarks and choose the bookmark for server.clj
  5. Run M-x nrepl-jack-in in emacs
  6. Hit C-c C-v to start the jetty server

The End

That's it! I hope you've found this article useful. I'm going to go have a life for a little while now. Haha, just kidding! I'm going to spend the next two hours hitting refresh on my reddit submission!

Comments