├── .dir-locals.el ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Readme.md ├── build.clj ├── deps.edn ├── dev ├── profile.clj └── user.clj ├── example ├── deps.edn ├── dev │ └── user.clj ├── resources │ └── db │ │ └── migration │ │ └── V1__pets.sql └── src │ └── example │ ├── adapters │ ├── flyway.clj │ ├── hikari.clj │ ├── jetty.clj │ └── reitit.clj │ ├── core.clj │ └── system.clj ├── notebooks ├── index.clj └── integrant.clj ├── pom.xml ├── src └── darkleaf │ └── di │ ├── core.clj │ ├── destructuring_map.clj │ ├── protocols.clj │ ├── ref.clj │ └── utils.clj └── test └── darkleaf └── di ├── add_side_dependency_test.clj ├── combine_dependencies_test.clj ├── component_test.clj ├── dependencies_test.clj ├── dependency_types_test.clj ├── deps_definition_test.clj ├── destructuring_map_test.clj ├── memoize_test.clj ├── opt_ref_test.clj ├── ref_test.clj ├── registries_test.clj ├── root_test.clj ├── service_test.clj ├── stop_test.clj ├── template_test.clj ├── tutorial ├── a_intro_test.clj ├── b_dependencies_test.clj ├── c_stop_test.clj ├── l_registries_test.clj ├── m_abstractions_test.clj ├── n_env_test.clj ├── o_data_dsl_test.clj ├── p_derive_test.clj ├── q_starting_many_keys_test.clj ├── r_multimethods_test.clj ├── x_add_side_dependency_test.clj ├── x_inspect_test.clj ├── x_instrument_test.clj.disabled ├── x_log_test.clj ├── x_ns_publics_test.clj ├── x_override_deps_test.clj ├── x_update_key_test.clj ├── y_graceful_stop_test.clj ├── y_multi_arity_service_test.clj ├── z_multi_system_test.clj └── z_two_databases_test.clj └── update_key_test.clj /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-aliases . ":dev")))) 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | container: clojure:temurin-17-tools-deps-1.11.1.1165 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/cache@v3 11 | with: 12 | path: | 13 | /root/.m2 14 | /root/.gitlibs 15 | key: test-${{ hashFiles('deps.edn') }} 16 | restore-keys: test- 17 | - run: clojure -X:dev:test 18 | - run: clojure -X:dev:doc 19 | - uses: peaceiris/actions-gh-pages@v3 20 | if: ${{ github.ref == 'refs/heads/master' }} 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | publish_dir: ./public 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cpcache 3 | .nrepl-port 4 | /target 5 | /.clerk 6 | /public 7 | .clj-kondo/ 8 | .lsp/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.6.0 2 | 3 | + Breaking: Clojure 1.12 4 | + Breaking: `p/Factory` protocol was changed 5 | + Breaking: the description of some factories was changed 6 | + Breaking: internal keys were redacted 7 | + Add `->memoize` 8 | 9 | # 3.5.0 10 | 11 | + Remove `*next-id*` 12 | + Update clerk 13 | + Prepare for new feature `di/->memoize` 14 | 15 | # 3.4.0 16 | 17 | + Add the description for envs (#42) 18 | + Fix description for undefined keys (#43) 19 | 20 | # 3.3.0 21 | 22 | + Add FactoryDescription protocol (#33) 23 | This release's headline feature allows you to fully inspect your system as data. 24 | If your development process heavily depends on feature flags, it is necessary to test your system. 25 | This feature now makes it possible. 26 | + Add better component validation (#39) 27 | + Wrap exceptions during the build process (500179a) 28 | + Lint di.core/with-open as clojure.core/with-open (#34) 29 | 30 | # 3.2.1 31 | 32 | + Fixed `di/add-side-dependency` bug (#32) 33 | 34 | # 3.2.0 35 | 36 | + Improved `di/update-key`. It will throw an exception on a non-existent key (#23) 37 | + [Added inspect middleware](https://github.com/darkleaf/di/commit/073471c9b3afcbcf52223b4607b9d21aa3865e35) 38 | + [Added log middleware](https://github.com/darkleaf/di/commit/cd52e5089d791b06004c669e6092eb98b040ef66) 39 | + [Added `di/with-open`](https://github.com/darkleaf/di/commit/7633474f3a2ed31cd810f73b7e703f7212114dd5) 40 | 41 | + [Added `di/*next-id*` to use it instead of `gensym` ](https://github.com/darkleaf/di/commit/b8ec1887fa5cd669af7663d74e03b2cfd2f0c58c) 42 | 43 | 44 | + [Added print-method for services](https://github.com/darkleaf/di/commit/b2868860c9b2a8149903345aebbb404d817c7ce2) 45 | + Improved naming of generated keys in update-key 46 | 47 | # 3.1.0 48 | 49 | + Internal refactoring: switched from recursive functions to loop/recur to minimize stack traces in exceptions. 50 | + Missing and circular dependency exceptions now include a stack of keys inside `ex-info` for easier debugging. 51 | + Improved key generation: better naming of generated keys in `update-key`. 52 | + Increased test coverage to ensure better reliability. 53 | + Updated comparison with Integrant. 54 | 55 | # 3.0.0 56 | 57 | ## New features 58 | 59 | * `di/ns-publics` registry middleware 60 | 61 | ## Breaking changes 62 | 63 | ### Explicit separation of components and services 64 | 65 | Use `{::di/kind :component}` to mark a component. 66 | 67 | A zero arity service are not a component of zero arity function now. 68 | 69 | 70 | ```clojure 71 | ;; v2 72 | (defn schema-component 73 | [{ddl `clickhouse/ddl 74 | ttl "TTL_DAY" 75 | :or {ttl "30"}}] 76 | (ddl ...)) 77 | 78 | ;; v3 79 | (defn schema-component 80 | {::di/kind :component} 81 | [{ddl `clickhouse/ddl 82 | ttl "TTL_DAY" 83 | :or {ttl "30"}}] 84 | (ddl ...)) 85 | ``` 86 | 87 | ```clojure 88 | ;; v2 89 | (defn my-service [] 90 | (fn [] 91 | ...)) 92 | 93 | ;; v3 94 | (defn my-service [] 95 | ...) 96 | ``` 97 | 98 | ### Explicit separation of keys in `di/update-key` 99 | 100 | Explicitly use `(di/ref)` or other factory to refer to a component. 101 | 102 | 103 | ```clojure 104 | ;; v2 105 | (di/update-key `reitit/route-data conj `raw-method/route-data) 106 | 107 | ;; v3 108 | (di/update-key `reitit/route-data conj (di/ref `raw-method/route-data)) 109 | ``` 110 | 111 | ```clojure 112 | ;; v2 113 | (di/update-key `raw-method/methods #(assoc %1 param-name %2) method))) 114 | 115 | ;; v3 116 | (di/update-key `raw-method/methods assoc param-name (di/ref method)))) 117 | ``` 118 | 119 | ### `di/derive` instead of `di/fmap` 120 | 121 | The `di/fmap` factory constructor was removed. 122 | 123 | Use `di/derive` instead. 124 | 125 | ### `di/instument` removing 126 | 127 | The `di/instument` registry middleware was removed. Maybe there will be a rewrited version. 128 | 129 | # 2.4.2 130 | 131 | ## Fixing `di/add-side-dependency` 132 | 133 | # 2.4.1 134 | 135 | ## Fixing `di/add-side-dependency` 136 | 137 | When root had eight dependencies a system with side depencency started up in the wrong order. 138 | 139 | # 2.4.0 140 | 141 | ## Starting many keys as a map 142 | 143 | Now you can pass a map as the key argument to start many keys: 144 | 145 | ```clojure 146 | (t/deftest lookup-test 147 | (with-open [root (di/start {:a `a :b `b})] 148 | (let [{:keys [a b]} root] 149 | (t/is (= :a a)) 150 | (t/is (= :b b))))) 151 | ``` 152 | 153 | # 2.3.0 154 | 155 | ## Env parsing 156 | 157 | With `di/env-parsing` middleware, you can add env parsers. 158 | 159 | ```clojure 160 | (defn jetty 161 | {::di/stop (memfn stop)} 162 | [{port :env.long/PORT 163 | handler `handler 164 | :or {port 8080}}] 165 | (jetty/run-jetty handler {:join? false 166 | :port port})) 167 | 168 | (di/start `jetty (di/env-parsing {:env.long parse-long})) 169 | ``` 170 | 171 | # 2.2.0 172 | 173 | ## Multimethods as services 174 | 175 | Now you can define a service with a multimethod: 176 | 177 | ```clojure 178 | (defmulti service 179 | {::di/deps [::x]} 180 | (fn [-deps kind] kind)) 181 | ``` 182 | 183 | # 2.1.0 184 | 185 | ## Starting many keys 186 | 187 | Now you can pass a vector as the key argument to start many keys: 188 | 189 | ```clojure 190 | (with-open [root (di/start [`handler `helper])] 191 | (let [[handler helper] root] 192 | ...)) 193 | ``` 194 | 195 | # 2.0.0 196 | 197 | Base version. 198 | 199 | # 1.x 200 | 201 | Don't use it. I forgot the details. 202 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Clojars Project](https://img.shields.io/clojars/v/org.clojars.darkleaf/di.svg)](https://clojars.org/org.clojars.darkleaf/di) 2 | [![cljdoc badge](https://cljdoc.org/badge/org.clojars.darkleaf/di)](https://cljdoc.org/d/org.clojars.darkleaf/di/CURRENT) 3 | 4 | # Dependency injection 5 | 6 | DI is a dependency injection framework that allows you to define dependencies as easily as you define function arguments. 7 | 8 | * [Documentation](https://darkleaf.github.io/di/) 9 | * [Example app](example/src/example/core.clj) 10 | * [Clj doc](https://cljdoc.org/d/org.clojars.darkleaf/di) 11 | * [Tests](test/darkleaf/di) 12 | 13 | ## Versions 14 | 15 | * See `1.0` branch for previous version 16 | * See `master` branch for current version 17 | 18 | ## License 19 | 20 | Copyright © 2022 Mikhail Kuzmin 21 | 22 | Licensed under Eclipse Public License v2.0 (see [LICENSE](LICENSE)). 23 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require 3 | [clojure.tools.build.api :as b] 4 | [clojure.string :as str])) 5 | 6 | (def lib 'org.clojars.darkleaf/di) 7 | (def version "3.5.0") 8 | (def class-dir "target/classes") 9 | (def basis (b/create-basis {:project "deps.edn"})) 10 | (def jar-file (format "target/%s.jar" (name lib))) 11 | (def scm-url "git@github.com:darkleaf/di.git") 12 | 13 | (defn sha 14 | [{:keys [dir path] :or {dir "."}}] 15 | (-> {:command-args (cond-> ["git" "rev-parse" "HEAD"] 16 | path (conj "--" path)) 17 | :dir (.getPath (b/resolve-path dir)) 18 | :out :capture} 19 | b/process 20 | :out 21 | str/trim)) 22 | 23 | (defn clean [_] 24 | (b/delete {:path "target"})) 25 | 26 | (defn jar [_] 27 | (b/write-pom {:class-dir class-dir 28 | :lib lib 29 | :version version 30 | :basis basis 31 | :src-dirs ["src"] 32 | :scm {:tag (sha nil) 33 | :connection (str "scm:git:" scm-url) 34 | :developerConnection (str "scm:git:" scm-url) 35 | :url scm-url}}) 36 | (b/copy-dir {:src-dirs ["src" "resources"] 37 | :target-dir class-dir}) 38 | (b/jar {:class-dir class-dir 39 | :jar-file jar-file})) 40 | 41 | (defn sync-pom [_] 42 | (b/copy-file {:src (str class-dir "/META-INF/maven/org.clojars.darkleaf/di/pom.xml") 43 | :target "pom.xml"})) 44 | 45 | (defn all [_] 46 | (clean nil) 47 | (jar nil) 48 | (sync-pom nil)) 49 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {} 2 | :aliases {:dev {:extra-paths ["dev" "test"] 3 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 4 | io.github.cognitect-labs/test-runner {:git/tag "v0.5.0" :git/sha "b3fd0d2"} 5 | io.github.clojure/tools.build {:git/tag "v0.8.4" :git/sha "8c3cd69"} 6 | slipset/deps-deploy {:mvn/version "0.2.0"} 7 | io.github.nextjournal/clerk {:mvn/version "0.17.1102"} 8 | integrant/integrant {:mvn/version "0.8.0"} 9 | com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.6.1"}} 10 | :jvm-opts ["-Djdk.attach.allowAttachSelf"]} 11 | :test {:extra-paths ["test"] 12 | :main-opts ["-m" "cognitect.test-runner"] 13 | :exec-fn cognitect.test-runner.api/test} 14 | :build {:exec-fn build/all} 15 | :deploy {:exec-fn deps-deploy.deps-deploy/deploy 16 | :exec-args {:installer :remote 17 | :artifact "target/di.jar"}} 18 | :doc {:exec-fn nextjournal.clerk/build! 19 | :exec-args {:index "notebooks/index.clj" 20 | :paths ["notebooks/*.clj" "test/**/*.clj"] 21 | :out-path "public"}}}} 22 | 23 | ;; clj -T:dev:build 24 | ;; clj -T:dev:deploy 25 | -------------------------------------------------------------------------------- /dev/profile.clj: -------------------------------------------------------------------------------- 1 | (ns profile 2 | (:require 3 | [darkleaf.di.core :as di] 4 | [clj-async-profiler.core :as prof])) 5 | 6 | (comment 7 | (prof/serve-ui 8080) 8 | 9 | (defn a 10 | {::di/kind :component} 11 | [] 12 | :a) 13 | 14 | (prof/profile {} 15 | (dotimes [_ 10000] 16 | (di/start `a))) 17 | 18 | 19 | (prof/generate-diffgraph 1 2 {}) 20 | ,,,) 21 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [nextjournal.clerk :as clerk])) 4 | 5 | (comment 6 | (clerk/serve! {:browse? true :watch-paths ["notebooks" "test"]}) 7 | (clerk/halt!) 8 | 9 | (clerk/show! "notebooks/index.clj") 10 | 11 | (clerk/show! 'darkleaf.di.tutorial.a-intro-test) 12 | 13 | 14 | 15 | (clerk/build! {:index "notebooks/index.clj" 16 | :paths ["notebooks/*.clj" "test/**/*.clj"]}) 17 | nil) 18 | -------------------------------------------------------------------------------- /example/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["dev" "src" "test" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"} 3 | org.clojars.darkleaf/di {:local/root "../"} 4 | ring/ring-core {:mvn/version "1.9.4"} 5 | ring/ring-jetty-adapter {:mvn/version "1.9.4"} 6 | ring/ring-mock {:mvn/version "0.4.0"} 7 | metosin/reitit-core {:mvn/version "0.5.15"} 8 | metosin/reitit-ring {:mvn/version "0.5.15"} 9 | metosin/ring-http-response {:mvn/version "0.9.3"} 10 | com.h2database/h2 {:mvn/version "2.1.210"} 11 | com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"} 12 | hikari-cp/hikari-cp {:mvn/version "2.14.0"} 13 | org.flywaydb/flyway-core {:mvn/version "8.5.7"} 14 | metosin/jsonista {:mvn/version "0.3.4"}}} 15 | -------------------------------------------------------------------------------- /example/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [darkleaf.di.core :as di] 4 | [example.system :as system])) 5 | 6 | (defonce root (atom nil)) 7 | 8 | (defn start [] 9 | (reset! root (di/start ::system/root (system/dev-registry)))) 10 | 11 | (defn stop [] 12 | (di/stop @root)) 13 | 14 | ;; call them from the repl 15 | (comment 16 | ;; open http://localhost:8888 17 | (start) 18 | (stop) 19 | nil) 20 | -------------------------------------------------------------------------------- /example/resources/db/migration/V1__pets.sql: -------------------------------------------------------------------------------- 1 | create table pets ( 2 | id integer primary key, 3 | name varchar(255) not null 4 | ); 5 | 6 | insert into pets values (1, 'cat'), (2, 'dog'); 7 | -------------------------------------------------------------------------------- /example/src/example/adapters/flyway.clj: -------------------------------------------------------------------------------- 1 | (ns example.adapters.flyway 2 | (:require 3 | [darkleaf.di.core :as-alias di] 4 | [example.adapters.hikari :as-alias hikari]) 5 | (:import 6 | (org.flywaydb.core Flyway))) 7 | 8 | (defn migrate 9 | {::di/kind :component} 10 | [{ds `hikari/datasource}] 11 | (.. (Flyway/configure) 12 | (dataSource ds) 13 | load 14 | migrate)) 15 | -------------------------------------------------------------------------------- /example/src/example/adapters/hikari.clj: -------------------------------------------------------------------------------- 1 | (ns example.adapters.hikari 2 | (:require 3 | [hikari-cp.core :as hikari] 4 | [darkleaf.di.core :as-alias di])) 5 | 6 | (defn datasource 7 | {::di/stop hikari/close-datasource} 8 | [{options ::options}] 9 | (hikari/make-datasource options)) 10 | -------------------------------------------------------------------------------- /example/src/example/adapters/jetty.clj: -------------------------------------------------------------------------------- 1 | (ns example.adapters.jetty 2 | (:require 3 | [darkleaf.di.core :as-alias di] 4 | [ring.adapter.jetty :as jetty])) 5 | 6 | (defn server 7 | {::di/stop (memfn stop)} 8 | [{handler ::handler 9 | port :env.long/PORT 10 | :or {port 8080}}] 11 | (jetty/run-jetty handler {:join? false 12 | :port port})) 13 | -------------------------------------------------------------------------------- /example/src/example/adapters/reitit.clj: -------------------------------------------------------------------------------- 1 | (ns example.adapters.reitit 2 | (:require 3 | [darkleaf.di.core :as-alias di] 4 | [reitit.ring :as r] 5 | [ring.middleware.keyword-params :as r.keyword-params] 6 | [ring.middleware.params :as r.params] 7 | [ring.util.http-response :as r.resp])) 8 | 9 | (defn default-handler [req] 10 | (r.resp/not-found)) 11 | 12 | (def route-data []) 13 | 14 | (defn handler 15 | {::di/kind :component} 16 | [{route-data `route-data}] 17 | (-> route-data 18 | (r/router) 19 | (r/ring-handler #'default-handler))) 20 | -------------------------------------------------------------------------------- /example/src/example/core.clj: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require 3 | [darkleaf.di.core :as di] 4 | [example.adapters.hikari :as-alias hikari] 5 | [jsonista.core :as json] 6 | [next.jdbc :as jdbc] 7 | [ring.core.protocols :as r.proto] 8 | [ring.util.http-response :as r.resp])) 9 | 10 | (defn- to-json-stream [x] 11 | (reify r.proto/StreamableResponseBody 12 | (write-body-to-stream [_ _ output-stream] 13 | (json/write-value output-stream x)))) 14 | 15 | (defn root-handler [{ds `hikari/datasource} -req] 16 | (let [pets (jdbc/execute! ds ["select * from pets"])] 17 | (-> pets 18 | #_(conj "You don't have to restart the system when you change the code. 19 | Just uncomment this and eval defn form.") 20 | to-json-stream 21 | (r.resp/ok) 22 | (r.resp/content-type "application/json")))) 23 | 24 | (def route-data 25 | (di/template [["/" {:get {:handler (di/ref `root-handler)}}]])) 26 | -------------------------------------------------------------------------------- /example/src/example/system.clj: -------------------------------------------------------------------------------- 1 | (ns example.system 2 | (:require 3 | [darkleaf.di.core :as di] 4 | [example.adapters.hikari :as-alias hikari] 5 | [example.adapters.flyway :as-alias flyway] 6 | [example.adapters.jetty :as-alias jetty] 7 | [example.adapters.reitit :as-alias reitit] 8 | [example.core :as core])) 9 | 10 | (defn base-registry [{:keys [some-feature-flag]}] 11 | [{::root (di/ref `jetty/server) 12 | ::jetty/handler (di/ref `reitit/handler) 13 | ::hikari/options (di/template {:adapter "h2" 14 | :url (di/ref "H2_URL")})} 15 | (di/update-key `reitit/route-data conj (di/ref `core/route-data)) 16 | (di/add-side-dependency `flyway/migrate) 17 | (di/env-parsing :env.long parse-long) 18 | #_(if some-feature-flag 19 | [(di/update-key `reitit/route-data conj ...)])]) 20 | 21 | (defn dev-registry [] 22 | (let [flags {:some-feature-flag true}] 23 | [(base-registry flags) 24 | {"PORT" "8888" 25 | "H2_URL" "jdbc:h2:mem:test"}])) 26 | -------------------------------------------------------------------------------- /notebooks/index.clj: -------------------------------------------------------------------------------- 1 | ^{:nextjournal.clerk/visibility {:code :hide}} 2 | (ns index 3 | {:nextjournal.clerk/toc true} 4 | (:require 5 | [nextjournal.clerk :as clerk] 6 | [darkleaf.di.core :as di] 7 | [darkleaf.di.protocols :as dip])) 8 | 9 | {::clerk/visibility {:code :hide}} 10 | 11 | ;; # Dependency injection 12 | 13 | ;; [DI](https://github.com/darkleaf/di) is a dependency injection framework 14 | ;; that allows you to define dependencies as easily as you define function arguments. 15 | 16 | ;; It uses plain clojure functions and [associative destructuring](https://clojure.org/guides/destructuring#_associative_destructuring) 17 | ;; to define a graph of functions and stateful objects. 18 | 19 | ;; ```clojure 20 | ;; (ns app.core 21 | ;; (:require 22 | ;; [darkleaf.di.core :as di] 23 | ;; [ring.adapter.jetty :as jetty] 24 | ;; [app.adapters.reitit :as-alias reitit] 25 | ;; [app.adapters.hikari :as-alias hikari] 26 | ;; [app.adapters.db :as-alias db])) 27 | ;; 28 | ;; (defn show-user [{ds ::db/datasource} req] 29 | ;; ...) 30 | ;; 31 | ;; (def route-data 32 | ;; (di/template 33 | ;; [["/users/:id" {:get {:handler (di/ref `show-user)}}]])) 34 | ;; 35 | ;; (defn jetty 36 | ;; {::di/stop (memfn stop)} 37 | ;; [{handler ::handler 38 | ;; port :env.long/PORT 39 | ;; :or {port 8080} 40 | ;; (jetty/run-jetty handler {:join? false, :port port})) 41 | ;; 42 | ;; (di/start `jetty 43 | ;; (di/env-parsing :env/long parse-long) 44 | ;; {::handler (di/ref `reitit/handler) 45 | ;; ::reitit/route-data (di/ref `reitit/data) 46 | ;; ::db/datasource (di/ref `hikari/datasource) 47 | ;; "PORT" "9090"}) 48 | ;; ``` 49 | 50 | ;; It is just a short snippet, please see [example app](https://github.com/darkleaf/di/tree/master/example). 51 | 52 | ;; ## Install 53 | 54 | ;; [![Clojars Project](https://img.shields.io/clojars/v/org.clojars.darkleaf/di.svg)](https://clojars.org/org.clojars.darkleaf/di) 55 | 56 | ;; ```edn 57 | ;; {:deps {org.clojars.darkleaf/di {:mvn/version "2.0.0"}}} 58 | ;; ;; or 59 | ;; {:deps {org.clojars.darkleaf/di {:git/url "https://github.com/darkleaf/di.git" 60 | ;; :sha "%SHA%"}}} 61 | ;; ``` 62 | 63 | ;; ## Comparison with other tools 64 | 65 | (clerk/html 66 | [:a {:href (clerk/doc-url "notebooks/integrant")} "Comparison of Integrant and DI"]) 67 | 68 | ;; ## Tutorial 69 | 70 | ;; Each chapter is a regular Clojure test namespace. 71 | ;; You can clone [the repo](https://github.com/darkleaf/di) and run each one in the REPL. 72 | 73 | ;; ### Base 74 | (clerk/html 75 | [:ul 76 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/a_intro_test")} "Intro"]] 77 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/b_dependencies_test")} "Dependencies"]] 78 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/c_stop_test")} "Stop"]] 79 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/l_registries_test")} "Registries"]] 80 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/m_abstractions_test")} "Abstractions"]] 81 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/n_env_test")} "Env"]] 82 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/o_data_dsl_test")} "Data DSL"]] 83 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/p_fmap_test")} "Fmap"]] 84 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/q_starting_many_keys_test")} "Starting many keys"]] 85 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/r_multimethods_test")} "Multimethods"]]]) 86 | 87 | ;; ### Advanced 88 | (clerk/html 89 | [:ul 90 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/x_add_side_dependency_test")} "Add a side dependency"]] 91 | #_[:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/x_instrument_test")} "Instrument"]] 92 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/x_update_key_test")} "Update key"]] 93 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/x_log_test")} "Log"]] 94 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/x_inspect_test")} "Inspect"]] 95 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/y_graceful_stop_test")} "Graceful stop"]] 96 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/y_multi_arity_service_test")} "Multi arity service"]] 97 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/z_multi_system_test")} "Multi system"]] 98 | [:li [:a {:href (clerk/doc-url "test/darkleaf/di/tutorial/z_two_databases_test")} "Two Databases"]]]) 99 | 100 | ;; ## Example app 101 | 102 | ;; [Example app](https://github.com/darkleaf/di/tree/master/example) 103 | 104 | ;; Start with [user.clj](https://github.com/darkleaf/di/blob/master/example/dev/user.clj) 105 | 106 | ;; ## API 107 | 108 | ;; [![cljdoc badge](https://cljdoc.org/badge/org.clojars.darkleaf/di)](https://cljdoc.org/d/org.clojars.darkleaf/di/CURRENT) 109 | 110 | ^{::clerk/visibility {:result :hide}} 111 | (defn view-doc [var] 112 | (clerk/html 113 | [:<> 114 | [:h4 115 | [:code (-> var symbol str)]] 116 | (some-> var meta :arglists clerk/code) 117 | (some-> var meta :doc clerk/md)])) 118 | 119 | ;; ### `darkleaf.di.core` 120 | (view-doc #'di/start) 121 | (view-doc #'di/stop) 122 | (view-doc #'di/inspect) 123 | (view-doc #'di/ref) 124 | (view-doc #'di/opt-ref) 125 | (view-doc #'di/template) 126 | (view-doc #'di/derive) 127 | (view-doc #'di/env-parsing) 128 | #_(view-doc #'di/instrument) 129 | (view-doc #'di/update-key) 130 | (view-doc #'di/add-side-dependency) 131 | (view-doc #'di/ns-publics) 132 | (view-doc #'di/log) 133 | (view-doc #'di/->memoize) 134 | (view-doc #'di/combine-dependencies) 135 | ;; ### `darkleaf.di.protocols` 136 | ;; `darkleaf.di.protocols/Factory` 137 | (view-doc #'dip/dependencies) 138 | (view-doc #'dip/build) 139 | (view-doc #'dip/description) 140 | 141 | 142 | ;; ## License 143 | ;; Copyright © 2022 Mikhail Kuzmin 144 | ;; 145 | ;; Licensed under Eclipse Public License v2.0. 146 | -------------------------------------------------------------------------------- /notebooks/integrant.clj: -------------------------------------------------------------------------------- 1 | ;; # Comparing Integrant and DI for Dependency Injection in Clojure 2 | ;; 3 | ;; This article compares two approaches to dependency injection in Clojure: [Integrant](https://github.com/weavejester/integrant) and [DI](https://github.com/darkleaf/di). 4 | ;; We'll explore their differences through code examples and discuss the pros and cons of each approach. 5 | 6 | (ns integrant 7 | {:nextjournal.clerk/toc true} 8 | (:require 9 | [integrant.core :as ig] 10 | [darkleaf.di.core :as di])) 11 | 12 | {:nextjournal.clerk/visibility {:result :hide}} 13 | 14 | ;; ## Assumptions 15 | ;; 16 | ;; - All code is written in a single file. 17 | ;; - Short namespaces like `:jetty/server` are used instead of `::jetty/server`. 18 | ;; - Real dependencies are omitted for brevity. For a complete example, see [the example app](https://github.com/darkleaf/di/tree/master/example). 19 | ;; 20 | ;; ## Code Examples 21 | ;; 22 | ;; First, we'll define a fake generic `stop` function to simulate stopping a component: 23 | 24 | (defn stop 25 | "Fake generic stop function" 26 | [x] 27 | ;; Placeholder for actual stop logic 28 | ,) 29 | 30 | ;; ### Jetty Server 31 | ;; 32 | ;; #### Using Integrant 33 | 34 | (defmethod ig/init-key :jetty/server [_ {:keys [handler port]}] 35 | ;; In a real application, you might use: 36 | ;; (jetty/run-jetty handler {:port port, :join? false}) 37 | [:jetty handler port]) 38 | 39 | (defmethod ig/halt-key! :jetty/server [_ server] 40 | (stop server)) 41 | 42 | ;; #### Using DI 43 | 44 | (defn jetty-server 45 | {::di/stop #(stop %)} 46 | [{handler :jetty/handler 47 | port "PORT"}] 48 | [:jetty handler port]) 49 | 50 | ;; With DI: 51 | ;; 52 | ;; - Use **symbols** for var names. 53 | ;; - Use **keywords** for abstract dependencies to define them later. 54 | ;; - Use **strings** for environment variables. 55 | ;; 56 | ;; ### Routes 57 | ;; 58 | ;; #### Using Integrant 59 | 60 | (defmethod ig/init-key :web/route-data [_ {:keys [root-handler]}] 61 | [["/" {:get {:handler root-handler}}]]) 62 | 63 | ;; #### Using DI 64 | 65 | (def route-data 66 | (di/template 67 | [["/" {:get {:handler (di/ref `root-handler)}}]])) 68 | 69 | ;; Here, ``(di/ref `root-handler)`` resolves to the `root-handler` var. 70 | ;; There's no need to define `root-handler` as a component in the system config. 71 | ;; 72 | ;; ### Reitit 73 | ;; 74 | ;; We need to transform `route-data` into a Ring handler. 75 | 76 | (defn route-data->handler [route-data] 77 | ;; In a real application, you might use: 78 | ;; (-> route-data 79 | ;; (ring/router) 80 | ;; (ring/ring-handler)) 81 | [:ring-handler route-data]) 82 | 83 | ;; #### Using Integrant 84 | 85 | (defmethod ig/init-key :web/handler [_ {:keys [route-data]}] 86 | (route-data->handler route-data)) 87 | 88 | ;; #### Using DI 89 | 90 | (def web-handler (di/derive `route-data route-data->handler)) 91 | 92 | ;; Alternatively: 93 | ;; 94 | ;; ```clojure 95 | ;; (defn web-handler [{route-data `route-data}] 96 | ;; (route-data->handler route-data)) 97 | ;; ``` 98 | ;; 99 | ;; Or: 100 | ;; 101 | ;; ```clojure 102 | ;; (defn web-handler [{:syms [route-data]}] 103 | ;; (route-data->handler route-data)) 104 | ;; ``` 105 | ;; 106 | ;; ### Handlers 107 | ;; 108 | ;; #### Using Integrant 109 | 110 | (defmethod ig/init-key :web/root-handler [_ {:keys []}] 111 | (fn [req] 112 | :ok)) 113 | 114 | ;; #### Using DI 115 | 116 | (defn root-handler [-deps req] 117 | :ok) 118 | 119 | ;; With DI, you don't need to restart the system while developing `root-handler`. 120 | ;; Just re-evaluate the `defn` form. 121 | ;; With Integrant, you'd need to use `ig/suspend-key!` and `ig/resume-key` 122 | ;; to preserve the system state. 123 | ;; 124 | ;; ### System Initialization 125 | ;; 126 | ;; #### Using Integrant 127 | 128 | (def ig-config 129 | {:jetty/server {:port 8080 130 | :handler (ig/ref :web/handler)} 131 | :web/route-data {:root-handler (ig/ref :web/root-handler)} 132 | :web/handler {:route-data (ig/ref :web/route-data)} 133 | :web/root-handler {}}) 134 | 135 | (ig/init ig-config) 136 | 137 | ;; #### Using DI 138 | ;; 139 | 140 | (di/start `jetty-server {"PORT" 8080 141 | :jetty/handler (di/ref `web-handler)}) 142 | 143 | ;; In DI: 144 | ;; 145 | ;; - `:jetty/handler` is an abstraction. 146 | ;; - You don't need to depend on a concrete key in the namespace with 147 | ;; the Jetty component. 148 | ;; - Only declare abstract dependencies in the registry. 149 | ;; - The registry allows you to override any key. 150 | ;; 151 | ;; ## Real Applications 152 | ;; 153 | ;; In a real application, you might have multiple databases and layers. 154 | ;; 155 | ;; > The vassal of my vassal is not my vassal. 156 | ;; 157 | ;; Components should declare their dependencies themselves. 158 | ;; For example, a handler depends on a database connection and an auth token decoder, 159 | ;; and the decoder depends on a secret key from an environment variable. 160 | ;; 161 | ;; ### Handling Dependencies 162 | ;; 163 | ;; With Integrant or Component, you have options for linking handlers and stateful components: 164 | ;; 165 | ;; 1. **Make every stateless handler a component.** 166 | ;; - Results in a lot of configuration. 167 | ;; 2. **Pass stateful components via the Ring request.** 168 | ;; - [Example](https://github.com/prestancedesign/usermanager-reitit-example/blob/main/src/usermanager/controllers/user.clj#L73). 169 | ;; 170 | ;; With DI, you can define dependencies directly: 171 | 172 | (defn root-handler* [{db :db/datasource 173 | decoder `token-decoder} 174 | req] 175 | :ok) 176 | 177 | (defn token-decoder [{key "SECRET_KEY"} token] 178 | :decoded) 179 | 180 | ;; This allows you to define as many stateless components as needed, 181 | ;; which is the main goal of DI. 182 | ;; 183 | ;; ## Subsystems 184 | ;; 185 | ;; In a web application with independent subsystems 186 | ;; (e.g., `/subsystem-a`, `/subsystem-b`), 187 | ;; each subsystem has its own `route-data` and uses common components. 188 | ;; 189 | ;; ### Using DI to Extend Routes 190 | ;; 191 | ;; With `di/update-key`, 192 | ;; you can extend base route-data with specific one in each subsystem. 193 | 194 | (def subsystem-a-route-data 195 | [["/subsystem-a/" '...]]) 196 | 197 | (defn subsystem-a [] 198 | (di/update-key `route-data concat (di/ref `subsystem-a-route-data))) 199 | 200 | ;; Starting the system: 201 | 202 | (di/start `jetty-server 203 | {"PORT" 8080 204 | :jetty/handler (di/ref `web-handler)} 205 | (subsystem-a) 206 | #_(subsystem-N)) 207 | 208 | ;; ## Feature Flags 209 | ;; 210 | ;; In most cases I prefer to use feature flags instead of branching. 211 | ;; It can be implemented easily with DI: 212 | 213 | (defn subsystem-a* [{:keys [subsystem-a-enabled]}] 214 | (when subsystem-a-enabled 215 | (di/update-key `route-data concat (di/ref `subsystem-a-route-data)))) 216 | 217 | (defn registry [flags] 218 | [{"PORT" 8080 219 | :jetty/handler (di/ref `web-handler)} 220 | (subsystem-a* flags) 221 | #_(subsystem-N flags)]) 222 | 223 | (di/start `jetty-server (registry {:subsystem-a-enabled true})) 224 | 225 | ;; ## Error Handling 226 | ;; 227 | ;; Both Integrant and Component can hinder REPL development 228 | ;; if `ig/init-key` throws an exception. 229 | ;; For example, if the Jetty server starts and listens on a port 230 | ;; but initialization fails, you might need to restart the REPL. 231 | ;; 232 | ;; DI is smart enough to stop already started components in such cases. 233 | ;; 234 | ;; *Note*: You can use [integrant-repl](https://github.com/weavejester/integrant-repl), which handles this scenario by [stopping components on failure](https://github.com/weavejester/integrant-repl/blob/master/src/integrant/repl.clj#L22). 235 | ;; 236 | ;; ## Aspect-Oriented Programming (AOP) 237 | ;; 238 | ;; While AOP can be a dangerous practice, 239 | ;; sometimes you need to add extra behavior to existing components 240 | ;; (e.g., logging, performance measurement, spec checking). 241 | ;; 242 | ;; With Integrant, there's no convenient way to update an existing component. 243 | ;; You'd need to [rename the component key](https://github.com/weavejester/integrant/issues/58) and reconfigure dependent components. 244 | ;; 245 | ;; With DI, you can use `di/update-key` and `di/instrument` to modify components. 246 | ;; 247 | ;; --- 248 | ;; 249 | ;; This comparison highlights how DI provides a flexible and dynamic approach 250 | ;; to dependency injection in Clojure, 251 | ;; especially in terms of REPL-driven development and managing complex dependencies. 252 | ;; Integrant, while powerful, may require more boilerplate during development. 253 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | org.clojars.darkleaf 6 | di 7 | di 8 | 9 | git@github.com:darkleaf/di.git 10 | scm:git:git@github.com:darkleaf/di.git 11 | scm:git:git@github.com:darkleaf/di.git 12 | 4f9c95a831dacae539c3e71965112f5df2dbe870 13 | 14 | 15 | 16 | Eclipse Public License 2.0 17 | https://www.eclipse.org/legal/epl-2.0/ 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | clojars 26 | https://repo.clojars.org/ 27 | 28 | 29 | 30 | 31 | org.clojure 32 | clojure 33 | 1.11.1 34 | 35 | 36 | 3.5.0 37 | 38 | -------------------------------------------------------------------------------- /src/darkleaf/di/core.clj: -------------------------------------------------------------------------------- 1 | ;; ********************************************************************* 2 | ;; * Copyright (c) 2022 Mikhail Kuzmin 3 | ;; * 4 | ;; * This program and the accompanying materials are made 5 | ;; * available under the terms of the Eclipse Public License 2.0 6 | ;; * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | ;; * 8 | ;; * SPDX-License-Identifier: EPL-2.0 9 | ;; **********************************************************************/ 10 | 11 | (ns darkleaf.di.core 12 | (:refer-clojure :exclude [ref key ns-publics derive with-open]) 13 | (:require 14 | [clojure.core :as c] 15 | [clojure.set :as set] 16 | [clojure.walk :as w] 17 | [darkleaf.di.destructuring-map :as map] 18 | [darkleaf.di.protocols :as p] 19 | [darkleaf.di.ref :as ref] 20 | [darkleaf.di.utils :as u :refer [?? try*]]) 21 | (:import 22 | (clojure.lang IDeref IFn Var Var$Unbound Indexed ILookup) 23 | (java.io FileNotFoundException Writer) 24 | (java.lang AutoCloseable) 25 | (java.util.function Function) 26 | (java.util.concurrent ConcurrentHashMap))) 27 | 28 | (set! *warn-on-reflection* true) 29 | 30 | (def ^:private dependency-type-priority 31 | {:required 1 32 | :optional 2}) 33 | 34 | (defn combine-dependencies 35 | "Combines dependencies. Use it with `reduce`. 36 | Dependencies are a hash map of a key and a dependency type." 37 | ([] {}) 38 | ([a] a) 39 | ([a b] 40 | (merge-with (fn [x y] 41 | (min-key dependency-type-priority x y)) 42 | a b))) 43 | 44 | (defn- combine-throwable 45 | "Combines throwables. Use it with `reduce`." 46 | ([] nil) 47 | ([^Throwable a b] 48 | (.addSuppressed a b) 49 | a)) 50 | 51 | (defn- implementation-detail? [frame] 52 | (-> frame :factory p/description ::implementation-detail)) 53 | 54 | (defn- missing-dependency! [stack] 55 | (let [key (-> stack peek :key)] 56 | (throw (ex-info (str "Missing dependency " key) 57 | {:type ::missing-dependency 58 | :stack (into [] 59 | (comp 60 | (remove implementation-detail?) 61 | (map :key)) 62 | stack)})))) 63 | 64 | (defn- circular-dependency! [stack] 65 | (let [key (-> stack peek :key)] 66 | (throw (ex-info (str "Circular dependency " key) 67 | {:type ::circular-dependency 68 | :stack (into [] 69 | (comp 70 | (remove implementation-detail?) 71 | (map :key)) 72 | stack)})))) 73 | 74 | (defn- update-head [stack f & args] 75 | (let [head (peek stack) 76 | tail (pop stack)] 77 | (conj tail (apply f head args)))) 78 | 79 | (defn- stack-frame [key dep-type factory] 80 | (let [deps (p/dependencies factory)] 81 | {:key key 82 | :dep-type dep-type 83 | :factory factory 84 | :declared-deps deps 85 | :remaining-deps (seq deps)})) 86 | 87 | (defn- ->add-stop [{:keys [*stop-list]}] 88 | (fn [stop] 89 | (swap! *stop-list conj stop))) 90 | 91 | (defn- build-obj* [ctx built-map head] 92 | (let [factory (:factory head) 93 | declared-deps (:declared-deps head) 94 | built-deps (select-keys built-map (keys declared-deps))] 95 | (p/build factory built-deps (->add-stop ctx)))) 96 | 97 | (defn- build-obj [ctx built-map stack] 98 | (try 99 | (build-obj* ctx built-map (peek stack)) 100 | (catch Exception ex 101 | (throw (ex-info "A failure occurred during the build process" 102 | {:type ::build-failure 103 | :stack (into [] 104 | (comp 105 | (remove implementation-detail?) 106 | (map :key)) 107 | stack)} 108 | ex))))) 109 | 110 | (defn- build [{:keys [registry] :as ctx} key] 111 | (loop [stack (list (stack-frame key :required (registry key))) 112 | built-map {}] 113 | (if (empty? stack) 114 | (built-map key) 115 | 116 | (let [head (peek stack) 117 | tail (pop stack) 118 | key (:key head) 119 | dep-type (:dep-type head) 120 | factory (:factory head) 121 | remaining-deps (:remaining-deps head)] 122 | 123 | (cond 124 | (contains? built-map key) 125 | (recur tail built-map) 126 | 127 | (u/seq-contains? (map :key tail) key) 128 | (circular-dependency! stack) 129 | 130 | (seq remaining-deps) 131 | (let [[key dep-type] (first remaining-deps)] 132 | (recur (-> stack 133 | (update-head update :remaining-deps rest) 134 | (conj (stack-frame key dep-type (registry key)))) 135 | built-map)) 136 | 137 | :else 138 | (let [obj (build-obj ctx built-map stack)] 139 | (case [obj dep-type] 140 | [nil :optional] (recur tail built-map) 141 | [nil :required] (missing-dependency! stack) 142 | (recur tail (assoc built-map key obj))))))))) 143 | 144 | (defn- try-run [proc] 145 | (try* 146 | (proc) 147 | nil 148 | (catch* [Exception AssertionError] ex 149 | ex))) 150 | 151 | (defn- try-run-all [procs] 152 | (->> procs 153 | (map try-run) 154 | (filter some?) 155 | (seq))) 156 | 157 | (defn- throw-many! [coll] 158 | (some->> coll 159 | seq 160 | (reduce combine-throwable) 161 | (throw))) 162 | 163 | (defn- try-stop-started [{:keys [*stop-list]}] 164 | (let [[stops _] (swap-vals! *stop-list empty)] 165 | (try-run-all stops))) 166 | 167 | (defn- try-build [ctx key] 168 | (try* 169 | (build ctx key) 170 | (catch* [Exception AssertionError] ex 171 | (let [exs (try-stop-started ctx) 172 | exs (cons ex exs)] 173 | (throw-many! exs))))) 174 | 175 | (def ^:private undefined-factory 176 | (reify p/Factory 177 | (dependencies [_]) 178 | (build [_ _ _] nil) 179 | (description [_] 180 | {::kind :undefined}))) 181 | 182 | (defn- undefined-registry [key] 183 | undefined-factory) 184 | 185 | (defn- apply-map [registry map] 186 | (fn [key] 187 | (if-some [[_ trivial] (find map key)] 188 | trivial 189 | (registry key)))) 190 | 191 | (defn- apply-middlewares [registry middlewares] 192 | (loop [registry registry 193 | [mw & tail :as middlewares] middlewares] 194 | (cond 195 | (empty? middlewares) registry 196 | (nil? mw) (recur registry tail) 197 | (fn? mw) (recur (mw registry) tail) 198 | (map? mw) (recur (apply-map registry mw) tail) 199 | (instance? Function mw) (recur (.apply ^Function mw registry) tail) 200 | (sequential? mw) (recur registry (concat mw tail)) 201 | :else (throw (IllegalArgumentException. "Wrong middleware kind"))))) 202 | 203 | (declare var->factory) 204 | 205 | (defn- try-requiring-resolve [key] 206 | (when (qualified-symbol? key) 207 | (try 208 | (requiring-resolve key) 209 | (catch FileNotFoundException _ nil)))) 210 | 211 | (defn- with-ns 212 | "Adds support to the registry for looking up vars." 213 | [registry] 214 | (fn [key] 215 | (?? (some-> key 216 | try-requiring-resolve 217 | var->factory) 218 | (registry key)))) 219 | 220 | (defn- with-env 221 | "Adds support to the registry for looking up environment variables." 222 | [registry] 223 | (fn [key] 224 | (?? (when (string? key) 225 | (reify p/Factory 226 | (dependencies [_]) 227 | (build [_ _ _] 228 | (System/getenv key)) 229 | (description [_] 230 | {::kind :env}))) 231 | (registry key)))) 232 | 233 | (declare ref template update-description) 234 | 235 | (defn- implicit-root [key] 236 | (let [[factory 237 | root-keys] (cond 238 | (vector? key) [(->> key (map ref) template) 239 | (set key)] 240 | (map? key) [(-> key (update-vals ref) template) 241 | (set (vals key))] 242 | :else [(-> key ref) 243 | #{key}]) 244 | factory (reify p/Factory 245 | (dependencies [_] 246 | (concat (p/dependencies factory) 247 | {::side-dependency :required})) 248 | (build [_ deps add-stop] 249 | (p/build factory deps add-stop)) 250 | (description [_] 251 | {::implementation-detail true}))] 252 | (fn [registry] 253 | (fn [key] 254 | (cond 255 | (= ::implicit-root key) factory 256 | (contains? root-keys key) (-> (registry key) 257 | (update-description assoc ::root true)) 258 | :else (registry key)))))) 259 | 260 | (defn- with-internals [registry] 261 | (fn [key] 262 | (case key 263 | ::side-dependency (reify p/Factory 264 | (dependencies [_]) 265 | (build [_ _ _] '_) 266 | (description [_] 267 | {::implementation-detail true})) 268 | (registry key)))) 269 | 270 | (def ^:private initial-registry 271 | (-> undefined-registry with-env with-ns with-internals)) 272 | 273 | (defn- start* ^AutoCloseable [key middlewares] 274 | (let [registry (apply-middlewares initial-registry middlewares) 275 | ctx {:registry registry 276 | :*stop-list (atom '())} 277 | obj (try-build ctx key)] 278 | ^{:type ::root 279 | ::print obj} 280 | (reify 281 | AutoCloseable 282 | (close [_] 283 | (->> (try-stop-started ctx) 284 | (throw-many!))) 285 | IDeref 286 | (deref [_] 287 | obj) 288 | Indexed 289 | (nth [_ i] 290 | (nth obj i)) 291 | (nth [_ i not-found] 292 | (nth obj i not-found)) 293 | (count [_] 294 | (count obj)) 295 | ILookup 296 | (valAt [_ key] 297 | (get obj key)) 298 | (valAt [_ key not-found] 299 | (get obj key not-found)) 300 | IFn 301 | (call [_] 302 | (.call ^IFn obj)) 303 | (run [_] 304 | (.run ^IFn obj)) 305 | (invoke [this] 306 | (.invoke ^IFn obj)) 307 | (invoke [_ a1] 308 | (.invoke ^IFn obj a1)) 309 | (invoke [_ a1 a2] 310 | (.invoke ^IFn obj a1 a2)) 311 | (invoke [_ a1 a2 a3] 312 | (.invoke ^IFn obj a1 a2 a3)) 313 | (invoke [_ a1 a2 a3 a4] 314 | (.invoke ^IFn obj a1 a2 a3 a4)) 315 | (invoke [_ a1 a2 a3 a4 a5] 316 | (.invoke ^IFn obj a1 a2 a3 a4 a5)) 317 | (invoke [_ a1 a2 a3 a4 a5 a6] 318 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6)) 319 | (invoke [_ a1 a2 a3 a4 a5 a6 a7] 320 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7)) 321 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8] 322 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8)) 323 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9] 324 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9)) 325 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] 326 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10)) 327 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11] 328 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11)) 329 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12] 330 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12)) 331 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13] 332 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13)) 333 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14] 334 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14)) 335 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15] 336 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15)) 337 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16] 338 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16)) 339 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17] 340 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17)) 341 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18] 342 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18)) 343 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19] 344 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19)) 345 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20] 346 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20)) 347 | (invoke [_ a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 args] 348 | (.invoke ^IFn obj a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 args)) 349 | (applyTo [_ args] 350 | (.applyTo ^IFn obj args))))) 351 | 352 | 353 | (defn start 354 | "Starts a system of dependent objects. 355 | 356 | key is a name of the system root. 357 | Use symbols for var names, keywords for abstract dependencies, 358 | and strings for environments variables. 359 | 360 | key is looked up in a registry. 361 | By default registry uses Clojure namespaces and system env 362 | to resolve symbols and strings, respectively. 363 | 364 | You can extend it with registry middlewares. 365 | Each middleware can be one of the following form: 366 | 367 | - a function `registry -> key -> Factory` 368 | - a map of key and `p/Factory` instance 369 | - nil, as no-op middleware 370 | - a sequence of the previous forms 371 | 372 | Middlewares also allows you to instrument built objects. 373 | It's useful for logging, schema validation, AOP, etc. 374 | See `update-key`. 375 | 376 | ```clojure 377 | (di/start `root 378 | {:my-abstraction implemntation 379 | `some-key replacement 380 | \"LOG_LEVEL\" \"info\"} 381 | [dev-middlwares test-middlewares] 382 | (if dev-routes? 383 | (di/update-key `route-data conj `dev-route-data) 384 | (di/instrument `log)) 385 | ``` 386 | 387 | Returns a container contains started root of the system. 388 | The container implements `AutoCloseable`, `IDeref`, `IFn`, `Indexed` and `ILookup`. 389 | 390 | Use `with-open` in tests to stop the system reliably. 391 | 392 | You can pass a vector as the key argument to start many keys: 393 | 394 | ```clojure 395 | (with-open [root (di/start [`handler `helper])] 396 | (let [[handler helper] root] 397 | ...)) 398 | ``` 399 | 400 | See the tests for use cases. 401 | See `update-key`." 402 | ^AutoCloseable [key & middlewares] 403 | (start* ::implicit-root [middlewares (implicit-root key)])) 404 | 405 | (defn stop 406 | "Stops the root of a system" 407 | [^AutoCloseable root] 408 | (cond 409 | (nil? root) nil 410 | (instance? Var$Unbound root) nil 411 | :else (.close root))) 412 | 413 | 414 | (defn- update-description [factory f & args] 415 | (reify p/Factory 416 | (dependencies [_] 417 | (p/dependencies factory)) 418 | (build [_ deps add-stop] 419 | (p/build factory deps add-stop)) 420 | (description [_] 421 | (apply f (p/description factory) args)))) 422 | 423 | (def ^:private key? (some-fn symbol? keyword? string?)) 424 | 425 | (defn ref 426 | "Returns a factory referencing to a key. 427 | 428 | ```clojure 429 | (def port (di/ref \"PORT\")) 430 | (defn server [{port `port}] ...) 431 | 432 | (def routes (di/template [[\"/posts\" (di/ref `handler)]])) 433 | 434 | (di/start `root {::my-abstraction (di/ref `my-implementation)}) 435 | ``` 436 | 437 | See `template`, `opt-ref`, `derive`, `p/build`." 438 | [key] 439 | (ref/->Ref key :required)) 440 | 441 | (defn opt-ref 442 | "Returns a factory referencing to a possible undefined key. 443 | Produces nil in that case. 444 | 445 | See `template`, `ref`, `derive`." 446 | [key] 447 | (ref/->Ref key :optional)) 448 | 449 | (defn template 450 | "Returns a factory for templating a data-structure. 451 | Replaces `ref` or `opt-ref` instances with built objects. 452 | 453 | ```clojure 454 | (def routes (di/template [[\"/posts\" (di/ref `handler)]])) 455 | ``` 456 | 457 | See `ref` and `opt-ref`." 458 | [form] 459 | ^{:type ::template 460 | ::print form} 461 | (reify p/Factory 462 | (dependencies [_] 463 | (transduce (map p/dependencies) 464 | combine-dependencies 465 | (tree-seq coll? seq form))) 466 | (build [_ deps add-stop] 467 | (w/postwalk #(p/build % deps add-stop) form)) 468 | (description [_] 469 | {::kind :template 470 | :template form}))) 471 | 472 | (defn derive 473 | "Applies `f` to an object built from `key`. 474 | 475 | ```clojure 476 | (def port (-> (di/derive \"PORT\" (fnil parse-long \"8080\")))) 477 | ``` 478 | 479 | See `ref`, `template`." 480 | [key f & args] 481 | {:pre [(key? key) 482 | (ifn? f)]} 483 | (reify p/Factory 484 | (dependencies [_] 485 | {key :optional}) 486 | (build [_ deps _] 487 | (apply f (deps key) args)) 488 | (description [_] 489 | {::kind :derive 490 | :key key 491 | :f f 492 | :args args}))) 493 | 494 | ;; We currently don't need this middleware. 495 | ;; It should be rewritten as `update-key`. 496 | ;; Also it has a poor documentation and a test coverage. 497 | ;; We might add a new implementation later. 498 | #_ 499 | (defn instrument 500 | "A registry middleware for instrumenting or decorating built objects. 501 | Use it for logging, schema checking, AOP, etc. 502 | 503 | f and args are keys. 504 | Also f can be a function in term of `ifn?`. 505 | 506 | A resolved f must be a function of `[object key & args] -> new-object`. 507 | f should not return a non-trivial instance of `p/Stoppable`. 508 | 509 | It is smart enough not to instrument f's dependencies with the same f 510 | to avoid circular dependencies. 511 | 512 | ```clojure 513 | (defn stateful-instrumentaion [{state :some/state} key object arg1 arg2] ...) 514 | (di/start ::root (di/instrument `stateful-instrumentation `arg1 ::arg2 \"arg3\"))) 515 | 516 | (defn stateless-instrumentaion [key object arg1 arg2 arg3] ...) 517 | (di/start ::root (di/instrument stateless-instrumentation `arg1 ::arg2 \"arg3\")) 518 | (di/start ::root (di/instrument #'stateless-instrumentation `arg1 ::arg2 \"arg3\")) 519 | ``` 520 | 521 | See `start`, `update-key`." 522 | [f & args] 523 | {:pre [(or (key? f) 524 | (ifn? f)) 525 | (every? key? args)]} 526 | (let [own-keys (cond-> (set args) 527 | (key? f) (conj f)) 528 | *under-construction (volatile! #{})] 529 | (fn [registry] 530 | (fn [key] 531 | (vswap! *under-construction conj key) 532 | (let [factory (registry key)] 533 | (if (some @*under-construction own-keys) 534 | (reify p/Factory 535 | (dependencies [_] 536 | (p/dependencies factory)) 537 | (build [_ deps] 538 | (vswap! *under-construction disj key) 539 | (p/build factory deps))) 540 | (reify p/Factory 541 | (dependencies [_] 542 | (merge (p/dependencies factory) 543 | (zipmap own-keys (repeat :required)))) 544 | (build [_ deps] 545 | (vswap! *under-construction disj key) 546 | (let [f (deps f f) 547 | args (map deps args) 548 | original (p/build factory deps) 549 | obj (apply f key (p/unwrap original) args)] 550 | (reify p/Stoppable 551 | ;; ??? 552 | ;; (unwrap [_] 553 | ;; (p/unwrap obj)) 554 | ;; (stop [_] 555 | ;; (p/stop original) 556 | ;; (p/stop obj)) 557 | (unwrap [_] 558 | obj) 559 | (stop [_] 560 | (p/stop original)))))))))))) 561 | 562 | (defn update-key 563 | "A registry middleware for updating built objects. 564 | 565 | target is a key to update. 566 | f and args are intances of `p/Factory`. 567 | For example, a factory can be a regular object or `(di/ref key)`. 568 | 569 | ```clojure 570 | (def routes []) 571 | (def subsystem-routes (di/template [[\"/posts\" (di/ref `handler)]])) 572 | 573 | (di/start ::root (di/update-key `routes conj (di/ref `subsystem-routes))) 574 | ``` 575 | 576 | See `start`, `derive`." 577 | [target f & args] 578 | {:pre [(key? target)]} 579 | (fn [registry] 580 | (let [factory (registry target) 581 | _ (when (= undefined-factory factory) 582 | (throw (ex-info (str "Can't update non-existent key " target) 583 | {:type ::non-existent-key 584 | :key target}))) 585 | factory (reify p/Factory 586 | (dependencies [_] 587 | (transduce (map p/dependencies) 588 | combine-dependencies 589 | (cons factory (cons f args)))) 590 | (build [_ deps add-stop] 591 | (let [t (p/build factory deps add-stop) 592 | f (p/build f deps add-stop) 593 | args (for [arg args] (p/build arg deps add-stop))] 594 | (apply f t args))) 595 | (description [_] 596 | (-> (p/description factory) 597 | (update ::update-key u/conjv 598 | (map p/description (cons f args))))))] 599 | (fn [key] 600 | (if (= target key) 601 | factory 602 | (registry key)))))) 603 | 604 | (defn add-side-dependency 605 | "A registry middleware for adding side dependencies. 606 | Use it for migrations or other side effects. 607 | 608 | 609 | ```clojure 610 | (defn flyway [{url \"DATABASE_URL\"}] 611 | (.. (Flyway/configure) 612 | ...)) 613 | 614 | (di/start ::root (di/add-side-dependency `flyway)) 615 | ```" 616 | [dep-key] 617 | (fn [registry] 618 | (fn [key] 619 | (let [factory (registry key)] 620 | (condp = key 621 | ::side-dependency (reify p/Factory 622 | (dependencies [_] 623 | ;; This is an incorrect implementation that does not preserve order. 624 | ;; (assoc (p/dependencies factory) 625 | ;; dep-key :required) 626 | (concat (p/dependencies factory) 627 | {dep-key :required})) 628 | (build [_ deps add-stop] 629 | (p/build factory deps add-stop)) 630 | (description [_] 631 | (p/description factory))) 632 | dep-key (update-description factory assoc ::side-dependency true) 633 | factory))))) 634 | 635 | 636 | (defn- arglists [variable] 637 | (-> variable meta :arglists)) 638 | 639 | (defn- defn? [variable] 640 | (-> variable arglists seq boolean)) 641 | 642 | (defn- dependencies-fn [variable] 643 | (transduce (comp 644 | (map first) 645 | (filter map?) 646 | (map map/dependencies)) 647 | combine-dependencies 648 | (arglists variable))) 649 | 650 | (defn- stop-fn [variable] 651 | (-> variable meta (::stop (fn no-op [_])))) 652 | 653 | (defn- validate-obj! [obj variable] 654 | (when (nil? obj) 655 | (throw (ex-info "A component fn must not return nil" 656 | {:type ::nil-return 657 | :variable variable})))) 658 | 659 | (defn- var->0-component [variable] 660 | (let [stop (stop-fn variable)] 661 | (reify p/Factory 662 | (dependencies [_]) 663 | (build [_ _ add-stop] 664 | (let [obj (variable)] 665 | (validate-obj! obj variable) 666 | (add-stop #(stop obj)) 667 | obj)) 668 | (description [_] 669 | {::kind :component})))) 670 | 671 | (defn- var->1-component [variable] 672 | (let [deps (dependencies-fn variable) 673 | stop (stop-fn variable)] 674 | (reify p/Factory 675 | (dependencies [_] 676 | deps) 677 | (build [_ deps add-stop] 678 | (let [obj (variable deps)] 679 | (validate-obj! obj variable) 680 | (add-stop #(stop obj)) 681 | obj)) 682 | (description [_] 683 | {::kind :component})))) 684 | 685 | (defn- service-factory [variable declared-deps] 686 | (reify p/Factory 687 | (dependencies [_] 688 | declared-deps) 689 | (build [_ deps _] 690 | (-> variable 691 | (partial deps) 692 | (with-meta {:type ::service 693 | ::print variable}))) 694 | (description [_] 695 | {::kind :service}))) 696 | 697 | (defn- var->0-service [variable] 698 | (reify p/Factory 699 | (dependencies [_]) 700 | (build [_ _ _] 701 | variable) 702 | (description [_] 703 | {::kind :service}))) 704 | 705 | (defn- var->service [variable] 706 | (let [deps (dependencies-fn variable)] 707 | (service-factory variable deps))) 708 | 709 | (defn- var->factory-defn [variable] 710 | (when (defn? variable) 711 | (let [m (meta variable) 712 | has-stop? (contains? m ::stop) 713 | kind (::kind m (cond has-stop? :component)) 714 | arities (->> variable arglists (map count))] 715 | (case kind 716 | :component (case arities 717 | [0] (var->0-component variable) 718 | [1] (var->1-component variable) 719 | (throw (ex-info 720 | "A component fn must only have 0 or 1 arity" 721 | {:type ::invalid-arity 722 | :variable variable 723 | :arities arities}))) 724 | #_service (case arities 725 | [0] (var->0-service variable) 726 | (var->service variable)))))) 727 | 728 | (defn- var->factory-meta-deps 729 | "for multimethods" 730 | [variable] 731 | (if-some [deps (some-> variable meta ::deps (zipmap (repeat :required)))] 732 | (service-factory variable deps))) 733 | 734 | (defn- var->factory-default [variable] 735 | @variable) 736 | 737 | (defn- var->factory [variable] 738 | (-> (?? (var->factory-meta-deps variable) 739 | (var->factory-defn variable) 740 | (var->factory-default variable)) 741 | (update-description assoc ::variable variable))) 742 | 743 | (extend-protocol p/Factory 744 | nil 745 | (dependencies [_] nil) 746 | (build [_ _ _] nil) 747 | (description [this] 748 | {::kind :trivial 749 | :object nil}) 750 | 751 | Object 752 | (dependencies [_] nil) 753 | (build [this _ _] this) 754 | (description [this] 755 | {::kind :trivial 756 | :object this})) 757 | 758 | 759 | (c/derive ::root ::instance) 760 | (c/derive ::template ::instance) 761 | (c/derive ::service ::instance) 762 | 763 | (defmethod print-method ::instance [o ^Writer w] 764 | (.write w "#") 765 | (.write w (-> o type symbol str)) 766 | (.write w " ") 767 | (binding [*out* w] 768 | (pr (-> o meta ::print)))) 769 | 770 | (defn- try-namespace [x] 771 | (when (ident? x) 772 | (namespace x))) 773 | 774 | (defn env-parsing 775 | "A registry middleware for parsing environment variables. 776 | You can define a dependency of env as a string key like \"PORT\", 777 | and its value will be a string. 778 | With this middleware, you can define it as a qualified keyword like :env.long/PORT, 779 | and its value will be a number. 780 | cmap is a map of prefixes and parsers. 781 | 782 | ```clojure 783 | (defn root [{port :env.long/PORT}] 784 | ...) 785 | 786 | (di/start `root (di/env-parsing :env.long parse-long 787 | :env.edn edn/read-string 788 | :env.json json/read-value)) 789 | ```" 790 | {:added "2.3.0"} 791 | [& {:as cmap}] 792 | {:pre [(map? cmap) 793 | (every? simple-keyword? (keys cmap)) 794 | (every? ifn? (vals cmap))]} 795 | (fn [registry] 796 | (fn [key] 797 | (let [key-ns (some-> key try-namespace keyword) 798 | key-name (name key) 799 | parser (cmap key-ns)] 800 | (if (some? parser) 801 | (reify p/Factory 802 | (dependencies [_] 803 | {key-name :optional}) 804 | (build [_ deps _] 805 | (some-> key-name deps parser)) 806 | (description [_] 807 | {::kind :middleware 808 | :middleware ::env-parsing 809 | :cmap cmap})) 810 | (registry key)))))) 811 | 812 | ;; (defn rename-deps [target rmap] 813 | ;; (let [inverted-rmap (set/map-invert rmap)] 814 | ;; (fn [registry] 815 | ;; (fn [key] 816 | ;; (let [factory (registry key)] 817 | ;; (if (= target key) 818 | ;; (reify p/Factory 819 | ;; (dependencies [_] 820 | ;; (let [deps (p/dependencies factory)] 821 | ;; (set/rename-keys deps rmap))) 822 | ;; (build [this deps] 823 | ;; (let [deps (set/rename-keys deps inverted-rmap)] 824 | ;; (p/build factory deps)))) 825 | ;; factory)))))) 826 | 827 | 828 | (defn- usefull-var? [var] 829 | (and (bound? var) 830 | (some? @var))) 831 | 832 | (defn ns-publics 833 | "A registry middleware that interprets a whole namespace as a component. 834 | A component will be a map of var names to corresponding components. 835 | 836 | The key of a component is a keyword with the namespace `:ns-publics` 837 | and a name containing the name of a target ns. 838 | For example `:ns-publics/io.gihub.my.ns`. 839 | 840 | This enables access to all public components, which is useful for testing. 841 | 842 | See the test darkleaf.di.tutorial.x-ns-publics-test. 843 | 844 | ```clojure 845 | (di/start :ns-publics/io.gihub.my.ns (di/ns-publics)) 846 | ```" 847 | [] 848 | (fn [registry] 849 | (fn [key] 850 | (if (and (qualified-keyword? key) 851 | (= "ns-publics" (namespace key))) 852 | (let [component-ns (symbol (name key)) 853 | component-vars (do 854 | (require component-ns) 855 | (c/ns-publics component-ns)) 856 | component-symbols (->> component-vars 857 | vals 858 | (filter usefull-var?) 859 | (map symbol)) 860 | deps (zipmap component-symbols 861 | (repeat :required))] 862 | (reify p/Factory 863 | (dependencies [_this] 864 | deps) 865 | (build [_this deps _] 866 | (update-keys deps #(-> % name keyword))) 867 | (description [_] 868 | {::kind :middleware 869 | :middleware ::ns-publics 870 | :ns component-ns}))) 871 | (registry key))))) 872 | 873 | (defmacro with-open 874 | "A `c/with-open` variant that supports destructuring in bindings. 875 | 876 | bindings => [name init ...] 877 | Evaluates body in a try expression with names bound to the values 878 | of the inits, and a finally clause that calls (.close name) on each 879 | name in reverse order." 880 | {:clj-kondo/lint-as 'clojure.core/with-open} 881 | [bindings & body] 882 | {:pre [(vector? bindings) 883 | (even? (count bindings))]} 884 | (if (zero? (count bindings)) 885 | `(do ~@body) 886 | (let [[binding-form init-expr] (subvec bindings 0 2)] 887 | `(let [resource# ~init-expr] 888 | (try 889 | (let [~binding-form resource#] 890 | (with-open ~(subvec bindings 2) 891 | ~@body)) 892 | (finally 893 | (.close resource#))))))) 894 | 895 | (defn log 896 | "A logging middleware. 897 | Calls `:after-build!` and `:after-demolish!` during `di/start`. 898 | Must be the last one in the middleware chain. 899 | Both callbacks are expected to accept 900 | the following arg `{:keys [key object]}`." 901 | [& {:keys [after-build! after-demolish!] 902 | #_#_:as opts 903 | :or {after-build! (fn no-op [_]) 904 | after-demolish! (fn no-op [_])}}] 905 | (fn [registry] 906 | (fn [key] 907 | (let [factory (registry key)] 908 | (if (-> factory p/description ::implementation-detail) 909 | factory 910 | (reify p/Factory 911 | (dependencies [_] 912 | (p/dependencies factory)) 913 | (build [_ deps add-stop] 914 | (let [obj (p/build factory deps add-stop)] 915 | (after-build! {:key key :object obj}) 916 | (add-stop #(after-demolish! {:key key :object obj})) 917 | obj)) 918 | (description [_] 919 | (assoc (p/description factory) 920 | ::log {:will-be-logged true 921 | #_#_:opts opts})))))))) 922 | 923 | (defn- with-inspect [registry] 924 | (fn [key] 925 | (let [factory (registry key) 926 | declared-deps (p/dependencies factory) 927 | description (p/description factory) 928 | info (into {} 929 | (filter (fn [[k v]] (some? v))) 930 | {:key key 931 | :dependencies (not-empty declared-deps) 932 | :description (not-empty description)}) 933 | result (if (::implementation-detail description) 934 | [] 935 | [info])] 936 | (reify p/Factory 937 | (dependencies [_] 938 | declared-deps) 939 | (build [_ deps _] 940 | (into result 941 | (comp 942 | (mapcat val) 943 | (distinct)) 944 | deps)))))) 945 | 946 | (defn inspect 947 | "Collects and returns a vector of keys along with their dependencies. 948 | Useful for inspecting enabled components and services. 949 | Evaluates all registries with middlewares applied. 950 | 951 | Expects the same arguments as `start` and returns a vector of keys with dependencies e.g.: 952 | 953 | ```clojure 954 | [{:key `root :dependencies {`foo :required `bar :optional}} 955 | {:key `foo} 956 | {:key `bar}] 957 | ```" 958 | [key & middlewares] 959 | (with-open [components (start* ::implicit-root 960 | [middlewares 961 | (implicit-root key) 962 | with-inspect])] 963 | @components)) 964 | 965 | 966 | (defn- first-mw? [registry] 967 | (identical? registry initial-registry)) 968 | 969 | (defn- add-factory-watch [factory f] 970 | (when-some [var (-> factory p/description ::variable)] 971 | ;; Every memoize instance has a new factory instance for a var. 972 | ;; It is ok to pass a factory as a key. 973 | (add-watch var factory (fn [_ _ _ _] (f))))) 974 | 975 | (defn- remove-factory-watch [factory] 976 | (when-some [var (-> factory p/description ::variable)] 977 | (remove-watch var factory))) 978 | 979 | (defn ->memoize 980 | "Returns a statefull middleware that memoizes all registry build accesses. 981 | 982 | To stop all memoized components use `(di/stop mem)`." 983 | ^AutoCloseable [& middlewares] 984 | (let [registry (apply-middlewares initial-registry middlewares) 985 | factories (ConcurrentHashMap.) 986 | objs (ConcurrentHashMap.) 987 | ctx {:*stop-list (atom '())} 988 | add-stop (->add-stop ctx)] 989 | (reify 990 | AutoCloseable 991 | (close [this] 992 | (doseq [[_ factory] factories] 993 | (remove-factory-watch factory)) 994 | (.clear factories) 995 | (.clear objs) 996 | (try-stop-started ctx)) 997 | Function 998 | (apply [this previous-registry] 999 | (when-not (first-mw? previous-registry) 1000 | (throw (ex-info "A memoized registry should be the first middleware" 1001 | {:type ::wrong-memoized-registry-position}))) 1002 | (fn [key] 1003 | (let [factory (.computeIfAbsent factories key 1004 | (fn [_] 1005 | (let [factory (registry key)] 1006 | (add-factory-watch factory 1007 | #(.remove factories key factory)) 1008 | factory)))] 1009 | (reify p/Factory 1010 | (dependencies [_] 1011 | (p/dependencies factory)) 1012 | (build [_ deps _add-stop] 1013 | (.computeIfAbsent objs [factory deps] 1014 | (fn [_] (p/build factory deps add-stop)))) 1015 | (description [_] 1016 | (p/description factory))))))))) 1017 | -------------------------------------------------------------------------------- /src/darkleaf/di/destructuring_map.clj: -------------------------------------------------------------------------------- 1 | ;; ********************************************************************* 2 | ;; * Copyright (c) 2022 Mikhail Kuzmin 3 | ;; * 4 | ;; * This program and the accompanying materials are made 5 | ;; * available under the terms of the Eclipse Public License 2.0 6 | ;; * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | ;; * 8 | ;; * SPDX-License-Identifier: EPL-2.0 9 | ;; **********************************************************************/ 10 | 11 | (ns darkleaf.di.destructuring-map 12 | (:refer-clojure :exclude [key])) 13 | 14 | (defn- key-tag [defaults key] 15 | (if (contains? defaults key) 16 | :optional 17 | :required)) 18 | 19 | (defn- parse-keys [k v defaults] 20 | (when (and (keyword? k) 21 | (= "keys" (name k)) 22 | (vector? v)) 23 | (reduce 24 | (fn [deps binding] 25 | (let [key (keyword (namespace k) (name binding)) 26 | tag (key-tag defaults binding)] 27 | (assoc deps key tag))) 28 | {} 29 | v))) 30 | 31 | (defn- parse-syms [k v defaults] 32 | (when (and (keyword? k) 33 | (= "syms" (name k)) 34 | (vector? v)) 35 | (reduce 36 | (fn [deps binding] 37 | (let [key (symbol (namespace k) (name binding)) 38 | tag (key-tag defaults binding)] 39 | (assoc deps key tag))) 40 | {} 41 | v))) 42 | 43 | (defn- parse-strs [k v defaults] 44 | (when (and (= :strs k) 45 | (vector? v)) 46 | (reduce 47 | (fn [deps binding] 48 | (let [key (name binding) 49 | tag (key-tag defaults binding)] 50 | (assoc deps key tag))) 51 | {} 52 | v))) 53 | 54 | (defn- parse-named-key [k v defaults] 55 | (when (and (simple-symbol? k) 56 | (keyword? v)) 57 | (let [key v 58 | tag (key-tag defaults k)] 59 | {key tag}))) 60 | 61 | (defn- parse-named-sym [k v defaults] 62 | (when (and (simple-symbol? k) 63 | (seq? v) 64 | (= 'quote (first v)) 65 | (symbol? (second v))) 66 | (let [key (second v) 67 | tag (key-tag defaults k)] 68 | {key tag}))) 69 | 70 | (defn- parse-named-str [k v defaults] 71 | (when (and (simple-symbol? k) 72 | (string? v)) 73 | (let [key v 74 | tag (key-tag defaults k)] 75 | {key tag}))) 76 | 77 | (defn dependencies 78 | "Parses destructuring map into map of dependency key and its type" 79 | [m] 80 | (let [defaults (:or m) 81 | m (dissoc m :or :as)] 82 | (reduce-kv (fn [deps k v] 83 | (merge deps 84 | (parse-keys k v defaults) 85 | (parse-syms k v defaults) 86 | (parse-strs k v defaults) 87 | (parse-named-key k v defaults) 88 | (parse-named-sym k v defaults) 89 | (parse-named-str k v defaults))) 90 | {} 91 | m))) 92 | -------------------------------------------------------------------------------- /src/darkleaf/di/protocols.clj: -------------------------------------------------------------------------------- 1 | ;; ********************************************************************* 2 | ;; * Copyright (c) 2022 Mikhail Kuzmin 3 | ;; * 4 | ;; * This program and the accompanying materials are made 5 | ;; * available under the terms of the Eclipse Public License 2.0 6 | ;; * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | ;; * 8 | ;; * SPDX-License-Identifier: EPL-2.0 9 | ;; **********************************************************************/ 10 | 11 | (ns darkleaf.di.protocols) 12 | 13 | (defprotocol Factory 14 | (dependencies [this] 15 | "Returns a map of a key and a dependency type. 16 | A type can be `:required` or `:optional`.") 17 | (build [this dependencies add-stop] 18 | "Builds an object from dependencies.") 19 | (description [this] 20 | "Returns a map with the factory description.")) 21 | -------------------------------------------------------------------------------- /src/darkleaf/di/ref.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc darkleaf.di.ref 2 | (:require 3 | [darkleaf.di.core :as-alias di] 4 | [darkleaf.di.protocols :as p]) 5 | (:import 6 | (java.io Writer))) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | ;; у нее роли 11 | ;; 1. в template 12 | ;; 2.1. в реестрах 13 | (defrecord Ref [key type] 14 | p/Factory 15 | (dependencies [_] 16 | {key type}) 17 | (build [_ deps _] 18 | (deps key)) 19 | (description [_] 20 | {::di/kind :ref 21 | :key key 22 | :type type})) 23 | 24 | (defmethod print-method Ref [o ^Writer w] 25 | (.write w "#darkleaf.di.core/") 26 | (.write w (case (:type o) 27 | :required "ref" 28 | :optional "opt-ref")) 29 | (.write w " ") 30 | (.write w (-> o :key str))) 31 | -------------------------------------------------------------------------------- /src/darkleaf/di/utils.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc darkleaf.di.utils 2 | (:import 3 | (java.util List))) 4 | 5 | (set! *warn-on-reflection* true) 6 | 7 | (defmacro ?? 8 | ([] nil) 9 | ([x] x) 10 | ([x & next] 11 | `(if-some [x# ~x] 12 | x# 13 | (?? ~@next)))) 14 | 15 | (defn- index-of 16 | "Returns the index of the first occurrence of `x` in `xs`." 17 | [^List xs x] 18 | (if (nil? xs) 19 | -1 20 | (.indexOf xs x))) 21 | 22 | (defn seq-contains? [xs x] 23 | (not (neg? (index-of xs x)))) 24 | 25 | (def conjv (fnil conj [])) 26 | 27 | (defmacro try* 28 | "Macro to catch multiple exceptions with one catch body. 29 | 30 | Usage: 31 | (try* 32 | (println :a) 33 | (println :b) 34 | (catch* [A B] e (println (class e))) 35 | (catch C e (println :C)) 36 | (finally (println :finally-clause))) 37 | 38 | Will be expanded to: 39 | (try 40 | (println :a) 41 | (println :b) 42 | (catch A e (println (class e))) 43 | (catch B e (println (class e))) 44 | (catch C e (println :C)) 45 | (finally (println :finally-clause))) 46 | 47 | https://clojure.atlassian.net/browse/CLJ-2124 48 | https://github.com/Gonzih/feeds2imap.clj/blob/master/src/feeds2imap/macro.clj" 49 | [& body] 50 | (letfn [(catch*? [form] 51 | (and (seq form) 52 | (= (first form) 'catch*))) 53 | (expand [[_catch* classes & catch-tail]] 54 | (map #(list* 'catch % catch-tail) classes)) 55 | (transform [form] 56 | (if (catch*? form) 57 | (expand form) 58 | [form]))] 59 | (cons 'try (mapcat transform body)))) 60 | 61 | (defmacro catch-some [& body] 62 | `(try 63 | ~@body 64 | nil 65 | (catch Exception ex# 66 | ex#))) 67 | -------------------------------------------------------------------------------- /test/darkleaf/di/add_side_dependency_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.add-side-dependency-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (t/deftest bug--array-map->hash-map--prepare 7 | (t/is (= clojure.lang.PersistentArrayMap 8 | (class {:a 1 9 | :b 2 10 | :c 3 11 | :d 4 12 | :e 5 13 | :f 6 14 | :g 7 15 | :h 8}))) 16 | (t/is (= clojure.lang.PersistentHashMap 17 | (class {:a 1 18 | :b 2 19 | :c 3 20 | :d 4 21 | :e 5 22 | :f 6 23 | :g 7 24 | :h 8 25 | :i 9})))) 26 | 27 | (defn- d1 28 | {::di/kind :component} 29 | [] 30 | :d1) 31 | 32 | (defn- d2 33 | {::di/kind :component} 34 | [] 35 | :d2) 36 | 37 | (defn- d3 38 | {::di/kind :component} 39 | [] 40 | :c) 41 | 42 | (defn- d4 43 | {::di/kind :component} 44 | [] 45 | :d4) 46 | 47 | (defn- d5 48 | {::di/kind :component} 49 | [] 50 | :d5) 51 | 52 | (defn- d6 53 | {::di/kind :component} 54 | [] 55 | :d6) 56 | 57 | (defn- d7 58 | {::di/kind :component} 59 | [] 60 | :d7) 61 | 62 | (defn- d8 63 | {::di/kind :component} 64 | [] 65 | :d8) 66 | 67 | (defn- extra-d9 68 | {::di/kind :component} 69 | [] 70 | :d9) 71 | 72 | (defn- extra-d10 73 | {::di/kind :component} 74 | [] 75 | :d10) 76 | 77 | 78 | (t/deftest bug-array-map->hash-map 79 | (let [log (atom []) 80 | after-build! (fn [{:keys [key]}] 81 | (swap! log conj key))] 82 | (with-open [root (di/start ::root 83 | {::root :ok} 84 | (di/add-side-dependency `d1) 85 | (di/add-side-dependency `d2) 86 | (di/add-side-dependency `d3) 87 | (di/add-side-dependency `d4) 88 | (di/add-side-dependency `d5) 89 | (di/add-side-dependency `d6) 90 | (di/add-side-dependency `d7) 91 | (di/add-side-dependency `d8) 92 | (di/add-side-dependency `extra-d9) 93 | (di/log :after-build! after-build!))] 94 | (t/is (= [::root `d1 `d2 `d3 `d4 `d5 `d6 `d7 `d8 `extra-d9] 95 | @log)) 96 | (t/is (= :ok @root))))) 97 | 98 | 99 | (t/deftest bug-array-map->hash-map-2 100 | (let [log (atom []) 101 | after-build! (fn [{:keys [key]}] 102 | (swap! log conj key))] 103 | (with-open [root (di/start ::root 104 | {::root :ok} 105 | (di/add-side-dependency `extra-d9) 106 | (di/add-side-dependency `d1) 107 | (di/add-side-dependency `d2) 108 | (di/add-side-dependency `d3) 109 | (di/add-side-dependency `d4) 110 | (di/add-side-dependency `d5) 111 | (di/add-side-dependency `d6) 112 | (di/add-side-dependency `d7) 113 | (di/add-side-dependency `d8) 114 | (di/add-side-dependency `extra-d10) 115 | (di/log :after-build! after-build!))] 116 | (t/is (= [::root 117 | `extra-d9 `d1 `d2 `d3 `d4 `d5 `d6 `d7 `d8 `extra-d10] 118 | @log)) 119 | (t/is (= :ok @root))))) 120 | 121 | (t/deftest bug-with-update-key 122 | (let [info (di/inspect ::root 123 | {::root 42 124 | ::unused 0 125 | ::side-dep :side-dep} 126 | (di/add-side-dependency ::side-dep) 127 | (di/update-key ::unused inc)) 128 | keys (into #{} 129 | (map :key) 130 | info)] 131 | (t/is (contains? keys ::side-dep)))) 132 | -------------------------------------------------------------------------------- /test/darkleaf/di/combine_dependencies_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.combine-dependencies-test 2 | (:require 3 | [darkleaf.di.core :as di] 4 | [clojure.test :as t])) 5 | 6 | (t/deftest combine-dependencies-test 7 | (t/are [expected input] 8 | (t/is (= expected (reduce di/combine-dependencies input))) 9 | {} 10 | [] 11 | 12 | {:a :required} 13 | [{:a :required}] 14 | 15 | {:a :optional} 16 | [{:a :optional}] 17 | 18 | {:a :required, :b :required} 19 | [{:a :required} {:b :required}] 20 | 21 | {:a :optional, :b :required} 22 | [{:a :optional} {:b :required}])) 23 | -------------------------------------------------------------------------------- /test/darkleaf/di/component_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.component-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.utils :refer [catch-some]])) 6 | 7 | (defn nil-component-0-arity 8 | {::di/kind :component} 9 | [] 10 | nil) 11 | 12 | (defn nil-component-1-arity 13 | {::di/kind :component} 14 | [-deps] 15 | nil) 16 | 17 | (t/deftest nil-component-0-arity-test 18 | (let [ex (catch-some (di/start `nil-component-0-arity))] 19 | (t/is (= ::di/nil-return (-> ex ex-cause ex-data :type))))) 20 | 21 | (t/deftest nil-component-1-arity-test 22 | (let [ex (catch-some (di/start `nil-component-1-arity))] 23 | (t/is (= ::di/nil-return (-> ex ex-cause ex-data :type))))) 24 | 25 | 26 | (defn component-2-arity 27 | {::di/kind :component} 28 | [deps extra-arg] 29 | :smth) 30 | 31 | (t/deftest component-2-arity-test 32 | (let [ex (catch-some (di/start `component-2-arity))] 33 | (t/is (= ::di/invalid-arity (-> ex ex-data :type))))) 34 | -------------------------------------------------------------------------------- /test/darkleaf/di/dependencies_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.dependencies-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as dip] 6 | [darkleaf.di.utils :refer [catch-some]])) 7 | 8 | ;; root 9 | ;; / \ 10 | ;; a b 11 | ;; \ / 12 | ;; c 13 | 14 | (defn root 15 | {::di/kind :component} 16 | [{a `a, b `b}] 17 | :root) 18 | 19 | (defn a 20 | {::di/kind :component} 21 | [{c `c}] 22 | :a) 23 | 24 | (defn b 25 | {::di/kind :component} 26 | [{c `c}] 27 | :b) 28 | 29 | (defn c 30 | {::di/kind :component} 31 | [] 32 | :c) 33 | 34 | (t/deftest order-test 35 | (let [log (atom []) 36 | after-build! (fn [{:keys [key]}] 37 | (swap! log conj [key :built])) 38 | after-demolish! (fn [{:keys [key]}] 39 | (swap! log conj [key :stopped]))] 40 | (with-open [root (di/start `root (di/log :after-build! after-build! 41 | :after-demolish! after-demolish!))]) 42 | (t/is (= [[`c :built] 43 | [`a :built] 44 | [`b :built] 45 | [`root :built] 46 | 47 | [`root :stopped] 48 | [`b :stopped] 49 | [`a :stopped] 50 | [`c :stopped]] 51 | @log)))) 52 | 53 | (defn root-path 54 | [{::syms [a-path b-path c-path]}] 55 | :done) 56 | 57 | (defn final-path 58 | [{}] 59 | :done) 60 | 61 | (defn a-path 62 | [{::syms [final-path]}] 63 | :done) 64 | 65 | (defn b-path 66 | [{::syms [missing-path]}] 67 | :done) 68 | 69 | (defn c-path 70 | [{::syms [final-path]}] 71 | :done) 72 | 73 | (t/deftest error-path-test 74 | (t/is (= {:type ::di/missing-dependency 75 | :stack [`missing-path `b-path `root-path]} 76 | (-> (di/start `root-path) 77 | catch-some 78 | ex-data)))) 79 | 80 | 81 | (defn parent 82 | [{::syms [missing-key]}] 83 | :done) 84 | 85 | 86 | (t/deftest missing-dependency-test 87 | (t/is (= {:type ::di/missing-dependency 88 | :stack [`missing-root]} 89 | (-> (di/start `missing-root) 90 | catch-some 91 | ex-data))) 92 | 93 | (t/is (= {:type ::di/missing-dependency 94 | :stack [`missing-key `parent]} 95 | (-> (di/start `parent) 96 | catch-some 97 | ex-data)))) 98 | 99 | 100 | (defn recursion-a 101 | [{::syms [recursion-b]}] 102 | :done) 103 | 104 | (defn recursion-b 105 | [{::syms [recursion-a]}] 106 | :done) 107 | 108 | (defn recursion-c 109 | [{::syms [recursion-c]}] 110 | :done) 111 | 112 | (t/deftest circular-dependency-test 113 | (t/is (= {:type ::di/circular-dependency 114 | :stack [`recursion-a `recursion-b `recursion-a]} 115 | (-> (di/start `recursion-a) 116 | catch-some 117 | ex-data))) 118 | 119 | 120 | 121 | (t/is (= {:type ::di/circular-dependency 122 | :stack [`recursion-c `recursion-c]} 123 | (-> (di/start `recursion-c) 124 | catch-some 125 | ex-data)))) 126 | -------------------------------------------------------------------------------- /test/darkleaf/di/dependency_types_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.dependency-types-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as p]) 6 | (:import 7 | (clojure.lang ExceptionInfo))) 8 | 9 | (defn factory [dependency-key dependency-type] 10 | (reify p/Factory 11 | (dependencies [_] 12 | {dependency-key dependency-type}) 13 | (build [_ deps _] 14 | [dependency-key (get deps dependency-key)]) 15 | (description [_]))) 16 | 17 | (t/deftest required-present-test 18 | (with-open [root (di/start ::root 19 | {::root (factory ::dependency :required) 20 | ::dependency 42})] 21 | (t/is (= [::dependency 42] @root)))) 22 | 23 | (t/deftest missed-root-test 24 | (t/is (thrown-with-msg? ExceptionInfo 25 | #"\AMissing dependency darkleaf.di.dependency-types-test/not-found\z" 26 | (di/start `not-found)))) 27 | 28 | (t/deftest required-missed-test 29 | (t/is (thrown-with-msg? ExceptionInfo 30 | #"\AMissing dependency :darkleaf.di.dependency-types-test/dependency\z" 31 | (di/start `root 32 | {`root (factory ::dependency :required)})))) 33 | 34 | (t/deftest required-circular-test 35 | (t/is (thrown-with-msg? ExceptionInfo 36 | #"\ACircular dependency :darkleaf.di.dependency-types-test/root\z" 37 | (di/start ::root 38 | {::root (factory ::root :required)})))) 39 | 40 | (t/deftest optional-present-test 41 | (with-open [root (di/start ::root 42 | {::root (factory ::dependency :optional) 43 | ::dependency 42})] 44 | (t/is (= [::dependency 42] @root)))) 45 | 46 | (t/deftest optional-missed-test 47 | (with-open [root (di/start ::root 48 | {::root (factory ::dependency :optional)})] 49 | (t/is (= [::dependency nil] @root)))) 50 | 51 | (t/deftest optional-circular-test 52 | (t/is (thrown-with-msg? ExceptionInfo 53 | #"\ACircular dependency :darkleaf.di.dependency-types-test/root\z" 54 | (di/start ::root {::root (factory ::root :optional)})))) 55 | -------------------------------------------------------------------------------- /test/darkleaf/di/deps_definition_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.deps-definition-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (defn binding-destructuring 7 | {::di/kind :component} 8 | [{skey :param 9 | qkey ::param 10 | ssym 'param 11 | qsym `param 12 | qsym* 'undefined-ns/param 13 | str "param"}] 14 | [skey qkey qsym qsym* ssym str]) 15 | 16 | (t/deftest binding-destructuring-test 17 | (with-open [root (di/start `binding-destructuring 18 | {:param :skey 19 | ::param :qkey 20 | 'param :ssym 21 | `param :qsym 22 | 'undefined-ns/param :qsym* 23 | "param" :str})] 24 | (t/is (= [:skey :qkey :qsym :qsym* :ssym :str] @root)))) 25 | 26 | 27 | (defn defaults 28 | {::di/kind :component} 29 | [{skey :param 30 | qkey ::param 31 | ssym 'param 32 | qsym `param 33 | qsym* 'undefined-ns/param 34 | str "param" 35 | :or {skey :skey 36 | qkey :qkey 37 | ssym :ssym 38 | qsym :qsym 39 | qsym* :qsym* 40 | str :str}}] 41 | [skey qkey qsym qsym* ssym str]) 42 | 43 | (t/deftest defaults-test 44 | (with-open [root (di/start `defaults)] 45 | (t/is (= [:skey :qkey :qsym :qsym* :ssym :str] @root)))) 46 | 47 | 48 | (defn keys-destructuring 49 | {::di/kind :component} 50 | [{:keys [skey] 51 | ::keys [qkey] 52 | :syms [ssym] 53 | ::syms [qsym] 54 | :undefined-ns/syms [qsym*] 55 | :strs [str]}] 56 | [skey qkey qsym qsym* ssym str]) 57 | 58 | (t/deftest keys-destructuring-test 59 | (with-open [root (di/start `keys-destructuring 60 | {:skey :skey 61 | ::qkey :qkey 62 | 'ssym :ssym 63 | `qsym :qsym 64 | 'undefined-ns/qsym* :qsym* 65 | "str" :str})] 66 | (t/is (= [:skey :qkey :qsym :qsym* :ssym :str] @root)))) 67 | -------------------------------------------------------------------------------- /test/darkleaf/di/destructuring_map_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.destructuring-map-test 2 | (:require 3 | [darkleaf.di.destructuring-map :as sut] 4 | [clojure.test :as t])) 5 | 6 | (t/deftest deps-test 7 | (t/are [expected input] 8 | (t/is (= (quote expected) 9 | (sut/dependencies (quote input)))) 10 | {} 11 | {} 12 | 13 | {} 14 | {:as x} 15 | 16 | {} 17 | {:or {a 1, b 2}} 18 | 19 | 20 | {:a :required, :b :required} 21 | {:keys [a b]} 22 | 23 | {:a :optional, :b :optional} 24 | {:keys [a b] :or {a :av, b :bv}} 25 | 26 | {::a :required, ::b :required} 27 | {::keys [a b]} 28 | 29 | {::a :optional, ::b :optional} 30 | {::keys [a b] :or {a :av, b :bv}} 31 | 32 | 33 | {a :required, b :required} 34 | {:syms [a b]} 35 | 36 | {a :optional, b :optional} 37 | {:syms [a b] :or {a :av, b :bv}} 38 | 39 | {foo/a :required, foo/b :required} 40 | {:foo/syms [a b]} 41 | 42 | {foo/a :optional, foo/b :optional} 43 | {:foo/syms [a b] :or {a :av, b :bv}} 44 | 45 | 46 | {"a" :required, "b" :required} 47 | {:strs [a b]} 48 | 49 | {"a" :optional, "b" :optional} 50 | {:strs [a b] :or {a :av, b :bv}} 51 | 52 | 53 | {:a :required, ::b :required} 54 | {a :a, b ::b} 55 | 56 | {:a :optional, ::b :optional} 57 | {a :a, b ::b, :or {a :av, b :bv}} 58 | 59 | 60 | {a :required, foo/b :required} 61 | {a 'a, b 'foo/b} 62 | 63 | {a :optional, foo/b :optional} 64 | {a 'a, b 'foo/b, :or {a :av, b :bv}} 65 | 66 | {"a" :required, "b" :required} 67 | {a "a", b "b"} 68 | 69 | {"a" :optional, "b" :optional} 70 | {a "a", b "b", :or {a :av, b :bv}})) 71 | -------------------------------------------------------------------------------- /test/darkleaf/di/memoize_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.memoize-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.utils :refer [catch-some]])) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (t/deftest memoize-test 10 | (let [a 'a 11 | identity* (memoize identity)] 12 | (identity* a) 13 | (t/is (not (identical? a (identity 'a)))) 14 | (t/is (identical? a (identity* 'a))))) 15 | 16 | (defn- some+identical? [a b] 17 | (and (some? a) 18 | (some? b) 19 | (identical? a b))) 20 | 21 | (defn- some+not-identical? [a b] 22 | (and (some? a) 23 | (some? b) 24 | (not (identical? a b)))) 25 | 26 | 27 | (defn a 28 | {::di/kind :component} 29 | [{_ ::param}] 30 | (Object.)) 31 | 32 | 33 | (t/deftest changed-not-identical-test 34 | (with-open [mem (di/->memoize {::param (Object.)}) 35 | first (di/start `a mem) 36 | second (di/start `a mem {::param (Object.)})] 37 | (t/is (some+not-identical? @first @second)))) 38 | 39 | 40 | (t/deftest changed-equal-and-identical-test 41 | (with-open [mem (di/->memoize {::param :equal-and-identical}) 42 | first (di/start `a mem) 43 | second (di/start `a mem {::param :equal-and-identical})] 44 | (t/is (some+identical? @first @second)))) 45 | 46 | 47 | (t/deftest changed-equal-but-not-identical-test 48 | (with-open [mem (di/->memoize {::param 'equal-but-not-identical}) 49 | first (di/start `a mem) 50 | second (di/start `a mem {::param 'equal-but-not-identical})] 51 | (t/is (some+identical? @first @second)))) 52 | 53 | 54 | (t/deftest changed-equal-but-different-test 55 | (with-open [mem (di/->memoize {::param []}) 56 | first (di/start `a mem) 57 | second (di/start `a mem {::param '()})] 58 | (t/is (some+identical? @first @second)))) 59 | 60 | (t/deftest start-stop-order-test 61 | (let [log (atom []) 62 | log-mw (fn [key-predicate] 63 | (di/log :after-build! 64 | #(when (key-predicate (:key %)) 65 | (swap! log conj [:start (:key %)])) 66 | :after-demolish! 67 | #(when (key-predicate (:key %)) 68 | (swap! log conj [:stop (:key %)])))) 69 | mem (di/->memoize {::param :param} (log-mw any?))] 70 | (-> (di/start `a mem) 71 | (di/stop)) 72 | (t/is (= [[:start ::param] 73 | [:start `a]] 74 | @log)) 75 | (swap! log empty) 76 | 77 | (-> (di/start `a mem) 78 | (di/stop)) 79 | (t/is (= [] @log)) 80 | 81 | (-> (di/start `a mem 82 | {::param :new-param} 83 | (log-mw #{::param})) 84 | (di/stop)) 85 | (t/is (= [[:start ::param] 86 | [:start `a] 87 | [:stop ::param]] 88 | @log)) 89 | (swap! log empty) 90 | 91 | (di/stop mem) 92 | (t/is (= [[:stop `a] 93 | [:stop `a] 94 | [:stop ::param]] 95 | @log)) 96 | (swap! log empty) 97 | 98 | (-> (di/start `a mem) 99 | (di/stop)) 100 | (t/is (= [[:start ::param] 101 | [:start `a]] 102 | @log)))) 103 | 104 | (t/deftest should-be-first-test 105 | (with-open [mem (di/->memoize)] 106 | (let [ex (catch-some (di/start `a {::param 42} mem))] 107 | (t/is (= ::di/wrong-memoized-registry-position 108 | (-> ex ex-data :type)))))) 109 | 110 | 111 | (t/deftest service-deps+body-change-test 112 | (with-open [mem (di/->memoize {::param-1 42 113 | ::param-2 0})] 114 | 115 | (defn service-1 [{param ::param-1}] 116 | [:a param]) 117 | (with-open [s (di/start `service-1 mem)] 118 | (t/is (= [:a 42] (s)))) 119 | 120 | (defn service-1 [{param ::param-2}] 121 | [:b param]) 122 | (with-open [s (di/start `service-1 mem)] 123 | (t/is (= [:b 0] (s)))))) 124 | 125 | 126 | (t/deftest component-deps+body-change-test 127 | (with-open [mem (di/->memoize {::param-1 42 128 | ::param-2 0})] 129 | 130 | (defn component-1 131 | {::di/kind :component} 132 | [{param ::param-1}] 133 | [:a param]) 134 | (with-open [s (di/start `component-1 mem)] 135 | (t/is (= [:a 42] @s))) 136 | 137 | (defn component-1 138 | {::di/kind :component} 139 | [{param ::param-2}] 140 | [:b param]) 141 | (with-open [s (di/start `component-1 mem)] 142 | (t/is (= [:b 0] @s))))) 143 | 144 | 145 | (t/deftest var-type-change-test 146 | (with-open [mem (di/->memoize {::param 42})] 147 | 148 | (def var-type-change-var :just-value) 149 | (with-open [s (di/start `var-type-change-var mem)] 150 | (t/is (= :just-value @s))) 151 | 152 | (defn var-type-change-var 153 | {::di/kind :component} 154 | [{param ::param}] 155 | [:a param]) 156 | (with-open [s (di/start `var-type-change-var mem)] 157 | (t/is (= [:a 42] @s))))) 158 | 159 | 160 | (t/deftest remove-watch-test 161 | (def remove-watch-var :_) 162 | (with-open [mem (di/->memoize) 163 | s (di/start `remove-watch-var mem)]) 164 | (t/is (= {} (.getWatches #'remove-watch-var)))) 165 | 166 | 167 | 168 | (t/deftest invalidation-log-test 169 | (let [log (atom []) 170 | log-mw (di/log :after-build! 171 | #(swap! log conj [:start (:key %)]) 172 | :after-demolish! 173 | #(swap! log conj [:stop (:key %)])) 174 | mem (di/->memoize {::param :param} log-mw)] 175 | 176 | (defn invalidation-a [] 177 | :a) 178 | 179 | (defn invalidation-b [{a `invalidation-a}] 180 | (a)) 181 | 182 | (-> (di/start `invalidation-b mem) 183 | (di/stop)) 184 | (t/is (= [[:start `invalidation-a] 185 | [:start `invalidation-b]] 186 | @log)) 187 | (swap! log empty) 188 | 189 | (-> (di/start `invalidation-b mem) 190 | (di/stop)) 191 | (t/is (= [] @log)) 192 | 193 | 194 | (defn invalidation-a [] 195 | :a') 196 | 197 | ;; A service without arguments is just a var 198 | ;; so `invalidation-b` received the same arguments. 199 | (-> (di/start `invalidation-b mem) 200 | (di/stop)) 201 | (t/is (= [[:start `invalidation-a]] 202 | @log)) 203 | (swap! log empty) 204 | 205 | 206 | (defn invalidation-a [{param ::param}] 207 | :a'') 208 | 209 | (-> (di/start `invalidation-b mem) 210 | (di/stop)) 211 | (t/is (= [[:start ::param] 212 | [:start `invalidation-a] 213 | [:start `invalidation-b]] 214 | @log)) 215 | (swap! log empty) 216 | 217 | 218 | (di/stop mem) 219 | (t/is (= [[:stop `invalidation-b] 220 | [:stop `invalidation-a] 221 | [:stop ::param] 222 | [:stop `invalidation-a] 223 | [:stop `invalidation-b] 224 | [:stop `invalidation-a]] 225 | @log)))) 226 | 227 | (comment 228 | 229 | (require '[clj-async-profiler.core :as prof]) 230 | (prof/serve-ui 8080) 231 | 232 | 233 | (def N 10000) 234 | 235 | (prof/profile {} 236 | (dotimes [_ N] 237 | (di/start `a {::param 42}))) 238 | 239 | 240 | 241 | (let [mem (di/->memoize {::param 42})] 242 | (prof/profile {} 243 | (dotimes [_ N] 244 | (di/start `a mem)))) 245 | 246 | 247 | (prof/generate-diffgraph 1 2 {}) 248 | 249 | 250 | 251 | (time 252 | (dotimes [_ N] 253 | (di/start `a {::param 42}))) 254 | 255 | (let [mem (di/->memoize {::param 42})] 256 | (time 257 | (dotimes [_ N] 258 | (di/start `a mem)))) 259 | 260 | 261 | ,,,) 262 | -------------------------------------------------------------------------------- /test/darkleaf/di/opt_ref_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.opt-ref-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (defn get-ref [{r ::ref 7 | :or {r :default}}] 8 | r) 9 | 10 | (t/deftest opt-ref-test 11 | (with-open [get-ref (di/start `get-ref 12 | {::ref (di/opt-ref ::replacement) 13 | ::replacement ::stub})] 14 | (t/is (= ::stub (get-ref))))) 15 | 16 | (t/deftest opt-ref-missed-test 17 | (with-open [get-ref (di/start `get-ref 18 | {::ref (di/opt-ref ::replacement) 19 | #_#_ 20 | ::replacement ::stub})] 21 | (t/is (= :default (get-ref))))) 22 | 23 | (t/deftest pr-test 24 | (t/is (= "#darkleaf.di.core/opt-ref :darkleaf.di.opt-ref-test/object" 25 | (pr-str (di/opt-ref ::object))))) 26 | -------------------------------------------------------------------------------- /test/darkleaf/di/ref_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.ref-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di]) 5 | (:import 6 | (clojure.lang ExceptionInfo))) 7 | 8 | (t/deftest ref-test 9 | (with-open [obj (di/start ::root 10 | {::root (di/ref ::replacement) 11 | ::replacement ::stub})] 12 | (t/is (= ::stub @obj)))) 13 | 14 | (t/deftest ref-missed-test 15 | (t/is (thrown-with-msg? ExceptionInfo 16 | #"\AMissing dependency darkleaf.di.ref-test/dep\z" 17 | (di/start ::root 18 | {::root (di/ref `dep)})))) 19 | 20 | (t/deftest pr-test 21 | (t/is (= "#darkleaf.di.core/ref :darkleaf.di.ref-test/object" 22 | (pr-str (di/ref ::object))))) 23 | -------------------------------------------------------------------------------- /test/darkleaf/di/registries_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.registries-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as p])) 6 | 7 | (def root 'root) 8 | 9 | (t/deftest ns-registry-test 10 | (with-open [root (di/start `root)] 11 | (t/is (= 'root @root))) 12 | (t/is (thrown? Throwable (di/start `undefined)))) 13 | 14 | (t/deftest env-registry-test 15 | (with-open [root (di/start "PATH")] 16 | (t/is (= (System/getenv "PATH") @root))) 17 | (t/is (thrown? Throwable (di/start "DI_UNDEFINED")))) 18 | 19 | (t/deftest map-registry-test 20 | (with-open [root (di/start `root {`root :stub})] 21 | (t/is (= :stub @root)))) 22 | 23 | (t/deftest sequential-registry-test 24 | (let [registries [{::a 1} 25 | [{::b 2}] 26 | [[{::c 3}]]]] 27 | (with-open [root (di/start ::a registries)] 28 | (t/is (= 1 @root))) 29 | (with-open [root (di/start ::b registries)] 30 | (t/is (= 2 @root))) 31 | (with-open [root (di/start ::c registries)] 32 | (t/is (= 3 @root))))) 33 | 34 | (t/deftest nil-registry-test 35 | (with-open [root (di/start `root nil)] 36 | (t/is (= 'root @root)))) 37 | 38 | 39 | (defn null-registry-middleware 40 | "It is an identity-like middleware. It does nothing." 41 | [] 42 | (fn [registry] 43 | (fn [key] 44 | (let [factory (registry key)] 45 | (reify p/Factory 46 | (dependencies [_] 47 | (p/dependencies factory)) 48 | (build [_ deps add-stop] 49 | (p/build factory deps add-stop)) 50 | (description [_] 51 | (p/description factory))))))) 52 | 53 | (t/deftest null-registry-middleware-test 54 | (with-open [root (di/start `root (null-registry-middleware))] 55 | (t/is (= 'root @root)))) 56 | -------------------------------------------------------------------------------- /test/darkleaf/di/root_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.root-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as p])) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (defn stoppable 10 | {::di/stop (fn [*stopped?] (reset! *stopped? true))} 11 | [{*stopped? ::*stopped?}] 12 | *stopped?) 13 | 14 | (t/deftest auto-closeable-test 15 | (let [*stopped? (atom false)] 16 | (with-open [root (di/start `stoppable {::*stopped? *stopped?})]) 17 | (t/is @*stopped?))) 18 | 19 | (t/deftest stoppable-test 20 | (let [*stopped? (atom false)] 21 | (di/stop (di/start `stoppable {::*stopped? *stopped?})) 22 | (t/is @*stopped?))) 23 | 24 | (t/deftest ideref-test 25 | (with-open [root (di/start ::root {::root 42})] 26 | (t/is (= 42 @root)))) 27 | 28 | (t/deftest ifn-test 29 | (with-open [root (di/start 30 | ::root 31 | {::root (fn 32 | ([] 0) 33 | ([a1] 1) 34 | ([a1 a2] 2) 35 | ([a1 a2 a3] 3) 36 | ([a1 a2 a3 a4] 4) 37 | ([a1 a2 a3 a4 a5] 5) 38 | ([a1 a2 a3 a4 a5 a6] 6) 39 | ([a1 a2 a3 a4 a5 a6 a7] 7) 40 | ([a1 a2 a3 a4 a5 a6 a7 a8] 8) 41 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9] 9) 42 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] 10) 43 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11] 11) 44 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12] 12) 45 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13] 13) 46 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14] 14) 47 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15] 15) 48 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16] 16) 49 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17] 17) 50 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18] 18) 51 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19] 19) 52 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20] 20) 53 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 & args] 21))})] 54 | (t/is (= 0 (.call ^java.util.concurrent.Callable root))) 55 | (t/is (= nil (.run ^java.lang.Runnable root))) 56 | (t/is (= 0 (root))) 57 | (t/is (= 1 (root 1))) 58 | (t/is (= 2 (root 1 2))) 59 | (t/is (= 3 (root 1 2 3))) 60 | (t/is (= 4 (root 1 2 3 4))) 61 | (t/is (= 5 (root 1 2 3 4 5))) 62 | (t/is (= 6 (root 1 2 3 4 5 6))) 63 | (t/is (= 7 (root 1 2 3 4 5 6 7))) 64 | (t/is (= 8 (root 1 2 3 4 5 6 7 8))) 65 | (t/is (= 9 (root 1 2 3 4 5 6 7 8 9))) 66 | (t/is (= 10 (root 1 2 3 4 5 6 7 8 9 10))) 67 | (t/is (= 11 (root 1 2 3 4 5 6 7 8 9 10 11))) 68 | (t/is (= 12 (root 1 2 3 4 5 6 7 8 9 10 11 12))) 69 | (t/is (= 13 (root 1 2 3 4 5 6 7 8 9 10 11 12 13))) 70 | (t/is (= 14 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14))) 71 | (t/is (= 15 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15))) 72 | (t/is (= 16 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16))) 73 | (t/is (= 17 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17))) 74 | (t/is (= 18 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18))) 75 | (t/is (= 19 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19))) 76 | (t/is (= 20 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20))) 77 | (t/is (= 21 (root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21))) 78 | (t/is (= 2 (apply root 1 [2]))))) 79 | 80 | (t/deftest pr-test 81 | (t/is (= "#darkleaf.di.core/root 42" 82 | (pr-str (di/start ::root {::root 42}))))) 83 | -------------------------------------------------------------------------------- /test/darkleaf/di/service_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.service-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (defn a [-deps] 7 | :a) 8 | 9 | (defmulti b 10 | {::di/deps []} 11 | identity) 12 | 13 | (t/deftest pr-test 14 | (with-open [root (di/start `a)] 15 | (t/is (= "#darkleaf.di.core/service #'darkleaf.di.service-test/a" 16 | (pr-str @root)))) 17 | (with-open [root (di/start `b)] 18 | (t/is (= "#darkleaf.di.core/service #'darkleaf.di.service-test/b" 19 | (pr-str @root))))) 20 | -------------------------------------------------------------------------------- /test/darkleaf/di/stop_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.stop-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (t/deftest nil-stop-test 7 | (t/is (nil? (di/stop nil)))) 8 | 9 | (t/deftest unbound-var-test 10 | (declare unbound-var) 11 | (t/is (nil? (di/stop unbound-var)))) 12 | -------------------------------------------------------------------------------- /test/darkleaf/di/template_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.template-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as p]) 6 | (:import 7 | (clojure.lang PersistentQueue))) 8 | 9 | (t/deftest template-ref-test 10 | (t/are [expected tmpl] 11 | (with-open [root (di/start ::root {::root tmpl ::dep :dep})] 12 | (t/is (= expected @root))) 13 | [:dep] 14 | (di/template [(di/ref ::dep)]) 15 | 16 | '(:dep) 17 | (di/template (list (di/ref ::dep))) 18 | 19 | {:key :dep} 20 | (di/template {:key (di/ref ::dep)}) 21 | 22 | #{:dep} 23 | (di/template #{(di/ref ::dep)}) 24 | 25 | [:dep] 26 | (di/template [(reify p/Factory 27 | (dependencies [_]) 28 | (build [_ deps _] :dep) 29 | (description [_]))]))) 30 | 31 | (t/deftest pr-test 32 | (t/is (= "#darkleaf.di.core/template [:a :b :c]" 33 | (pr-str (di/template [:a :b :c]))))) 34 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/a_intro_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Intro 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.a-intro-test 5 | {:nextjournal.clerk/visibility {:result :hide} 6 | :nextjournal.clerk/toc true} 7 | (:require 8 | [clojure.test :as t] 9 | [darkleaf.di.core :as di]) 10 | (:import 11 | (java.time Instant))) 12 | 13 | ;; Let's start. 14 | ;; In this chapter I'll show you how to deal with compoonents. 15 | 16 | ;; ## Trivial system 17 | 18 | ;; The following test describes the most trivial system that 19 | ;; contains the most trivial component. 20 | 21 | (def a ::a) 22 | 23 | ;; `root` is a system root. 24 | ;; To get root's value deref it. 25 | ;; To stop a system use `di/stop`. 26 | 27 | (t/deftest a-test 28 | (let [root (di/start `a)] 29 | (t/is (= ::a @root)) 30 | (di/stop root))) 31 | 32 | ;; ## AutoCloseable 33 | 34 | ;; A root implements `AutoCloseable` 35 | ;; so in tests we should use `with-open` macro 36 | ;; for propperly stopping. 37 | 38 | (t/deftest a'-test 39 | (with-open [root (di/start `a)] 40 | (t/is (= ::a @root)))) 41 | 42 | ;; ## Component 43 | 44 | ;; A component definition is a function of 0 or 1 arity 45 | ;; with `{::di/kind :componnent}` meta. 46 | 47 | (defn b 48 | {::di/kind :component} 49 | [] 50 | (Instant/now)) 51 | 52 | (t/deftest b-test 53 | (with-open [root (di/start `b)] 54 | (t/is (inst? @root)))) 55 | 56 | ;; ## Dependencies 57 | 58 | ;; To define a component dependes from other components 59 | ;; define a function of one argument. 60 | ;; DI will parse associative destructuing to get dependencides of the component. 61 | ;; We'll condider compoenent dependencies in the next chapter. 62 | ;; But now we will use placeholder. 63 | 64 | (defn c 65 | {::di/kind :component} 66 | [-deps] 67 | ::c) 68 | 69 | (t/deftest c-test 70 | (with-open [root (di/start `c)] 71 | (t/is (= ::c @root)))) 72 | 73 | ;; ## Services 74 | 75 | ;; A service is a function with or without dependencies. 76 | 77 | (defn d [] 78 | ::d) 79 | 80 | ;; `root` is a wrapper, and it implements `clojure.lang.IFn`, just like `clojure.lang.Var`. 81 | ;; So you can just call `root`. 82 | 83 | (t/deftest d-test 84 | (with-open [root (di/start `d)] 85 | (t/is (= ::d (@root) (root))))) 86 | 87 | (defn d* [-deps] 88 | ::d) 89 | 90 | (t/deftest d*-test 91 | (with-open [root (di/start `d*)] 92 | (t/is (= ::d (@root) (root))))) 93 | 94 | (defn e [-deps arg] 95 | [::e arg]) 96 | 97 | (t/deftest e-test 98 | (with-open [root (di/start `e)] 99 | (t/is (= [::e 42] (root 42))))) 100 | 101 | ;; ## Interactive Development 102 | 103 | ;; You don't need to restart the whole system if you redefine a service. 104 | ;; Just redefine a Var. 105 | ;; It's very helpful for interactive development. 106 | 107 | ;; It does not work if you change definition of dependencies, 108 | ;; so in this case you have to restart the system. 109 | 110 | ;; The new implementation of a service will receive the same dependencies. 111 | ;; To check that, I have to look a little ahead and define component with a dependency. 112 | ;; As I said we consider deps in the next chapter. 113 | 114 | (t/deftest f-test 115 | (defn f [{x ::x} arg] 116 | [::f x arg]) 117 | (with-open [root (di/start `f {::x :x})] 118 | (defn f [deps arg] 119 | [::new-f (deps ::x) arg]) 120 | (t/is (= [::new-f :x 42] (root 42))))) 121 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/b_dependencies_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Dependencies 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.b-dependencies-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di]) 9 | (:import 10 | (clojure.lang ExceptionInfo))) 11 | 12 | ;; DI uses associative destructuring syntax to define dependencies of a component. 13 | ;; https://clojure.org/guides/destructuring#_associative_destructuring 14 | 15 | ;; There is a mapping between keys and components. 16 | ;; A key can be symbol, keyword, or string. 17 | ;; In this chapter we'll use only symbols. 18 | 19 | ;; If we use symbols DI will try to resolve a component's var. 20 | 21 | ;; In the following example the `root` component depends on 22 | ;; the `a` and `b` and the `b` is optional. 23 | ;; You also can get all component dependencies by the `deps` binding. 24 | 25 | (defn root 26 | {::di/kind :component} 27 | [{a `a 28 | ::syms [b] 29 | :or {b ::default} 30 | :as deps}] 31 | [:root a b deps]) 32 | 33 | (def a ::a) 34 | 35 | (t/deftest root-test 36 | (with-open [root (di/start `root)] 37 | (t/is (= [:root ::a ::default {`a ::a}] @root)))) 38 | 39 | ;; `di/start` can accepts additional arguments. 40 | ;; In the following example the argument is a map registry. 41 | ;; I use it to define local keys. 42 | ;; In general they are middlewares but I'll describe it later. 43 | 44 | (t/deftest root-with-extra-deps-test 45 | (with-open [root (di/start `root {`b ::b})] 46 | (t/is (= [:root ::a ::b {`a ::a `b ::b}] @root)))) 47 | 48 | ;; Dependencies are required by default. 49 | ;; There is no defenition of `a'` so DI will throw an exception. 50 | 51 | (defn root' 52 | {::di/kind :component} 53 | [{a `a'}] 54 | [::root a]) 55 | 56 | (t/deftest root'-test 57 | (t/is (thrown-with-msg? ExceptionInfo 58 | #"Missing dependency darkleaf.di.tutorial.b-dependencies-test/a'" 59 | (di/start `root')))) 60 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/c_stop_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Stop 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.c-stop-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; To stop a component, you should teach DI how to do it. 11 | ;; Use `::di/stop` to define a stop function. 12 | 13 | (defn root 14 | {::di/stop #(reset! % true)} 15 | [{::keys [*stopped?]}] 16 | *stopped?) 17 | 18 | (t/deftest stop-test 19 | (let [*stopped? (atom false)] 20 | (with-open [root (di/start `root {::*stopped? *stopped?})] 21 | (t/is (= false @@root))) 22 | (t/is @*stopped?))) 23 | 24 | ;; In most cases, a component will be a Java class. 25 | ;; To prevent reflection calls use `memfn` 26 | ;; ```clojue 27 | ;; (defn- connection-manager 28 | ;; {::di/stop (memfn ^AutoCloseable close)} 29 | ;; [{max-conn :env.long/CONNECTION_MANAGER_MAX_CONN 30 | ;; :or {max-conn 50}}] 31 | ;; (ConnctionManager. max-conn)) 32 | ;; ``` 33 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/l_registries_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Registries 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.l-registries-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; Here we use undefined dependencies. 11 | 12 | (defn value 13 | {::di/kind :component} 14 | [{dep-a `dep-a 15 | dep-b `dep-b}] 16 | [:value dep-a dep-b]) 17 | 18 | ;; To locally define or redefine a dependency we should use registries. 19 | 20 | (t/deftest map-registry 21 | (with-open [root (di/start `value {`dep-a :a `dep-b :b})] 22 | (t/is (= [:value :a :b] @root))) 23 | 24 | (with-open [root (di/start `value {`value :replacement})] 25 | (t/is (= :replacement @root))) 26 | 27 | (with-open [root (di/start `value {`dep-a :a} {`dep-b :b})] 28 | (t/is (= [:value :a :b] @root))) 29 | 30 | ;; last wins 31 | (with-open [root (di/start `value 32 | {`dep-a :a `dep-b :b} 33 | {`dep-a :a' `dep-b :b'})] 34 | (t/is (= [:value :a' :b'] @root)))) 35 | 36 | 37 | ;; To avoid using `(apply di/start ...)`, 38 | ;; we can use an seqable value as a single registry. 39 | ;; See `clojure.core/seqable?`. 40 | (t/deftest seqable-registry 41 | (with-open [root (di/start `value [{`dep-a :a} 42 | [{`dep-b :b}]])] 43 | (t/is (= [:value :a :b] @root)))) 44 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/m_abstractions_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Abstractions 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.m-abstractions-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; For some reasons, we may want to not depend on specific vars. 11 | ;; In this case, use keywords instead of symbols to define dependencies. 12 | ;; Later in the main function you will be able to bind all parts of your application. 13 | 14 | (defn get-user [{ds ::datasource} id] 15 | (ds id)) 16 | 17 | (defn get-current-user [{session ::session 18 | get-user `get-user}] 19 | (-> session :user-id get-user)) 20 | 21 | (defn ring-handler [{get-current-user `get-current-user} -req] 22 | {:status 200 :body (str "Hi, " (get-current-user) "!")}) 23 | 24 | (t/deftest handler-test 25 | (with-open [root (di/start `ring-handler 26 | {::datasource {1 "John"} 27 | ::session {:user-id 1}})] 28 | (t/is (= {:status 200 :body "Hi, John!"} 29 | (root {}))))) 30 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/n_env_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Env 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.n-env-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; Like symbols and keywords, you can also use strings for keys. 11 | ;; String keys are resolved into values of environment variables. 12 | 13 | (defn root 14 | {::di/kind :component} 15 | [{path "PATH"}] 16 | [:root path]) 17 | 18 | (def PATH (System/getenv "PATH")) 19 | 20 | (t/deftest root-test 21 | (with-open [root (di/start `root)] 22 | (t/is (= [:root PATH] @root)))) 23 | 24 | ;; As of 2.3.0, there is `di/env-parsing` registry middleware 25 | ;; to parse values of environment variables. 26 | ;; You can define a dependency of env as a string key like \"PORT\", 27 | ;; and its value will be a string. 28 | ;; With this middleware, you can define it as a qualified keyword like :env.long/PORT, 29 | ;; and its value will be a number. 30 | 31 | (defn jetty 32 | {::di/kind :component} 33 | [{port :env.long/PORT 34 | :or {port 8080}}] 35 | [:jetty port]) 36 | 37 | (t/deftest jetty-test 38 | (with-open [jetty (di/start `jetty 39 | (di/env-parsing {:env.long parse-long}))] 40 | (t/is (= [:jetty 8080] @jetty))) 41 | (with-open [jetty (di/start `jetty 42 | (di/env-parsing :env.long parse-long) 43 | {"PORT" "8081"})] 44 | (t/is (= [:jetty 8081] @jetty)))) 45 | 46 | (defn required-env 47 | {::di/kind :component} 48 | [{enabled :env.bool/ENABLED}] 49 | [:enabled enabled]) 50 | 51 | (defn optional-env 52 | {::di/kind :component} 53 | [{enabled :env.bool/ENABLED 54 | :or {enabled true}}] 55 | [:enabled enabled]) 56 | 57 | (t/deftest env-test 58 | (t/is (thrown? clojure.lang.ExceptionInfo 59 | (di/start `required-env 60 | (di/env-parsing {:env.bool #(= "true" %)})))) 61 | 62 | (with-open [sys (di/start `required-env 63 | (di/env-parsing {:env.bool #(= "true" %)}) 64 | {"ENABLED" "false"})] 65 | (t/is (= [:enabled false] @sys))) 66 | 67 | (with-open [sys (di/start `optional-env 68 | (di/env-parsing {:env.bool #(= "true" %)}))] 69 | (t/is (= [:enabled true] @sys))) 70 | 71 | (with-open [sys (di/start `optional-env 72 | (di/env-parsing {:env.bool #(= "true" %)}) 73 | {"ENABLED" "false"})] 74 | (t/is (= [:enabled false] @sys)))) 75 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/o_data_dsl_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Data DSL 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.o-data-dsl-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; It is often to use data-DSLs in Clojure, such as reitit routing, 11 | ;; and DI offers tools to handle them easily. 12 | ;; Here they are: `di/template` and `di/ref`. 13 | 14 | ;; You can also use `di/opt-ref` for optional dependencies. 15 | ;; If there is no defined key opt-ref resolves to nil. 16 | 17 | (def route-data 18 | (di/template 19 | [["/" {:get {:handler (di/ref `root-handler)}}] 20 | ["/news" {:get {:handler (di/ref `news-handler)}}]])) 21 | 22 | (t/deftest template-test 23 | (letfn [(root-handler [req]) 24 | (news-handler [req])] 25 | (with-open [root (di/start `route-data {`root-handler root-handler 26 | `news-handler news-handler})] 27 | (t/is (= [["/" {:get {:handler root-handler}}] 28 | ["/news" {:get {:handler news-handler}}]] 29 | @root))))) 30 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/p_derive_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Derive 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.p-derive-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; In some cases, your components may have a complex structure or require transfromation. 11 | ;; You can use `di/derive` to transform a component. 12 | 13 | ;; The first way 14 | 15 | (defn port 16 | {::di/kind :component} 17 | [{port "PORT"}] 18 | (parse-long port)) 19 | 20 | (t/deftest port-test 21 | (with-open [root (di/start `port {"PORT" "8080"})] 22 | (t/is (= 8080 @root)))) 23 | 24 | 25 | ;; The second way 26 | 27 | (def port' (di/derive "PORT" parse-long)) 28 | 29 | (t/deftest port'-test 30 | (with-open [root (di/start `port' {"PORT" "8080"})] 31 | (t/is (= 8080 @root)))) 32 | 33 | 34 | (def -box (di/template [(di/opt-ref ::a) 35 | (di/opt-ref ::b) 36 | (di/opt-ref ::c)])) 37 | (def box (di/derive `-box (partial filter some?))) 38 | 39 | (t/deftest box-test 40 | (with-open [root (di/start `box {::b :b})] 41 | (t/is (= [:b] @root)))) 42 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/q_starting_many_keys_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Starting many keys 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.q-starting-many-keys-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [darkleaf.di.core :as di] 8 | [clojure.test :as t])) 9 | 10 | ;; The standard `with-open` does not support destructuring in bindings. 11 | ;; Use `di/with-open` to handle resources with destructuring support. 12 | 13 | (def a :a) 14 | (def b :b) 15 | 16 | (t/deftest verbose-test 17 | (di/with-open [[a b] (di/start ::root {::root (di/template [(di/ref `a) (di/ref `b)])})] 18 | (t/is (= :a a)) 19 | (t/is (= :b b)))) 20 | 21 | ;; The root container implements `clojure.lang.Indexed` 22 | ;; so you can use destructuring without derefing the root. 23 | 24 | (t/deftest indexed-test 25 | (di/with-open [[a b] (di/start [`a `b])] 26 | (t/is (= :a a)) 27 | (t/is (= :b b)))) 28 | 29 | ;; The root container implements `clojure.lang.ILookup` 30 | ;; so you can use destructuring without derefing the root. 31 | 32 | (t/deftest lookup-test 33 | (di/with-open [{:keys [a b]} (di/start {:a `a :b `b})] 34 | (t/is (= :a a)) 35 | (t/is (= :b b)))) 36 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/r_multimethods_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Multimethods 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.r-multimethods-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; You can use `defmulti` like `defn` to define a service. 11 | ;; Instead of `defn`, there is no way to get a definition of dependencies 12 | ;; and we have to define them as `::di/deps` on metadata. 13 | 14 | (defmulti service 15 | {::di/deps [::x]} 16 | (fn [-deps kind] kind)) 17 | 18 | (defmethod service :default [{x ::x} kind] 19 | [kind x]) 20 | 21 | (t/deftest required-dep-test 22 | (with-open [root (di/start `service {::x :value})] 23 | (t/is (= [:kind :value] (root :kind))))) 24 | 25 | ;; `::di/deps` defines only required dependencies, mostly for simplicity. 26 | ;; If you need to use an optional dependency, 27 | ;; simply convert it to a required dependency by adding a default value. 28 | 29 | (defn- wrap-default [x default] 30 | (if (some? x) x default)) 31 | 32 | (def dep (di/derive ::optional wrap-default :default-value)) 33 | 34 | (t/deftest optional-dep-test 35 | (with-open [root (di/start `service {::x (di/ref `dep), ::optional :value})] 36 | (t/is (= [:kind :value] (root :kind)))) 37 | 38 | (with-open [root (di/start `service {::x (di/ref `dep)})] 39 | (t/is (= [:kind :default-value] (root :kind))))) 40 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_add_side_dependency_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Add side dependency 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.x-add-side-dependency-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; Actually, the rest of the `di/start` arguments are middlewares. 11 | ;; Maps and collections are special cases of ones. 12 | ;; Middlewares allow us to implement extra features. 13 | 14 | ;; In this test I'll show you how to perform a side effect like a database migration. 15 | 16 | (defn root 17 | {::di/kind :component} 18 | [] 19 | 'root) 20 | 21 | (defn migrations 22 | {::di/kind :component} 23 | [{::keys [*migrated?]}] 24 | (reset! *migrated? true)) 25 | 26 | (t/deftest add-side-dependency-test 27 | (let [*migrated? (atom false)] 28 | (with-open [root (di/start `root 29 | (di/add-side-dependency `migrations) 30 | {::*migrated? *migrated?})] 31 | (t/is @*migrated?) 32 | (t/is (= 'root @root))))) 33 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_inspect_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.tutorial.x-inspect-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di] 5 | [darkleaf.di.protocols :as p] 6 | [darkleaf.di.tutorial.x-ns-publics-test :as x-ns-publics-test])) 7 | 8 | (t/deftest env-test 9 | (t/is (= [{:key "FOO" 10 | :description {::di/kind :env 11 | ::di/root true}}] 12 | (di/inspect "FOO")))) 13 | 14 | 15 | (t/deftest fixed-env-test 16 | (t/is (= [{:key "FOO" 17 | :description {::di/kind :trivial 18 | :object "value" 19 | ::di/root true}}] 20 | (di/inspect "FOO" {"FOO" "value"})))) 21 | 22 | 23 | (def variable :obj) 24 | 25 | (t/deftest variable-test 26 | (t/is (= [{:key `variable 27 | :description {::di/kind :trivial 28 | :object :obj 29 | ::di/root true 30 | ::di/variable #'variable}}] 31 | (di/inspect `variable)))) 32 | 33 | 34 | (def variable+factory 35 | (reify p/Factory 36 | (dependencies [_]) 37 | (build [_ _ _] :ok) 38 | (description [_] {}))) 39 | 40 | (t/deftest variable+factory-test 41 | (t/is (= [{:key `variable+factory 42 | :description {::di/root true 43 | ::di/variable #'variable+factory}}] 44 | (di/inspect `variable+factory)))) 45 | 46 | 47 | (def variable+description 48 | (reify p/Factory 49 | (dependencies [_]) 50 | (build [_ _ _] :ok) 51 | (description [_] 52 | {::di/kind ::variable+description}))) 53 | 54 | (t/deftest variable+description-test 55 | (t/is (= [{:key `variable+description 56 | :description {::di/kind ::variable+description 57 | ::di/root true 58 | ::di/variable #'variable+description}}] 59 | (di/inspect `variable+description)))) 60 | 61 | 62 | (def variable+template 63 | (di/template [42])) 64 | 65 | (t/deftest variable+template-test 66 | (t/is (= [{:key `variable+template 67 | :description {::di/kind :template 68 | :template [42] 69 | ::di/root true 70 | ::di/variable #'variable+template}}] 71 | (di/inspect `variable+template)))) 72 | 73 | 74 | (defn component-0-arity 75 | {::di/kind :component} 76 | [] 77 | :ok) 78 | 79 | (t/deftest component-0-arity-test 80 | (t/is (= [{:key `component-0-arity 81 | :description {::di/kind :component 82 | ::di/root true 83 | ::di/variable #'component-0-arity}}] 84 | (di/inspect `component-0-arity)))) 85 | 86 | 87 | (defn component-1-arity 88 | {::di/kind :component} 89 | [-deps] 90 | :ok) 91 | 92 | (t/deftest component-1-arity-test 93 | (t/is (= [{:key `component-1-arity 94 | :description {::di/kind :component 95 | ::di/root true 96 | ::di/variable #'component-1-arity}}] 97 | (di/inspect `component-1-arity)))) 98 | 99 | 100 | (defn service-0-arity 101 | {::di/kind :service} 102 | [] 103 | :ok) 104 | 105 | (t/deftest service-0-arity-test 106 | (t/is (= [{:key `service-0-arity 107 | :description {::di/kind :service 108 | ::di/root true 109 | ::di/variable #'service-0-arity}}] 110 | (di/inspect `service-0-arity)))) 111 | 112 | 113 | (defn service-n-arity 114 | {::di/kind :service} 115 | [-deps] 116 | :ok) 117 | 118 | (t/deftest service-n-arity-test 119 | (t/is (= [{:key `service-n-arity 120 | :description {::di/kind :service 121 | ::di/root true 122 | ::di/variable #'service-n-arity}}] 123 | (di/inspect `service-n-arity)))) 124 | 125 | 126 | (defmulti multimethod-service 127 | {::di/deps []} 128 | (fn [-deps kind] kind)) 129 | 130 | (t/deftest multimethod-service-test 131 | (t/is (= [{:key `multimethod-service 132 | :description {::di/kind :service 133 | ::di/root true 134 | ::di/variable #'multimethod-service}}] 135 | (di/inspect `multimethod-service)))) 136 | 137 | 138 | (t/deftest ref-test 139 | (t/is (= [{:key `foo 140 | :dependencies {`bar :required} 141 | :description {::di/kind :ref 142 | :key `bar 143 | :type :required 144 | ::di/root true}} 145 | {:key `bar 146 | :description {::di/kind :undefined}}] 147 | (di/inspect `foo {`foo (di/ref `bar)})))) 148 | 149 | 150 | (t/deftest template-test 151 | (t/is (= [{:key `foo 152 | :dependencies {`bar :required} 153 | :description {::di/kind :template 154 | :template [42 (di/ref `bar)] 155 | ::di/root true}} 156 | {:key `bar 157 | :description {::di/kind :undefined}}] 158 | (di/inspect `foo {`foo (di/template [42 (di/ref `bar)])})))) 159 | 160 | 161 | (t/deftest derive-test 162 | (t/is (= [{:key `foo 163 | :dependencies {`bar :optional} 164 | :description {::di/kind :derive 165 | :key `bar 166 | :f str 167 | :args ["arg"] 168 | ::di/root true}} 169 | {:key `bar 170 | :description {::di/kind :undefined}}] 171 | (di/inspect `foo {`foo (di/derive `bar str "arg")})))) 172 | 173 | 174 | (t/deftest trivial-nil-test 175 | (t/is (= [{:key `foo 176 | :description {::di/kind :trivial 177 | :object nil 178 | ::di/root true}}] 179 | (di/inspect `foo {`foo nil})))) 180 | 181 | 182 | (t/deftest trivial-obj-test 183 | (t/is (= [{:key `foo 184 | :description {::di/kind :trivial 185 | :object str 186 | ::di/root true}}] 187 | (di/inspect `foo {`foo str})))) 188 | 189 | 190 | (t/deftest update-key-test 191 | (t/is (= [{:key `a 192 | :dependencies {`b :required} 193 | :description {::di/kind :trivial 194 | :object :obj 195 | ::di/root true 196 | ::di/update-key 197 | [[{::di/kind :trivial 198 | :object str} 199 | {::di/kind :ref 200 | :key `b 201 | :type :required}] 202 | [{::di/kind :trivial 203 | :object identity}]]}} 204 | {:key `b 205 | :description {::di/kind :trivial 206 | :object "b"}}] 207 | (di/inspect `a 208 | {`a :obj 209 | `b "b"} 210 | (di/update-key `a str (di/ref `b)) 211 | (di/update-key `a identity))))) 212 | 213 | 214 | (t/deftest add-side-dependency-test 215 | (t/is (= [{:key `a 216 | :description {::di/kind :trivial 217 | :object :obj 218 | ::di/root true}} 219 | {:key `side-dep-1 220 | :description {::di/kind :trivial 221 | :object :side-dep 222 | ::di/side-dependency true}} 223 | {:key `side-dep-2 224 | :description {::di/kind :trivial 225 | :object :side-dep 226 | ::di/side-dependency true}}] 227 | (di/inspect `a 228 | {`a :obj 229 | `side-dep-1 :side-dep 230 | `side-dep-2 :side-dep} 231 | (di/add-side-dependency `side-dep-1) 232 | (di/add-side-dependency `side-dep-2))))) 233 | 234 | 235 | (t/deftest ns-publics-test 236 | (t/is (= [{:key :ns-publics/darkleaf.di.tutorial.x-ns-publics-test 237 | :dependencies {`x-ns-publics-test/service :required 238 | `x-ns-publics-test/component :required 239 | `x-ns-publics-test/ok-test :required} 240 | :description {::di/kind :middleware 241 | :middleware ::di/ns-publics 242 | :ns 'darkleaf.di.tutorial.x-ns-publics-test 243 | ::di/root true}} 244 | {:key `x-ns-publics-test/service 245 | :dependencies {`x-ns-publics-test/component :required} 246 | :description {::di/kind :service 247 | ::di/variable #'x-ns-publics-test/service}} 248 | {:key `x-ns-publics-test/component 249 | :description {::di/kind :component 250 | ::di/variable #'x-ns-publics-test/component}} 251 | {:key `x-ns-publics-test/ok-test 252 | :description {::di/kind :trivial 253 | :object x-ns-publics-test/ok-test 254 | ::di/variable #'x-ns-publics-test/ok-test}}] 255 | (di/inspect :ns-publics/darkleaf.di.tutorial.x-ns-publics-test 256 | (di/ns-publics))))) 257 | 258 | 259 | (t/deftest env-parsing-test 260 | (t/is (= [{:key :env.long/PORT 261 | :dependencies {"PORT" :optional} 262 | :description {::di/kind :middleware 263 | :middleware ::di/env-parsing 264 | :cmap {:env.long parse-long} 265 | ::di/root true}} 266 | {:key "PORT" 267 | :description {::di/kind :trivial 268 | :object "8080"}}] 269 | (di/inspect :env.long/PORT 270 | (di/env-parsing :env.long parse-long) 271 | {"PORT" "8080"})))) 272 | 273 | 274 | (t/deftest log-test 275 | (t/is (= [{:key `foo 276 | :description {::di/kind :trivial 277 | :object :obj 278 | ::di/log {:will-be-logged true 279 | #_#_:opts nil} 280 | ::di/root true}}] 281 | (di/inspect `foo 282 | {`foo :obj} 283 | (di/log))))) 284 | 285 | 286 | (def variable-factory-regression 287 | (reify p/Factory 288 | (dependencies [_]) 289 | (build [_ _ _] :ok) 290 | (description [_]))) 291 | 292 | (t/deftest variable-factory-regression-test 293 | (t/is (= :ok 294 | @(di/start `variable-factory-regression)))) 295 | 296 | 297 | (t/deftest vector-test 298 | (t/is (= [{:key "A" 299 | :description {::di/kind :env 300 | ::di/root true}} 301 | {:key "B" 302 | :description {::di/kind :env 303 | ::di/root true}}] 304 | (di/inspect ["A" "B"])))) 305 | 306 | 307 | (t/deftest map-test 308 | (t/is (= [{:key "A" 309 | :description {::di/kind :env 310 | ::di/root true}} 311 | {:key "B" 312 | :description {::di/kind :env 313 | ::di/root true}}] 314 | (di/inspect {:a "A" :b "B"})))) 315 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_instrument_test.clj.disabled: -------------------------------------------------------------------------------- 1 | ;; # Instrument 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.x-instrument-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [clojure.spec.alpha :as s] 9 | [darkleaf.di.core :as di]) 10 | (:import 11 | (clojure.lang ExceptionInfo))) 12 | 13 | ;; Actually, the rest of the `di/start` arguments are middlewares. 14 | ;; Maps and collections are special cases of ones. 15 | ;; Middlewares are usefull for logging, schema validation, AOP, etc. 16 | 17 | ;; For this case we have `di/instrument` middleware. 18 | ;; https://en.wikipedia.org/wiki/Decorator_pattern 19 | 20 | (s/def root vector?) 21 | (s/def ::a keyword?) 22 | 23 | (defn root 24 | {::di/kind :component} 25 | [{::keys [a]}] 26 | [:root a]) 27 | 28 | (t/deftest ok-test 29 | (with-open [root (di/start `root 30 | {::a :a} 31 | (di/instrument s/assert*))] 32 | (t/is (= [:root :a] @root)))) 33 | 34 | (t/deftest fail-test 35 | (t/is (thrown-with-msg? ExceptionInfo 36 | #"\ASpec assertion failed" 37 | (di/start `root 38 | {::a 42} 39 | (di/instrument s/assert*))))) 40 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_log_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.tutorial.x-log-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | (defn a 7 | {::di/kind :component} 8 | [] 9 | :a) 10 | 11 | (defn b [{a `a}] 12 | :b) 13 | 14 | (defn c 15 | {::di/kind :component} 16 | [{b `b}] 17 | :c) 18 | 19 | (t/deftest log 20 | (let [logs (atom []) 21 | after-build! (fn [{:keys [key object]}] 22 | (swap! logs conj [:built key (pr-str object)])) 23 | after-demolish! (fn [{:keys [key object]}] 24 | (swap! logs conj [:demolished key (pr-str object)]))] 25 | (with-open [root (di/start `c (di/log :after-build! after-build! 26 | :after-demolish! after-demolish!))]) 27 | (t/is (= [[:built `a ":a"] 28 | [:built `b 29 | "#darkleaf.di.core/service #'darkleaf.di.tutorial.x-log-test/b"] 30 | [:built `c ":c"] 31 | [:demolished `c ":c"] 32 | [:demolished `b 33 | "#darkleaf.di.core/service #'darkleaf.di.tutorial.x-log-test/b"] 34 | [:demolished `a ":a"]] 35 | @logs)))) 36 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_ns_publics_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.tutorial.x-ns-publics-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | ;; excluded 7 | (def nil-component nil) 8 | 9 | ;; excluded 10 | (def unbound-component) 11 | 12 | (defn component 13 | {::di/kind :component} 14 | [] 15 | :component) 16 | 17 | (defn service [{component `component} arg] 18 | [component arg]) 19 | 20 | (t/deftest ok-test 21 | (with-open [system (di/start :ns-publics/darkleaf.di.tutorial.x-ns-publics-test 22 | (di/ns-publics))] 23 | (t/is (map? @system)) 24 | (t/is (= #{:component :service :ok-test} 25 | (set (keys @system)))) 26 | (t/is (= :component (:component system))) 27 | (t/is (= [:component :my-arg] ((:service system) :my-arg))))) 28 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_override_deps_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.tutorial.x-override-deps-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di])) 5 | 6 | ;; (defn a [{dep ::dep 7 | ;; common ::common}] 8 | ;; [:a dep common]) 9 | 10 | ;; (defn b [{dep ::dep 11 | ;; common ::common}] 12 | ;; [:b dep common]) 13 | 14 | ;; (t/deftest ok 15 | ;; (with-open [root (di/start [`a `b] 16 | ;; {::dep-a :dep-a 17 | ;; ::dep-b :dep-b 18 | ;; ::common :c} 19 | ;; (di/rename-deps `a {::dep ::dep-a}) 20 | ;; (di/rename-deps `b {::dep ::dep-b}))] 21 | ;; (let [[a b] root] 22 | ;; (t/is (= [:a :dep-a :c] a)) 23 | ;; (t/is (= [:b :dep-b :c] b))))) 24 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/x_update_key_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Update key 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.x-update-key-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; In most cases you just want to instrument or update one dependency. 11 | 12 | (def route-data []) 13 | 14 | (defn subsystem-a-route-data 15 | {::di/kind :component} 16 | [-deps] 17 | ["/a"]) 18 | 19 | (t/deftest update-key-test 20 | (with-open [root (di/start `route-data 21 | (di/update-key `route-data conj 22 | (di/ref `subsystem-a-route-data) 23 | ["/b"] 24 | nil) 25 | {})] 26 | (t/is (= [["/a"] ["/b"] nil] @root)))) 27 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/y_graceful_stop_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Graceful stop 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.y-graceful-stop-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di] 9 | [darkleaf.di.utils :refer [catch-some]])) 10 | 11 | ;; The DI tries to stop components that are already started 12 | ;; if another component fails while it is starting. 13 | 14 | ;; It throws with original exception. 15 | ;; All other possible exceptions are added as suppressed. 16 | 17 | (defn root 18 | {::di/kind :component} 19 | [{dep `dep 20 | on-start-root-ex ::on-start-root-ex}] 21 | (throw on-start-root-ex)) 22 | 23 | (defn dep 24 | {::di/stop (fn [on-stop-dep-ex] (throw on-stop-dep-ex))} 25 | [{on-stop-dep-ex ::on-stop-dep-ex}] 26 | on-stop-dep-ex) 27 | 28 | 29 | (t/deftest graceful-start-test 30 | (let [on-start-root-ex (ex-info "on start root" {}) 31 | on-stop-dep-ex (ex-info "on stop dep" {}) 32 | registry {::on-start-root-ex on-start-root-ex 33 | ::on-stop-dep-ex on-stop-dep-ex} 34 | ex (-> (di/start `root registry) 35 | catch-some)] 36 | (t/is (= ::di/build-failure (-> ex ex-data :type))) 37 | (t/is (= [`root] (-> ex ex-data :stack))) 38 | (t/is (= on-start-root-ex (-> ex ex-cause))) 39 | (t/is (= [on-stop-dep-ex] (-> ex .getSuppressed seq))))) 40 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/y_multi_arity_service_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Multi arity service 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.y-multi-arity-service-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.test :as t] 8 | [darkleaf.di.core :as di])) 9 | 10 | ;; DI collects dependencies from all arities and only then resolves dependencies. 11 | 12 | (defn multi-arity-service 13 | ([{a `a, :as deps}] 14 | (multi-arity-service deps :a1)) 15 | ([{b `b, :as deps} arg] 16 | (multi-arity-service deps arg :a2)) 17 | ([deps arg1 arg2] 18 | [::result deps arg1 arg2])) 19 | 20 | (t/deftest multi-arity-service-test 21 | (with-open [s (di/start `multi-arity-service {`a :a, `b :b})] 22 | ;; each arity gets all the dependencies 23 | (t/is (= [::result {`a :a, `b :b} :a1 :a2] (s))) 24 | (t/is (= [::result {`a :a, `b :b} :arg1 :a2] (s :arg1))) 25 | (t/is (= [::result {`a :a, `b :b} :arg1 :arg2] (s :arg1 :arg2))))) 26 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/z_multi_system_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Multi system 2 | ^{:nextjournal.clerk/visibility {:code :hide}} 3 | (ns darkleaf.di.tutorial.z-multi-system-test 4 | {:nextjournal.clerk/visibility {:result :hide}} 5 | (:require 6 | [clojure.test :as t] 7 | [darkleaf.di.core :as di])) 8 | 9 | ;; In some cases, you may need multiple systems and sharing a subsystem between them. 10 | ;; In that case just pass specify the subsystem in registry. 11 | 12 | ;; To get a value of the subsystem, you should `deref` it as you would for a regular system root. 13 | ;; Also you should manually stop systems in reverse order. 14 | 15 | (defn shared 16 | {::di/kind :component} 17 | [] 18 | (Object.)) 19 | 20 | (defn server 21 | {::di/kind :component} 22 | [{name ::name 23 | shared `shared}] 24 | [name shared]) 25 | 26 | (t/deftest multi-system-test 27 | (with-open [shared (di/start `shared) 28 | a (di/start `server {`shared @shared 29 | ::name :a}) 30 | b (di/start `server {`shared @shared 31 | ::name :b})] 32 | (t/is (= :a (first @a))) 33 | (t/is (= :b (first @b))) 34 | (t/is (identical? (second @a) (second @b))))) 35 | -------------------------------------------------------------------------------- /test/darkleaf/di/tutorial/z_two_databases_test.clj: -------------------------------------------------------------------------------- 1 | ;; # Two databases 2 | 3 | ^{:nextjournal.clerk/visibility {:code :hide}} 4 | (ns darkleaf.di.tutorial.z-two-databases-test 5 | {:nextjournal.clerk/visibility {:result :hide}} 6 | (:require 7 | [clojure.string :as str] 8 | [clojure.test :as t] 9 | [darkleaf.di.core :as di] 10 | [darkleaf.di.protocols :as p])) 11 | 12 | ;; In DI, each key corresponds to one object. So if you want to use two databases 13 | ;; you have to define two keys. 14 | 15 | (defn db-factory [db-name] 16 | (let [db-name (-> db-name name str/upper-case) 17 | url-key (str "DB_" db-name "_URL") 18 | user-key (str "DB_" db-name "_USER") 19 | password-key (str "DB_" db-name "_PASSWORD")] 20 | (reify p/Factory 21 | (dependencies [_] 22 | {url-key :required 23 | user-key :required 24 | password-key :required}) 25 | (build [_ deps _] 26 | [::db (deps url-key) (deps user-key) (deps password-key)])))) 27 | 28 | (def db-a (db-factory :a)) 29 | (def db-b (db-factory :b)) 30 | 31 | (defn root 32 | {::di/kind :component} 33 | [{db-a `db-a 34 | db-b `db-b}] 35 | [db-a db-b]) 36 | 37 | (t/deftest root-test 38 | (with-open [root (di/start `root {"DB_A_URL" "tcp://a" 39 | "DB_A_USER" "user_a" 40 | "DB_A_PASSWORD" "secret" 41 | "DB_B_URL" "tcp://b" 42 | "DB_B_USER" "user_b" 43 | "DB_B_PASSWORD" "super-secret"})] 44 | (t/is (= [[::db "tcp://a" "user_a" "secret"] 45 | [::db "tcp://b" "user_b" "super-secret"]] 46 | @root)))) 47 | -------------------------------------------------------------------------------- /test/darkleaf/di/update_key_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.di.update-key-test 2 | (:require 3 | [clojure.test :as t] 4 | [darkleaf.di.core :as di]) 5 | (:import 6 | (clojure.lang ExceptionInfo))) 7 | 8 | (t/deftest bug--update-non-existent-key-test 9 | (t/is (thrown-with-msg? 10 | ExceptionInfo 11 | #"\ACan't update non-existent key :darkleaf.di.update-key-test/component-with-typo\z" 12 | (di/start ::component 13 | {::component 1} 14 | (di/update-key ::component-with-typo inc))))) 15 | 16 | (t/deftest expected--update-not-used-key-test 17 | (with-open [system (di/start ::component 18 | {::component 1 19 | ::not-used-component 2} 20 | (di/update-key ::not-used-component inc))] 21 | (t/is (= 1 @system)))) 22 | --------------------------------------------------------------------------------