├── .gitignore ├── README.md ├── dev ├── example.clj └── user.clj ├── epl-v10.html ├── project.clj ├── src └── com │ └── redbrainlabs │ ├── system_graph.clj │ └── system_graph │ └── utils.clj └── test └── com └── redbrainlabs ├── system_graph └── utils_test.clj └── system_graph_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | .nrepl-port 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # system-graph 2 | 3 | 'system-graph' is a Clojure library for using Prismatic's [Graph] in large system composition. 4 | 5 | 'Graph' provides a form of dependency injection which does all of the hard work. 'system-graph' 6 | builds on top of this to allow Graphs to be compiled so that `SystemGraph`s are returned. These 7 | `SystemGraph`s implement a `Lifecycle` protocol that enables the components of the system to be 8 | started and shut down in a coordinated fashion. The beauty of using 'Graph' is that [the correct 9 | order] of the components is implicitly defined by the Graph's [Fnks]. 10 | 11 | [Graph]: https://github.com/Prismatic/plumbing#graph-the-functional-swiss-army-knife 12 | [Fnks]: https://github.com/Prismatic/plumbing#bring-on-defnk 13 | [the correct order]: http://en.wikipedia.org/wiki/Topological_sorting 14 | 15 | ## Releases and Dependency Information 16 | 17 | * Releases are published to [Clojars] 18 | 19 | * Latest stable release is [0.3.0](https://clojars.org/com.redbrainlabs/system-graph/versions/0.3.0) 20 | 21 | * [All releases](https://clojars.org/com.redbrainlabs/system-graph/versions) 22 | 23 | [Leiningen] dependency information: 24 | 25 | [com.redbrainlabs/system-graph "0.3.0"] 26 | 27 | [Maven] dependency information: 28 | 29 | 30 | com.redbrainlabs 31 | system-graph 32 | 0.3.0 33 | 34 | 35 | [Component]: https://github.com/stuartsierra/component 36 | [Clojars]: http://clojars.org/ 37 | [Leiningen]: http://leiningen.org/ 38 | [Maven]: http://maven.apache.org/ 39 | 40 | 41 | ## Compatibility 42 | 43 | Version 0.3.0 of 'system-graph' requires Clojure version 1.7.0 or later. 44 | 45 | Version 0.2.1 of 'system-graph' is compatible with Clojure versions 1.4.0 and higher. 46 | 47 | ## Introduction 48 | 49 | While dependency injection and containers that manage `Lifecycle` is [nothing][DI] [new][pico] this 50 | approach has been gaining traction in the Clojure community recently. Rather than adding [yet][jig] 51 | [another][teuta] `Lifecycle` protocol to the Clojure ecosystem 'system-graph' uses Stuart Sierra's 52 | [Component] library. Please *[read the documentation for Component][Component docs]* for an overview of this 53 | approach as well as some great advice on applying it within a Clojure application. 54 | 55 | [DI]: http://www.martinfowler.com/articles/injection.html 56 | [pico]: http://picocontainer.codehaus.org/ 57 | [jig]: https://github.com/juxt/jig#components 58 | [teuta]: https://github.com/vmarcinko/teuta#component-lifecycle 59 | [Component docs]: https://github.com/stuartsierra/component/blob/master/README.md#introduction 60 | 61 | ## Usage 62 | 63 | For a detailed walkthrough see the [example system]. Below is a quick walkthrough for people already familiar with 64 | [Graph] and [Component]. 65 | 66 | [example system]: https://github.com/RedBrainLabs/system-graph/blob/master/dev/example.clj 67 | 68 | ```clojure 69 | (ns your-app 70 | (:require [com.redbrainlabs.system-graph :as system-graph] 71 | [com.stuartsierra.component :refer [Lifecycle] :as lifecycle] 72 | [plumbing.core :refer [defnk fnk]])) 73 | 74 | ``` 75 | 76 | ### Create Components with fnk constructors 77 | 78 | ```clojure 79 | (defrecord ComponentA [foo] 80 | Lifecycle 81 | (start [this] 82 | (println "Starting Component A") 83 | this) 84 | (stop [this] 85 | (println "Stopping Component A") 86 | this)) 87 | 88 | (defnk component-A [foo] 89 | (->ComponentA foo)) 90 | 91 | (defrecord ComponentB [component-a bar] 92 | Lifecycle 93 | (start [this] 94 | ;; By this time ComponentA will have already started 95 | (println "Starting Component B") 96 | this) 97 | (stop [this] 98 | ;; This will be called before ComponentA is stopped 99 | (println "Stopping Component B") 100 | this)) 101 | 102 | (defnk component-B [component-a bar] 103 | (map->ComponentB {:component-a component-a :bar bar})) 104 | 105 | (defrecord ComponentC [component-a component-b baz] 106 | Lifecycle 107 | (start [this] 108 | (println "Starting Component C") 109 | this) 110 | (stop [this] 111 | (println "Stopping Component C") 112 | this)) 113 | 114 | (defnk component-c [component-a component-b baz] 115 | (map->ComponentC {:component-a component-a 116 | :component-b component-b 117 | :baz baz})) 118 | ``` 119 | 120 | ### Create your system-graph 121 | 122 | ```clojure 123 | 124 | ;; notice how the keys of the Graph need to be the same as the 125 | ;; named parameters of the fnks used withing the graph. 126 | (def system-graph 127 | {:component-a component-a 128 | :component-b component-b 129 | :component-c component-c}) 130 | 131 | ;; You can optionally compile your system-graph into a fnk. 132 | (def init-system (system-graph/eager-compile system-graph)) 133 | ``` 134 | 135 | ### Create a System from the system-graph 136 | 137 | ``` clojure 138 | (def system (init-system {:foo 42 :bar 23 :baz 90})) 139 | ;; #com.redbrainlabs.system_graph.SystemGraph{:component-a #foo.ComponentA{:foo 42}, ...} 140 | 141 | ;; Alternatively, you can skip the compilation step and use system-graph/init-system: 142 | (def system (system-graph/init-system system-graph {:foo 42 :bar 23 :baz 90})) 143 | ;; #com.redbrainlabs.system_graph.SystemGraph{:component-a #foo.ComponentA{:foo 42}, ...} 144 | ``` 145 | 146 | ### Start and stop the System 147 | 148 | ```clojure 149 | (alter-var-root #'system lifecycle/start) 150 | ;; Starting Component A 151 | ;; Starting Component B 152 | ;; Starting Component C 153 | ;; #com.redbrainlabs.system_graph.SystemGraph{:component-a #foo.ComponentA{:foo 42}, ...} 154 | 155 | (alter-var-root #'system lifecycle/stop) 156 | ;; Stopping Component C 157 | ;; Stopping Component B 158 | ;; Stopping Component A 159 | ;; #com.redbrainlabs.system_graph.SystemGraph{:component-a #foo.ComponentA{:foo 42}, ...} 160 | 161 | ``` 162 | 163 | Again, please see the [example system] for a more detailed explanation. 164 | 165 | ## Declaring dependencies 166 | 167 | The nice thing about using Graph is that the dependency graph is computed automatically 168 | using the fnks. One downside to this is that it requires that you place all of a component's 169 | dependencies in the fnk constructor. It also requires that the names of your components be 170 | consistent across Graphs and fnks. Contrast this to 'Component' where you have to explicitly 171 | list your component's dependencies out like so: 172 | 173 | ```clojure 174 | (component/using 175 | (example-component config-options) 176 | {:database :db 177 | :scheduler :scheduler}) 178 | ;; ^ ^ 179 | ;; | | 180 | ;; | \- Keys in the ExampleSystem record 181 | ;; | 182 | ;; \- Keys in the ExampleComponent record 183 | ``` 184 | 185 | While this may seem onerous it does allow you to list dependencies that aren't needed by 186 | your component but need to start before it can start (e.g. components that require that 187 | the database be put in a certain state). It also allows you to have context-specific names 188 | within each component. 189 | 190 | 'system-graph' provides the best of both worlds. If the name of your dependent component is 191 | one-to-one with your sytem then you do not need to do anything. If you would like to have 192 | context-specific names within a component, like the example above, then use 'component's 193 | API on your fnk constructor like so: 194 | 195 | ```clojure 196 | (component/using 197 | fnk-that-creates-example-component 198 | {:database :db}) 199 | ;; ^ ^ 200 | ;; | | 201 | ;; | \- Keys in the ExampleSystem record 202 | ;; | 203 | ;; \- Keys in the ExampleComponent record 204 | ``` 205 | 206 | Here is a full example: 207 | 208 | ```clojure 209 | (ns repl-example 210 | (:require [com.stuartsierra.component :refer [Lifecycle] :as component] 211 | [com.redbrainlabs.system-graph :as system-graph] 212 | [plumbing.core :refer [fnk]])) 213 | 214 | 215 | (defrecord DummyComponent [name started] 216 | Lifecycle 217 | (start [this] (assoc this :started true)) 218 | (stop [this] (assoc this :started false))) 219 | 220 | (defn dummy-component [name] 221 | (->DummyComponent name false)) 222 | 223 | (def graph {:a (fnk [] 224 | (dummy-component :a)) 225 | :b (-> (fnk [a] 226 | (-> (dummy-component :b) 227 | (assoc :foo a))) 228 | (component/using {:foo :a}))}) 229 | ;;; ^ note how we are 'using' on our fnk 230 | 231 | (def init (system-graph/eager-compile graph)) 232 | 233 | (def system (init {})) 234 | ;; => #com.stuartsierra.component.SystemMap{ 235 | ;; :a #repl_example.DummyComponent{:name :a, :started false} 236 | ;; :b #repl_example.DummyComponent{:name :b, :started false, 237 | ;; :foo #repl_example.DummyComponent{:name :a, :started false}}} 238 | 239 | ;; note ^ how the :foo in :b is :a and has not been started! 240 | 241 | (def started-system (component/start system)) 242 | ;; => #com.stuartsierra.component.SystemMap{ 243 | ;; :a #repl_example.DummyComponent{:name :a, :started true} 244 | ;; :b #repl_example.DummyComponent{:name :b, :started true, 245 | ;; :foo #repl_example.DummyComponent{:name :a, :started true}}} 246 | 247 | ;; ^ before start of :b was called the :a was started and then assoc'ed onto :b as :foo 248 | ``` 249 | 250 | ## References / More Information 251 | 252 | * [Graph: Abstractions for Structured Computation](http:blog.getprismatic.com/blog/2013/2/1/graph-abstractions-for-structured-computation) 253 | * [Graph: composable production systems in Clojure](http://www.infoq.com/presentations/Graph-Clojure-Prismatic) (video) 254 | * [Slides from talk](https://github.com/strangeloop/strangeloop2012/raw/master/slides/sessions/Wolfe-Graph.pdf) (PDF) 255 | * [Prismatic's "Graph" at Strange Loop](http://blog.getprismatic.com/blog/2012/10/1/prismatics-graph-at-strange-loop.html) 256 | * [Component's Documentation][Component docs] 257 | * [Clojure in the Large](http://www.infoq.com/presentations/Clojure-Large-scale-patterns-techniques) (video) 258 | * [My Clojure Workflow, Reloaded](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) 259 | * [reloaded](https://github.com/stuartsierra/reloaded) Leiningen template 260 | 261 | 262 | ## Change Log 263 | 264 | * Version [0.3.0] released on September 24, 2015 265 | * Upgraded to 'plumbing' 0.5.0 (latest version of Schema, yay!) 266 | * Upgraded to 'component' 0.3.0 - this requires clojure 1.7.0 or higher! 267 | * Version [0.2.1] released on May 18, 2015 268 | * Upgraded to 'plumbing' 0.4.3 269 | * Version [0.2.0] released on April 27, 2014 270 | * Upgraded to 'plumbing' 0.2.2 and 'component' 0.2.1 271 | * Using 'component's generic `SystemMap`. Got rid of `SystemGraph` wrapper. 272 | * Fixed [#2] where dependent components were not assoced onto the requiring 273 | component before being started. 'system-graph' is now using 274 | `component/using` as it should to declare the dependencies. 275 | * 'component' metadata (via `component/using`) on fnks are propogated to 276 | resulting components. This allows for context-specific names and for 277 | declaring side-effecty deps that aren't really needed in the component. 278 | * Version [0.1.0] released on November 4, 2013 279 | 280 | [#2]: https://github.com/RedBrainLabs/system-graph/issues/2 281 | [0.1.0]: https://github.com/redbrianlabs/system-graph/tree/system-graph-0.1.0 282 | [0.2.0]: https://github.com/redbrianlabs/system-graph/tree/system-graph-0.2.0 283 | [0.2.1]: https://github.com/redbrianlabs/system-graph/tree/system-graph-0.2.1 284 | 285 | 286 | ## Copyright and License 287 | 288 | Copyright © 2013 Ben Mabey and Red Brain Labs, All rights reserved. 289 | 290 | The use and distribution terms for this software are covered by the 291 | [Eclipse Public License 1.0] which can be found in the file 292 | epl-v10.html at the root of this distribution. By using this software 293 | in any fashion, you are agreeing to be bound by the terms of this 294 | license. You must not remove this notice, or any other, from this 295 | software. 296 | 297 | [Eclipse Public License 1.0]: http://opensource.org/licenses/eclipse-1.0.php 298 | -------------------------------------------------------------------------------- /dev/example.clj: -------------------------------------------------------------------------------- 1 | ;; The majority of this file originated from Component's example which 2 | ;; is MIT licensed by Stuart Sierra. 3 | ;; https://github.com/stuartsierra/component/blob/master/dev/examples.clj 4 | (ns example 5 | (:require [com.redbrainlabs.system-graph :as system-graph] 6 | [com.stuartsierra.component :as component] 7 | [plumbing.core :refer [defnk fnk]] 8 | [schema 9 | [core :as s] 10 | [macros :as sm]])) 11 | 12 | ;;; Schema Configs 13 | 14 | (def DBConfig {(s/required-key :host) String 15 | (s/required-key :port) Number}) 16 | 17 | (def ExampleComponentConfig {:foo s/Any :bar s/Any}) 18 | 19 | (def Config {:db DBConfig 20 | :example-component ExampleComponentConfig}) 21 | 22 | ;;; Dummy functions to use in the examples 23 | 24 | (defn connect-to-database [host port] 25 | (println ";; Opening database connection") 26 | (reify java.io.Closeable 27 | (close [_] (println ";; Closing database connection")))) 28 | 29 | (defn execute-query [& _] 30 | (println ";; execute-query")) 31 | 32 | (defn execute-insert [& _] 33 | (println ";; execute-insert")) 34 | 35 | ;; this constructor is a fnk so it can be used in a graph below 36 | (defnk new-scheduler [] 37 | (reify component/Lifecycle 38 | (start [this] 39 | (println ";; Starting scheduler") 40 | this) 41 | (stop [this] 42 | (println ";; Stopping scheduler") 43 | this))) 44 | 45 | 46 | ;;; Example database component 47 | 48 | ;; To define a component, define a Clojure record that implements the 49 | ;; `Lifecycle` protocol. 50 | 51 | (defrecord Database [host port connection] 52 | ;; Implement the Lifecycle protocol 53 | component/Lifecycle 54 | 55 | (start [component] 56 | (println ";; Starting database") 57 | ;; In the 'start' method, initialize this component 58 | ;; and start it running. For example, connect to a 59 | ;; database, create thread pools, or initialize shared 60 | ;; state. 61 | (let [conn (connect-to-database host port)] 62 | ;; Return an updated version of the component with 63 | ;; the run-time state assoc'd in. 64 | (assoc component :connection conn))) 65 | 66 | (stop [component] 67 | (println ";; Stopping database") 68 | ;; In the 'stop' method, shut down the running 69 | ;; component and release any external resources it has 70 | ;; acquired. 71 | (.close connection) 72 | ;; Return the component, optionally modified. 73 | component)) 74 | 75 | ;; Provide a constructor function that takes in the essential 76 | ;; configuration parameters of the component, leaving the 77 | ;; runtime state blank. 78 | 79 | ;; To play nicely with Graph this constructor function should 80 | ;; be a fnk. Note how we are using the nested-map destructuring 81 | ;; syntax provided by Graph. This is of course optional but 82 | ;; we've found it useful for configuration maps. 83 | 84 | (defnk new-database [[:config db]] 85 | (s/validate DBConfig db) 86 | (map->Database db)) 87 | 88 | ;; Define the functions implementing the behavior of the 89 | ;; component to take the component itself as an argument. 90 | 91 | (defn get-user [database username] 92 | (execute-query (:connection database) 93 | "SELECT * FROM users WHERE username = ?" 94 | username)) 95 | 96 | (defn add-user [database username favorite-color] 97 | (execute-insert (:connection database) 98 | "INSERT INTO users (username, favorite_color)" 99 | username favorite-color)) 100 | 101 | 102 | ;;; Second Example Component 103 | 104 | ;; Define other components in terms of the components on which they 105 | ;; depend. 106 | 107 | (defrecord ExampleComponent [options cache database scheduler] 108 | component/Lifecycle 109 | 110 | (start [this] 111 | (println ";; Starting ExampleComponent") 112 | ;; In the 'start' method, a component may assume that its 113 | ;; dependencies are available and have already been started. 114 | (assoc this :admin (get-user database "admin"))) 115 | 116 | (stop [this] 117 | (println ";; Stopping ExampleComponent") 118 | ;; Likewise, in the 'stop' method, a component may assume that its 119 | ;; dependencies will not be stopped until AFTER it is stopped. 120 | this)) 121 | 122 | ;; Since we are using the dependency graph built up by fnk constructors 123 | ;; each constructor should have its needed deps declared in the signature. 124 | ;; In general, the constructor should not depend on other components 125 | ;; being started. 126 | 127 | (defnk new-example-component [database scheduler [:config example-component]] 128 | (s/validate ExampleComponentConfig example-component) 129 | (map->ExampleComponent {:options example-component 130 | :database database 131 | :scheduler scheduler 132 | :cache (atom {})})) 133 | 134 | 135 | ;;; Example System 136 | 137 | ;; Components are composed into systems. A system is a component which 138 | ;; knows how to start and stop other components. 139 | 140 | ;; Now the fun begins! Instead of specifying the dependency links manually 141 | ;; we can place all of our fnks into a single Graph and let it work out 142 | ;; the dependency graph for us. As long as we have been consistent with 143 | ;; our naming of the components across the various fnks everything will 144 | ;; be injected for us. 145 | 146 | (def example-graph 147 | {:database new-database 148 | :scheduler new-scheduler 149 | :example-component new-example-component}) 150 | 151 | ;; You can optionaly compile the Graph into a fnk. 152 | (def example-system (system-graph/eager-compile example-graph)) 153 | 154 | ;; stub out a config loader 155 | (defn load-config [] 156 | {:config 157 | {:db {:host "dbhost.com" :port 123} 158 | :example-component {:foo 42 :bar 22}}}) 159 | 160 | ;; Sample usage: 161 | (comment 162 | 163 | ;; We can see the Schema our system returns and takes (if you're on the latest SNAPSHOT): 164 | (s/fn-schema example-system) 165 | ;; (=> {:example-component Any, :scheduler Any, :db Any} 166 | ;; {:config {:example-component Any, :db Any}}) 167 | 168 | ;; BTW, once Schema has been fully integrated into fnk our 169 | ;; more detailed Schemas that we defined at the top of this file 170 | ;; will be able to be used giving us great documentation on what 171 | ;; a system requires for initialization. 172 | 173 | ;; Lets use the compiled fnk we made from above.. 174 | (def system (example-system (load-config))) 175 | ;;=> #'examples/system 176 | (class system) 177 | ;;=> #'examples/system 178 | ;; com.redbrainlabs.system_graph.SystemGraph 179 | 180 | (alter-var-root #'system component/start) 181 | ;; Starting database 182 | ;; Opening database connection 183 | ;; Starting scheduler 184 | ;; Starting ExampleComponent 185 | ;; execute-query 186 | ;;=> #com.redbrainlabs.system_graph.SystemGraph{ ... } 187 | 188 | (alter-var-root #'system component/stop) 189 | ;; Stopping ExampleComponent 190 | ;; Stopping scheduler 191 | ;; Stopping database 192 | ;; Closing database connection 193 | ;;=> #com.redbrainlabs.system_graph.SystemGraph{ ... } 194 | ) 195 | 196 | ;; Local Variables: 197 | ;; clojure-defun-style-default-indent: t 198 | ;; End: 199 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Tools for interactive development with the REPL. This file should 3 | not be included in a production build of the application." 4 | (:require 5 | [clojure.java.io :as io] 6 | [clojure.java.javadoc :refer (javadoc)] 7 | [clojure.pprint :refer (pprint print-table)] 8 | [clojure.reflect :refer (reflect)] 9 | [clojure.repl :refer (apropos dir doc find-doc pst source)] 10 | [clojure.set :as set] 11 | [clojure.string :as str] 12 | [clojure.tools.namespace.repl :refer (refresh refresh-all)] 13 | 14 | [plumbing.core :refer :all] 15 | [midje.repl :as test] 16 | 17 | [com.redbrainlabs.system-graph])) 18 | 19 | (def system 20 | "A Var containing an object representing the application under 21 | development." 22 | nil) 23 | 24 | (defn init 25 | "Creates and initializes the system under development in the Var 26 | #'system." 27 | [] 28 | ;; TODO 29 | ) 30 | 31 | (defn start 32 | "Starts the system running, updates the Var #'system." 33 | [] 34 | ;; TODO 35 | ) 36 | 37 | (defn stop 38 | "Stops the system if it is currently running, updates the Var 39 | #'system." 40 | [] 41 | ;; TODO 42 | ) 43 | 44 | (defn go 45 | "Initializes and starts the system running." 46 | [] 47 | (init) 48 | (start) 49 | :ready) 50 | 51 | (defn reset 52 | "Stops the system, reloads modified source files, and restarts it." 53 | [] 54 | (stop) 55 | (refresh :after 'user/go)) 56 | -------------------------------------------------------------------------------- /epl-v10.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eclipse Public License - Version 1.0 6 | 23 | 24 | 25 | 26 | 27 | 28 |

