a cljs library that helps you transform one tree into another and to remember related branches.
In UIs, it is common to transform JSON data received from a backend REST service into data better suited for representation on screen. After this transformation is made, it is useful to remember which original fields are associated with the new ones (e.g. for highlighting invalid UI fields based on backend-validated JSON fields)
This is an experiment to capture that relationship during the actual process of transformation, by performing the transformation with objects we are calling "hammocks."
Hammocks are a bit like Om cursors, except they are anchored to two separate trees: a read-only "source" tree and write-only "destination" tree. These anchor points on the hammock move along their respective trees as data is transformed from source to destination. A log of the anchor positions is kept for each transformation in order to remember the relationship between source and destination branches.
Add to your dependencies vector in project.clj:
[hammock "0.2.2"]
(ns example
(:require [hammock.core :as hm]))
Suppose you have data in some source format:
{:foo 1
:bar 2}
And you want to transform it into some destination format:
{:my-foo {:value 1}
:my-bar {:value 2}}
Also, you want to remember the mapping between the two formats:
SRC-KEYS DST-KEYS
----------------------------------
[:foo] <---> [:my-foo :value]
[:bar] <---> [:my-bar :value]
Well sometimes a destination value can depend on multiple source values:
{:my-foo {:value 1}
:my-bar {:value 2}
:sum {:value 3}} ;; <--- foo + bar
So a source->destination mapping would now look like:
SRC-KEYS DST-KEYS
----------------------------------------
[:foo] ----> [:my-foo :value]
[:sum :value]
[:bar] ----> [:my-bar :value]
[:sum :value]
And a destination->source mapping would look like:
DST-KEYS SRC-KEYS
------------------------------------------
[:my-foo :value] ----> [:foo]
[:my-bar :value] ----> [:bar]
[:sum :value] ----> [:foo]
[:bar]
The following examples can be run from a REPL:
$ ./scripts/compile_cljsc # one-time only
$ ./scripts/repl
cljs.user> (require '[hammock.core :as hm])
Create a hammock h
to transform a source tree src
:
(def src {:foo 1 :bar 2})
(def h (hm/create src))
Use hm/copy!
to perform simple copies to the destination tree using the given
destination and source keys. (They can be a keyword or a vector of keywords)
;; DST-KEY SRC-KEY
(hm/copy! h [:my-foo :value] :foo)
(hm/copy! h [:my-bar :value] :bar)
And use hm/result
to get the transformed destination tree:
(def dst (hm/result h))
;; => {:my-foo {:value 1}
;; :my-bar {:value 2}}
The :anchors
metadata on the result will remember the forward/inverse
mappings of the keys between the formats. (Notice the keys are normalized
to vectors of keywords)
(-> dst meta :anchors :forward)
;; SRC-KEYS DST-KEYS
;; => {[:foo] #{[:my-foo :value]}
;; [:bar] #{[:my-bar :value]}}
(-> dst meta :anchors :inverse)
;; DST-KEYS SRC-KEYS
;; => {[:my-foo :value] #{[:foo]}
;; [:my-bar :value] #{[:bar]}}
There is a command for manually setting a destination value, which is useful for computing destination value from multiple source values.
(def sum (+ (:foo src) (:bar src)))
(hm/man! h [:sum :value] sum)
You can include optional dependent source keys as the last argument so we can trace those keys to our computed value:
(hm/man! h [:sum :value] sum [:foo :bar])
And the new result will reflect the addition:
(def dst (hm/result h))
;; => {:my-foo {:value 1}
;; :my-bar {:value 2}
;; :sum {:value 3}}
(-> dst meta :anchors :forward)
;; SRC-KEYS DST-KEYS
;; => {[:foo] #{[:my-foo :value]
;; [:sum :value]}
;; [:bar] #{[:my-bar :value]
;; [:sum :value]}}
(-> dst meta :anchors :inverse)
;; DST-KEYS SRC-KEYS
;; => {[:my-foo :value] #{[:foo]}
;; [:my-bar :value] #{[:bar]}
;; [:sum :value] #{[:foo]
;; [:bar]}}
We can create composable transformations using functions that take a
hammock object h
:
(defn unpack-thing [h]
(hm/copy! h [:my-foo :value] :foo)
(hm/copy! h [:my-bar :value] :bar))
We can then use this function to perform sub-transformations. We do this by
passing the function to hm/nest!
, causing it to receive a relative hammock
whose anchors are moved to the given keys.
(def src {:a {:foo 1 :bar 2}
:b {:foo 3 :bar 4}})
(def h (hm/create src))
(hm/nest! h :my-a :a unpack-thing)
(hm/nest! h :my-b :b unpack-thing)
(hm/result h)
;; => {:my-a {:my-foo {:value 1}
;; :my-bar {:value 2}}
;; :my-b {:my-foo {:value 3}
;; :my-bar {:value 4}}}
And we can update unpack-thing
to manually create a sum value:
(defn unpack-thing [h]
(hm/copy! h [:my-foo :value] :foo)
(hm/copy! h [:my-bar :value] :bar)
(let [sum (+ (:foo h) (:bar h)) ;; <-- NOTE: lookups on a hammock return source values
keys-used [:foo :bar]]
(hm/man! h [:sum :value] sum keys-used)))
(hm/nest! h :my-a :a unpack-thing)
(hm/nest! h :my-b :b unpack-thing)
(hm/result h)
;; => {:my-a {:my-foo {:value 1}
;; :my-bar {:value 2}
;; :sum {:value 3}} ;; <-- added sum
;; :my-b {:my-foo {:value 3}
;; :my-bar {:value 4}
;; :sum {:value 7}}} ;; <-- added sum
There is support for simple 1-to-1 vector transformations using hm/map!
.
(def src {:vals [{:foo 1 :bar 2}
{:foo 3 :bar 4}]})
(def h (hm/create src))
(hm/map! h :my-vals :vals unpack-thing)
(def dst (hm/result h))
;; => {:my-vals [{:my-foo {:value 1}
;; :my-bar {:value 2}
;; :sum {:value 3}}
;; {:my-foo {:value 3}
;; :my-bar {:value 4}
;; :sum {:value 7}}]}
You can see the resulting anchors below:
-> dst meta :anchors :forward)
;; SRC-KEYS DST-KEYS
;; => {[:vals 0 :foo] #{[:my-vals 0 :my-foo :value]
;; [:my-vals 0 :sum :value]}
;; [:vals 0 :bar] #{[:my-vals 0 :my-bar :value]
;; [:my-vals 0 :sum :value]}
;; [:vals 1 :foo] #{[:my-vals 1 :my-foo :value]
;; [:my-vals 1 :sum :value]}
;; [:vals 1 :bar] #{[:my-vals 1 :my-bar :value]
;; [:my-vals 1 :sum :value]}}
(-> dst meta :anchors :inverse)
;; DST-KEYS SRC-KEYS
;; => {[:my-vals 0 :my-foo :value] #{[:vals 0 :foo]}
;; [:my-vals 0 :my-bar :value] #{[:vals 0 :bar]}
;; [:my-vals 0 :sum :value] #{[:vals 0 :foo]
;; [:vals 0 :bar]}
;; [:my-vals 1 :my-foo :value] #{[:vals 1 :foo]}
;; [:my-vals 1 :my-bar :value] #{[:vals 1 :bar]}
;; [:my-vals 1 :sum :value] #{[:vals 1 :foo]
;; [:vals 1 :bar]}}
$ lein cljsbuild test
Copyright © 2014 Shaun Williams
Distributed under the Eclipse Public License either version 1.0 or any later version.
Can you improve this documentation? These fine people already did:
Shaun Williams & Shaun LeBronEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close