Scripting in Racket

by Michael Ruggiero

A few years ago I was struck by Adrian Laiacano’s piece “Keeping tabs on your data analysis workflow.” Laiacano offers a compelling argument for doing one’s best to codify ad-hoc data analyses into composable tools. This led me on a very big kick to write more generalized tools instead of one-offs.

In my case, the obvious language choice is Python. I love it, and rarely get to write it for my day job (it’s a Scala team). But working with your go-to language can be a mixed bag. When you use one language a lot for the same type of task, you tend to think and plan in that language exclusively, where problems reveal themselves through approaches that feel most natural to that language. Nothing wrong with that, but it makes it harder for me to learn new languages if I think in one favorite language at the expense of others.

At any rate, I was looking at an old Scheme book and wondered if I could use Racket for scripting. I thought I would try it with simple query: what’s my team working on now in Pivotal?

I started off with the command line. How can I get args from the command line into a basic runner function?

(define (usage)
  (printf "Arguments needed: <token> <project-id>\n"))

(define (runner args)
  nil)

(define (main)
  (let ([cli-args (current-command-line-arguments)])
        (if (> 2 (vector-length cli-args))
            (usage)
            (runner cli-args))))

How do we call a web service and get back a string? The net/url package gives us a basic GET call. We can then copy-port the response into a string:

(require net/url
         racket/port)

(define (fetch url header)
  (define in (get-pure-port url header))
  (define out (open-output-string))
  (copy-port in out)
  (get-output-string out))

(define (api-call url header)
  (fetch url header))

To get the current Pivotal iteration for a given team/project, you pass an API token in the header, and a project-id on the URL. I also want the readable names of the first owner of each story, so I’ll need to grab our org’s membership from the memberships endpoint.

(define PIVOTAL-URI
  "https://www.pivotaltracker.com/services/v5/projects")

(define ITERATION-PATH
  "/iterations?scope=current")

(define MEMBERSHIPS-PATH
  "/memberships")

(define (pivotal-url project-id)
  (define url-string
    (string-append PIVOTAL-URI "/" project-id ITERATION-PATH))
  (string->url url-string))

(define (members-url project-id)
  (define url-string
    (string-append PIVOTAL-URI "/" project-id MEMBERSHIPS-PATH))
  (string->url url-string))

(define (pivotal-header token)
  (list (string-append "X-TrackerToken:" token)))

As you would expect, the API returns a lot of JSON.

The Racket JSON library (specifically string->jsexpr) helps convert the JSON to a list of very small structs:

(require json)

(struct story
  (name owned-by-id kind current-state owner-name) #:transparent)

(define (iteration-stories iteration)
  (hash-ref (car (string->jsexpr iteration)) 'stories))

(define (story-node->story node users)
    (story
     (hash-ref node 'name)
     (hash-ref node 'owned_by_id)
     (hash-ref node 'kind)
     (hash-ref node 'current_state)
     (hash-ref users (hash-ref node 'owned_by_id))))

(define (extract-stories iteration memberships)
  (let ([users (extract-users (string->jsexpr  memberships))])
    (map (λ [s]
           (story-node->story s users))
         (iteration-stories iteration))))

(define (extract-users nodes)
  (let ([m (make-hash)])
    (for ([n nodes])
      (let* ([person (hash-ref n 'person)]
             [id (hash-ref person 'id)]
             [name (hash-ref person 'name)])
        (hash-set! m id name)))
    m))

This code extracts stories from the iteration JSON. extract-users pulls user nodes from the memberships API call, and sets the id/name pairs into a hash that matches human-readable names with each story’s first owner id. When you pass that hash to the story-node->story function, each story struct has the human-readable name.

All that is left is binning the stories by name and printing them out. This is just a command-line script, so we don’t get too fancy here:

(define (grouped stories)
  (group-by (λ (s)
              (story-owner-name s))
            stories))

(define (print-grouped grouped)
  (let ([formatter
         (λ (story)
           (~a (story-current-state story)
               " - " (story-owner-name story)
               " - " (story-name story)
               #:max-width 90 #:limit-marker "..."))])
    (for ([g grouped])
      (for ([s g])
        (printf "~a\n" (formatter s))))))

group-by does just what it says; then some rather ugly formatting (from racket/format) prints out a line that doesn’t take up too much space.

When you run this you get a birds-eye view of what’s completed, and what’s underway, for this iteration. You can read the whole script in this gist.

So what did I learn? I learned that I write Racket like a Python developer. Functions doing text manipulation, filtering, and taking fairly few chances.

I’m still not great at naming, abstractions, or writing anything extensible in Racket. The other funny thing is that Racket’s real superpower — creating new languages — does not come into play here. It’s just a script. So I wonder if this really fits the task.

On the other hand, it was very fun to write, because Racket asks you to think differently about your problem. Yes, I could have written this with thirty lines of Python, but what would I have learned?