Eclipse Public License - v 1.0

29 | 30 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 31 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 32 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 33 | AGREEMENT.

34 | 35 |

1. DEFINITIONS

36 | 37 |

"Contribution" means:

38 | 39 |

a) in the case of the initial Contributor, the initial 40 | code and documentation distributed under this Agreement, and

41 |

b) in the case of each subsequent Contributor:

42 |

i) changes to the Program, and

43 |

ii) additions to the Program;

44 |

where such changes and/or additions to the Program 45 | originate from and are distributed by that particular Contributor. A 46 | Contribution 'originates' from a Contributor if it was added to the 47 | Program by such Contributor itself or anyone acting on such 48 | Contributor's behalf. Contributions do not include additions to the 49 | Program which: (i) are separate modules of software distributed in 50 | conjunction with the Program under their own license agreement, and (ii) 51 | are not derivative works of the Program.

52 | 53 |

"Contributor" means any person or entity that distributes 54 | the Program.

55 | 56 |

"Licensed Patents" mean patent claims licensable by a 57 | Contributor which are necessarily infringed by the use or sale of its 58 | Contribution alone or when combined with the Program.

59 | 60 |

"Program" means the Contributions distributed in accordance 61 | with this Agreement.

62 | 63 |

"Recipient" means anyone who receives the Program under 64 | this Agreement, including all Contributors.

