Hammock
tie two trees together to track a transformation
Install / Use
/learn @shaunlebron/HammockREADME
BETTER IDEA: the use-case for reshaping data might best be handled by adopting a Domain Driven Architecture that allows client-side querying (e.g. Om-Next paired with Datomic).
hammock 
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.
Usage
Add to your dependencies vector in project.clj:
[hammock "0.2.2"]
(ns example
(:require [hammock.core :as hm]))
Transforms and Mappings
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]
Using hammock
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]}}
Manual writing
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]}}
Composability
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
Sequences
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]}}
Running tests
$ lein cljsbuild test
Similar Technologies
The following technologies transform a JSON tree into another JSON tree. Each allows specification of a desired transform using declarative, custom expressions.
License
Copyright © 2014 Shaun Williams
Distributed under the Eclipse Public License either version 1.0 or any later version.
