├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── ReleaseNotes.md ├── profiles.clj ├── project.clj ├── runners ├── runner-none-node.js └── runner-none.js ├── src ├── clj │ └── com │ │ └── palletops │ │ └── leaven │ │ └── component.clj └── cljx │ └── com │ └── palletops │ ├── leaven.cljx │ └── leaven │ ├── protocols.cljx │ └── schema.cljx └── test ├── clj └── com │ └── palletops │ └── leaven │ ├── component_test.clj │ └── readme_test.clj └── cljx └── com └── palletops ├── leaven └── schema_test.cljx └── leaven_test.cljx /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .repl 11 | out 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | before_script: 4 | - lein2 version 5 | script: lein2 test 6 | after_success: 7 | - lein2 pallet-release push 8 | env: 9 | global: 10 | secure: "CHvqrWxkvim1+ynWW7EQXw/ogacnZ9QX/UnC7zs2sUUjTygvWrLSgCGTrnzhMkreErqW4FDFnkw75n6mkESeU6ygBY4Ea78TPxxYA7LSOoAItAjJt6uYzslZgHIsBYNqLWDuN3XajvoJ6qoKjkPDngY0lTcSEzYQFMtOjnZGHbE=" 11 | -------------------------------------------------------------------------------- /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 New York 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaven 2 | 3 | > __leav·en__ /ˈlevən/ 4 | > 5 | > noun - a pervasive influence that modifies something or transforms it for the better. 6 | > 7 | > verb - permeate and modify or transform (something) for the better. 8 | 9 | A lightweight component model for clojure and clojurescript. 10 | 11 | ## Install 12 | 13 | Add `[com.palletops/leaven "0.3.1"]` to your `:dependencies`. 14 | 15 | ## Usage 16 | 17 | For users of components, `com.palletops.leaven` provides the `start` 18 | and `stop` functions, and the `defsystem` macro. There is also a 19 | `status` function that can be used with components that support it. 20 | 21 | Both `start` and `stop` take a single component as an argument and 22 | return an updated component - components are normally immutable, so 23 | you should always use the updated component in the return value. 24 | 25 | The `defsystem` macro is used to define a composite component, made up 26 | of a map of components, each identified by a symbol. The 27 | sub-components are specified as a vector of symbols, and are started 28 | in the order specified and stop in the reverse order. The macro 29 | defines a record, and you instantiate the record with the 30 | sub-component instances. 31 | 32 | You can specify a body in `defsystem`, just as you would to 33 | `defrecord`, in order to implement other protocols on your system. 34 | 35 | You can pass a map of options to defsystem, after the component names 36 | and before any body. 37 | 38 | The `:depends` option takes a map from system component to the 39 | components it depends on, and ensures components see the started 40 | versions of their dependencies. If the dependent components are named 41 | the same as the component, then a sequence of keywords is used as the 42 | value for the map entry. If the dependent components have different 43 | names, then a map is used from the component keyword to the matching 44 | dependent component keyword. Note that this does not (currently) 45 | influence the component ordering. 46 | 47 | The `:on-start` and `:on-stop` options are used to specify functions 48 | that will be called when a component is started or stopped. The value 49 | is a map from the component keyword to a function that takes the 50 | system and the keyword of the started component, and returns a 51 | possibly updated system. 52 | 53 | For component providers, `com.palletops.leaven.protocols` provides the 54 | `Startable` and `Stoppable` protocols, that require the implementation 55 | of the `start` and `stop` methods respectively. The `Queryable` 56 | protocol provides for a `status` method. 57 | 58 | ### Example 59 | 60 | We define a component that will provide an increasing sequence of 61 | numbers via a `core.async` channel. We implement the `Startable` and 62 | `Stoppable` protocols for the component. 63 | 64 | ```clj 65 | (require '[clojure.core.async :as async] 66 | '[com.palletops.leaven :as leaven] 67 | '[com.palletops.leaven.protocols :refer [Startable Stoppable]) 68 | 69 | (defrecord Counter [init-val channel loop-chan] 70 | Startable 71 | (start [component] 72 | (assoc component :loop-chan 73 | (async/go-loop [n init-val] 74 | (async/>! channel n) 75 | (recur (inc n))))) 76 | Stoppable 77 | (stop [component] 78 | (async/close! channel) 79 | (assoc component :loop-chan nil))) 80 | ``` 81 | 82 | Note that the record contains fields for both the configuration and 83 | the runtime state of the component. 84 | 85 | We instantiate the component with one of the record's constructor 86 | functions. In this example we use a var to hold the component, but 87 | this is in no way required. 88 | 89 | ```clj 90 | (def c (async/chan)) 91 | (def counter (map->Counter {:init-val 1 :channel c})) 92 | ``` 93 | 94 | We can start the component: 95 | 96 | ```clj 97 | (alter-var-root #'counter leaven/start) 98 | ``` 99 | 100 | Now we can get values from the channel: 101 | 102 | ```clj 103 | (async/ 1 104 | (async/ 2 105 | (async/ 3 106 | ``` 107 | 108 | If we had tried to read the channel before starting the component, the 109 | call to `async/ 4 121 | (async/ nil 122 | ``` 123 | 124 | We're going to define another component now that, take a channel and 125 | will double what is put into it. 126 | 127 | 128 | ```clj 129 | (defrecord Doubler [in-chan out-chan ctrl-chan loop-chan] 130 | Startable 131 | (start [component] 132 | (let [ctrl-chan (async/chan)] 133 | (assoc component 134 | :loop-chan (async/go 135 | (loop [] 136 | (let [[v _] (async/alts! [in-chan ctrl-chan])] 137 | (if (not= ::stop v) 138 | (let [[v _] (async/alts! 139 | [[out-chan (* 2 v)] ctrl-chan])] 140 | (if (not= ::stop v) 141 | (recur)))))) 142 | (async/close! out-chan)) 143 | :ctrl-chan ctrl-chan))) 144 | Stoppable 145 | (stop [component] 146 | (async/>!! ctrl-chan ::stop) 147 | (assoc component :loop-chan nil :ctrl-chan nil))) 148 | ``` 149 | 150 | We'll use these components to define a system 151 | 152 | ```clj 153 | (leaven/defsystem Evens [counter doubler]) 154 | (defn evens [out-chan] 155 | (let [c1 (async/chan)] 156 | (Evens. 157 | (map->Counter {:init-val 1 :channel c1}) 158 | (map->Doubler {:in-chan c :out-chan out-chan})))) 159 | 160 | (def c (async/chan)) 161 | (def sys (evens c)) 162 | (alter-var-root #'sys leaven/start) 163 | (async/ 2 164 | (async/ 4 165 | (alter-var-root #'sys leaven/stop) 166 | (async/ nil 167 | ``` 168 | 169 | ### Comparison with Component Example 170 | 171 | This is the example from the [Component][Component] readme, translated 172 | for leaven. 173 | 174 | ```clj 175 | (ns com.example.your-application 176 | (:require [com.palletops.leaven :as leaven])) 177 | 178 | (defrecord Database [host port connection] 179 | leaven/Startable 180 | (start [component] 181 | (let [conn (connect-to-database host port)] 182 | (assoc component :connection conn))) 183 | 184 | (stop [component] 185 | (.close connection) 186 | (assoc component :connection nil))) 187 | 188 | (defn database [host port] 189 | (map->Database {:host host :port port})) 190 | 191 | (defrecord ExampleComponent [options cache database] 192 | leaven/Stoppable 193 | (start [this] 194 | (assoc this :admin (get-user database "admin"))) 195 | 196 | (stop [this] 197 | this)) 198 | 199 | (defn example-component [{:keys [config-options db]}] 200 | (map->ExampleComponent {:db db 201 | :options config-options 202 | :cache (atom {})})) 203 | 204 | (defsystem ExampleSystem [db app]) 205 | 206 | (defn example-system [config-options] 207 | (let [{:keys [host port]} config-options 208 | db (database host port)] 209 | (map->ExampleSystem ; normal record constructor 210 | :db db 211 | :app (example-component 212 | {:config-options config-options 213 | :db db})))) 214 | ``` 215 | 216 | ## Library authors 217 | 218 | Library authors are encouraged to provide a leaven component in a 219 | separate namespace. By making the dependency on leaven have a 220 | `provided` scope, you do not force the dependency on your users. 221 | 222 | In [leiningen][leiningen], you can make a dependency have `provided` 223 | scop by adding it under the `:provided` profile. 224 | 225 | ## Why another component library? 226 | 227 | The [Component][Component] framework pioneered a component model for 228 | clojure, and provides an excellent rationale for components. 229 | 230 | We wanted something that: 231 | - did explicit dependency order, with no need for a `using` function, 232 | - would work in clojurescript 233 | 234 | ## License 235 | 236 | Copyright © 2014 Hugo Duncan 237 | 238 | Distributed under the Eclipse Public License either version 1.0 or (at 239 | your option) any later version. 240 | 241 | [Component]:https://github.com/stuartsierra/component "Stuart Sierra's Component" 242 | [leiningen]:https://github.com/technomancy/leiningen "Leiningen" 243 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | 3 | - Move user-provided protocol impls after generic 4 | This allows the user to provide their own versions of start, stop, or 5 | status for systems. 6 | 7 | - Update api-builder and build tool versions 8 | 9 | - Bump prismatic schema to version 0.4.0 10 | 11 | ## 0.3.0 12 | 13 | - Add type extenders for component interop 14 | Adds extend-leaven and extend-component for converting component types to 15 | Leaven and Components, respectively. 16 | 17 | - Add :depends option 18 | Adds an option to specify dependencies of a system sub-component using a 19 | declarative map. The map is used to ensure dependencies are updated when 20 | a system starts or stops. 21 | 22 | - Ensure deepest exception is propagated 23 | When an exception occurs on a system operation, ensure the returned system 24 | map is from the deepest exception (and has the most correct state). 25 | 26 | - Add options to defsystem 27 | Allow passing of per sub-component on-start and on-stop functions to 28 | defsystem. This enables, for example, propogation of the started 29 | component to other sub-components. 30 | 31 | Adds an update-components function that can be used to update dependent 32 | components and can be used as an on-start function. 33 | 34 | ## 0.2.1 35 | 36 | - Use symbols for components in defsystem 37 | Makes defsystem consistent with defrecord and deftype. 38 | 39 | Closes #3 40 | 41 | ## 0.2.0 42 | 43 | - Break ILifecycle into Startable and Stoppable 44 | Allow components to implement `start` and `stop` individually as required. 45 | 46 | This is a breaking change. 47 | 48 | Closes #1 49 | 50 | ## 0.1.2 51 | 52 | - Allow defsystem to take a body 53 | The body is forwarded to the defrecord's body, in order to allow you to 54 | implement other protocols on your system. 55 | 56 | ## 0.1.1 57 | 58 | - Try and include some files in the jar this time! 59 | 60 | ## 0.1.0 61 | 62 | - Initial release 63 | -------------------------------------------------------------------------------- /profiles.clj: -------------------------------------------------------------------------------- 1 | {:provided {:dependencies [[org.clojure/clojure "1.6.0"] 2 | [org.clojure/clojurescript "0.0-3211"] 3 | [com.stuartsierra/component "0.2.2"]]} 4 | 5 | :cljs-test {:cljx 6 | {:builds 7 | ;; Using :replace and repeating the src paths, as 8 | ;; otherwise test paths get generated multiple times. 9 | ^:replace [{:source-paths ["src/cljx"] 10 | :output-path "target/generated/src/clj" 11 | :rules :clj} 12 | {:source-paths ["src/cljx"] 13 | :output-path "target/generated/src/cljs" 14 | :rules :cljs} 15 | {:source-paths ["test/cljx"] 16 | :output-path "target/generated/test/clj" 17 | :rules :clj} 18 | {:source-paths ["test/cljx"] 19 | :output-path "target/generated/test/cljs" 20 | :rules :cljs}]} 21 | :cljsbuild 22 | ^:replace 23 | {:test-commands 24 | {"tests" ["phantomjs" "runners/runner-none.js" 25 | "target/unit-test" "target/unit-test.js"] 26 | ;; "node-tests" ["node" "runners/runner-none-node.js" 27 | ;; "target/unit-test-node" 28 | ;; "target/unit-test-node.js"] 29 | } 30 | :builds 31 | [{:id "test" 32 | :source-paths ["target/generated/src/clj" 33 | "target/generated/src/cljs" 34 | "target/generated/test/cljs"] 35 | :compiler {:output-to "target/unit-test.js" 36 | :output-dir "target/unit-test" 37 | :source-map "target/unit-test.js.map" 38 | :optimizations :none 39 | :pretty-print true}} 40 | ;; {:id "test-node" 41 | ;; :source-paths ["target/generated/src/clj" 42 | ;; "target/generated/src/cljs" 43 | ;; "target/generated/test/cljs"] 44 | ;; :compiler {:output-to "target/unit-test-node.js" 45 | ;; :target :nodejs 46 | ;; :output-dir "target/unit-test-node" 47 | ;; :optimizations :none 48 | ;; :pretty-print true}} 49 | ]} 50 | } 51 | :clj-test {:test-paths ["test/clj" "target/generated/test/clj"] 52 | :dependencies [[org.clojure/core.async "0.1.346.0-17112a-alpha"]]} 53 | :cljx {:dependencies [[com.keminglabs/cljx "0.6.0"]] 54 | ;; plugin doesn't seem to add this now? 55 | :repl-options {:nrepl-middleware [cljx.repl-middleware/wrap-cljx]}} 56 | :dev-base {:plugins [[lein-pallet-release "RELEASE"] 57 | [com.cemerick/austin "0.1.6"] 58 | [com.cemerick/clojurescript.test "0.3.3"] 59 | [lein-cljsbuild "1.0.5"]]} 60 | :dev [:dev-base :cljx :cljs-test :clj-test] 61 | :test [:cljs-test :clj-test]} 62 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.palletops/leaven "0.3.2-SNAPSHOT" 2 | :description "A lightweight component library for clojure and clojurescript." 3 | :url "https://github.com/palletops/leaven" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[com.palletops/api-builder "0.3.1"]] 7 | :plugins [[com.keminglabs/cljx "0.5.0"]] 8 | :prep-tasks [["cljx" "once"]] 9 | :source-paths ["src/clj" "target/generated/src/clj"] 10 | :resource-paths ["target/generated/src/cljs"] 11 | :test-paths ["target/generated/test/clj"] 12 | :aliases {"compile" ["do" "compile," "cljsbuild" "once"] 13 | "test" ["do" "test," "cljsbuild" "once" "test," "cljsbuild" "test"] 14 | "auto-test" ["do" "clean," "cljsbuild" "auto" "test"]} 15 | :cljsbuild {:builds []} 16 | :cljx {:builds [{:source-paths ["src/cljx"] 17 | :output-path "target/generated/src/clj" 18 | :rules :clj} 19 | {:source-paths ["src/cljx"] 20 | :output-path "target/generated/src/cljs" 21 | :rules :cljs}]}) 22 | -------------------------------------------------------------------------------- /runners/runner-none-node.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------------------------- 2 | // 3 | // I am a "runner" script for use with phantomjs and cemerick/clojurescript.test 4 | // I handle the case where the cljsbuild setting is ':optimizations :none' 5 | // 6 | var path = require("path"), 7 | fs = require("fs"), 8 | args = process.argv.slice(4); 9 | var thisName = process.argv[1].split('/').slice(-1); 10 | 11 | var usage = [ 12 | "", 13 | "Usage: nodejs " + thisName + " output-dir output-to [tweaks]", 14 | "", 15 | "Where:", 16 | " - \"output-dir\" and \"output-to\" should match the paths you supplied ", 17 | " to cljsbuild in the project.clj. (right next to \":optimizations :none\").", 18 | " - [tweaks] is zero or more of either:", 19 | " (1) an extra javascript file - e.g. path/to/my-shim.js ", 20 | " (2) arbitrary javascript code fragments. E.g. window.something=flag", 21 | " These tweaks will be applied to the test page prior to load of test code." 22 | ].join("\n"); 23 | 24 | //-- Colors --------------------------------------------------------------------------------------- 25 | 26 | function yellow(text) { 27 | return "\u001b[31m" + text + "\u001b[0m"; 28 | } 29 | 30 | function red(text) { 31 | return "\u001b[33m" + text + "\u001b[0m"; 32 | } 33 | 34 | function green(text) { 35 | return "\u001b[32m" + text + "\u001b[0m"; 36 | } 37 | 38 | 39 | //-- Commandline --------------------------------------------------------------------------------- 40 | 41 | if (process.argv.length < 4) { 42 | console.log(usage); 43 | process.exit(1); 44 | } 45 | 46 | // google base dir 47 | var output_to = process.argv[2]; 48 | if (output_to.slice(-1) != path.sep) // we want a trailing '/' 49 | output_to = output_to + path.sep; 50 | if (!fs.existsSync(output_to)) { 51 | console.log(red('\nError: output_to directory doesn\'t exist: ' + output_to)) 52 | process.exit(1) 53 | } 54 | 55 | var googBasedir = path.join(process.cwd(), output_to, 'goog') 56 | if (!fs.existsSync(googBasedir)) { 57 | console.log(red('\nError: goog directory doesn\'t exist: ' + googBasedir)) 58 | process.exit(1) 59 | } 60 | 61 | // test file 62 | var testFile = process.argv[3]; // output-to parameter. Eg test.js 63 | if (!fs.existsSync(testFile)) { 64 | console.log(red('\nError: test file doesn\'t exist: ' + testFile)); 65 | process.exit(1) 66 | } 67 | var haveCljsTest = function () { 68 | return (typeof cemerick !== "undefined" && 69 | typeof cemerick.cljs !== "undefined" && 70 | typeof cemerick.cljs.test !== "undefined" && 71 | typeof cemerick.cljs.test.run_all_tests === "function"); 72 | }; 73 | 74 | var failIfCljsTestUndefined = function () { 75 | if (!haveCljsTest()) { 76 | var messageLines = [ 77 | "", 78 | "ERROR: cemerick.cljs.test was not required.", 79 | "", 80 | "You can resolve this issue by ensuring [cemerick.cljs.test] appears", 81 | "in the :require clause of your test suite namespaces.", 82 | "Also make sure that your build has actually included any test files.", 83 | "", 84 | "Also remember that Node.js can be only used with simple/advanced ", 85 | "optimizations, not with none/whitespace.", 86 | "" 87 | ]; 88 | console.error(messageLines.join("\n")); 89 | process.exit(1); 90 | } 91 | } 92 | 93 | //-- Load Google Clojure ---------------------------------------------------------------------------- 94 | // global.CLOSURE_BASE_PATH = googBasedir; 95 | // require('closure').Closure(global); 96 | require(path.join(googBasedir, 'bootstrap', 'nodejs.js')) 97 | //-- Handle Any Tweaks ----------------------------------------------------------------------------- 98 | 99 | args.forEach(function (arg) { 100 | var file = path.join(process.cwd(), arg); 101 | if (fs.existsSync(file)) { 102 | try { 103 | // using eval instead of require here so that `this` is the "real" 104 | // top-level scope, not the module 105 | eval("(function () {" + fs.readFileSync(file, {encoding: "UTF-8"}) + "})()"); 106 | } catch (e) { 107 | failIfCljsTestUndefined(); 108 | console.log("Error in file: \"" + file + "\""); 109 | console.log(e); 110 | } 111 | } else { 112 | try { 113 | eval("(function () {" + arg + "})()"); 114 | } catch (e) { 115 | console.log("Could not evaluate expression: \"" + arg + "\""); 116 | console.log(e); 117 | } 118 | } 119 | }); 120 | 121 | //-- Load code into our test page ---------------------------------------------------------------- 122 | goog.nodeGlobalRequire(testFile); 123 | 124 | // This loop is where a lot of important work happens 125 | // It will inject both the unittests and code-to-be-tested into the page 126 | for(var namespace in goog.dependencies_.nameToPath) 127 | goog.require(namespace); // will trigger CLOSURE_IMPORT_SCRIPT calls which injectJs into page 128 | 129 | failIfCljsTestUndefined(); // check this before trying to call set_print_fn_BANG_ 130 | 131 | //-- Run the tests ------------------------------------------------------------------------------- 132 | // 133 | // All the code is now loaded into the test page. Time to test. 134 | console.log("about to run tests") 135 | 136 | cemerick.cljs.test.set_print_fn_BANG_(function(x) { 137 | // since console.log *itself* adds a newline 138 | var x = x.replace(/\n$/, ""); 139 | if (x.length > 0) console.log(x); 140 | }); 141 | 142 | var success = (function() { 143 | var results = cemerick.cljs.test.run_all_tests(); 144 | cemerick.cljs.test.on_testing_complete(results, function () { 145 | process.exit(cemerick.cljs.test.successful_QMARK_(results) ? 0 : 1); 146 | }); 147 | })(); 148 | -------------------------------------------------------------------------------- /runners/runner-none.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------------------------- 2 | // 3 | // I am a "runner" script for use with phantomjs and cemerick/clojurescript.test 4 | // I handle the case where the cljsbuild setting is ':optimizations :none' 5 | // 6 | // See: https://github.com/mike-thompson-day8/cljsbuild-none-test-seed 7 | // 8 | var fs = require('fs'); 9 | var system = require('system'); 10 | var thisName = system.args[0].split('/').slice(-1); 11 | 12 | var usage = [ 13 | "", 14 | "Usage: phantomjs " + thisName + " output-dir output-to [tweaks]", 15 | "", 16 | "Where:", 17 | " - \"output-dir\" and \"output-to\" should match the paths you supplied ", 18 | " to cljsbuild in the project.clj. (right next to \":optimizations :none\").", 19 | " - [tweaks] is zero or more of either:", 20 | " (1) an extra javascript file - e.g. path/to/my-shim.js ", 21 | " (2) arbitrary javascript code fragments. E.g. window.something=flag", 22 | " These tweaks will be applied to the test page prior to load of test code." 23 | ].join("\n"); 24 | 25 | 26 | //-- Colors --------------------------------------------------------------------------------------- 27 | 28 | function yellow(text) { 29 | return "\u001b[31m" + text + "\u001b[0m"; 30 | } 31 | 32 | function red(text) { 33 | return "\u001b[33m" + text + "\u001b[0m"; 34 | } 35 | 36 | function green(text) { 37 | return "\u001b[32m" + text + "\u001b[0m"; 38 | } 39 | 40 | 41 | //-- Commandline --------------------------------------------------------------------------------- 42 | 43 | if (system.args.length < 3) { 44 | console.log(usage); 45 | phantom.exit(1); 46 | } 47 | 48 | // google base dir 49 | var output_dir = system.args[1]; 50 | if (output_dir.slice(-1) != "/") // we want a trailing '/' 51 | output_dir = output_dir + "/"; 52 | if (!fs.isDirectory(output_dir)) { 53 | console.log(red('\nError: output_dir directory doesn\'t exist: ' + output_dir)) 54 | phantom.exit(1) 55 | } 56 | 57 | 58 | var googBasedir = output_dir + "goog/" 59 | if (!fs.isDirectory(googBasedir)) { 60 | console.log(red('\nError: goog directory doesn\'t exist: ' + googBasedir)) 61 | phantom.exit(1) 62 | } 63 | 64 | var BASE_JS = googBasedir + "base.js"; 65 | if (!fs.exists(BASE_JS)) { 66 | console.log(red('\nError: base.js doesn\'t exist: ' + BASE_JS)); 67 | phantom.exit(1) 68 | } 69 | 70 | var DEPS_JS = googBasedir + "deps.js"; 71 | if (!fs.exists(DEPS_JS)) { 72 | console.log(red('\nError: deps.js doesn\'t exist: ' + DEPS_JS)); 73 | phantom.exit(1) 74 | } 75 | 76 | // test file 77 | var testFile = system.args[2]; // output-to parameter. Eg test.js 78 | if (!fs.exists(testFile)) { 79 | console.log(red('\nError: test file doesn\'t exist: ' + testFile)); 80 | phantom.exit(1) 81 | } 82 | 83 | 84 | //-- Initialise Test Page ------------------------------------------------------------------------- 85 | 86 | // We'll do our testing in this page. 87 | var page = require('webpage').create(); 88 | 89 | // When the test page produces console output, make it visible to the user, with colours. 90 | page.onConsoleMessage = function (line) { 91 | if (line === "[NEWLINE]") 92 | return; 93 | 94 | line = line.replace(/\[NEWLINE\]/g, "\n"); 95 | 96 | // add colour 97 | if (-1 != line.indexOf('ERROR')) { 98 | line = red(line); 99 | } 100 | else if (-1 != line.indexOf('FAIL')) { 101 | line = yellow(line); 102 | } 103 | else if (-1 != line.indexOf('Testing complete')) { 104 | line = green(line); 105 | } 106 | 107 | console.log(line); 108 | }; 109 | 110 | 111 | page.onError = function(msg, trace) { 112 | var msgStack = ['ERROR: ' + msg]; 113 | if (trace) { 114 | msgStack.push('STACK TRACE:'); 115 | trace.forEach(function(t) { 116 | msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + 117 | (t.function ? ' (in function ' + t.function + ')' : '')); 118 | }); 119 | } 120 | console.log(red(msgStack.join('\n'))); 121 | 122 | var runAllTestsIsDefined = page.evaluate(function() { 123 | return (typeof cemerick !== "undefined" && 124 | typeof cemerick.cljs !== "undefined" && 125 | typeof cemerick.cljs.test !== "undefined" && 126 | typeof cemerick.cljs.test.run_all_tests === "function"); 127 | }); 128 | if (!runAllTestsIsDefined) { 129 | var messageLines = [ 130 | "", 131 | "Possible cause: the namespace cemerick.cljs.test isn't defined.", 132 | "", 133 | "To resolve: ensure [cemerick.cljs.test] appears", 134 | "in the :require clause of your test suite.", 135 | "Also, ensure there's one or more test files.", 136 | ]; 137 | console.error(messageLines.join("\n")); 138 | } 139 | 140 | phantom.exit(1); 141 | } 142 | 143 | 144 | //-- Handle Any Tweaks ----------------------------------------------------------------------------- 145 | 146 | 147 | for (var i = 3; i < system.args.length; i++) { 148 | var arg = system.args[i]; 149 | if (fs.exists(arg)) { 150 | if (!page.injectJs(arg)) 151 | throw new Error("Failed to inject " + arg); 152 | } else { 153 | page.evaluateJavaScript("(function () { " + arg + ";" + " })"); 154 | } 155 | } 156 | 157 | 158 | //-- Plan Of Action ------------------------------------------------------------------------------- 159 | // 160 | // 161 | // With the ':optimisation :none' setting, 'test.js' will only contain dependency 162 | // information. Inspect the file and you'll only see calls to goog.addDependency(). 163 | // All the real code (which we want loaded into our test page) is in the 164 | // js files referenced. 165 | // 166 | // So this script has to: 167 | // - load and interpret the dependency information in test.js 168 | // To do that we need to use 'goog' (Google Closure runtime) 169 | // And that means importing base.js into this phantom context. 170 | // - inject unittest javascript into the testing page IN THE CORRECT DEPENDENCY ORDER. 171 | // Again, we have to use goog, plus some monkey patching. 172 | // - run the "test-runner" suppied by cemerick/clojurescript.test 173 | // - organise that runner output is correctly displayed for the world to see. 174 | // 175 | // 176 | 177 | //-- Load Google Clojure ---------------------------------------------------------------------------- 178 | 179 | // We need 'goog' in this phantom context, so we can interpret the dependencies in test.js 180 | phantom.injectJs(BASE_JS) 181 | 182 | // Load the two "dependencies" files into the phantom content (not the page context!!) 183 | // these two files contain lots of addDependancy calls 184 | phantom.injectJs(DEPS_JS) 185 | phantom.injectJs(testFile) 186 | 187 | // Tell goog that javscript imports should be interpreted as js injections into the test page 188 | goog.global.CLOSURE_IMPORT_SCRIPT = function(path) { 189 | page.injectJs(googBasedir + path); 190 | return true; 191 | }; 192 | 193 | 194 | 195 | //-- Load code into our test page ---------------------------------------------------------------- 196 | 197 | // we need 'goog' in the test page because there'll be references to it within the javascript we load 198 | page.injectJs(BASE_JS); 199 | 200 | // This loop is where a lot of important work happens 201 | // It will inject both the unittests and code-to-be-tested into the page 202 | for(var namespace in goog.dependencies_.nameToPath) 203 | goog.require(namespace); // will trigger CLOSURE_IMPORT_SCRIPT calls which injectJs into page 204 | 205 | 206 | //-- Run the tests ------------------------------------------------------------------------------- 207 | // 208 | // All the code is now loaded into the test page. Time to test. 209 | 210 | // Hack: use an alert handler to detect when the tests are finished. 211 | var specialMarker = "phantom-exit-code:"; 212 | page.onAlert = function (msg) { 213 | var exitCode = msg.replace(specialMarker, ""); 214 | if (msg != exitCode) 215 | phantom.exit(parseInt(exitCode)); 216 | }; 217 | 218 | 219 | page.evaluate(function (specialMarker) { 220 | 221 | cemerick.cljs.test.set_print_fn_BANG_(function(x) { 222 | console.log(x.replace(/\n/g, "[NEWLINE]")); // since console.log *itself* adds a newline 223 | }); 224 | 225 | var results = cemerick.cljs.test.run_all_tests(); 226 | 227 | cemerick.cljs.test.on_testing_complete(results, function () { 228 | window.alert(specialMarker + (cemerick.cljs.test.successful_QMARK_(results) ? 0 : 1)); 229 | }); 230 | 231 | }, 232 | specialMarker); 233 | -------------------------------------------------------------------------------- /src/clj/com/palletops/leaven/component.clj: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven.component 2 | "Adapters for Component components" 3 | (:require 4 | [com.palletops.leaven :as leaven] 5 | [com.palletops.leaven.protocols :as protocols] 6 | [com.stuartsierra.component :as component])) 7 | 8 | (defmacro extend-leaven 9 | "Extend a component type with the leaven protocols." 10 | [component-class] 11 | `(extend-type ~component-class 12 | protocols/Startable 13 | (~'start [~'component] 14 | (component/start ~'component)) 15 | protocols/Stoppable 16 | (~'stop [~'component] 17 | (component/stop ~'component)))) 18 | 19 | (defmacro extend-component 20 | "Extend a leaven type with the component protocol." 21 | [component-class] 22 | `(extend-type ~component-class 23 | component/Lifecycle 24 | (~'start [~'component] (leaven/start ~'component)) 25 | (~'stop [~'component] (leaven/stop ~'component)))) 26 | -------------------------------------------------------------------------------- /src/cljx/com/palletops/leaven.cljx: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven 2 | "A component composition library." 3 | #+clj 4 | (:require 5 | [com.palletops.leaven.protocols :as protocols] 6 | [com.palletops.api-builder.api :refer [defn-api]] 7 | [schema.core :as schema :refer [=>]]) 8 | #+cljs 9 | (:require-macros 10 | [com.palletops.api-builder.api :refer [defn-api]] 11 | [schema.core :refer [=>]]) 12 | #+cljs 13 | (:require 14 | [com.palletops.leaven.protocols :as protocols] 15 | [schema.core :as schema])) 16 | 17 | (defn-api start 18 | "Start a component." 19 | {:sig [[schema/Any :- schema/Any]]} 20 | [component] 21 | (if (protocols/startable? component) 22 | (protocols/start component) 23 | component)) 24 | 25 | (defn-api stop 26 | "Stop a component." 27 | {:sig [[schema/Any :- schema/Any]]} 28 | [component] 29 | (if (protocols/stoppable? component) 30 | (protocols/stop component) 31 | component)) 32 | 33 | (defn-api status 34 | "Ask a component for its status." 35 | {:sig [[schema/Any :- schema/Any]]} 36 | [component] 37 | (if (protocols/queryable? component) 38 | (protocols/status component))) 39 | 40 | (defn-api startable? 41 | "Predicate for testing whether `x` satisfies the Startable protocol." 42 | {:sig [[schema/Any :- schema/Any]]} 43 | [x] 44 | (protocols/startable? x)) 45 | 46 | (defn-api stoppable? 47 | "Predicate for testing whether `x` satisfies the Stoppable protocol." 48 | {:sig [[schema/Any :- schema/Any]]} 49 | [x] 50 | (protocols/stoppable? x)) 51 | 52 | (defn-api queryable? 53 | "Predicate for testing whether `x` satisfies the Queryable protocol." 54 | {:sig [[schema/Any :- schema/Any]]} 55 | [x] 56 | (protocols/queryable? x)) 57 | 58 | (defn ^:internal apply-components 59 | "Execute a function on a sequence of components from a record. 60 | Exceptions are caught and propagate with a `:system` data element 61 | that contains the partially updated system component, a `:component` 62 | key that is the keyword for the component that caused the exception, 63 | and `:incomplete` which is a sequence of keywords for the components 64 | where the operation was not completed." 65 | [f rec sub-components operation-name on-f] 66 | (loop [rec rec cs sub-components] 67 | (if-let [c (first cs)] 68 | (let [post-f (get on-f c) 69 | res (try 70 | (-> (update-in rec [c] f) 71 | (cond-> post-f (post-f c))) 72 | (catch #+cljs js/Error #+clj Throwable e 73 | (let [d (ex-data e)] 74 | (throw 75 | (ex-info 76 | (str "Exception while " operation-name 77 | " " c 78 | " system sub-component.") 79 | (merge 80 | {:system rec 81 | :component c 82 | :sub-components sub-components 83 | :completed (subvec sub-components 84 | 0 (- (count sub-components) 85 | (count cs))) 86 | :uncompleted cs 87 | :operation-name operation-name 88 | :type ::system-failed} 89 | (if (= ::system-failed (:type d)) 90 | ;; ensure we report the most nested details 91 | d)) 92 | e)))))] 93 | (recur res (rest cs))) 94 | rec))) 95 | 96 | (defn-api update-components 97 | "Returns a function to update the system components given by 98 | `component-specs`. 99 | 100 | 101 | , with a sub-component, assuming the component acts like a map. 102 | 103 | Can be used as a value in an :on-start option map in defrecord to 104 | get components updated with their started values." 105 | {:sig [[(schema/either 106 | [schema/Keyword] 107 | {schema/Keyword schema/Keyword}) 108 | :- (=> schema/Any schema/Any schema/Keyword)]]} 109 | [component-specs] 110 | (fn [component sub-kw] 111 | (reduce 112 | #(cond 113 | (keyword? %2) (assoc-in %1 [%2 sub-kw] (sub-kw component)) 114 | :else (assoc-in %1 [(key %2) (val %2)] (sub-kw component))) 115 | component 116 | component-specs))) 117 | 118 | (defn comp-on-f 119 | "Compose two functions for use with an on-start or on-finish 120 | function." 121 | [f1 f2] 122 | (fn [c kw] 123 | (-> c 124 | (f1 :channel) 125 | (f2 :channel)))) 126 | 127 | #+clj 128 | (defn dependents 129 | "Invert the specified dependencies." 130 | [depends] 131 | (reduce 132 | (fn [res [k deps]] 133 | (if (map? deps) 134 | (reduce 135 | (fn [res [sk sv]] 136 | (assoc-in res [sk k] sv)) 137 | res deps) 138 | (reduce 139 | (fn [res sk] 140 | (assoc-in res [sk k] sk)) 141 | res deps))) 142 | {} 143 | depends)) 144 | 145 | #+clj 146 | (defn process-depends 147 | "Convert :depends into :on-start and :on-stop function declarations." 148 | [{:keys [depends] :as options}] 149 | (reduce 150 | (fn [res [k v]] 151 | (let [f `(update-components ~v)] 152 | (-> res 153 | (assoc-in [:on-start k] f) 154 | (assoc-in [:on-stop k] f)))) 155 | (dissoc options :depends) 156 | (dependents depends))) 157 | 158 | 159 | #+clj 160 | (defmacro ^:api defsystem 161 | "Macro to build a system defrecord out of `components`, a sequence 162 | of keywords that specify the sub-components. The record will 163 | implement Startable, Stoppable and Queryable by calling the protocol 164 | methods on each of the components. The `start` method calls the 165 | sub-components in the specified order. The `stop` method calls the 166 | sub-components in the reverse order. 167 | 168 | An option map may be supplied after the sub-component vector. 169 | 170 | The :depends key can be used to specify components that depend on 171 | each system component. The value must be a map where the keys are 172 | keywords and the values are either a sequence of (system component) 173 | keywords, or a map from (system component) keyword to a keyword 174 | specifying the key to be updated in that component. A simple 175 | keyword is used for the case where the sub-component keyword matches 176 | the component key. A map entry is used when the keyword needs to be 177 | translated. 178 | 179 | {:depends 180 | {:sub-comp2 [:sub-comp1] 181 | :sub-comp3 {:sub-comp1 :comp1}}} 182 | 183 | The :on-start and :on-stop key can be used to pass functions that 184 | are called after the sub-component is operated on. Each function 185 | must take component, and sub-component keyword, and return a 186 | possibly modified component map. 187 | 188 | {:on-start 189 | {:sub-comp1 (update-subcomponent :sub-comp2) 190 | :sub-comp2 #(assoc-in %1 [:sub-comp3 :comp1] %3)}} 191 | 192 | The :depends map is used to generate :on-start and :on-stop 193 | functions, which will override any specified :on-start and :on-stop 194 | values. 195 | 196 | A body can be supplied as used by defrecord, to implement extra 197 | protocols on the system." 198 | [record-name components & body] 199 | (let [component-syms (mapv (comp symbol name) components) 200 | component-kws (mapv (comp keyword name) components) 201 | rcomponents (vec (reverse component-kws)) 202 | options (let [opts (first body)] 203 | (if (map? opts) (process-depends opts))) 204 | body (if options 205 | (rest body) 206 | body) 207 | option-sym (gensym "options")] ; auto gensym always gives same value 208 | `(do 209 | (def ~option-sym ~options) ; defrecord functions can't access lexical scope 210 | (defrecord ~record-name [~@component-syms] 211 | protocols/Startable 212 | (~'start [component#] 213 | (apply-components 214 | start component# ~component-kws "starting" (:on-start ~option-sym))) 215 | protocols/Stoppable 216 | (~'stop [component#] 217 | (apply-components 218 | stop component# ~rcomponents "stopping" (:on-stop ~option-sym))) 219 | protocols/Queryable 220 | (~'status [component#] 221 | (apply-components 222 | status component# ~rcomponents "querying status" nil)) 223 | ~@body)))) 224 | -------------------------------------------------------------------------------- /src/cljx/com/palletops/leaven/protocols.cljx: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven.protocols 2 | "Protocols for leaven components") 3 | 4 | (defprotocol Startable 5 | "Basic lifecycle for a component." 6 | (start [component] 7 | "Start a component. Returns an updated component.")) 8 | 9 | (defprotocol Stoppable 10 | (stop [component] 11 | "Stop a component. Returns an updated component.")) 12 | 13 | (defprotocol Queryable 14 | "Allows a component to implement a status function, which may just 15 | have side effects (like logging)." 16 | (status [component] 17 | "Allow a component to be queried for status.")) 18 | 19 | (defn startable? 20 | "Predicate for testing whether `x` satisfies the Startable protocol." 21 | [x] 22 | (satisfies? Startable x)) 23 | 24 | (defn stoppable? 25 | "Predicate for testing whether `x` satisfies the Stoppable protocol." 26 | [x] 27 | (satisfies? Stoppable x)) 28 | 29 | (defn queryable? 30 | "Predicate for testing whether `x` satisfies the Queryable protocol." 31 | [x] 32 | (satisfies? Queryable x)) 33 | -------------------------------------------------------------------------------- /src/cljx/com/palletops/leaven/schema.cljx: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven.schema 2 | "Optional schema support for leaven protocols." 3 | (:require 4 | [schema.core :as schema] 5 | [com.palletops.leaven.protocols :refer [startable? stoppable? queryable?]])) 6 | 7 | (def Startable 8 | (schema/pred startable? "startable?")) 9 | 10 | (def Stoppable 11 | (schema/pred stoppable? "stoppable?")) 12 | 13 | (def Queryable 14 | (schema/pred queryable? "status?")) 15 | -------------------------------------------------------------------------------- /test/clj/com/palletops/leaven/component_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven.component-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [com.palletops.leaven :as leaven] 5 | [com.palletops.leaven.component :refer :all] 6 | [com.palletops.leaven.protocols :as protocols] 7 | [com.stuartsierra.component :as component])) 8 | 9 | (defrecord C [state] 10 | component/Lifecycle 11 | (start [component] 12 | (assoc component :state :started)) 13 | (stop [component] 14 | (assoc component :state :stopped))) 15 | 16 | (extend-leaven C) 17 | 18 | (deftest component->leaven-test 19 | (let [c (C. :c)] 20 | (is (= :c (:state c))) 21 | (is (= :started (:state (leaven/start c)))) 22 | (is (= :stopped (:state (leaven/stop c)))))) 23 | 24 | (defrecord L [state] 25 | protocols/Startable 26 | (start [component] 27 | (assoc component :state :started)) 28 | protocols/Stoppable 29 | (stop [component] 30 | (assoc component :state :stopped))) 31 | 32 | (extend-component L) 33 | 34 | (deftest leaven->component-test 35 | (let [l (L. :l)] 36 | (is (= :l (:state l))) 37 | (is (= :started (:state (component/start l)))) 38 | (is (= :stopped (:state (component/stop l)))))) 39 | -------------------------------------------------------------------------------- /test/clj/com/palletops/leaven/readme_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven.readme-test 2 | (:require 3 | [com.palletops.leaven :as leaven] 4 | [com.palletops.leaven.protocols :refer [Startable Stoppable]] 5 | [clojure.core.async :as async] 6 | [clojure.test :refer :all])) 7 | 8 | ;; We define a component that will provide an increasing sequence of 9 | ;; numbers via a `core.async` channel. We implement the `Startable` 10 | ;; protocol for the component. 11 | 12 | (defrecord Counter [init-val channel loop-chan] 13 | Startable 14 | (start [component] 15 | (assoc component :loop-chan 16 | (async/go-loop [n init-val] 17 | (async/>! channel n) 18 | (recur (inc n))))) 19 | Stoppable 20 | (stop [component] 21 | (async/close! channel) 22 | (assoc component :loop-chan nil))) 23 | 24 | ;; Note that the record contains fields for both the configuration and 25 | ;; the runtime state of the component. 26 | 27 | ;; We instantiate the component with one of the record's constructor 28 | ;; functions. In this example we use a var to hold the component, but 29 | ;; this is in no way required. 30 | 31 | (def c (async/chan)) 32 | (def counter (map->Counter {:init-val 1 :channel c})) 33 | 34 | ;; We can start the component: 35 | 36 | (deftest counter-test 37 | (alter-var-root #'counter leaven/start) 38 | (is (:loop-chan counter)) 39 | (is (= 1 (async/!! ctrl-chan ::stop) 65 | (assoc component :loop-chan nil :ctrl-chan nil))) 66 | 67 | (leaven/defsystem Evens [:counter :doubler]) 68 | 69 | (defn evens [out-chan] 70 | (let [c1 (async/chan)] 71 | (Evens. 72 | (map->Counter {:init-val 1 :channel c1}) 73 | (map->Doubler {:in-chan c1 :out-chan out-chan})))) 74 | 75 | (def c2 (async/chan)) 76 | (def sys (evens c2)) 77 | 78 | (deftest evens-test 79 | (alter-var-root #'sys leaven/start) 80 | (is (= 2 (async/A))) 23 | (is (stoppable? (->A))) 24 | (is (queryable? (->A))) 25 | (testing "success" 26 | (is (schema/validate Startable (->A))) 27 | (is (schema/validate Stoppable (->A))) 28 | (is (schema/validate Queryable (->A)))) 29 | (testing "failure" 30 | (is (thrown? #+cljs js/Error #+clj Exception 31 | (schema/validate Startable {}))) 32 | (is (thrown? #+cljs js/Error #+clj Exception 33 | (schema/validate Stoppable {}))) 34 | (is (thrown? #+cljs js/Error #+clj Exception 35 | (schema/validate Queryable {}))))) 36 | -------------------------------------------------------------------------------- /test/cljx/com/palletops/leaven_test.cljx: -------------------------------------------------------------------------------- 1 | (ns com.palletops.leaven-test 2 | #+clj 3 | (:require 4 | [com.palletops.leaven :as leaven 5 | :refer [start stop defsystem startable? stoppable? update-components]] 6 | [com.palletops.leaven.protocols :refer [Startable Stoppable]] 7 | [clojure.test :refer [is deftest testing]]) 8 | #+cljs 9 | (:require-macros 10 | [cemerick.cljs.test :refer [is deftest testing]] 11 | [com.palletops.leaven :refer [defsystem]]) 12 | #+cljs 13 | (:require 14 | [com.palletops.leaven :as leaven 15 | :refer [start stop startable? stoppable? update-components]] 16 | [com.palletops.leaven.protocols :as impl :refer [Startable Stoppable]] 17 | [cemerick.cljs.test :as t])) 18 | 19 | (defrecord TestA [s] 20 | Startable 21 | (start [c] c) 22 | Stoppable 23 | (stop [c] c)) 24 | 25 | (deftest x 26 | (let [a (->TestA 1)] 27 | (is (= a (start a))) 28 | (is (= a (stop a))))) 29 | 30 | (defrecord TestB [start-a stop-a v] 31 | Startable 32 | (start [c] (update-in c [:start-a] swap! (fnil conj []) v)) 33 | Stoppable 34 | (stop [c] (update-in c [:stop-a] swap! (fnil conj []) v))) 35 | 36 | (defsystem TestSystem [:b1 :b2]) 37 | 38 | (defn test-system [] 39 | (let [start-a (atom []) 40 | stop-a (atom [])] 41 | [start-a stop-a (->TestSystem 42 | (->TestB start-a stop-a :b1) 43 | (->TestB start-a stop-a :b2))])) 44 | 45 | (deftest sequence-test 46 | (let [[start-a stop-a s] (test-system) 47 | s1 (start s)] 48 | (is (= [:b1 :b2] @start-a) "Starts in specified order") 49 | (is (= [] @stop-a) "Start doesn't stop") 50 | (let [s2 (stop s1)] 51 | (is (= [:b1 :b2] @start-a) "Stop propagates state") 52 | (is (= [:b2 :b1] @stop-a)) "Stops in reverse order"))) 53 | 54 | (defrecord TestThrow [] 55 | Startable 56 | (start [c] (throw (ex-info "start-failed" {}))) 57 | Stoppable 58 | (stop [c] (throw (ex-info "stop-failed" {})))) 59 | 60 | (defn test-throw-system [] 61 | (->TestSystem 62 | (->TestA :a) 63 | (->TestThrow))) 64 | 65 | (deftest exception-test 66 | (let [s (test-throw-system)] 67 | (is (thrown-with-msg? #+cljs js/Error #+clj Exception 68 | #"Exception while starting.*" 69 | (start s))) 70 | (is (thrown-with-msg? #+cljs js/Error #+clj Exception 71 | #"Exception while stopping.*" 72 | (stop s))) 73 | (try 74 | (start s) 75 | (catch #+cljs js/Error #+clj Exception e 76 | (let [{:keys [component system sub-components completed]} 77 | (ex-data e)] 78 | (is (= :b2 component) "Reports the failed component")))) 79 | (try 80 | (stop s) 81 | (catch #+cljs js/Error #+clj Exception e 82 | (let [{:keys [component system sub-components completed]} 83 | (ex-data e)] 84 | (is (= :b2 component) "Reports the failed component")))))) 85 | 86 | (defprotocol P (p [_] "return a :p")) 87 | (defsystem TestExtend [b1 b2] 88 | P 89 | (p [_] :p)) 90 | 91 | (deftest defsystem-extend-test 92 | (is (= :p (p (TestExtend. nil nil))))) 93 | 94 | 95 | (defrecord TestComp [v] 96 | Startable 97 | (start [component] 98 | (assoc component :v ::v))) 99 | 100 | (defsystem SystemOptionsTest [:b1 :b2] 101 | {:on-start {:b1 (update-components [:b2])} 102 | :on-stop {:b1 #(assoc-in %1 [%2 :v] nil)}}) 103 | 104 | (deftest defsystem-options-test 105 | (let [c (TestComp. nil) 106 | s (map->SystemOptionsTest {:b1 c 107 | :b2 {:b1 c}})] 108 | (is (nil? (get-in s [:b2 :b1 :v]))) 109 | (let [ss (start s)] 110 | (is (= ::v (get-in ss [:b2 :b1 :v]))) 111 | (let [st (stop ss)] 112 | (is (nil? (get-in st [:b1 :v]))) 113 | (is (= ::v (get-in ss [:b2 :b1 :v]))))))) 114 | 115 | (defsystem TestSystemOptions2 [:b1 :b2] 116 | {:on-start {:b1 (update-components {:b2 :b11})}}) 117 | 118 | (deftest defsystem-options-on-start-map-test 119 | (let [c (TestComp. nil) 120 | s (map->TestSystemOptions2 {:b1 c 121 | :b2 {:b11 c}}) 122 | ss (start s)] 123 | (is (= ::v (get-in ss [:b2 :b11 :v]))))) 124 | 125 | (defsystem TestSystemOptions3 [:b1 :b2] 126 | {:depends {:b2 [:b1]}}) 127 | 128 | (deftest defsystem-options-depends-vector-test 129 | (let [c (TestComp. nil) 130 | s (map->TestSystemOptions3 {:b1 c 131 | :b2 {:b1 c}}) 132 | ss (start s)] 133 | (is (= ::v (get-in ss [:b2 :b1 :v]))))) 134 | 135 | (defsystem TestSystemOptions4 [:b1 :b2] 136 | {:depends {:b2 {:b1 :b11}}}) 137 | 138 | (deftest defsystem-options-depends-map-test 139 | (let [c (TestComp. nil) 140 | s (map->TestSystemOptions4 {:b1 c 141 | :b2 {:b11 c}}) 142 | ss (start s)] 143 | (is (= ::v (get-in ss [:b2 :b11 :v]))))) 144 | --------------------------------------------------------------------------------