65 | 66 |

2. GRANT OF RIGHTS

67 | 68 |

a) Subject to the terms of this Agreement, each 69 | Contributor hereby grants Recipient a non-exclusive, worldwide, 70 | royalty-free copyright license to reproduce, prepare derivative works 71 | of, publicly display, publicly perform, distribute and sublicense the 72 | Contribution of such Contributor, if any, and such derivative works, in 73 | source code and object code form.

74 | 75 |

b) Subject to the terms of this Agreement, each 76 | Contributor hereby grants Recipient a non-exclusive, worldwide, 77 | royalty-free patent license under Licensed Patents to make, use, sell, 78 | offer to sell, import and otherwise transfer the Contribution of such 79 | Contributor, if any, in source code and object code form. This patent 80 | license shall apply to the combination of the Contribution and the 81 | Program if, at the time the Contribution is added by the Contributor, 82 | such addition of the Contribution causes such combination to be covered 83 | by the Licensed Patents. The patent license shall not apply to any other 84 | combinations which include the Contribution. No hardware per se is 85 | licensed hereunder.

86 | 87 |

c) Recipient understands that although each Contributor 88 | grants the licenses to its Contributions set forth herein, no assurances 89 | are provided by any Contributor that the Program does not infringe the 90 | patent or other intellectual property rights of any other entity. Each 91 | Contributor disclaims any liability to Recipient for claims brought by 92 | any other entity based on infringement of intellectual property rights 93 | or otherwise. As a condition to exercising the rights and licenses 94 | granted hereunder, each Recipient hereby assumes sole responsibility to 95 | secure any other intellectual property rights needed, if any. For 96 | example, if a third party patent license is required to allow Recipient 97 | to distribute the Program, it is Recipient's responsibility to acquire 98 | that license before distributing the Program.

