├── hammock.png ├── .travis.yml ├── scripts ├── brepl ├── repl ├── compile_cljsc ├── unit-test.html ├── repl.clj ├── brepl.clj ├── unit-test.js └── phantomjs-shims.js ├── .gitignore ├── test ├── test-runner.cljs └── hammock │ └── tests.cljs ├── project.clj ├── CHANGES.md ├── src └── hammock │ └── core.cljs ├── README.md └── LICENSE /hammock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaunlebron/hammock/HEAD/hammock.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein 3 | script: lein cljsbuild test 4 | -------------------------------------------------------------------------------- /scripts/brepl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rlwrap lein trampoline run -m clojure.main scripts/brepl.clj 3 | -------------------------------------------------------------------------------- /scripts/repl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rlwrap lein trampoline run -m clojure.main scripts/repl.clj 3 | -------------------------------------------------------------------------------- /scripts/compile_cljsc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lein trampoline run -m clojure.main -e "(compile 'cljs.repl.node) (compile 'cljs.repl.browser) (compile 'cljs.core)" 3 | -------------------------------------------------------------------------------- /scripts/unit-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /scripts/out/ 7 | /target/ 8 | *.js 9 | *.js.map 10 | .lein-deps-sum 11 | .lein-repl-history 12 | .lein-plugins/ 13 | *.swp 14 | /out/ 15 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.node :as node]) 4 | 5 | (repl/repl* (node/repl-env) 6 | {:output-dir "out" 7 | :optimizations :none 8 | :cache-analysis true 9 | :source-map true}) 10 | -------------------------------------------------------------------------------- /scripts/brepl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.browser :as browser]) 4 | 5 | (repl/repl* (browser/repl-env) 6 | {:output-dir "out" 7 | :optimizations :none 8 | :cache-analysis true 9 | :source-map true}) 10 | -------------------------------------------------------------------------------- /test/test-runner.cljs: -------------------------------------------------------------------------------- 1 | (ns test-runner 2 | (:require 3 | [cljs.test :refer-macros [run-tests]] 4 | [hammock.tests])) 5 | 6 | 7 | (enable-console-print!) 8 | 9 | (defn runner [] 10 | (if (cljs.test/successful? 11 | (run-tests 12 | 'hammock.tests)) 13 | 0 14 | 1)) 15 | -------------------------------------------------------------------------------- /scripts/unit-test.js: -------------------------------------------------------------------------------- 1 | 2 | var page = require('webpage').create(); 3 | var url = phantom.args[0]; 4 | 5 | page.onConsoleMessage = function (message) { 6 | console.log(message); 7 | }; 8 | 9 | function exit(code) { 10 | setTimeout(function(){ phantom.exit(code); }, 0); 11 | phantom.onError = function(){}; 12 | } 13 | 14 | console.log("Loading URL: " + url); 15 | 16 | page.open(url, function (status) { 17 | if (status != "success") { 18 | console.log('Failed to open ' + url); 19 | phantom.exit(1); 20 | } 21 | 22 | console.log("Running test."); 23 | 24 | var result = page.evaluate(function() { 25 | return test_runner.runner(); 26 | }); 27 | 28 | if (result != 0) { 29 | console.log("*** Test failed! ***"); 30 | exit(1); 31 | } 32 | else { 33 | console.log("Test succeeded."); 34 | exit(0); 35 | } 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject hammock "0.2.2" 2 | :description "tie two trees together to track a transformation" 3 | :url "https://github.com/shaunlebron/hammock" 4 | 5 | :plugins [[lein-cljsbuild "1.0.4"] 6 | [lein-npm "0.4.0"]] 7 | 8 | :dependencies [[org.clojure/clojure "1.6.0"] 9 | [org.clojure/clojurescript "0.0-2665" :scope "provided"]] 10 | 11 | :node-dependencies [[source-map-support "0.2.8"]] 12 | 13 | :source-paths ["src"] 14 | 15 | :clean-targets ["scripts/out" 16 | "scripts/hammock.test.js" 17 | "scripts/hammock.test.js.map"] 18 | 19 | :cljsbuild { 20 | :test-commands {"test" ["phantomjs" "scripts/unit-test.js" "scripts/unit-test.html"]} 21 | :builds [{:id "test" 22 | :source-paths ["src" "test"] 23 | :compiler { 24 | :output-to "scripts/hammock.test.js" 25 | :output-dir "scripts/out" 26 | :source-map "scripts/hammock.test.js.map" 27 | :cache-analysis true 28 | :optimizations :whitespace}}]}) 29 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2 2 | 3 | - 0.2.1 was deployed with `cljs` and `clojure` class files for some reason... 4 | 5 | ## 0.2.1 6 | 7 | ### changes 8 | 9 | - operations return the current destination tree value 10 | 11 | ## 0.2.0 12 | 13 | ### changes 14 | 15 | - `old-tree` renamed to `src-tree` 16 | - `new-tree` renamed to `dst-tree` 17 | - add `result` function for retrieving destination tree with `:anchors` metadata 18 | 19 | ## 0.1.1 20 | 21 | ### bug fixes 22 | 23 | - `old-tree` no longer tries to deref its map 24 | 25 | ## 0.1.0 26 | 27 | ### initial features 28 | 29 | - `create` create a new hammock 30 | - `copy!` set new tree based on old tree 31 | - `nest!` move hammock to separate branches and execute function 32 | - `map!` map an old sequence to a new sequence with a map function receiving a nested hammock 33 | - `man!` manually set new tree value, and manually set related branches 34 | - `ILookup` read from old tree at the given hammock 35 | - `old-tree` get the hammock's entire old tree 36 | - `new-tree` get the hammock's entire new transformed tree 37 | - `anchors` get the hammock's map of old<->new related branches 38 | -------------------------------------------------------------------------------- /scripts/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Ap = Array.prototype; 4 | var slice = Ap.slice; 5 | var Fp = Function.prototype; 6 | 7 | if (!Fp.bind) { 8 | // PhantomJS doesn't support Function.prototype.bind natively, so 9 | // polyfill it whenever this module is required. 10 | Fp.bind = function(context) { 11 | var func = this; 12 | var args = slice.call(arguments, 1); 13 | 14 | function bound() { 15 | var invokedAsConstructor = func.prototype && (this instanceof func); 16 | return func.apply( 17 | // Ignore the context parameter when invoking the bound function 18 | // as a constructor. Note that this includes not only constructor 19 | // invocations using the new keyword but also calls to base class 20 | // constructors such as BaseClass.call(this, ...) or super(...). 21 | !invokedAsConstructor && context || this, 22 | args.concat(slice.call(arguments)) 23 | ); 24 | } 25 | 26 | // The bound function must share the .prototype of the unbound 27 | // function so that any object created by one constructor will count 28 | // as an instance of both constructors. 29 | bound.prototype = func.prototype; 30 | 31 | return bound; 32 | }; 33 | } 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /src/hammock/core.cljs: -------------------------------------------------------------------------------- 1 | (ns hammock.core) 2 | 3 | (defprotocol IHammock 4 | (-dst-tree [this]) 5 | (-src-tree [this]) 6 | (-anchors [this]) 7 | (-copy! [this dst-key src-key d-fn]) 8 | (-nest! [this dst-key src-key h-fn]) 9 | (-map! [this dst-key src-key h-fn]) 10 | (-man! [this dst-ks value src-keys])) 11 | 12 | (defn- norm-path 13 | [k] 14 | (if (sequential? k) (vec k) [k])) 15 | 16 | (defn- join-path 17 | [path k] 18 | (vec (concat path (norm-path k)))) 19 | 20 | (defn- remember-anchor! 21 | [key-path val-path anchors dir] 22 | (if (get-in @anchors [dir key-path]) 23 | (swap! anchors update-in [dir key-path] conj val-path) 24 | (swap! anchors assoc-in [dir key-path] (hash-set val-path)))) 25 | 26 | (defn- remember-anchors! 27 | [src-path dst-path anchors] 28 | (remember-anchor! src-path dst-path anchors :forward) 29 | (remember-anchor! dst-path src-path anchors :inverse)) 30 | 31 | (deftype Hammock [src src-path dst dst-path anchors] 32 | IHammock 33 | (-src-tree [this] src) 34 | (-dst-tree [this] @dst) 35 | (-anchors [this] @anchors) 36 | 37 | (-copy! [this dst-key src-key d-fn] 38 | (let [src-path (join-path src-path src-key) 39 | dst-path (join-path dst-path dst-key) 40 | src-val (d-fn (get-in src src-path))] 41 | (swap! dst assoc-in dst-path src-val) 42 | (remember-anchors! src-path dst-path anchors) 43 | @dst)) 44 | 45 | (-nest! [this dst-key src-key h-fn] 46 | (let [src-path (join-path src-path src-key) 47 | dst-path (join-path dst-path dst-key) 48 | new-h (Hammock. src src-path dst dst-path anchors)] 49 | (h-fn new-h) 50 | @dst)) 51 | 52 | (-map! [this dst-key src-key h-fn] 53 | (let [src-path (join-path src-path src-key) 54 | dst-path (join-path dst-path dst-key) 55 | src-val (get-in src src-path)] 56 | (swap! dst assoc-in dst-path []) 57 | (dotimes [i (count src-val)] 58 | (let [src-path (join-path src-path i) 59 | dst-path (join-path dst-path i) 60 | new-h (Hammock. src src-path dst dst-path anchors)] 61 | (h-fn new-h))) 62 | @dst)) 63 | 64 | (-man! [this dst-key value src-keys] 65 | (let [dst-path (join-path dst-path dst-key)] 66 | (swap! dst assoc-in dst-path value) 67 | (doseq [k src-keys] 68 | (let [src-path (join-path src-path k)] 69 | (remember-anchors! src-path dst-path anchors))) 70 | @dst)) 71 | 72 | ILookup 73 | (-lookup [this k] 74 | (-lookup this k nil)) 75 | (-lookup [this k not-found] 76 | (-lookup (get-in src src-path) k not-found))) 77 | 78 | (defn create 79 | ([src] (create src (atom {}))) 80 | ([src dst] (create src dst (atom {}))) 81 | ([src dst anchors] (create src [] dst [] anchors)) 82 | ([src src-path dst dst-path anchors] 83 | (let [src-path (norm-path src-path) 84 | dst-path (norm-path dst-path)] 85 | (Hammock. src src-path dst dst-path anchors)))) 86 | 87 | (defn copy! 88 | ([h dst-key src-key] 89 | (copy! h dst-key src-key identity)) 90 | ([h dst-key src-key d-fn] 91 | (-copy! h dst-key src-key d-fn))) 92 | 93 | (defn nest! 94 | [h dst-key src-key h-fn] 95 | (-nest! h dst-key src-key h-fn)) 96 | 97 | (defn map! 98 | [h dst-key src-key h-fn] 99 | (-map! h dst-key src-key h-fn)) 100 | 101 | (defn man! 102 | ([h dst-key value] 103 | (man! h dst-key value nil)) 104 | ([h dst-key value src-keys] 105 | (-man! h dst-key value src-keys))) 106 | 107 | (defn src-tree 108 | [h] 109 | (-src-tree h)) 110 | 111 | (defn dst-tree 112 | [h] 113 | (-dst-tree h)) 114 | 115 | (defn anchors 116 | [h] 117 | (-anchors h)) 118 | 119 | (defn result 120 | [h] 121 | (with-meta 122 | (dst-tree h) 123 | {:anchors (anchors h)})) 124 | -------------------------------------------------------------------------------- /test/hammock/tests.cljs: -------------------------------------------------------------------------------- 1 | (ns hammock.tests 2 | (:require 3 | [hammock.core :as hm] 4 | [cljs.test :refer-macros [deftest is testing run-tests]])) 5 | 6 | (deftest test-basic 7 | (testing "Testing read-out operations." 8 | (let [src {:foo "bar" :bar ["hi" "there"]} 9 | dst (atom {:hello "world"}) 10 | h (hm/create src dst)] 11 | (is (= src (hm/src-tree h))) 12 | (is (= @dst (hm/dst-tree h))) 13 | (is (= {} (hm/anchors h))))) 14 | (testing "Testing lookup" 15 | (let [src {:foo "bar" :bar ["hi" "there"]} 16 | h (hm/create src)] 17 | (is (= "bar" (:foo h))) 18 | (is (= "there" (get-in h [:bar 1]))))) 19 | (testing "Testing nested lookup." 20 | (let [src {:foo {:bar "hi"}} 21 | dst (atom {}) 22 | anchors (atom {}) 23 | h (hm/create src [:foo] dst [] anchors) 24 | h2 (hm/create src :foo dst [] anchors)] 25 | (is (= "hi" (:bar h))) 26 | (is (= "hi" (:bar h2))))) 27 | (testing "Testing copy!" 28 | (let [src {:fooBar "hi" :booFar "bye"} 29 | dst (atom {}) 30 | anchors (atom {}) 31 | h (hm/create src dst anchors) 32 | expected-anchors {:forward {[:fooBar] #{[:foo-bar]} 33 | [:booFar] #{[:boo-far]}} 34 | :inverse {[:foo-bar] #{[:fooBar]} 35 | [:boo-far] #{[:booFar]}}}] 36 | (hm/copy! h :foo-bar :fooBar) 37 | (hm/copy! h :boo-far :booFar) 38 | (is (= "hi" (:foo-bar @dst))) 39 | (is (= "bye" (:boo-far @dst))) 40 | (is (= @anchors expected-anchors)) 41 | (let [result (hm/result h) 42 | expected-result {:foo-bar "hi" :boo-far "bye"} 43 | result-anchors (-> result meta :anchors)] 44 | (is (= result expected-result)) 45 | (is (= result-anchors expected-anchors))))) 46 | (testing "Testing nest!" 47 | (let [src {:fooBar "hi" :booFar "bye"} 48 | dst (atom {}) 49 | anchors (atom {}) 50 | h (hm/create src dst anchors) 51 | expected-anchors {:forward {[:fooBar] #{[:foo :bar]} 52 | [:booFar] #{[:boo :far]}} 53 | :inverse {[:foo :bar] #{[:fooBar]} 54 | [:boo :far] #{[:booFar]}}}] 55 | (hm/nest! h :foo [] (fn [h] (hm/copy! h :bar :fooBar))) 56 | (hm/nest! h :boo [] (fn [h] (hm/copy! h :far :booFar))) 57 | (is (= "hi" (-> @dst :foo :bar))) 58 | (is (= "bye" (-> @dst :boo :far))) 59 | (is (= @anchors expected-anchors)))) 60 | (testing "Testing map!" 61 | (let [src {:foo [{:a 1 :b 2} 62 | {:a 3 :b 4} 63 | {:a 5 :b 6}]} 64 | dst (atom {}) 65 | anchors (atom {}) 66 | expected-dst {:foo {:things [{:a 1 :c 3} 67 | {:a 3 :c 7} 68 | {:a 5 :c 11}]}} 69 | h (hm/create src dst anchors) 70 | mapping (fn [h] 71 | (hm/copy! h :a :a) 72 | (hm/man! h :c (+ (:a h) (:b h)) [:a :b])) 73 | expected-anchors {:forward {[:foo 0 :a] #{[:foo :things 0 :a] [:foo :things 0 :c]} 74 | [:foo 0 :b] #{[:foo :things 0 :c]} 75 | [:foo 1 :a] #{[:foo :things 1 :a] [:foo :things 1 :c]} 76 | [:foo 1 :b] #{[:foo :things 1 :c]} 77 | [:foo 2 :a] #{[:foo :things 2 :a] [:foo :things 2 :c]} 78 | [:foo 2 :b] #{[:foo :things 2 :c]}} 79 | :inverse {[:foo :things 0 :a] #{[:foo 0 :a]} 80 | [:foo :things 0 :c] #{[:foo 0 :a] [:foo 0 :b]} 81 | [:foo :things 1 :a] #{[:foo 1 :a]} 82 | [:foo :things 1 :c] #{[:foo 1 :a] [:foo 1 :b]} 83 | [:foo :things 2 :a] #{[:foo 2 :a]} 84 | [:foo :things 2 :c] #{[:foo 2 :a] [:foo 2 :b]}}}] 85 | (hm/map! h [:foo :things] :foo mapping) 86 | (is (= @dst expected-dst)) 87 | (is (= @anchors expected-anchors)))) 88 | (testing "Testing man!" 89 | (let [src {:fooBar "hi" :booFar "bye"} 90 | dst (atom {}) 91 | anchors (atom {}) 92 | h (hm/create src dst anchors) 93 | hibye (str (:fooBar h) (:booFar h)) 94 | byehi (str (:booFar h) (:fooBar h)) 95 | expected-anchors {:forward {[:fooBar] #{[:foo :bar] [:boo :far]} 96 | [:booFar] #{[:foo :bar] [:boo :far]}} 97 | :inverse {[:foo :bar] #{[:fooBar] [:booFar]} 98 | [:boo :far] #{[:booFar] [:fooBar]}}}] 99 | (hm/man! h [:foo :bar] hibye [:fooBar :booFar]) 100 | (hm/man! h [:boo :far] byehi [:booFar :fooBar]) 101 | (is (= "hibye" (-> @dst :foo :bar))) 102 | (is (= "byehi" (-> @dst :boo :far))) 103 | (is (= @anchors expected-anchors))))) 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __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). 2 | 3 | [Domain Driven Architecture]:http://www.infoq.com/presentations/domain-driven-architecture 4 | 5 | # hammock [![Build Status](https://travis-ci.org/shaunlebron/hammock.svg)](https://travis-ci.org/shaunlebron/hammock) 6 | 7 | a cljs library that helps you transform one tree into another and to remember related branches. 8 | 9 | ![illustration](hammock.png) 10 | 11 | In UIs, it is common to transform JSON data received from a backend REST 12 | service into data better suited for representation on screen. After this 13 | transformation is made, it is useful to remember which original fields are 14 | associated with the new ones (e.g. for highlighting invalid UI fields based on 15 | backend-validated JSON fields) 16 | 17 | This is an experiment to capture that relationship during the actual process of 18 | transformation, by performing the transformation with objects we are calling 19 | "hammocks." 20 | 21 | Hammocks are a bit like [Om cursors], except they are anchored to two separate 22 | trees: a read-only "source" tree and write-only "destination" tree. These 23 | anchor points on the hammock move along their respective trees as data is 24 | transformed from source to destination. A log of the anchor positions is kept 25 | for each transformation in order to remember the relationship between source 26 | and destination branches. 27 | 28 | ## Usage 29 | 30 | Add to your dependencies vector in project.clj: 31 | 32 | ``` 33 | [hammock "0.2.2"] 34 | ``` 35 | 36 | ```clj 37 | (ns example 38 | (:require [hammock.core :as hm])) 39 | ``` 40 | 41 | ## Transforms and Mappings 42 | 43 | Suppose you have data in some source format: 44 | 45 | ```clj 46 | {:foo 1 47 | :bar 2} 48 | ``` 49 | 50 | And you want to transform it into some destination format: 51 | 52 | ```clj 53 | {:my-foo {:value 1} 54 | :my-bar {:value 2}} 55 | ``` 56 | 57 | Also, you want to remember the mapping between the two formats: 58 | 59 | ``` 60 | SRC-KEYS DST-KEYS 61 | ---------------------------------- 62 | [:foo] <---> [:my-foo :value] 63 | [:bar] <---> [:my-bar :value] 64 | ``` 65 | 66 | Well sometimes a destination value can depend on multiple source values: 67 | 68 | ```clj 69 | {:my-foo {:value 1} 70 | :my-bar {:value 2} 71 | :sum {:value 3}} ;; <--- foo + bar 72 | ``` 73 | 74 | So a source->destination mapping would now look like: 75 | 76 | ``` 77 | SRC-KEYS DST-KEYS 78 | ---------------------------------------- 79 | [:foo] ----> [:my-foo :value] 80 | [:sum :value] 81 | [:bar] ----> [:my-bar :value] 82 | [:sum :value] 83 | ``` 84 | 85 | And a destination->source mapping would look like: 86 | 87 | ``` 88 | DST-KEYS SRC-KEYS 89 | ------------------------------------------ 90 | [:my-foo :value] ----> [:foo] 91 | [:my-bar :value] ----> [:bar] 92 | [:sum :value] ----> [:foo] 93 | [:bar] 94 | ``` 95 | 96 | ## Using hammock 97 | 98 | The following examples can be run from a REPL: 99 | 100 | ``` 101 | $ ./scripts/compile_cljsc # one-time only 102 | $ ./scripts/repl 103 | 104 | cljs.user> (require '[hammock.core :as hm]) 105 | ``` 106 | 107 | Create a hammock `h` to transform a source tree `src`: 108 | 109 | ```clj 110 | (def src {:foo 1 :bar 2}) 111 | (def h (hm/create src)) 112 | ``` 113 | 114 | Use `hm/copy!` to perform simple copies to the destination tree using the given 115 | destination and source keys. (They can be a keyword or a vector of keywords) 116 | 117 | 118 | ```clj 119 | ;; DST-KEY SRC-KEY 120 | (hm/copy! h [:my-foo :value] :foo) 121 | (hm/copy! h [:my-bar :value] :bar) 122 | ``` 123 | 124 | And use `hm/result` to get the transformed destination tree: 125 | 126 | ```clj 127 | (def dst (hm/result h)) 128 | ;; => {:my-foo {:value 1} 129 | ;; :my-bar {:value 2}} 130 | ``` 131 | 132 | The `:anchors` metadata on the result will remember the forward/inverse 133 | mappings of the keys between the formats. (Notice the keys are normalized 134 | to vectors of keywords) 135 | 136 | ```clj 137 | (-> dst meta :anchors :forward) 138 | ;; SRC-KEYS DST-KEYS 139 | ;; => {[:foo] #{[:my-foo :value]} 140 | ;; [:bar] #{[:my-bar :value]}} 141 | 142 | (-> dst meta :anchors :inverse) 143 | ;; DST-KEYS SRC-KEYS 144 | ;; => {[:my-foo :value] #{[:foo]} 145 | ;; [:my-bar :value] #{[:bar]}} 146 | ``` 147 | 148 | ### Manual writing 149 | 150 | There is a command for manually setting a destination value, which is useful 151 | for computing destination value from multiple source values. 152 | 153 | ```clj 154 | (def sum (+ (:foo src) (:bar src))) 155 | (hm/man! h [:sum :value] sum) 156 | ``` 157 | 158 | You can include optional dependent source keys as the last argument so we can 159 | trace those keys to our computed value: 160 | 161 | ```clj 162 | (hm/man! h [:sum :value] sum [:foo :bar]) 163 | ``` 164 | 165 | And the new result will reflect the addition: 166 | 167 | ```clj 168 | (def dst (hm/result h)) 169 | ;; => {:my-foo {:value 1} 170 | ;; :my-bar {:value 2} 171 | ;; :sum {:value 3}} 172 | 173 | (-> dst meta :anchors :forward) 174 | ;; SRC-KEYS DST-KEYS 175 | ;; => {[:foo] #{[:my-foo :value] 176 | ;; [:sum :value]} 177 | ;; [:bar] #{[:my-bar :value] 178 | ;; [:sum :value]}} 179 | 180 | (-> dst meta :anchors :inverse) 181 | ;; DST-KEYS SRC-KEYS 182 | ;; => {[:my-foo :value] #{[:foo]} 183 | ;; [:my-bar :value] #{[:bar]} 184 | ;; [:sum :value] #{[:foo] 185 | ;; [:bar]}} 186 | ``` 187 | 188 | ### Composability 189 | 190 | We can create composable transformations using functions that take a 191 | hammock object `h`: 192 | 193 | ```clj 194 | (defn unpack-thing [h] 195 | (hm/copy! h [:my-foo :value] :foo) 196 | (hm/copy! h [:my-bar :value] :bar)) 197 | ``` 198 | 199 | We can then use this function to perform sub-transformations. We do this by 200 | passing the function to `hm/nest!`, causing it to receive a relative hammock 201 | whose anchors are moved to the given keys. 202 | 203 | ```clj 204 | (def src {:a {:foo 1 :bar 2} 205 | :b {:foo 3 :bar 4}}) 206 | 207 | (def h (hm/create src)) 208 | 209 | (hm/nest! h :my-a :a unpack-thing) 210 | (hm/nest! h :my-b :b unpack-thing) 211 | 212 | (hm/result h) 213 | ;; => {:my-a {:my-foo {:value 1} 214 | ;; :my-bar {:value 2}} 215 | ;; :my-b {:my-foo {:value 3} 216 | ;; :my-bar {:value 4}}} 217 | ``` 218 | 219 | And we can update `unpack-thing` to manually create a sum value: 220 | 221 | ```clj 222 | (defn unpack-thing [h] 223 | (hm/copy! h [:my-foo :value] :foo) 224 | (hm/copy! h [:my-bar :value] :bar) 225 | 226 | (let [sum (+ (:foo h) (:bar h)) ;; <-- NOTE: lookups on a hammock return source values 227 | keys-used [:foo :bar]] 228 | (hm/man! h [:sum :value] sum keys-used))) 229 | 230 | (hm/nest! h :my-a :a unpack-thing) 231 | (hm/nest! h :my-b :b unpack-thing) 232 | 233 | (hm/result h) 234 | ;; => {:my-a {:my-foo {:value 1} 235 | ;; :my-bar {:value 2} 236 | ;; :sum {:value 3}} ;; <-- added sum 237 | ;; :my-b {:my-foo {:value 3} 238 | ;; :my-bar {:value 4} 239 | ;; :sum {:value 7}}} ;; <-- added sum 240 | ``` 241 | 242 | ### Sequences 243 | 244 | There is support for simple 1-to-1 vector transformations using `hm/map!`. 245 | 246 | ```clj 247 | (def src {:vals [{:foo 1 :bar 2} 248 | {:foo 3 :bar 4}]}) 249 | 250 | (def h (hm/create src)) 251 | 252 | (hm/map! h :my-vals :vals unpack-thing) 253 | 254 | (def dst (hm/result h)) 255 | ;; => {:my-vals [{:my-foo {:value 1} 256 | ;; :my-bar {:value 2} 257 | ;; :sum {:value 3}} 258 | ;; {:my-foo {:value 3} 259 | ;; :my-bar {:value 4} 260 | ;; :sum {:value 7}}]} 261 | ``` 262 | 263 | You can see the resulting anchors below: 264 | 265 | ```clj 266 | -> dst meta :anchors :forward) 267 | ;; SRC-KEYS DST-KEYS 268 | ;; => {[:vals 0 :foo] #{[:my-vals 0 :my-foo :value] 269 | ;; [:my-vals 0 :sum :value]} 270 | ;; [:vals 0 :bar] #{[:my-vals 0 :my-bar :value] 271 | ;; [:my-vals 0 :sum :value]} 272 | ;; [:vals 1 :foo] #{[:my-vals 1 :my-foo :value] 273 | ;; [:my-vals 1 :sum :value]} 274 | ;; [:vals 1 :bar] #{[:my-vals 1 :my-bar :value] 275 | ;; [:my-vals 1 :sum :value]}} 276 | 277 | (-> dst meta :anchors :inverse) 278 | ;; DST-KEYS SRC-KEYS 279 | ;; => {[:my-vals 0 :my-foo :value] #{[:vals 0 :foo]} 280 | ;; [:my-vals 0 :my-bar :value] #{[:vals 0 :bar]} 281 | ;; [:my-vals 0 :sum :value] #{[:vals 0 :foo] 282 | ;; [:vals 0 :bar]} 283 | ;; [:my-vals 1 :my-foo :value] #{[:vals 1 :foo]} 284 | ;; [:my-vals 1 :my-bar :value] #{[:vals 1 :bar]} 285 | ;; [:my-vals 1 :sum :value] #{[:vals 1 :foo] 286 | ;; [:vals 1 :bar]}} 287 | ``` 288 | 289 | ## Running tests 290 | 291 | ``` 292 | $ lein cljsbuild test 293 | ``` 294 | 295 | ## Similar Technologies 296 | 297 | The following technologies transform a JSON tree into another JSON tree. Each 298 | allows specification of a desired transform using declarative, custom 299 | expressions. 300 | 301 | - [jq](http://stedolan.github.io/jq/): (for command-line) 302 | - [jolt](https://github.com/bazaarvoice/jolt): (for Java) 303 | - [json2json](https://github.com/joelvh/json2json): (for JS) 304 | 305 | ## License 306 | 307 | Copyright © 2014 Shaun Williams 308 | 309 | Distributed under the Eclipse Public License either version 1.0 or any 310 | later version. 311 | 312 | [Om cursors]: https://github.com/swannodette/om/wiki/Cursors 313 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------