Supdate

Clojars Project badge

supdate is a small Clojure/ClojureScript library for transforming nested data structures in a straightforward and efficient way.

supdate lets you express nested transformations as data structures which match the schema of the input, and then executes these transformations efficiently. You could say supdate is to data transformation what plumatic/schema is to data validation. It plays in the same arena as Specter, complementing it nicely for many basic cases where Specter would be too much.

Example

The library can be downloaded from Clojars. Require it with the following:

(require '[vvvvalvalval.supdate.api :as supd])
Here's a basic example (it's interactive, feel free to modify the code!):

;; Our example uses some string functions
(require '[clojure.string :as str])

;; Let's define some example data
(def input
  {:bands [{:band/id 3141
            :band/name "Led Zeppelin"
            :band/members [{:person/name "robert plant"}
                           {:person/name "jimmy page"}
                           {:person/name "john bonham"}
                           {:person/name "john paul jones"}]}
           {:band/id 8242
            :band/name "The White stripes"
            :band/members [{:person/name "jack white"}
                           {:person/name "meg white"}]}]})

;; ... then transform it:
(supd/supdate
  input
  {:bands [{:band/name str/upper-case
            :band/members [{:person/name str/capitalize}]}]})

The rules

supdate works by defining transforms as data structures.

The most elementary transform you can encounter is just a function:


(supd/supdate 0 inc)

(supd/supdate
  "Jimmy Page"
  (fn [s] (str/split s " ")))

Transforming sequences

Wrapping a transform in a vector means you want to apply it to a sequence of inputs:

(supd/supdate
  [2 3 5 7 11 13 17 19]
  [inc])

(supd/supdate
  (range 12)
  [#(* % %)])

Transforming maps

Wrapping a transform in a map means you want to transform the value at the key:

(supd/supdate
  {:name "Alice" :age 7}
  {:age inc})
Note that the transformation only occurs if the key is present!

(supd/supdate
  {:name "Ilúvatar"}
  {:age inc})
Finally, if you use false as a value instead of a transform, it means you want the key to be dissoc'ed:

(supd/supdate
  {:name "Alice" :age 7}
  {:age false})

Chaining tranforms

Recall that if you put a transform in a one-element vector, it is applied sequentially. If you put several transforms in a vector, it does something else: it applies the transforms one after the other, left to right.

(supd/supdate
  " jimmy page  "
  [str/trim str/capitalize])
What if you want to do both, chaining tranforms and applying them in sequences? Well, you can use 2 vectors:

(supd/supdate
  (range 10)
  [[inc inc inc]])

Composing tranforms

supdate really becomes interesting when you start nesting transforms together:

(def input1
  {:band/members [{:id 1 :name ["jimmy" "page"] :plays "guitar"}
                  {:id 2 :name ["robert" "plant"] :plays "voice"}
                  {:id 3 :name ["john" "paul" "jones"] :plays "bass"}
                  {:id 4 :name ["john" "bonham"] :plays "drums"}]})

(supd/supdate
  input1
  {:band/members [{:id false
                   :name [[str/capitalize]
                          #(str/join " " %)]
                   :plays keyword}]})

Is supdate fast?

Yes! Supdate will tend to outperform code hand-written using update-in or assoc-in, using several optimizations.

It will avoid doing several 'deep dives' in the data, doing all it has to do once at a given path.

What's more, supd/supdate is actually a macro, leveraging static information on a best-effort basis to compile to efficient code.

Finally, you can also use supd/compile to pre-compile transforms, and achieve a similar effect more dynamically:


(def input
  {:bands [{:band/id 3141
            :band/name "Led Zeppelin"
            :band/members [{:person/name "robert plant"}
                           {:person/name "jimmy page"}
                           {:person/name "john bonham"}
                           {:person/name "john paul jones"}]}
           {:band/id 8242
            :band/name "The White stripes"
            :band/members [{:person/name "jack white"}
                           {:person/name "meg white"}]}]})

;; Pre-compiling our transform
(def prettify-bands
  (supd/compile
    {:bands [{:band/name str/upper-case
             :band/members [{:person/name str/capitalize}]}]}))

;; Running it later:
(prettify-bands input)

Parting words

Feel free to give feedback by opening a Github issue!

Ths page is live and interactive powered by the klipse plugin:

  1. Live: The code is executed in your browser
  2. Interactive: You can modify the code and it is evaluated as you type