99 | 100 |

d) Each Contributor represents that to its knowledge it 101 | has sufficient copyright rights in its Contribution, if any, to grant 102 | the copyright license set forth in this Agreement.

103 | 104 |

3. REQUIREMENTS

105 | 106 |

A Contributor may choose to distribute the Program in object code 107 | form under its own license agreement, provided that:

108 | 109 |

a) it complies with the terms and conditions of this 110 | Agreement; and

111 | 112 |

b) its license agreement:

113 | 114 |

i) effectively disclaims on behalf of all Contributors 115 | all warranties and conditions, express and implied, including warranties 116 | or conditions of title and non-infringement, and implied warranties or 117 | conditions of merchantability and fitness for a particular purpose;

118 | 119 |

ii) effectively excludes on behalf of all Contributors 120 | all liability for damages, including direct, indirect, special, 121 | incidental and consequential damages, such as lost profits;

122 | 123 |

iii) states that any provisions which differ from this 124 | Agreement are offered by that Contributor alone and not by any other 125 | party; and

126 | 127 |

iv) states that source code for the Program is available 128 | from such Contributor, and informs licensees how to obtain it in a 129 | reasonable manner on or through a medium customarily used for software 130 | exchange.

131 | 132 |

When the Program is made available in source code form:

133 | 134 |

