├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── deps.edn ├── doc ├── Makefile ├── cljdoc.edn ├── content-docinfo.html └── content.adoc ├── mvn-upload.sh ├── pom.xml ├── scripts ├── build ├── build.clj ├── repl ├── repl.clj ├── watch └── watch.clj ├── src └── lentes │ └── core.cljc ├── test └── lentes │ └── tests.cljc └── tools.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml.asc 5 | *.jar 6 | *.class 7 | /.lein-* 8 | /.nrepl-port 9 | /*-init.clj 10 | /doc/dist 11 | /out 12 | /repl 13 | /node_modules 14 | /nashorn_code_cache 15 | /.cpcache 16 | /.rebel_readline_history -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: false 3 | lein: lein 4 | 5 | script: 6 | - ./scripts/build 7 | - nvm install v6.2.2 8 | - node --version 9 | - node out/tests.js 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | jdk: 16 | - oraclejdk8 17 | 18 | notifications: 19 | email: 20 | recipients: 21 | - niwi@niwi.nz 22 | on_success: change 23 | on_failure: change 24 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## Version 1.4.0-SNAPSHOT 4 | 5 | Date: 2024-09-03 6 | 7 | - Improved docs (thank you @blak3mill3r, @ComedyTomedy, @luposlip, and @rgkirch). 8 | 9 | Date: 2020-01-07 10 | 11 | - Fix internal cache race conditions. 12 | - Add Atom2 impl (thanks to @jellelicht). 13 | 14 | 15 | ## Version 1.3.3 ## 16 | 17 | Date: 2019-10-22 18 | 19 | - Fix jvm impl (bug introduced in previous commits). 20 | 21 | 22 | ## Version 1.3.2 ## 23 | 24 | Date: 2019-10-22 25 | 26 | Same as 1.3.1 (just docs updates). 27 | 28 | 29 | ## Version 1.3.1 ## 30 | 31 | Date: 2019-10-22 32 | 33 | Same as 1.3.0 (just docs updates). 34 | 35 | 36 | ## Version 1.3.0 ## 37 | 38 | Date: 2019-10-22 39 | 40 | - Improve equality checking on derived atoms. 41 | - Minor code/performance improvements. 42 | 43 | 44 | ## Version 1.2.0 ## 45 | 46 | Date: 2016-09-30 47 | 48 | - Change license to BSD 2-Clause 49 | - Remove the deprecated `getter` function. 50 | - Fix `select-keys` lense in cljs impl. 51 | - Improve derived atom perfromance. 52 | Adding cache and proxying the watchers in order to reduce 53 | N watchers to derived atom to 1 watcher to the source atom. 54 | 55 | 56 | ## Version 1.1.0 ## 57 | 58 | Date: 2016-06-22 59 | 60 | - Improve documentation. 61 | - Add missing IAtom marker for derived atom (cljs only) 62 | - Add the ability to create read only derived refs. 63 | - Add `derive` function as substitute to `focus-atom` (backward compatible). 64 | - Add arity 1 for `lens` function that allows create read only lenses. 65 | - Add the ability to disable equality check on derived atoms. 66 | - Deprecate `getter` in favor of `lens`. 67 | 68 | 69 | ## Version 1.0.1 ## 70 | 71 | Date: 2016-03-20 72 | 73 | - Minor code style fixes. 74 | - Add getter lense shortcut. 75 | 76 | 77 | ## Version 1.0.0 ## 78 | 79 | Date: 2016-02-26 80 | 81 | Initial version (split from cats 1.2.1). 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributed Code # 2 | 3 | In order to keep *lentes* completely free and unencumbered by copyright, all new 4 | contributors to the *lentes* code base are asked to dedicate their contributions to 5 | the public domain. If you want to send a patch or enhancement for possible inclusion 6 | in the *lentes* source tree, please accompany the patch with the following 7 | statement: 8 | 9 | The author or authors of this code dedicate any and all copyright interest 10 | in this code to the public domain. We make this dedication for the benefit of 11 | the public at large and to the detriment of our heirs and successors. We 12 | intend this dedication to be an overt act of relinquishment in perpetuity of 13 | all present and future rights to this code under copyright law. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019 Andrey Antukh 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lentes - functional references for clojure(script) # 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/funcool/lentes.svg)](https://clojars.org/funcool/lentes) 4 | 5 | [![Clojars Project](https://img.shields.io/clojars/v/funcool/lentes.svg?include_prereleases)](https://clojars.org/funcool/lentes) 6 | 7 | **Documentation**: https://cljdoc.org/d/funcool/lentes/1.4.0-SNAPSHOT 8 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {} 2 | :paths ["src"] 3 | :aliases 4 | {:dev 5 | {:extra-paths ["test" "target"] 6 | :extra-deps 7 | {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} 8 | com.bhauman/rebel-readline {:mvn/version "0.1.4"} 9 | org.clojure/clojurescript {:mvn/version "1.10.520"} 10 | org.clojure/clojure {:mvn/version "1.10.1"} 11 | org.clojure/test.check {:mvn/version "0.9.0"}}} 12 | :repl 13 | {:main-opts ["-m" "rebel-readline.main"]} 14 | :ancient 15 | {:main-opts ["-m" "deps-ancient.deps-ancient"] 16 | :extra-deps {deps-ancient {:mvn/version "RELEASE"}}} 17 | :jar 18 | {:extra-deps {seancorfield/depstar {:mvn/version "RELEASE"}} 19 | :main-opts ["-m" "hf.depstar.jar" "target/lentes.jar"]}}} 20 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | all: doc 2 | 3 | api: 4 | cd .. 5 | lein doc 6 | 7 | doc: 8 | mkdir -p dist/latest/ 9 | asciidoctor -a docinfo -a stylesheet! -o dist/latest/index.html content.adoc 10 | 11 | github: api doc 12 | ghp-import -m "Generate documentation" -b gh-pages dist/ 13 | git push origin gh-pages 14 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [["User Guide" {:file "doc/content.adoc"}] 2 | ["Changelog" {:file "CHANGES.md"}]]} 3 | -------------------------------------------------------------------------------- /doc/content-docinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /doc/content.adoc: -------------------------------------------------------------------------------- 1 | = lentes - lenses for clojure(script) 2 | Andrey Antukh, 3 | :toc: left 4 | :!numbered: 5 | :idseparator: - 6 | :idprefix: 7 | :source-highlighter: pygments 8 | :pygments-style: friendly 9 | :sectlinks: 10 | 11 | 12 | == Introduction 13 | 14 | Lentes is an implementation of functional references modeled as 15 | functions (in the same way as transducers). Lenses are a 16 | generalization of get and put mapping to a particular part of a data 17 | structure. 18 | 19 | Please see the <> for an overview of what 20 | lentes has to offer. 21 | 22 | 23 | === Project Maturity 24 | 25 | Since _lentes_ is a young project there can be some API breakage. 26 | 27 | 28 | === Install 29 | 30 | The simplest way to use _lentes_ in a clojure project, is by including it in the 31 | dependency vector on your *_project.clj_* file: 32 | 33 | [source, clojure] 34 | ---- 35 | [funcool/lentes "1.3.3"] 36 | ---- 37 | 38 | 39 | [[quick-start]] 40 | == Quick Start 41 | 42 | Do not be scared with the package name and its description. It is pretty 43 | straightforward to use and understand. A lens consists out of couple of 44 | functions responsible for reading and altering nested data structures. 45 | 46 | 47 | === Introduction to lenses 48 | 49 | Let's create a getter and setter function: 50 | 51 | [source, clojure] 52 | ---- 53 | (defn my-getter 54 | [state] 55 | (:foo state)) 56 | 57 | (defn my-setter 58 | [state f] 59 | (update state :foo f)) 60 | ---- 61 | 62 | These can be used to access or modify data: 63 | 64 | [source, clojure] 65 | ---- 66 | (def data {:foo 1 :bar 2}) 67 | 68 | (my-getter data) 69 | ;; => 1 70 | 71 | (my-setter data inc) 72 | ;; => {:foo 2 :bar 2} 73 | ---- 74 | 75 | We can generalize the getting and setting using *lenses abstraction*: 76 | 77 | [source, clojure] 78 | ---- 79 | (require '[lentes.core :as l]) 80 | 81 | (def mylens (l/lens my-getter my-setter)) 82 | 83 | (l/focus mylens data) 84 | ;; => 1 85 | 86 | (l/over mylens inc data) 87 | ;; => {:foo 2 :bar 2} 88 | ---- 89 | 90 | Lentes comes with some helpers for creating commonly used lenses. As a 91 | result we don't have to write our own `my-getter` and `my-setter` 92 | functions for common use-cases. Keep reading to find out what helpers 93 | are provided with lentes. 94 | 95 | [source, clojure] 96 | ---- 97 | (def mylens (l/key :foo)) 98 | 99 | (l/focus mylens data) 100 | ;; => 1 101 | 102 | (l/over mylens inc data) 103 | ;; => {:foo 2 :bar 2} 104 | ---- 105 | 106 | 107 | === Working with atoms 108 | 109 | Lentes succinctly interfaces with atoms. The `l/derive` function 110 | allows you to create derived atoms that act like limited versions of 111 | the original atom. This technique is also named `materialized views`. 112 | 113 | [source, clojure] 114 | ---- 115 | (def state1 (atom {:foo 1 :bar 1})) 116 | (def state2 (l/derive (l/key :foo) state1)) 117 | 118 | (satisfies? IAtom state2) 119 | ;; => true 120 | 121 | @state2 122 | ;; => 1 123 | 124 | (swap! state2 inc) 125 | @state2 126 | ;; => 2 127 | 128 | @state1 129 | ;; => {:foo 2 :bar 2} 130 | ---- 131 | 132 | The derived atoms has very useful properties such as they are *lazy* 133 | (no code executed until is really needed) and *smart* (does not 134 | trigger watches if the focused data is not affected). 135 | 136 | This is especially useful, when you want to create materialized views 137 | of the global state, and use them together with 138 | link:https://github.com/tonsky/rum[rum] facilities to react on atom 139 | changes, allowing components to re-render only when the affected state 140 | is changed. 141 | 142 | 143 | == Reference 144 | 145 | Lentes offers functions to easily define composable lenses. 146 | 147 | Just as important are a handful of functions that take a lens and 148 | data. When called they return either the value or modified data. 149 | 150 | The examples below cover basic usage of lentes' lenses. It also 151 | demonstrates what the `focus`, `put` and `over` functions do. 152 | 153 | 154 | === Builtin lenses 155 | 156 | ==== Identity 157 | 158 | The most basic lens is the `identity` lens. It allows you to get and 159 | set the data as is. 160 | 161 | The `focus` function allows you to get the value the lens is "focused" 162 | on. 163 | 164 | In this example we create an identity lens. We then call the focus 165 | function with the lens and a vector as arguments. It doesn't get any 166 | simpler then this. 167 | 168 | [source, clojure] 169 | ---- 170 | (require '[lentes.core :as l]) 171 | 172 | (l/focus l/id [0 1 2 3]) 173 | ;; => [0 1 2 3] 174 | ---- 175 | 176 | As you can see `focus` just returned the data as is. 177 | 178 | We have two other core functions: 179 | 180 | - `put` allows us to set a value a lens is focusing on. 181 | 182 | - `over` lets us apply a function over the focused value of a lens. 183 | 184 | [source, clojure] 185 | ---- 186 | (l/put l/id 42 [0 1 2 3]) 187 | ;; => 42 188 | 189 | (l/over l/id count [0 1 2 3]) 190 | ;; => 4 191 | ---- 192 | 193 | We have only mentioned the `id` lens. Lentes provides more lens helpers. It's 194 | also possible to create your own lenses for your specific needs. 195 | 196 | ==== Sequences 197 | 198 | There are some builtin lenses that work on sequences. These are the `fst`, 199 | `snd` and `nth` lens: 200 | 201 | .Example using `fst` lens 202 | [source, clojure] 203 | ---- 204 | ;; Focus over the first element of a vector 205 | (l/focus l/fst [1 2 3]) 206 | ;; => 1 207 | 208 | ;; Apply a function over first element of a vector 209 | (l/over l/fst inc [1 2 3]) 210 | ;; => [2 2 3] 211 | 212 | ;; Replace the first value of an element of a vector 213 | (l/put l/fst 42 [1 2 3]) 214 | ;; => [42 2 3] 215 | ---- 216 | 217 | .Example using the `nth` lens 218 | [source, clojure] 219 | ---- 220 | (l/focus (l/nth 2) [1 2 3]) 221 | ;; => 3 222 | 223 | (l/over (l/nth 2) inc [1 2 3]) 224 | ;; => [1 2 4] 225 | 226 | (l/put (l/nth 2) 42 [1 2 3]) 227 | ;; => [1 2 42] 228 | ---- 229 | 230 | 231 | ==== Associative data structures 232 | 233 | There's `key` and `select-keys` for focusing on one or multiple keys respectively: 234 | 235 | .Example focusing in a specific key/keys of associative data structure 236 | [source, clojure] 237 | ---- 238 | (l/focus (l/key :a) {:a 1 :b 2}) 239 | ;; => 1 240 | 241 | (l/over (l/key :a) str {:a 1 :b 2}) 242 | ;; => {:a "1", :b 2} 243 | 244 | (l/put (l/key :a) 42 {:a 1 :b 2}) 245 | ;; => {:a 42, :b 2} 246 | 247 | (l/focus (l/select-keys [:a]) {:a 1 :b 2}) 248 | ;; => {:a 1} 249 | 250 | (l/over (l/select-keys [:a :c]) 251 | (fn [m] 252 | (zipmap (keys m) (repeat 42))) 253 | {:a 1 :b 2}) 254 | ;; => {:b 2, :a 42} 255 | 256 | (l/put (l/select-keys [:a :c]) 257 | {:a 0} 258 | {:a 1 :b 2 :c 42}) 259 | ;; => {:b 2, :a 0} 260 | ---- 261 | 262 | `in` for focusing on a path: 263 | 264 | .Example focusing in nested data structures 265 | [source, clojure] 266 | ---- 267 | (l/focus (l/in [:a :b]) 268 | {:a {:b {:c 42}}}) 269 | ;; => {:c 42} 270 | 271 | (l/over (l/in [:a :b]) #(zipmap (vals %) (keys %)) 272 | {:a {:b {:c 42}}}) 273 | ;; => {:a {:b {42 :c}}} 274 | 275 | (l/put (l/in [:a :b]) 276 | 42 277 | {:a {:b {:c 42}}}) 278 | ;; => {:a {:b 42}} 279 | ---- 280 | 281 | Let's take a look at a combinator that will let us build a unit-conversion lens 282 | called `units`. We have to supply a function to convert from unit `a` to unit `b` 283 | and viceversa: 284 | 285 | .Example defining a "unit conversion" lens 286 | [source, clojure] 287 | ---- 288 | (defn sec->min [sec] (/ sec 60)) 289 | (defn min->sec [min] (* min 60)) 290 | 291 | (def mins (l/units sec->min 292 | min->sec)) 293 | 294 | (l/focus mins 120) 295 | ;; => 2 296 | 297 | (l/put mins 3 120) 298 | ;; => 180 299 | 300 | (l/over mins inc 60) 301 | ;; => 120 302 | ---- 303 | 304 | 305 | ==== Conditionals 306 | 307 | Conditional lenses are defined using a predicate function as argument. 308 | It only focuses on the value when the called predicate returns true. 309 | The predicate is called with the value as argument. 310 | 311 | .Example focusing using conditional lenses 312 | [source, clojure] 313 | ---- 314 | (l/focus (l/passes even?) 2) 315 | ;; => 2 316 | 317 | (l/over (l/passes even?) inc 2) 318 | ;; => 3 319 | 320 | (l/put (l/passes even?) 42 2) 321 | ;; => 42 322 | 323 | (l/focus (l/passes even?) 1) 324 | ;; => nil 325 | 326 | (l/over (l/passes even?) inc 1) 327 | ;; => 1 328 | 329 | (l/put (l/passes even?) 42 1) 330 | ;; => 1 331 | ---- 332 | 333 | 334 | === Composition 335 | 336 | One of the big advantages of this lenses implementation is because it is 337 | implemented in terms of function composition, much in the same line as 338 | transducers. Let see a example: 339 | 340 | [source, clojure] 341 | ---- 342 | (def my-lens (comp l/fst (l/nth 2))) 343 | 344 | (def data 345 | [[0 1 2] 346 | [3 4 5]]) 347 | 348 | (l/focus my-lens data) 349 | ;; => 2 350 | 351 | (l/put my-lens 42 data) 352 | ;; => [[0 1 42] [3 4 5]] 353 | ---- 354 | 355 | Lenses compose with regular function composition and, like transducers, the 356 | combined lens runs from left to right. 357 | 358 | 359 | == Developers Guide 360 | 361 | === Philosophy 362 | 363 | Five most important rules: 364 | 365 | - Beautiful is better than ugly. 366 | - Explicit is better than implicit. 367 | - Simple is better than complex. 368 | - Complex is better than complicated. 369 | - Readability counts. 370 | 371 | All contributions to _lentes_ should keep these important rules in mind. 372 | 373 | 374 | === Contributing 375 | 376 | Please read `CONTRIBUTING.md` file on the root of repository. 377 | 378 | 379 | === Source Code 380 | 381 | _lentes_ is open source and can be found on 382 | link:https://github.com/funcool/lentes[github]. 383 | 384 | You can clone the public repository with this command: 385 | 386 | [source,text] 387 | ---- 388 | git clone https://github.com/funcool/lentes 389 | ---- 390 | 391 | 392 | === Run tests 393 | 394 | For running tests just execute this: 395 | 396 | .Run tests on node platform 397 | [source, text] 398 | ---- 399 | clojure -Adev tools build:tests 400 | node ./target/tests.js 401 | ---- 402 | 403 | .Run tests on JVM platform 404 | ---- 405 | clojure -Adev -m lentes.tests 406 | ---- 407 | 408 | 409 | === License 410 | 411 | _lentes_ is licensed under BSD (2-Clause) license: 412 | 413 | ---- 414 | Copyright (c) 2015-2019 Andrey Antukh 415 | 416 | All rights reserved. 417 | 418 | Redistribution and use in source and binary forms, with or without 419 | modification, are permitted provided that the following conditions are met: 420 | 421 | * Redistributions of source code must retain the above copyright notice, this 422 | list of conditions and the following disclaimer. 423 | 424 | * Redistributions in binary form must reproduce the above copyright notice, 425 | this list of conditions and the following disclaimer in the documentation 426 | and/or other materials provided with the distribution. 427 | 428 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 429 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 430 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 431 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 432 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 433 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 434 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 435 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 436 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 437 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 438 | ---- 439 | -------------------------------------------------------------------------------- /mvn-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mvn deploy:deploy-file -Dfile=target/lentes.jar -DpomFile=pom.xml -DrepositoryId=clojars -Durl=https://clojars.org/repo/ 3 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | funcool 5 | lentes 6 | jar 7 | 1.4.0-SNAPSHOT 8 | lentes 9 | Functional references for Clojure and ClojureScript 10 | https://github.com/funcool/lentes 11 | 12 | 13 | BSD (2-Clause) 14 | http://opensource.org/licenses/BSD-2-Clause 15 | 16 | 17 | 18 | scm:git:git://github.com/funcool/lentes.git 19 | scm:git:ssh://git@github.com/funcool/lentes.git 20 | master 21 | https://github.com/funcool/lentes 22 | 23 | 24 | src 25 | 26 | 27 | 28 | clojars 29 | https://repo.clojars.org/ 30 | 31 | 32 | 33 | 34 | org.clojure 35 | clojure 36 | 1.10.1 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lein trampoline run -m clojure.main scripts/build.clj 3 | -------------------------------------------------------------------------------- /scripts/build.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (println "Building ...") 4 | 5 | (let [start (System/nanoTime)] 6 | (b/build 7 | (b/inputs "test" "src") 8 | {:main 'lentes.tests 9 | :parallel-build false 10 | :output-to "out/tests.js" 11 | :output-dir "out/tests" 12 | :target :nodejs 13 | :optimizations :none 14 | :pretty-print true 15 | :language-in :ecmascript6 16 | :language-out :ecmascript5 17 | :verbose true}) 18 | (println "... done. Elapsed" (/ (- (System/nanoTime) start) 1e9) "seconds")) 19 | -------------------------------------------------------------------------------- /scripts/repl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rlwrap lein trampoline run -m clojure.main scripts/repl.clj 3 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.nashorn :as nashorn] 4 | '[cljs.repl.node :as node]) 5 | 6 | (cljs.repl/repl 7 | (cljs.repl.node/repl-env) 8 | :output-dir "out" 9 | :cache-analysis true) 10 | -------------------------------------------------------------------------------- /scripts/watch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lein trampoline run -m clojure.main scripts/watch.clj 3 | -------------------------------------------------------------------------------- /scripts/watch.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (b/watch 4 | (b/inputs "test" "src") 5 | {:main 'lentes.tests 6 | :parallel-build false 7 | :output-to "out/tests.js" 8 | :output-dir "out/tests" 9 | :target :nodejs 10 | :optimizations :none 11 | :pretty-print true 12 | :language-in :ecmascript6 13 | :language-out :ecmascript5 14 | :verbose true}) 15 | -------------------------------------------------------------------------------- /src/lentes/core.cljc: -------------------------------------------------------------------------------- 1 | (ns lentes.core 2 | (:refer-clojure :exclude [nth key keys vals filter select-keys cat derive]) 3 | (:require [clojure.core :as c])) 4 | 5 | ;; constructors 6 | 7 | (defn lens 8 | "Given a function for getting the focused value from a state 9 | (getter) and a function that takes the state and update 10 | function (setter), constructs a lens." 11 | ([getter] 12 | (fn [next] 13 | (fn 14 | ([s] 15 | (next (getter s))) 16 | ([s f] 17 | (throw (ex-info "Read only lens!" {})))))) 18 | ([getter setter] 19 | (fn [next] 20 | (fn 21 | ([s] 22 | (next (getter s))) 23 | ([s f] 24 | (setter s #(next % f))))))) 25 | 26 | ;; base lenses 27 | 28 | (defn id-setter 29 | "The identity setter, applies the function to the state." 30 | [s f] 31 | (f s)) 32 | 33 | (defn const-setter 34 | "The constant setter, returns the state unaltered." 35 | [s _] 36 | s) 37 | 38 | (def id 39 | "Identity lens." 40 | (lens identity id-setter)) 41 | 42 | ;; API 43 | 44 | (defn focus 45 | "Given a lens and a state, return the value focused by the lens." 46 | [lens s] 47 | (let [getter (lens identity)] 48 | (getter s))) 49 | 50 | (defn over 51 | "Given a setter, a function and a state, apply the function over 52 | the value focused by the setter." 53 | [st f s] 54 | (let [setter (st id-setter)] 55 | (setter s f))) 56 | 57 | (defn put 58 | "Given a setter, a new value and a state, replace the value focused by 59 | the lens with the new one." 60 | [st v s] 61 | (over st (constantly v) s)) 62 | 63 | ;; combinators 64 | 65 | (defn units 66 | "Given a function from unit A to unit B and another in the 67 | opposite direction, construct a lens that focuses and updates 68 | a converted value." 69 | [one->other other->one] 70 | (lens one->other 71 | (fn [s f] 72 | (other->one (f (one->other s)))))) 73 | 74 | ;; lenses 75 | 76 | (defn passes 77 | "Given a predicate, return a lens that focuses on an element only 78 | if it passes the predicate. 79 | 80 | The lens is not well-behaved, it depends on the outcome of the predicate." 81 | [applies?] 82 | (lens (fn [s] 83 | (when (applies? s) 84 | s)) 85 | (fn [s f] 86 | (if (applies? s) 87 | (f s) 88 | s)))) 89 | 90 | (defn nth 91 | "Given a number, returns a lens that focuses on the given index of 92 | a collection." 93 | [n] 94 | (lens (fn [s] (c/nth s n)) 95 | (fn [s f] (update s n f)))) 96 | 97 | (def fst (nth 0)) 98 | (def snd (nth 1)) 99 | 100 | (defn- sequential-empty 101 | [coll] 102 | (cond 103 | (map? coll) {} 104 | (set? coll) #{} 105 | :else [])) 106 | 107 | (def tail 108 | "A lens into the tail of a collection." 109 | (lens rest 110 | (fn [s f] 111 | (into (sequential-empty s) 112 | (cons (first s) 113 | (f (rest s))))))) 114 | 115 | (defn key 116 | "Given a key, returns a lens that focuses on the given key of 117 | an associative data structure." 118 | [k] 119 | (lens (fn [s] (get s k)) 120 | (fn [s f] (update s k f)))) 121 | 122 | (defn select-keys 123 | "Return a lens focused on the given keys in an associative data 124 | structure." 125 | [ks] 126 | (lens (fn [s] (c/select-keys s ks)) 127 | (fn [s f] 128 | (merge (apply dissoc s ks) 129 | (-> (c/select-keys s ks) 130 | f 131 | (c/select-keys ks)))))) 132 | 133 | (defn in 134 | "Given a path and optionally a default value, return a lens that 135 | focuses the given path in an associative data structure." 136 | ([path] (in path nil)) 137 | ([path default] 138 | (lens (fn [s] (get-in s path default)) 139 | (fn [s f] (update-in s path f))))) 140 | 141 | ;; interop 142 | 143 | (defn- prefix-key 144 | [key id] 145 | (keyword (str id "-" (name key)))) 146 | 147 | (def ^:private +empty+ #?(:clj (Object.) :cljs (js/Object.))) 148 | 149 | #?(:clj 150 | (deftype RWFocus [id lens src equals? 151 | ^:unsynchronized-mutable watchers-added 152 | ^:unsynchronized-mutable watchers 153 | ^:unsynchronized-mutable srccache 154 | ^:unsynchronized-mutable cache] 155 | 156 | clojure.lang.IDeref 157 | (deref [self] 158 | (locking self 159 | (let [source (deref src)] 160 | (if (identical? (.-srccache self) source) 161 | (.-cache self) 162 | (let [result (focus lens source)] 163 | (set! (.-srccache self) source) 164 | (set! (.-cache self) result) 165 | result))))) 166 | 167 | clojure.lang.IAtom 168 | (reset [self newval] 169 | (focus lens (swap! src #(put lens newval %)))) 170 | (swap [self f] 171 | (focus lens (swap! src (fn [s] (over lens f s))))) 172 | (swap [self f x] 173 | (focus lens (swap! src (fn [s] (over lens #(f % x) s))))) 174 | (swap [self f x y] 175 | (focus lens (swap! src (fn [s] (over lens #(f % x y) s))))) 176 | (swap [self f x y more] 177 | (focus lens (swap! src (fn [s] (over lens #(apply f % x y more) s))))) 178 | 179 | clojure.lang.IAtom2 180 | (resetVals [self newval] 181 | (mapv (partial focus lens) (swap-vals! src #(put lens newval %)))) 182 | (swapVals [self f] 183 | (mapv (partial focus lens) (swap-vals! src (fn [s] (over lens f s))))) 184 | (swapVals [self f x] 185 | (mapv (partial focus lens) (swap-vals! src (fn [s] (over lens #(f % x) s))))) 186 | (swapVals [self f x y] 187 | (mapv (partial focus lens) (swap-vals! src (fn [s] (over lens #(f % x y) s))))) 188 | (swapVals [self f x y more] 189 | (mapv (partial focus lens) (swap-vals! src (fn [s] (over lens #(apply f % x y more) s))))) 190 | 191 | clojure.lang.IRef 192 | (addWatch [self key cb] 193 | (locking self 194 | (set! (.-watchers self) (assoc watchers key cb)) 195 | (when-not ^boolean (.-watchers-added self) 196 | (set! (.-watchers-added self) true) 197 | (add-watch src id 198 | (fn [_ _ oldv newv] 199 | (when-not (identical? oldv newv) 200 | (let [old' (focus lens oldv) 201 | new' (focus lens newv)] 202 | (set! (.-srccache self) newv) 203 | (set! (.-cache self) new') 204 | (when-not (equals? old' new') 205 | (run! (fn [[key wf]] (wf key self old' new')) 206 | (.-watchers self)))))))) 207 | self)) 208 | 209 | (removeWatch [self key] 210 | (locking self 211 | (set! (.-watchers self) (dissoc watchers key)) 212 | (when (empty? watchers) 213 | (set! (.-watchers-added self) false) 214 | (remove-watch src id))))) 215 | 216 | :cljs 217 | (deftype RWFocus [id lens src equals? 218 | ^:mutable watchers-added 219 | ^:mutable watchers 220 | ^:mutable srccache 221 | ^:mutable cache] 222 | IAtom 223 | IDeref 224 | (-deref [self] 225 | (let [source (deref src)] 226 | (if (identical? (.-srccache self) source) 227 | (.-cache self) 228 | (let [result (focus lens source)] 229 | (set! (.-srccache self) source) 230 | (set! (.-cache self) result) 231 | result)))) 232 | 233 | IReset 234 | (-reset! [self newval] 235 | (swap! src #(put lens newval %)) 236 | (deref self)) 237 | 238 | ISwap 239 | (-swap! [self f] 240 | (swap! src (fn [s] (over lens f s))) 241 | (deref self)) 242 | (-swap! [self f x] 243 | (swap! src (fn [s] (over lens #(f % x) s))) 244 | (deref self)) 245 | (-swap! [self f x y] 246 | (swap! src (fn [s] (over lens #(f % x y) s))) 247 | (deref self)) 248 | (-swap! [self f x y more] 249 | (swap! src (fn [s] (over lens #(apply f % x y more) s))) 250 | (deref self)) 251 | 252 | IWatchable 253 | (-add-watch [self key cb] 254 | (set! (.-watchers self) (assoc watchers key cb)) 255 | (when-not ^boolean (.-watchers-added self) 256 | (set! (.-watchers-added self) true) 257 | (add-watch src id 258 | (fn [_ _ oldv newv] 259 | (when-not (identical? oldv newv) 260 | (let [old' (focus lens oldv) 261 | new' (focus lens newv)] 262 | (set! (.-srccache self) newv) 263 | (set! (.-cache self) new') 264 | (when-not (equals? old' new') 265 | (run! (fn [[key wf]] (wf key self old' new')) 266 | (.-watchers self)))))))) 267 | self) 268 | 269 | (-remove-watch [self key] 270 | (set! (.-watchers self) (dissoc watchers key)) 271 | (when (empty? watchers) 272 | (set! (.-watchers-added self) false) 273 | (remove-watch src id))))) 274 | 275 | #?(:clj 276 | (deftype ROFocus [id lens src equals? 277 | ^:unsynchronized-mutable watchers-added 278 | ^:unsynchronized-mutable watchers 279 | ^:unsynchronized-mutable srccache 280 | ^:unsynchronized-mutable cache] 281 | clojure.lang.IDeref 282 | (deref [self] 283 | (locking self 284 | (let [source (deref src)] 285 | (if (identical? (.-srccache self) source) 286 | (.-cache self) 287 | (let [result (focus lens source)] 288 | (set! (.-srccache self) source) 289 | (set! (.-cache self) result) 290 | result))))) 291 | 292 | clojure.lang.IRef 293 | (addWatch [self key cb] 294 | (locking self 295 | (set! (.-watchers self) (assoc watchers key cb)) 296 | (when-not ^boolean (.-watchers-added self) 297 | (set! (.-watchers-added self) true) 298 | (add-watch src id 299 | (fn [_ _ oldv newv] 300 | (when-not (identical? oldv newv) 301 | (let [old' (focus lens oldv) 302 | new' (focus lens newv)] 303 | (set! (.-srccache self) newv) 304 | (set! (.-cache self) new') 305 | (when-not (equals? old' new') 306 | (run! (fn [[key wf]] (wf key self old' new')) 307 | (.-watchers self)))))))) 308 | self)) 309 | 310 | (removeWatch [self key] 311 | (locking self 312 | (set! (.-watchers self) (dissoc watchers key)) 313 | (when (empty? watchers) 314 | (set! (.-watchers-added self) false) 315 | (remove-watch src id))))) 316 | 317 | :cljs 318 | (deftype ROFocus [id lens src equals? 319 | ^:mutable watchers-added 320 | ^:mutable watchers 321 | ^:mutable srccache 322 | ^:mutable cache] 323 | IAtom 324 | IDeref 325 | (-deref [self] 326 | (let [source (deref src)] 327 | (if (identical? (.-srccache self) source) 328 | (.-cache self) 329 | (let [result (focus lens source)] 330 | (set! (.-srccache self) source) 331 | (set! (.-cache self) result) 332 | result)))) 333 | IWatchable 334 | (-add-watch [self key cb] 335 | (set! (.-watchers self) (assoc watchers key cb)) 336 | (when-not ^boolean (.-watchers-added self) 337 | (set! (.-watchers-added self) true) 338 | (add-watch src id 339 | (fn [_ _ oldv newv] 340 | (when-not (identical? oldv newv) 341 | (let [old' (focus lens oldv) 342 | new' (focus lens newv)] 343 | (set! (.-srccache self) newv) 344 | (set! (.-cache self) new') 345 | (when-not (equals? old' new') 346 | (run! (fn [[key wf]] (wf key self old' new')) 347 | (.-watchers self)))))))) 348 | self) 349 | 350 | (-remove-watch [self key] 351 | (set! (.-watchers self) (dissoc watchers key)) 352 | (when (empty? watchers) 353 | (set! (.-watchers-added self) false) 354 | (remove-watch src id))))) 355 | 356 | (defn derive 357 | "Create a derived atom from another atom with the provided lens. 358 | 359 | The returned atom is lazy, so no code is executed until the user 360 | requires it. 361 | 362 | By default the derived atom does not trigger updates if the data 363 | does not affect it (determined by lens), but this behavior can 364 | be deactivated by passing `:equals?` to `false` as the third options 365 | parameter. You also may pass `=` as `equals?` parameter if you want 366 | value comparison instead of reference comparison with `identical?`. 367 | 368 | You can explicitly create read only refs (not atoms, because the 369 | returned object satisifies watchable and ref but not atom interface) 370 | by passing `:read-only?` as `true` as option on the optional third 371 | parameter." 372 | ([lens src] 373 | (derive lens src nil)) 374 | ([lens src {:keys [read-only? equals?] 375 | :or {read-only? false 376 | equals? identical?}}] 377 | (let [id (gensym "lentes-ref")] 378 | (if read-only? 379 | (ROFocus. id lens src equals? false {} +empty+ +empty+) 380 | (RWFocus. id lens src equals? false {} +empty+ +empty+))))) 381 | -------------------------------------------------------------------------------- /test/lentes/tests.cljc: -------------------------------------------------------------------------------- 1 | (ns lentes.tests 2 | (:refer-clojure :exclude [derive]) 3 | #?(:cljs 4 | (:require [cljs.test :as t] 5 | [clojure.test.check] 6 | [clojure.test.check.generators :as gen :include-macros true] 7 | [clojure.test.check.properties :as prop :include-macros true] 8 | [clojure.test.check.clojure-test :refer-macros (defspec)] 9 | [lentes.core :as l]) 10 | :clj 11 | (:require [clojure.test :as t] 12 | [clojure.test.check] 13 | [clojure.test.check.clojure-test :refer (defspec)] 14 | [clojure.test.check.generators :as gen] 15 | [clojure.test.check.properties :as prop] 16 | [lentes.core :as l]))) 17 | 18 | ;; laws 19 | 20 | (defn first-lens-law 21 | [{:keys [gen lens xgen] :or {xgen gen/any}}] 22 | (prop/for-all 23 | [s gen x xgen] 24 | (t/is (= x (l/focus lens (l/put lens x s)))))) 25 | 26 | (defn second-lens-law 27 | [{:keys [gen lens]}] 28 | (prop/for-all 29 | [s gen] 30 | (t/is (= s (l/put lens (l/focus lens s) s))))) 31 | 32 | (defn third-lens-law 33 | [{:keys [gen lens xgen] :or {xgen gen/any}}] 34 | (prop/for-all 35 | [s gen 36 | a xgen 37 | b xgen] 38 | (t/is (= (l/put lens a s) 39 | (l/put lens a (l/put lens b s)))))) 40 | 41 | ;; generators 42 | 43 | (def vector-gen 44 | (gen/vector gen/any 10)) 45 | 46 | (def nested-vector-gen 47 | (gen/vector vector-gen 10)) 48 | 49 | ;; id 50 | 51 | (def id-lens 52 | {:gen gen/any 53 | :lens l/id}) 54 | 55 | (defspec id-first-lens-law 10 56 | (first-lens-law id-lens)) 57 | 58 | (defspec id-second-lens-law 10 59 | (second-lens-law id-lens)) 60 | 61 | (defspec id-third-lens-law 10 62 | (third-lens-law id-lens)) 63 | 64 | ;; passes 65 | 66 | (t/deftest passes 67 | (let [odd (l/passes odd?)] 68 | (t/testing "focus" 69 | (t/is (= 3 (l/focus odd 3))) 70 | (t/is (nil? (l/focus odd 2)))) 71 | (t/testing "put" 72 | (t/is (= 42 (l/put odd 42 3))) 73 | (t/is (= 2 (l/put odd 42 2)))) 74 | (t/testing "over" 75 | (t/is (= 4 (l/over odd inc 3))) 76 | (t/is (= 2 (l/over odd inc 2)))))) 77 | 78 | (t/deftest passes-comp 79 | (let [fstodd (comp l/fst (l/passes odd?))] 80 | (t/testing "focus" 81 | (t/is (= 1 (l/focus fstodd [1 2 3]))) 82 | (t/is (nil? (l/focus fstodd [2 3 4])))) 83 | (t/testing "put" 84 | (t/is (= [42 2 3] (l/put fstodd 42 [1 2 3]))) 85 | (t/is (= [2 3 4] (l/put fstodd 42 [2 3 4])))) 86 | (t/testing "over" 87 | (t/is (= [2 2 3] (l/over fstodd inc [1 2 3]))) 88 | (t/is (= [2 3 4] (l/over fstodd inc [2 3 4])))))) 89 | 90 | ;; nth 91 | 92 | (defspec nth-first-lens-law 10 93 | (first-lens-law {:gen vector-gen 94 | :lens (l/nth 0)})) 95 | 96 | (defspec nth-second-lens-law 10 97 | (second-lens-law {:gen vector-gen 98 | :lens (l/nth 0)})) 99 | 100 | (defspec nth-third-lens-law 10 101 | (third-lens-law {:gen vector-gen 102 | :lens (l/nth 0)})) 103 | 104 | ;;; nth composition 105 | 106 | (def ffst (comp l/fst l/fst)) 107 | 108 | (def ffst-lens 109 | {:gen nested-vector-gen 110 | :lens ffst}) 111 | 112 | (defspec ffst-first-lens-law 10 113 | (first-lens-law ffst-lens)) 114 | 115 | (defspec ffst-second-lens-law 10 116 | (second-lens-law ffst-lens)) 117 | 118 | (defspec ffst-third-lens-law 10 119 | (third-lens-law ffst-lens)) 120 | 121 | ;; tail 122 | 123 | (def tail-lens 124 | {:gen vector-gen 125 | :xgen vector-gen 126 | :lens l/tail}) 127 | 128 | (defspec tail-first-lens-law 10 129 | (first-lens-law tail-lens)) 130 | 131 | (defspec tail-second-lens-law 10 132 | (second-lens-law tail-lens)) 133 | 134 | (defspec tail-third-lens-law 10 135 | (third-lens-law tail-lens)) 136 | 137 | ;; associative 138 | 139 | (defn with-key 140 | [k] 141 | (gen/fmap (partial hash-map k) gen/any)) 142 | 143 | (def key-lens 144 | {:gen (with-key :foo) 145 | :lens (l/key :foo)}) 146 | 147 | (defspec key-first-lens-law 10 148 | (first-lens-law key-lens)) 149 | 150 | (defspec key-second-lens-law 10 151 | (second-lens-law key-lens)) 152 | 153 | (defspec key-third-lens-law 10 154 | (third-lens-law key-lens)) 155 | 156 | ;; select-keys 157 | 158 | (defn with-keys 159 | [ks] 160 | (gen/fmap (fn [v] 161 | (zipmap ks (repeat v))) 162 | gen/any)) 163 | 164 | (def select-keys-lens 165 | {:gen (with-keys [:a :b]) 166 | :xgen (with-keys [:a :b]) 167 | :lens (l/select-keys [:a :b])}) 168 | 169 | (defspec select-keys-first-lens-law 10 170 | (first-lens-law select-keys-lens)) 171 | 172 | (defspec select-keys-second-lens-law 10 173 | (second-lens-law select-keys-lens)) 174 | 175 | (defspec select-keys-third-lens-law 10 176 | (third-lens-law select-keys-lens)) 177 | 178 | ;; in 179 | 180 | (def in-lens 181 | {:gen (gen/fmap (fn [m] 182 | (merge m {:a {:b {:c 42}}})) 183 | (gen/map gen/keyword gen/any)) 184 | :lens (l/in [:a :b :c])}) 185 | 186 | (defspec in-first-lens-law 10 187 | (first-lens-law in-lens)) 188 | 189 | (defspec in-second-lens-law 10 190 | (second-lens-law in-lens)) 191 | 192 | (defspec in-third-lens-law 10 193 | (third-lens-law in-lens)) 194 | 195 | ;; derived lenses 196 | 197 | (defn sec->min 198 | [sec] 199 | (/ sec 60)) 200 | 201 | (defn min->sec 202 | [min] 203 | (* min 60)) 204 | 205 | (def min-lens 206 | {:gen gen/int 207 | :xgen gen/int 208 | :lens (l/units sec->min min->sec)}) 209 | 210 | (defspec min-first-lens-law 10 211 | (first-lens-law min-lens)) 212 | 213 | (defspec min-second-lens-law 10 214 | (second-lens-law min-lens)) 215 | 216 | (defspec min-third-lens-law 10 217 | (third-lens-law min-lens)) 218 | 219 | ;; interop 220 | 221 | (t/deftest derive-rw 222 | (let [source (atom [0 1 2 3 4]) 223 | fsource (l/derive l/fst source)] 224 | (t/is (= @fsource 0)) 225 | 226 | #?@(:clj [(t/is (instance? clojure.lang.IAtom fsource)) 227 | (t/is (instance? clojure.lang.IAtom2 fsource)) 228 | (t/is (instance? clojure.lang.IDeref fsource)) 229 | (t/is (instance? clojure.lang.IRef fsource))] 230 | :cljs [(t/is (satisfies? IDeref fsource)) 231 | (t/is (satisfies? IReset fsource)) 232 | (t/is (satisfies? ISwap fsource)) 233 | (t/is (satisfies? IWatchable fsource))]) 234 | 235 | (swap! source #(subvec % 1)) 236 | (t/is (= @source [1 2 3 4])) 237 | (t/is (= @fsource 1)) 238 | 239 | (reset! fsource 42) 240 | (t/is (= @source [42 2 3 4])) 241 | (t/is (= @fsource 42)))) 242 | 243 | (t/deftest derive-ro 244 | (let [source (atom [0 1 2 3 4]) 245 | fsource (l/derive l/fst source {:read-only? true})] 246 | (t/is (= @fsource 0)) 247 | 248 | #?@(:clj [(t/is (not (instance? clojure.lang.IAtom fsource))) 249 | (t/is (not (instance? clojure.lang.IAtom2 fsource))) 250 | (t/is (instance? clojure.lang.IDeref fsource)) 251 | (t/is (instance? clojure.lang.IRef fsource))] 252 | :cljs [(t/is (satisfies? IDeref fsource)) 253 | (t/is (not (satisfies? IReset fsource))) 254 | (t/is (not (satisfies? ISwap fsource))) 255 | (t/is (satisfies? IWatchable fsource))]) 256 | 257 | (swap! source #(subvec % 1)) 258 | (t/is (= @source [1 2 3 4])) 259 | (t/is (= @fsource 1)))) 260 | 261 | (t/deftest derive-watches-rw 262 | (let [source (atom [0 1 2 3 4]) 263 | watched (volatile! nil) 264 | fsource (l/derive l/fst source)] 265 | (add-watch fsource :test (fn [key ref old new] 266 | (vreset! watched [ref old new]))) 267 | 268 | (swap! source #(subvec % 1)) 269 | (t/is (= @watched 270 | [fsource 0 1])) 271 | 272 | (swap! fsource inc) 273 | (t/is (= @watched 274 | [fsource 1 2])) 275 | 276 | (remove-watch fsource :test))) 277 | 278 | (t/deftest derive-watches-ro 279 | (let [source (atom [0 1 2 3 4]) 280 | watched (volatile! nil) 281 | fsource (l/derive l/fst source {:read-only? true})] 282 | (add-watch fsource :test (fn [key ref old new] 283 | (vreset! watched [ref old new]))) 284 | 285 | (swap! source #(subvec % 1)) 286 | (t/is (= @watched 287 | [fsource 0 1])) 288 | 289 | (remove-watch fsource :test))) 290 | 291 | #?(:cljs 292 | (do 293 | (enable-console-print!) 294 | (set! *main-cli-fn* #(t/run-tests))) 295 | :clj 296 | (defn -main 297 | [& args] 298 | (let [{:keys [fail]} (t/run-all-tests #"^lentes.tests.*")] 299 | (if (pos? fail) 300 | (System/exit fail) 301 | (System/exit 0))))) 302 | 303 | #?(:cljs 304 | (defmethod t/report [:cljs.test/default :end-run-tests] 305 | [m] 306 | (if (t/successful? m) 307 | (set! (.-exitCode js/process) 0) 308 | (set! (.-exitCode js/process) 1)))) 309 | -------------------------------------------------------------------------------- /tools.clj: -------------------------------------------------------------------------------- 1 | (require '[clojure.java.shell :as shell]) 2 | 3 | (require '[cljs.build.api :as api] 4 | '[cljs.repl :as repl] 5 | '[cljs.repl.node :as node]) 6 | 7 | (require '[rebel-readline.core] 8 | '[rebel-readline.clojure.main] 9 | '[rebel-readline.clojure.line-reader] 10 | '[rebel-readline.clojure.service.local] 11 | '[rebel-readline.cljs.service.local] 12 | '[rebel-readline.cljs.repl]) 13 | 14 | (defmulti task first) 15 | 16 | (defmethod task :default 17 | [args] 18 | (let [all-tasks (-> task methods (dissoc :default) keys sort) 19 | interposed (->> all-tasks (interpose ", ") (apply str))] 20 | (println "Unknown or missing task. Choose one of:" interposed) 21 | (System/exit 1))) 22 | 23 | (defmethod task "repl:jvm" 24 | [args] 25 | (rebel-readline.core/with-line-reader 26 | (rebel-readline.clojure.line-reader/create 27 | (rebel-readline.clojure.service.local/create)) 28 | (clojure.main/repl 29 | :prompt (fn []) ;; prompt is handled by line-reader 30 | :read (rebel-readline.clojure.main/create-repl-read)))) 31 | 32 | (defmethod task "repl:node" 33 | [args] 34 | (rebel-readline.core/with-line-reader 35 | (rebel-readline.clojure.line-reader/create 36 | (rebel-readline.cljs.service.local/create)) 37 | (cljs.repl/repl 38 | (node/repl-env) 39 | :prompt (fn []) ;; prompt is handled by line-reader 40 | :read (rebel-readline.cljs.repl/create-repl-read) 41 | :output-dir "out" 42 | :cache-analysis false))) 43 | 44 | (def build-options 45 | {:main 'lentes.tests 46 | :output-to "target/tests.js" 47 | :source-map "target/tests.js.map" 48 | :output-dir "target/tests" 49 | :optimizations :advanced 50 | :target :nodejs 51 | :pretty-print false 52 | :pseudo-names false 53 | :verbose true}) 54 | 55 | (defmethod task "build:tests" 56 | [args] 57 | (api/build (api/inputs "src" "test") build-options)) 58 | 59 | (defmethod task "watch:tests" 60 | [args] 61 | (println "Start watch loop...") 62 | (letfn [(run-tests [] 63 | (let [{:keys [out err]} (shell/sh "node" "out/tests.js")] 64 | (println out err))) 65 | (start-watch [] 66 | (try 67 | (api/watch (api/inputs "src" "test") 68 | (assoc build-options 69 | :watch-fn run-tests 70 | :source-map true 71 | :optimizations :none)) 72 | (catch Exception e 73 | (println "ERROR:" e) 74 | (Thread/sleep 2000) 75 | start-watch)))] 76 | (trampoline start-watch))) 77 | 78 | ;;; Build script entrypoint. This should be the last expression. 79 | 80 | (task *command-line-args*) 81 | --------------------------------------------------------------------------------