├── .gitignore ├── LICENSE ├── README.md ├── project.clj ├── quantum-entanglement1.png ├── src └── entanglement │ └── core.cljs └── test ├── runtests.cljs ├── test.html └── testcursor.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Entanglement 2 | ============== 3 | 4 | > "Spooky action at a distance" -Einstein 5 | 6 | What if an atom could take its values from another atom? 7 | 8 | Entanglement 10 | 11 | Same data, different representation. 12 | 13 | 14 | ```clj 15 | ;; Those two atoms **SHARE THE SAME DATA** 16 | atom-a 17 | # 18 | 19 | atom-b 20 | # 21 | 22 | ;; Being the same data, you can reset one... 23 | 24 | (reset! atom-b ["Bob" 35]) 25 | ["Bob" 35] 26 | 27 | ;; and the other will reflect this change! 28 | 29 | atom-a 30 | # 31 | ``` 32 | 33 | You can now have an abstaction layer between your state (atom) and 34 | your program. 35 | 36 | 37 | Usage 38 | ----- 39 | 40 | Add this to your project dependencies: 41 | 42 | [![Clojars Project](http://clojars.org/org.clojars.frozenlock/entanglement/latest-version.svg)](http://clojars.org/org.clojars.frozenlock/entanglement) 43 | 44 | 45 | In your namespace declaration: `(:require [entanglement.core :refer [entangle]])`. 46 | 47 | To re-create `atom-a` and `atom-b` from the example above: 48 | 49 | ```clj 50 | ;; Let's create the reference atom 51 | (def atom-a (atom {:first-name "Mike" :last-name "Moran" :age 22 :sex "M"})) 52 | 53 | ;; And now we make an atom taking values from the first atom: 54 | (def atom-b (entangle atom-a 55 | ;; getter function 56 | (fn [s-a] 57 | [(:first-name s-a) (:age s-a)]) 58 | ;; optional setter function 59 | (fn [s-a new-value] 60 | (assoc s-a 61 | :first-name (first new-value) 62 | :age (last new-value))) 63 | ;;optional validator 64 | :validator 65 | (fn [new-value] 66 | (assert (-> new-value first string?) "First value should be a string") 67 | (assert (-> new-value last number?) "Second value should be a number")) 68 | ;;optional identifier to avoid duplicate watches (see docstring) 69 | :identifier 70 | :my-entangled-atom)) 71 | ``` 72 | 73 | Rationale 74 | ------- 75 | 76 | An atom is a nice state abstraction. You have this 'thing' that holds 77 | data and with which you must thread carefully. It has a limited set of 78 | functions to play with it (`deref`,`reset!`, `swap!`, 79 | `add-watch`/`remove-watch`). Simple, yet functional. 80 | 81 | The problem is that the atom does not necessarily match the simplest 82 | code architecture. 83 | 84 | If you build your code to reflect how the data is stored in the atom, 85 | you are adding complexity. The more detached the atom structure is 86 | from the code logic, the more you have to juggle the data around. It 87 | also means that your code becomes intertwined with this particular 88 | atom. 89 | 90 | Before you know it, your entire code base is dependant on one or more 91 | atoms having a particular structure. So long for reusable functions. 92 | 93 | `Entanglement` proproses to create atoms from other atoms and linking 94 | the data together. It lets you build an 'interface' that presents 95 | the data like you want it. Build your code in the simplest way 96 | possible, assuming the atom will match what you want. 97 | 98 | ```clj 99 | ;; what was... 100 | (swap! my-atom update-in [:some :path :that :might :be :quite :deep] my-fn) 101 | 102 | ;; ...can now become 103 | 104 | (swap! my-atom my-fn) 105 | ``` 106 | 107 | "Wait, this looks a lot like cursors?" 108 | 109 | Right, because it is! In fact, cursors are a subset of entangled atoms. 110 | Here is an implement of cursors with `entanglement`: 111 | ```clj 112 | (defn cursor 113 | "Create a cursor. Behaves like a normal atom for the value at the 114 | specified path." 115 | [a path] 116 | (if-not (seq path) ;; if the path is emtpy, just return the atom... 117 | a 118 | (entangle a 119 | #(get-in % path) 120 | #(assoc-in %1 path %2) 121 | :identifier [::cursor a path]))) 122 | ``` 123 | 124 | (Cursors are such a common case that we already provide the `cursor` 125 | function in `entanglement.core`.) 126 | 127 | Cursors, lenses, wraps... they are all symptomatic of the need to 128 | detach atom data structure from the code. 129 | 130 | Every atom made with `entanglement` is 100% opaque. From the functions 131 | point of view, it's just like any other atom. 132 | 133 | 134 | Warning 135 | ------- 136 | 137 | Watches are not added to entangled atoms, but rather to the source 138 | atoms. This means that one should be careful when adding watches 139 | because *they won't be automatically GCed* even if the entangled atoms 140 | are. 141 | 142 | 143 | License 144 | ------- 145 | 146 | Copyright (c) 2015 Frozenlock 147 | 148 | Distributed under the Eclipse Public License, the same as Clojure. 149 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clojars.frozenlock/entanglement "0.0.4" 2 | :description "Spooky action at a distance (between atoms)" 3 | :url "https://github.com/Frozenlock/entanglement" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0-alpha4"] 7 | [org.clojure/clojurescript "0.0-2356"]] 8 | :plugins [[lein-cljsbuild "1.0.3"] 9 | [com.cemerick/clojurescript.test "0.3.3"]] 10 | :profiles {:test {:cljsbuild 11 | {:builds 12 | {:client {:source-paths ^:replace 13 | ["test" "src"]}}}}} 14 | :source-paths ["src"] 15 | :cljsbuild 16 | {:builds 17 | {:client {:source-paths ["src"] 18 | :compiler 19 | { 20 | :output-dir "target/client" 21 | :output-to "target/cljs-client.js" 22 | :pretty-print true}}}}) 23 | -------------------------------------------------------------------------------- /quantum-entanglement1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frozenlock/entanglement/ab9bcc1904a97743d58936d01129e6822ac1cda8/quantum-entanglement1.png -------------------------------------------------------------------------------- /src/entanglement/core.cljs: -------------------------------------------------------------------------------- 1 | (ns entanglement.core) 2 | 3 | (deftype Entangled [source-atom meta validator watches 4 | getter setter derefer identifier] 5 | Object 6 | (equiv [this other] 7 | (-equiv this other)) 8 | 9 | IAtom 10 | 11 | IEquiv 12 | (-equiv [this other] 13 | (and (instance? Entangled other) 14 | (or (= identifier (.-identifier other)) 15 | (= identical? this other)) 16 | (= source-atom (.-source-atom other)))) 17 | 18 | IDeref 19 | (-deref [this] 20 | (let [derefer (or derefer (fn [_ a g] 21 | (g (-deref a))))] 22 | (derefer this source-atom getter))) 23 | 24 | IMeta 25 | (-meta [_] meta) 26 | 27 | IWithMeta 28 | (-with-meta [o meta] 29 | (Entangled. source-atom meta validator watches 30 | getter setter derefer identifier)) 31 | 32 | ;; Every watch is added to the source atom. This way, every time a 33 | ;; source atom is modified, it will ripple thought all the entangled 34 | ;; children. 35 | 36 | ;; Might cause memory leaks? (If we add-watch an entangled atom, it is 37 | ;; now referenced in the source atom, meaning it shouldn't be 38 | ;; GCed...) 39 | IWatchable 40 | (-add-watch [this key f] 41 | (add-watch source-atom [(or identifier this) key] 42 | (fn [_ _ oldval newval] 43 | (when-not (= oldval newval) ;; the getter fn can be 44 | ;; expensive, better to 45 | ;; avoid it if we can. 46 | (let [old (getter oldval) 47 | new (getter newval)] 48 | (when-not (= old new) 49 | (f key this old new)))))) 50 | this) 51 | 52 | (-remove-watch [this key] 53 | (remove-watch source-atom [(or identifier this) key]) 54 | this) 55 | 56 | 57 | ;; possibility of creating a read-only atom by omitting a setter 58 | ;; function 59 | 60 | IReset 61 | (-reset! [a new-value] 62 | (if setter 63 | (let [validate (.-validator a)] 64 | (when-not (nil? validate) 65 | (assert (validate new-value) "Validator rejected reference state")) 66 | (do (swap! source-atom setter new-value) 67 | new-value)) 68 | (throw (js/Error. "Read-only: no setter provided for this atom.")))) 69 | 70 | ISwap 71 | (-swap! [a f] 72 | (reset! a (f (-deref a)))) 73 | (-swap! [a f x] 74 | (reset! a (f (-deref a) x))) 75 | (-swap! [a f x y] 76 | (reset! a (f (-deref a) x y))) 77 | (-swap! [a f x y more] 78 | (reset! a (apply f (-deref a) x y more))) 79 | 80 | IPrintWithWriter 81 | (-pr-writer [a writer opts] 82 | (-write writer "#")) 85 | 86 | IHash 87 | (-hash [this] (goog/getUid this))) 88 | 89 | 90 | 91 | ;;;;; Main API 92 | 93 | (defn entangle 94 | "Return an atom which applies custom getter and setter to the 95 | source-atom for every lookup/update/watches. 96 | 97 | getter: (fn [derefed-source-atom] ...) 98 | setter [optional]: (fn [derefed-source-atom new-value]...) 99 | 100 | derefer [optional]: (fn [this(new-atom) source-atom getter] ....) 101 | 102 | identifier [optional]: :some-id -- more info below 103 | 104 | When creating delicate entanglement (when the datastructure between 105 | the source atom and the new atom are quite different), it is 106 | suggested to provide a validator function. 107 | 108 | The validator and meta arguments act the same way as for normal 109 | atoms. 110 | 111 | A 'read-only' atom can be created simply by omitting or passing nil 112 | as the setter argument. Any attempt to modify directly the returned 113 | atom will result in an error. 114 | 115 | Because we can't test for equality between functions (getter and 116 | setter), basic entangled atoms can't test for equality. This is 117 | especially important when adding watches, as they need to test for 118 | equality for potential duplicates. To avoid this problem, it's 119 | possible to provide a 'identifier' field which contains the object 120 | on which the equality should be tested." 121 | ([source-atom getter] (entangle source-atom getter nil)) 122 | ([source-atom getter setter & {:keys [meta validator derefer identifier]}] 123 | (assert (satisfies? IAtom source-atom) "Only atoms can be entangled.") 124 | (Entangled. source-atom meta validator nil getter setter derefer identifier))) 125 | 126 | 127 | ;;; Simple cursor implementation using entanglement 128 | 129 | (defn cursor 130 | "Create a cursor. Behaves like a normal atom for the value at the 131 | specified path." 132 | [a path] 133 | (if-not (seq path) ;; if the path is emtpy, just return the atom... 134 | a 135 | (entangle a 136 | #(get-in % path) 137 | #(assoc-in %1 path %2) 138 | :identifier [::cursor a path]))) ;; <- identifier to test for equality 139 | 140 | -------------------------------------------------------------------------------- /test/runtests.cljs: -------------------------------------------------------------------------------- 1 | (ns runtests 2 | (:require-macros [cemerick.cljs.test 3 | :refer (is deftest with-test run-tests testing)]) 4 | (:require [testcursor] 5 | [cemerick.cljs.test :as t])) 6 | 7 | (enable-console-print!) 8 | 9 | (def test-results (atom nil)) 10 | 11 | (defn ^:export run-all-tests [] 12 | (println "-----------------------------------------") 13 | (try 14 | (reset! test-results (t/run-all-tests)) 15 | (catch js/Object e 16 | (do 17 | (println "Testrun failed\n" e "\n" (.-stack e)) 18 | (reset! test-results {:error e})))) 19 | (println "-----------------------------------------")) 20 | 21 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Entanglement 5 | 8 | 9 | 10 |

Check the console

11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/testcursor.cljs: -------------------------------------------------------------------------------- 1 | (ns testcursor 2 | (:require-macros [cemerick.cljs.test 3 | :refer (is deftest with-test run-tests testing)]) 4 | (:require [cemerick.cljs.test :as t] 5 | [entanglement.core :as ent])) 6 | 7 | ;; Here we test if the cursors act as a normal atom 8 | 9 | (deftest values 10 | (let [test-atom (atom {:a {:b {:c {:d 1}}}}) 11 | test-cursor (ent/cursor test-atom [:a :b :c :d]) 12 | test-cursor2 (ent/cursor test-atom [])] ;; nasty edge case 13 | 14 | (is (= cljs.core/Atom (type test-cursor2))) 15 | 16 | ;; get the initial values 17 | (is (= (get-in @test-atom [:a :b :c :d]) 18 | @test-cursor)) 19 | 20 | (is (= (get-in @test-atom []) 21 | @test-cursor2)) 22 | 23 | ;; now we update the cursor with a reset 24 | (reset! test-cursor 2) 25 | (is (= @test-cursor 2)) 26 | (is (= (get-in @test-atom [:a :b :c :d]) 2)) 27 | 28 | (reset! test-cursor2 3) 29 | (is (= @test-cursor2 3)) 30 | (is (= @test-atom 3)) 31 | (reset! test-atom {:a {:b {:c {:d 1}}}}) ;; restore test-atom 32 | 33 | ;; swap 34 | (reset! test-cursor {}) ;; empty map 35 | (swap! test-cursor assoc :z 3) 36 | (is (= @test-cursor {:z 3})) 37 | (is (= (get-in @test-atom [:a :b :c :d]) 38 | {:z 3})) 39 | 40 | (reset! test-cursor2 {}) ;; empty map 41 | (swap! test-cursor2 assoc :z 3) 42 | (is (= @test-cursor2 {:z 3})) 43 | (is (= (get-in @test-atom []) 44 | {:z 3})))) 45 | 46 | 47 | (deftest atom-behaviors 48 | (let [test-atom (atom {:a {:b {:c {:d 1}}}}) 49 | test-cursor (ent/cursor test-atom [:a :b :c :d]) 50 | witness (atom nil)] 51 | ;; per the description, reset! should return the new values 52 | (is (= {} 53 | (reset! test-cursor {}))) 54 | 55 | ;; per the description, swap! should return the new values 56 | (is (= {:z [1 2 3]} 57 | (swap! test-cursor assoc :z [1 2 3]))) 58 | 59 | ;; watches should behave like with a normal atom 60 | (reset! test-cursor "old") 61 | (add-watch test-cursor :w #(reset! witness {:key %1 :ref %2 :old %3 :new %4})) 62 | (reset! test-cursor "new") ;; this should trigger the watch function 63 | (is (= (:key @witness) :w)) 64 | (is (= (:ref @witness) test-cursor)) 65 | (is (= (:old @witness) "old")) 66 | (is (= (:new @witness) "new")) 67 | ;; can we remove the watch? 68 | (remove-watch test-cursor :w) 69 | (reset! test-cursor "removed") 70 | (is (= (:new @witness) "new")) ;; shouldn't have changed 71 | )) 72 | --------------------------------------------------------------------------------