a) it must be made available under this Agreement; and

135 | 136 |

b) a copy of this Agreement must be included with each 137 | copy of the Program.

138 | 139 |

Contributors may not remove or alter any copyright notices contained 140 | within the Program.

141 | 142 |

Each Contributor must identify itself as the originator of its 143 | Contribution, if any, in a manner that reasonably allows subsequent 144 | Recipients to identify the originator of the Contribution.

145 | 146 |

4. COMMERCIAL DISTRIBUTION

147 | 148 |

Commercial distributors of software may accept certain 149 | responsibilities with respect to end users, business partners and the 150 | like. While this license is intended to facilitate the commercial use of 151 | the Program, the Contributor who includes the Program in a commercial 152 | product offering should do so in a manner which does not create 153 | potential liability for other Contributors. Therefore, if a Contributor 154 | includes the Program in a commercial product offering, such Contributor 155 | ("Commercial Contributor") hereby agrees to defend and 156 | indemnify every other Contributor ("Indemnified Contributor") 157 | against any losses, damages and costs (collectively "Losses") 158 | arising from claims, lawsuits and other legal actions brought by a third 159 | party against the Indemnified Contributor to the extent caused by the 160 | acts or omissions of such Commercial Contributor in connection with its 161 | distribution of the Program in a commercial product offering. The 162 | obligations in this section do not apply to any claims or Losses 163 | relating to any actual or alleged intellectual property infringement. In 164 | order to qualify, an Indemnified Contributor must: a) promptly notify 165 | the Commercial Contributor in writing of such claim, and b) allow the 166 | Commercial Contributor to control, and cooperate with the Commercial 167 | Contributor in, the defense and any related settlement negotiations. The 168 | Indemnified Contributor may participate in any such claim at its own 169 | expense.

170 | 171 |

For example, a Contributor might include the Program in a commercial 172 | product offering, Product X. That Contributor is then a Commercial 173 | Contributor. If that Commercial Contributor then makes performance 174 | claims, or offers warranties related to Product X, those performance 175 | claims and warranties are such Commercial Contributor's responsibility 176 | alone. Under this section, the Commercial Contributor would have to 177 | defend claims against the other Contributors related to those 178 | performance claims and warranties, and if a court requires any other 179 | Contributor to pay any damages as a result, the Commercial Contributor 180 | must pay those damages.

181 | 182 |

5. NO WARRANTY

183 | 184 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 185 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 186 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 187 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 188 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 189 | responsible for determining the appropriateness of using and 190 | distributing the Program and assumes all risks associated with its 191 | exercise of rights under this Agreement , including but not limited to 192 | the risks and costs of program errors, compliance with applicable laws, 193 | damage to or loss of data, programs or equipment, and unavailability or 194 | interruption of operations.

195 | 196 |

6. DISCLAIMER OF LIABILITY

197 | 198 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 199 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 200 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 201 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 202 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 203 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 204 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 205 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

206 | 207 |

7. GENERAL

208 | 209 |

If any provision of this Agreement is invalid or unenforceable under 210 | applicable law, it shall not affect the validity or enforceability of 211 | the remainder of the terms of this Agreement, and without further action 212 | by the parties hereto, such provision shall be reformed to the minimum 213 | extent necessary to make such provision valid and enforceable.

214 | 215 |

If Recipient institutes patent litigation against any entity 216 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 217 | Program itself (excluding combinations of the Program with other 218 | software or hardware) infringes such Recipient's patent(s), then such 219 | Recipient's rights granted under Section 2(b) shall terminate as of the 220 | date such litigation is filed.

221 | 222 |

All Recipient's rights under this Agreement shall terminate if it 223 | fails to comply with any of the material terms or conditions of this 224 | Agreement and does not cure such failure in a reasonable period of time 225 | after becoming aware of such noncompliance. If all Recipient's rights 226 | under this Agreement terminate, Recipient agrees to cease use and 227 | distribution of the Program as soon as reasonably practicable. However, 228 | Recipient's obligations under this Agreement and any licenses granted by 229 | Recipient relating to the Program shall continue and survive.

230 | 231 |

Everyone is permitted to copy and distribute copies of this 232 | Agreement, but in order to avoid inconsistency the Agreement is 233 | copyrighted and may only be modified in the following manner. The 234 | Agreement Steward reserves the right to publish new versions (including 235 | revisions) of this Agreement from time to time. No one other than the 236 | Agreement Steward has the right to modify this Agreement. The Eclipse 237 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 238 | assign the responsibility to serve as the Agreement Steward to a 239 | suitable separate entity. Each new version of the Agreement will be 240 | given a distinguishing version number. The Program (including 241 | Contributions) may always be distributed subject to the version of the 242 | Agreement under which it was received. In addition, after a new version 243 | of the Agreement is published, Contributor may elect to distribute the 244 | Program (including its Contributions) under the new version. Except as 245 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 246 | rights or licenses to the intellectual property of any Contributor under 247 | this Agreement, whether expressly, by implication, estoppel or 248 | otherwise. All rights in the Program not expressly granted under this 249 | Agreement are reserved.

250 | 251 |

This Agreement is governed by the laws of the State of New York and 252 | the intellectual property laws of the United States of America. No party 253 | to this Agreement will bring a legal action under this Agreement more 254 | than one year after the cause of action arose. Each party waives its 255 | rights to a jury trial in any resulting litigation.

256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.redbrainlabs/system-graph "0.3.0" 2 | :description "Graph + Component for large system composition" 3 | :url "https://github.com/redbrainlabs/system-graph" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[com.stuartsierra/component "0.3.0"] 7 | [prismatic/plumbing "0.5.0"]] 8 | :profiles {:dev {:plugins [[lein-midje "3.1.3-RC2"]] 9 | :dependencies [[org.clojure/tools.namespace "0.2.4"] 10 | [org.clojure/clojure "1.7.0"] 11 | [midje "1.7.0"]] 12 | :source-paths ["dev"]}} 13 | :aliases {"dumbrepl" ["trampoline" "run" "-m" "clojure.main/main"]}) 14 | -------------------------------------------------------------------------------- /src/com/redbrainlabs/system_graph.clj: -------------------------------------------------------------------------------- 1 | (ns com.redbrainlabs.system-graph 2 | (:require [clojure.set :as set] 3 | 4 | [com.stuartsierra.component :refer [Lifecycle] :as component] 5 | [plumbing.core :as plumbing] 6 | [plumbing.graph :as graph] 7 | [schema.core :as s] 8 | 9 | [com.redbrainlabs.system-graph.utils :refer [topo-sort comp-fnk fnk-deps]])) 10 | 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | ;;; Private 13 | 14 | (defn- lifecycle-components 15 | "Filters the component-keys to only the vals of computed-system that satisfy Lifecycle. 16 | The filtering preseves the ordering of the original component-keys." 17 | [component-keys computed-system] 18 | (->> component-keys 19 | (map (fn [k] [k (get computed-system k)])) 20 | (filter (fn [[k component]] 21 | (and 22 | (satisfies? Lifecycle component) 23 | ;; this check is needed since 'component' now includes a default 24 | ;; impl on Object... but there is an implicit requirement that 25 | ;; the component is an IObj for metadata support. 26 | (isa? (class component) clojure.lang.IObj)))) 27 | (mapv first))) 28 | 29 | (def dependencies :com.stuartsierra.component/dependencies) 30 | 31 | (defn- attach-component-metadata [computed-system original-graph compiled-fnk lifecycle-comps] 32 | (let [lifecycle-comps (set lifecycle-comps) 33 | lifecycle-deps (->> lifecycle-comps 34 | (map (fn [k] 35 | (let [original-fnk (get original-graph k) 36 | original-dep-metadata (-> original-fnk meta dependencies) 37 | inferred-deps (-> original-fnk 38 | fnk-deps 39 | set 40 | (set/intersection lifecycle-comps) 41 | (set/difference (-> original-dep-metadata vals set)))] 42 | (merge (zipmap inferred-deps inferred-deps) original-dep-metadata)))) 43 | (zipmap lifecycle-comps))] 44 | (reduce-kv (fn [m* k deps] 45 | (update-in m* [k] component/using deps)) 46 | computed-system lifecycle-deps))) 47 | 48 | (defn- create-system-map [original-graph compiled-fnk computed-system] 49 | (let [lifecycle-comps (lifecycle-components (topo-sort original-graph) computed-system)] 50 | (-> computed-system 51 | (attach-component-metadata original-graph compiled-fnk lifecycle-comps) 52 | component/map->SystemMap))) 53 | 54 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 55 | ;;; Public 56 | 57 | (defn compile-as-system-graph 58 | "Compiles a Prismatic graph using `compile` and wraps the functionality so that 59 | the resulting fnk will return a 'component' SystemMap that complies with Lifecycle. 60 | 61 | All of the dependency information implicyt with the graph and fnks is carried over 62 | into the SystemMap and component vals using `component/using`. " 63 | [compile g] 64 | (if (fn? g) 65 | g 66 | (let [g (-> (plumbing/map-vals (partial compile-as-system-graph compile) g) graph/->graph) 67 | fnk (compile g)] 68 | (comp-fnk (partial create-system-map g fnk) fnk)))) 69 | 70 | (def eager-compile 71 | "Performs a #'plumbing.graph/eager-compile on the graph so that all the 72 | computed results from the compiled fnk are SystemGraphs which satisfy Lifecycle." 73 | (partial compile-as-system-graph graph/eager-compile)) 74 | 75 | (def eager-interpreted 76 | "Performs a #'plumbing.graph/eager-compile on the graph so that all the 77 | computed results from the compiled fnk are SystemGraphs which satisfy Lifecycle." 78 | (partial compile-as-system-graph graph/interpreted-eager-compile)) 79 | 80 | (defn init-system 81 | "Analogous to #'plumbing.graph/run, initializes the graph as a SystemGraph with the given input. " 82 | [g input] 83 | ((eager-interpreted g) input)) 84 | 85 | (defn start-system 86 | "Recursively starts the system components in the correct order as implicity defined in the graph." 87 | [system-graph] 88 | (component/start-system system-graph)) 89 | 90 | (defn stop-system 91 | "Recursively stops the system components in the reverse order in which they were started." 92 | [system-graph] 93 | (component/stop-system system-graph)) 94 | -------------------------------------------------------------------------------- /src/com/redbrainlabs/system_graph/utils.clj: -------------------------------------------------------------------------------- 1 | (ns com.redbrainlabs.system-graph.utils 2 | "Utillity fns for working with Prismatic's Graph and fnk" 3 | (:require [plumbing.graph :as graph] 4 | [schema.core :as s])) 5 | 6 | (defn topo-sort 7 | "Returns the topological sort of a Prismatic graph" 8 | [g] 9 | (-> g graph/->graph keys vec)) 10 | 11 | ;; TODO: see if plumbing already has a fn for this... or helper fns that seem less intrusive.. 12 | (defn fnk-deps [fnk] 13 | (->> fnk 14 | s/fn-schema 15 | :input-schemas ;; [[#schema.core.One{:schema {Keyword Any, :funk Any, :x Any}, :optional? false, :name arg0}]] 16 | ffirst 17 | :schema 18 | keys 19 | (filter keyword?))) 20 | 21 | ;; TODO: see if plumbing's `comp-partial` does what I need (as suggested by Jason Wolfe) 22 | (defn comp-fnk 23 | "Composes the given given fnk with the provided fn. Only handles the binary case." 24 | [f fnk] 25 | ;; TODO: handle other fnks (verifying input/output schemas) and the variadic case 26 | (let [comped (-> (comp f fnk) 27 | (with-meta (meta fnk)))] 28 | ;; compose the positional function as well if present 29 | (if [(-> fnk meta :plumbing.fnk.impl/positional-info)] 30 | (vary-meta comped update-in [:plumbing.fnk.impl/positional-info 0] (partial comp f)) 31 | comped))) 32 | -------------------------------------------------------------------------------- /test/com/redbrainlabs/system_graph/utils_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.redbrainlabs.system-graph.utils-test 2 | (:require [midje.sweet :refer :all] 3 | [plumbing.core :refer [fnk]] 4 | [plumbing.graph :as graph] 5 | [schema.core :as s] 6 | 7 | [com.redbrainlabs.system-graph.utils :refer :all])) 8 | 9 | (facts "#'topo-sort" 10 | ;; sanity/characteristic test since we are relying on Graph's impl.. 11 | (topo-sort {:a (fnk [x] (inc x)) 12 | :b (fnk [a] (inc a)) 13 | :c (fnk [b] (inc b))}) => [:a :b :c]) 14 | 15 | (facts "#'comp-fnk" 16 | (let [square-fnk (fnk [x] (* x x)) 17 | with-inc (comp-fnk inc square-fnk)] 18 | 19 | (fact "composes the regular fn with the fnk" 20 | (with-inc {:x 5}) => 26) 21 | 22 | (fact "composes the positional fn in the metadata as well" 23 | ((-> with-inc meta :plumbing.fnk.impl/positional-info first) 5) => 26) 24 | 25 | (fact "preserves the original fnk's schema" 26 | (s/fn-schema with-inc) => (s/fn-schema square-fnk)))) 27 | 28 | (facts "#'fnk-deps" 29 | (fnk-deps (fnk [a b c])) => (just [:a :b :c] :in-any-order) 30 | (fact "works on compiled graphs" 31 | (-> {:a (fnk [x] x) :b (fnk [y] y)} graph/eager-compile fnk-deps) => (just [:x :y] :in-any-order))) 32 | -------------------------------------------------------------------------------- /test/com/redbrainlabs/system_graph_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.redbrainlabs.system-graph-test 2 | (:require [com.stuartsierra.component :refer [Lifecycle] :as component] 3 | [plumbing.core :refer [fnk]] 4 | [midje.sweet :refer :all] 5 | 6 | [com.redbrainlabs.system-graph :refer :all])) 7 | 8 | 9 | (def lifecycle-sort :com.redbrainlabs.system-graph/lifecycle-sort) 10 | 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | ;;; Integration tests... since this library is 100% glue 13 | 14 | (defrecord Lifecycler [!started !stopped key value] 15 | Lifecycle 16 | (start [this] 17 | (swap! !started conj key) 18 | this) 19 | (stop [this] 20 | (swap! !stopped conj key) 21 | this)) 22 | 23 | (facts "system-graph" 24 | (let [deps-started (atom []) 25 | deps-stopped (atom []) 26 | lifecycler (partial ->Lifecycler deps-started deps-stopped) 27 | subgraph {:x-squared (fnk [x] (lifecycler :x-squared (* x x))) 28 | :x-cubed (fnk [x x-squared] ;;(prn {:x x :x-squared x-squared}) 29 | (lifecycler :x-cubed (* x (:value x-squared))))} 30 | graph {:subgraph subgraph 31 | :y (fnk [[:subgraph x-cubed]] (lifecycler :y (inc (:value x-cubed)))) 32 | :x-inc (fnk [x] (lifecycler :x-inc (inc x))) 33 | :no-lifecycle (fnk [x-inc] :something-that-doesnt-implement-lifecycle)} 34 | system-graph (init-system graph {:x 4})] 35 | 36 | (fact "starts the lifecycle deps using a toposort" 37 | (component/start system-graph) 38 | @deps-started => [:x-squared :x-cubed :y :x-inc]) 39 | (fact "stops the deps in the opposite order" 40 | (component/stop system-graph) 41 | @deps-stopped => [:x-inc :y :x-cubed :x-squared]))) 42 | 43 | (defrecord DummyComponent [name started] 44 | Lifecycle 45 | (start [this] (assoc this :started true)) 46 | (stop [this] (assoc this :started false))) 47 | 48 | (defn dummy-component [name] 49 | (->DummyComponent name false)) 50 | 51 | (facts "dependent components" 52 | (let [graph {:a (fnk [] 53 | (dummy-component :a)) 54 | :b (fnk [a] 55 | (-> (dummy-component :b) 56 | (assoc :a a)))} 57 | system-graph (init-system graph {}) 58 | started-system (start-system system-graph)] 59 | (facts "are passed in to fnks before they are started" 60 | (:b system-graph) => (just {:a {:name :a, :started false}, :name :b, :started false})) 61 | (facts "are started and assoced onto lifecycle components before then dependent's #'start is called" 62 | (:b started-system) => (just {:a {:name :a, :started true}, :name :b, :started true})))) 63 | 64 | (facts "dependent components with different names that the system's names" 65 | (let [graph {:a (fnk [] 66 | (dummy-component :a)) 67 | :b (-> (fnk [a] 68 | (-> (dummy-component :b) 69 | (assoc :foo a))) 70 | (component/using {:foo :a}))} 71 | system-graph (init-system graph {}) 72 | started-system (start-system system-graph)] 73 | (facts "are passed in to fnks before they are started" 74 | (:b system-graph) => (just {:foo {:name :a, :started false}, :name :b, :started false})) 75 | (facts "are started and assoced onto lifecycle components with the different name before then dependent's #'start is called" 76 | (:b started-system) => (just {:foo {:name :a, :started true}, :name :b, :started true})))) 77 | 78 | (defrecord StatefulDummyComponent [name started !counter] 79 | Lifecycle 80 | (start [this] (swap! !counter update-in [:started] inc) (assoc this :started true)) 81 | (stop [this] (swap! !counter update-in [:stopped] inc) (assoc this :started false))) 82 | 83 | (defn stateful-dummy-component [name] 84 | (->StatefulDummyComponent name false (atom {:started 0 :stopped 0}))) 85 | 86 | (facts "systems can be started and stopped multiple times" 87 | (let [graph {:a (fnk [] 88 | (stateful-dummy-component :a)) 89 | :b (fnk [a] 90 | (-> (stateful-dummy-component :b) 91 | (assoc :a a)))} 92 | system-graph (init-system graph {}) 93 | cycled-system (reduce (fn [sys _] (-> sys start-system stop-system)) system-graph (range 5))] 94 | 95 | 96 | (-> cycled-system :a :!counter deref) => {:started 5, :stopped 5} 97 | (-> cycled-system start-system :b :a :started) => true 98 | (-> cycled-system start-system stop-system :b :started) => false 99 | ;; TODO: investgate this to see if this is a system-graph bug or component.. 100 | ;; it looks like component doesn't assoc the deps on stop.. but the newer version says it does.. 101 | ;; (-> cycled-system start-system stop-system :b :a :started) => false 102 | )) 103 | --------------------------------------------------------------------------------