├── .dir-locals.el ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bb.edn ├── bin ├── kaocha └── proj ├── deps.edn ├── dev └── user.clj ├── src ├── .gitkeep └── lambdaisland │ ├── integreat.clj │ └── integreat │ ├── dev.clj │ ├── dev │ └── state.clj │ ├── dotenv.clj │ ├── prod.clj │ └── test.clj ├── test └── .gitkeep └── tests.edn /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test")))) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .nrepl-port 3 | target 4 | repl 5 | scratch.clj 6 | .shadow-cljs 7 | target 8 | yarn.lock 9 | node_modules/ 10 | .DS_Store 11 | resources/public/ui 12 | .store 13 | out 14 | .#* 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## Added 4 | 5 | ## Fixed 6 | 7 | ## Changed 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integreat 2 | 3 | Experimental integrant integration library 4 | 5 | 6 | 7 | 8 | 9 | ## Rationale and Scope 10 | 11 | This library 12 | 13 | - replaces integrant-repl 14 | - provides common boilerplate for dev/test/prod setup of integrant 15 | - pulls settings and secrets (simple key-values, sensitive or not) out of your 16 | Integrant config 17 | - makes secrets/settings management pluggable (env vars, edn, .env, Vault, etc) 18 | - fail if secret/setting is not present, instead of continuing with `nil` 19 | - allows splitting your integrant config into multiple EDN files 20 | 21 | ## How it works 22 | 23 | Integrant starts from a _config_ map, which then gets used to initialize a 24 | _system_. Integreat introduces one extra concept, the _setup_, which is used to 25 | build up the Integrant config. 26 | 27 | ```clojure 28 | (ns my-app.system 29 | (:require [clojure.java.io :as io])) 30 | 31 | (def setup 32 | {:settings [(io/resource "my-app/settings.edn")] 33 | :secrets [(io/resource "my-app/secrets.edn")] 34 | :components [(io/resource "my-app/system.edn")] 35 | :keys [:my-app/server]} 36 | ``` 37 | 38 | Only `:components` is mandatory, it is a vector of maps or files/resources 39 | (anything readable by Aero). They are all read and merged into a single map, 40 | which becomes your Integrant config. 41 | 42 | Currently Integreat will throw if a key is present in multiple component maps, 43 | as a safeguard to make sure you don't accidentally have the same component 44 | config specified in two locations. However it can be useful to deliberately 45 | "layer" these, having later `:components` entries override earlier once, this 46 | could become something you can opt into. 47 | 48 | `system.edn` could look like this 49 | 50 | ```clojure 51 | {:app.http/http 52 | {:port #setting :http-port 53 | :storage #ig/ref :app.persistance/storage-service} 54 | 55 | :app.persistance/storage-service 56 | {:api-key #secret :storage-api-key}} 57 | ``` 58 | 59 | In there you can see regular integrant references (`#ig/ref`), but also 60 | references to _settings_ and _secrets_. These can be read from a separate 61 | Aero-processed EDN file, but they could also come from environment variables, a 62 | `.env` file, Java properties, or a configuration management system like 63 | Hashicorp Vault. 64 | 65 | In your setup `:secrets` and `:settings` are vectors which can contain 66 | functions, maps, or files/resources (anything readably by Aero). These are 67 | checked in order to find a certain key. If none of them provides a value for a 68 | key then an error is thrown. This is important, as it catches situations where a 69 | setting/secret was introduced, but not yet configured in a certain environment 70 | (starting with local dev). 71 | 72 | We provide a few functions that can be used to provide these settings/secrets. 73 | 74 | - `env`: Look up as environment variable. 75 | - `env-edn`: Look up as environment variable, then attempt to parse as EDN. Will return the string value if parsing fails. 76 | - `properties`: Look up as Java properties 77 | - `properties-edn`: Look up as Java properties, then attempt to parse as EDN. Will return the string value if parsing fails. 78 | - `dotenv`: Read and parse a local `.env` file. 79 | 80 | `env`, `env-edn`, and `dotenv` will all convert the secret/setting keywords into 81 | an environment variable name using the same conventions: letters get uppercased, 82 | dashes and periods become underscores, a namespace separator (`/`) becomes a 83 | double underscore `__`. Other special characters are munged per 84 | `clojure.core/munge`. If a string is used with `#secret "..."` or `:setting 85 | "...`" then it is assumed to be a valid env var name and is left alone. 86 | 87 | - `:foo.bar/baz-baq` => `"FOO_BAR__BAZ_BAQ"` 88 | 89 | `properties`/`properties-edn` will drop the initial `:` from keywords, but will 90 | otherwise use the key as-is. 91 | 92 | So your setup could look like this: 93 | 94 | ```clojure 95 | (def setup 96 | {:components [(io/resource "my-app/system.edn")] 97 | :settings [integreat/env 98 | (io/resource "settings.edn")] 99 | :secrets [integreat/env 100 | (integreat/dotenv ".env" {:expand? true})]}) 101 | ``` 102 | 103 | With `settings.edn` 104 | 105 | ```clojure 106 | {:http-port #profile {:prod 80 :dev 8080 :test 8081}} 107 | ``` 108 | 109 | And `.env` 110 | 111 | ```shell 112 | STORAGE_API_KEY="api-123abc..." 113 | ``` 114 | 115 | With this `setup` in hand we can now use it to start a dev, test, or prod 116 | system. Integreat provides three namespaces for these three scenarios. 117 | 118 | - `lambdaisland.integreat.prod` for use in your main namespace 119 | - `lambdaisland.integreat.dev` for use in `user.clj` 120 | - `lambdaisland.integreat.test` for use in tests 121 | 122 | Main namespace, here the system will be loaded with `:profile :prod`. A shutdown 123 | hook is registered to stop the system when the process terminates. No reference 124 | to the system is kept. 125 | 126 | ```clojure 127 | (ns my-app.main 128 | (:gen-class) 129 | (:require [lambdaisland.integreat.prod :as igreat-prod] 130 | [my-app.system :as system])) 131 | 132 | (defn -main [] 133 | (igreat-prod/go system/setup)) 134 | ``` 135 | 136 | For dev we create a number of helpers in `user.clj`, most of which delegate to 137 | `integreat.dev`. These provide integration with `clojure.tools.namespace` to 138 | support code reloading. 139 | 140 | ```clojure 141 | (ns user) 142 | 143 | (defmacro jit [sym] 144 | `(requiring-resolve '~sym)) 145 | 146 | (defn setup [] 147 | @(jit co.gaiwan.repohack.system/setup)) 148 | 149 | (defn browse [] 150 | ((jit clojure.java.browse/browse-url) 151 | (str "http://localhost:" 152 | ((jit lambdaisland.integreat.dev/setting) (setup) :http-port)))) 153 | 154 | (defn go [] 155 | ((jit lambdaisland.integreat.dev/go) (setup))) 156 | 157 | (defn reset [] ((jit lambdaisland.integreat.dev/reset))) 158 | (defn reset-all [] ((jit lambdaisland.integreat.dev/reset-all))) 159 | (defn clear [] ((jit lambdaisland.integreat.dev/clear))) 160 | (defn halt [] ((jit lambdaisland.integreat.dev/halt))) 161 | (defn suspend [] ((jit lambdaisland.integreat.dev/suspend))) 162 | (defn resume [] ((jit lambdaisland.integreat.dev/resume))) 163 | 164 | (defn ig-config [] @(jit lambdaisland.integreat.dev.state/config)) 165 | (defn ig-system [] @(jit lambdaisland.integreat.dev.state/system)) 166 | ``` 167 | 168 | For tests the main thing we provide is a function which creates a fixture 169 | function. This will spin up a system with the `:test` profile, bind it to 170 | `integreat.test/*system*`, and tear it down again once the test finishes. 171 | 172 | You can provide keys to start, in case your test only relies on part of the 173 | system. 174 | 175 | ```clojure 176 | (ns app-test 177 | (:require 178 | [app.system :as system] 179 | [clojure.test :refer :all] 180 | [lambdaisland.integreat.test :as igreat-test])) 181 | 182 | (use-fixtures :once (igreat-test/wrap-system 183 | system/setup 184 | {:keys [:app.persistance/storage-service]})) 185 | 186 | (deftest some-test 187 | ,,,) 188 | 189 | (comment 190 | ;; You can use these while working on tests in the REPL 191 | (igreat-test/init! system/setup) 192 | (igreat-test/halt!) 193 | ) 194 | ``` 195 | 196 | You can create a namespace with test helpers that provide convenience access to 197 | your system components, e.g. to query the database. 198 | 199 | ```clojure 200 | (ns test-helpers 201 | (:require 202 | [lambdaisland.integreat.test :as igreat-test] 203 | [some.database :as db)) 204 | 205 | (defn q [qry] 206 | (db/q (:app.database/conn igreat-test/*system*) qry)) 207 | ``` 208 | 209 | ## License 210 | 211 | Copyright © 2022 Arne Brasseur and Contributors 212 | 213 | Licensed under the term of the Mozilla Public License 2.0, see LICENSE. 214 | 215 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" 3 | :sha "8b3ef2e1fec97493ad89a39c962cca323c76ff8d" 4 | #_#_:local/root "../open-source"}}} 5 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec clojure -M:test -m kaocha.runner "$@" 4 | -------------------------------------------------------------------------------- /bin/proj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns proj 4 | (:require [lioss.main :as lioss])) 5 | 6 | (lioss/main 7 | {:license :mpl 8 | :inception-year 2022 9 | :description "" 10 | :group-id "com.lambdaisland"}) 11 | 12 | ;; Local Variables: 13 | ;; mode:clojure 14 | ;; End: 15 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | 3 | :deps 4 | {integrant/integrant {:mvn/version "0.8.0"} 5 | aero/aero {:mvn/version "1.1.6"} 6 | org.clojure/tools.namespace {:mvn/version "1.0.0"} 7 | io.github.cdimascio/dotenv-java {:mvn/version "2.2.4"}} 8 | 9 | :aliases 10 | {:dev 11 | {:extra-paths ["dev"] 12 | :extra-deps {djblue/portal {:mvn/version "RELEASE"}}} 13 | 14 | :test 15 | {:extra-paths ["test"] 16 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.66.1034"}}}}} 17 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (defmacro jit [sym] 4 | `(requiring-resolve '~sym)) 5 | 6 | (defn browse [] 7 | ((jit clojure.java.browse/browse-url) "http://localhost:8000")) 8 | 9 | (def portal-instance (atom nil)) 10 | 11 | (defn portal 12 | "Open a Portal window and register a tap handler for it. The result can be 13 | treated like an atom." 14 | [] 15 | ;; Portal is both an IPersistentMap and an IDeref, which confuses pprint. 16 | (prefer-method @(jit clojure.pprint/simple-dispatch) clojure.lang.IPersistentMap clojure.lang.IDeref) 17 | (let [p ((jit portal.api/open) @portal-instance)] 18 | (reset! portal-instance p) 19 | (add-tap (jit portal.api/submit)) 20 | p)) 21 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plexus/integreat/e44496866cabae36d2fec1de587942caa058a337/src/.gitkeep -------------------------------------------------------------------------------- /src/lambdaisland/integreat.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat 2 | (:require [aero.core :as aero] 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str] 6 | [clojure.walk :as walk] 7 | [integrant.core :as ig] 8 | [lambdaisland.integreat.dotenv :as dotenv])) 9 | 10 | (defrecord Setting [k]) 11 | (defrecord Secret [k]) 12 | 13 | (defmethod aero/reader 'ig/ref [_ _tag value] (ig/ref value)) 14 | (defmethod aero/reader 'ig/refset [_ _tag value] (ig/refset value)) 15 | 16 | (defmethod aero/reader 'setting [_ _ k] (->Setting k)) 17 | (defmethod aero/reader 'secret [_ _ k] (->Secret k)) 18 | 19 | (defn env-munge [s] 20 | (str/upper-case (str/replace (munge s) #"\." "_"))) 21 | 22 | (defn key->env-var [k] 23 | (if (string? k) 24 | k 25 | (str (when (qualified-ident? k) 26 | (str (env-munge (namespace k)) 27 | "__")) 28 | (env-munge (name k))))) 29 | 30 | (comment 31 | (key->env-var :foo.bar/baz-baq) 32 | ;; => "FOO_BAR__BAZ_BAQ" 33 | (key->env-var :baz-baq) 34 | ;; => "BAZ_BAQ" 35 | ) 36 | 37 | (defn env [k] 38 | (System/getenv (key->env-var k))) 39 | 40 | (defn env-edn [k] 41 | (if-let [v (env k)] 42 | (try 43 | (edn/read-string v) 44 | (catch Exception _ 45 | v)))) 46 | 47 | (defn properties [k] 48 | (System/getProperty (if (string? k) 49 | k 50 | (subs (str k) 1)))) 51 | 52 | (defn properties-edn [k] 53 | (if-let [v (env k)] 54 | (try 55 | (edn/read-string v) 56 | (catch Exception _ 57 | v)))) 58 | 59 | (defn dotenv 60 | ([] 61 | (dotenv ".env" nil)) 62 | ([slurpable] 63 | (dotenv slurpable nil)) 64 | ([slurpable opts] 65 | (fn [k] 66 | (get 67 | (dotenv/parse-dotenv (slurp slurpable) opts) 68 | (key->env-var k))))) 69 | 70 | (defn build-config-provider [sources aero-opts] 71 | (if (or (sequential? sources) (nil? sources)) 72 | (reduce 73 | (fn [acc c] 74 | (let [lookup (if (or (fn? c) (map? c)) 75 | c 76 | (aero/read-config c aero-opts))] 77 | (fn [k] 78 | (if-some [v (lookup k)] 79 | v 80 | (acc k))))) 81 | (fn [k] 82 | (throw 83 | (ex-info "Config key not found in any of the sources" 84 | {:key k 85 | :sources sources 86 | :aero-opts aero-opts}))) 87 | (reverse sources)) 88 | (recur [sources] aero-opts))) 89 | 90 | (defn read-components [sources aero-opts] 91 | (if (sequential? sources) 92 | (reduce 93 | (fn [acc c] 94 | (merge-with 95 | (fn [_ _] 96 | (throw 97 | (ex-info "Key provided by multiple configuration sources" 98 | (filter (set (keys acc)) 99 | (keys c))))) 100 | acc 101 | (if (map? c) 102 | c 103 | (aero/read-config c aero-opts)))) 104 | {} 105 | sources) 106 | (recur [sources] aero-opts))) 107 | 108 | (defn read-system-config [{:as setup 109 | :keys [settings 110 | secrets 111 | components]} 112 | aero-opts] 113 | (let [setting-provider (build-config-provider settings aero-opts) 114 | secret-provider (build-config-provider secrets aero-opts)] 115 | (doto (walk/postwalk 116 | (fn [o] 117 | (cond 118 | (instance? Setting o) 119 | (setting-provider (:k o)) 120 | 121 | (instance? Secret o) 122 | (secret-provider (:k o)) 123 | 124 | :else o)) 125 | (read-components components aero-opts)) 126 | (ig/load-namespaces)))) 127 | -------------------------------------------------------------------------------- /src/lambdaisland/integreat/dev.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat.dev 2 | (:require [integrant.core :as ig] 3 | [lambdaisland.integreat :as igreat] 4 | [lambdaisland.integreat.dev.state :as state] 5 | [clojure.tools.namespace.repl :as repl])) 6 | 7 | (repl/disable-reload! (find-ns 'integrant.core)) 8 | 9 | (defn setting 10 | ([setup k] 11 | (setting k nil)) 12 | ([setup k aero-opts] 13 | ((igreat/build-config-provider 14 | (:settings setup) 15 | (merge {:profile :dev} aero-opts)) 16 | k))) 17 | 18 | (defn secret 19 | ([setup k] 20 | (setting k nil)) 21 | ([setup k aero-opts] 22 | ((igreat/build-config-provider 23 | (:secrets setup) 24 | (merge {:profile :dev} aero-opts)) 25 | k))) 26 | 27 | (defn prep [setup aero-opts] 28 | (alter-var-root #'state/setup (constantly setup)) 29 | (alter-var-root #'state/aero-opts (constantly aero-opts)) 30 | (let [config (igreat/read-system-config (if (var? setup) 31 | @setup 32 | setup) 33 | (merge {:profile :dev} 34 | aero-opts))] 35 | 36 | (alter-var-root #'state/config (constantly config)))) 37 | 38 | (defn- halt-system [system] 39 | (when system (ig/halt! system))) 40 | 41 | (defn- build-system [build wrap-ex] 42 | (try 43 | (build) 44 | (catch clojure.lang.ExceptionInfo ex 45 | (if-let [system (:system (ex-data ex))] 46 | (try 47 | (ig/halt! system) 48 | (catch clojure.lang.ExceptionInfo halt-ex 49 | (throw (wrap-ex ex halt-ex))))) 50 | (throw ex)))) 51 | 52 | (defn- init-system [config keys] 53 | (build-system 54 | (if keys 55 | #(ig/init config keys) 56 | #(ig/init config)) 57 | #(ex-info "Config failed to init; also failed to halt failed system" 58 | {:init-exception %1} 59 | %2))) 60 | 61 | (defn- resume-system [config system] 62 | (build-system 63 | #(ig/resume config system) 64 | #(ex-info "Config failed to resume; also failed to halt failed system" 65 | {:resume-exception %1} 66 | %2))) 67 | 68 | (defn init 69 | ([] (init nil)) 70 | ([keys] 71 | (alter-var-root #'state/system (fn [sys] 72 | (halt-system sys) 73 | (init-system state/config keys))) 74 | :initiated)) 75 | 76 | (defn go 77 | ([setup] (go setup nil)) 78 | ([setup aero-opts] 79 | (prep setup aero-opts) 80 | (init (:keys setup)))) 81 | 82 | (defn clear [] 83 | (alter-var-root #'state/system (fn [sys] (halt-system sys) nil)) 84 | (alter-var-root #'state/config (constantly nil)) 85 | :cleared) 86 | 87 | (defn halt [] 88 | (halt-system state/system) 89 | (alter-var-root #'state/system (constantly nil)) 90 | :halted) 91 | 92 | (defn suspend [] 93 | (when state/system (ig/suspend! state/system)) 94 | :suspended) 95 | 96 | (defn resume [] 97 | (if state/setup 98 | (let [cfg (prep state/setup state/aero-opts)] 99 | (alter-var-root #'state/config (constantly cfg)) 100 | (alter-var-root #'state/system (fn [sys] 101 | (if sys 102 | (resume-system cfg sys) 103 | (init-system cfg nil)))) 104 | :resumed) 105 | (throw (Error. "No system setup found.")))) 106 | 107 | (defn reset [] 108 | (suspend) 109 | (repl/refresh :after 'lambdaisland.integreat.dev/resume)) 110 | 111 | (defn reset-all [] 112 | (suspend) 113 | (repl/refresh-all :after 'lambdaisland.integreat.dev/resume)) 114 | -------------------------------------------------------------------------------- /src/lambdaisland/integreat/dev/state.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat.dev.state 2 | (:require [clojure.tools.namespace.repl :as repl])) 3 | 4 | (repl/disable-reload!) 5 | 6 | (def config nil) 7 | (def system nil) 8 | (def setup nil) 9 | (def aero-opts nil) 10 | -------------------------------------------------------------------------------- /src/lambdaisland/integreat/dotenv.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat.dotenv 2 | "Parsing of dotenv files" 3 | (:require [clojure.string :as str])) 4 | 5 | (defn split-at-pred [pred coll] 6 | (loop [[y & ys] coll 7 | xs []] 8 | (cond 9 | (pred y) 10 | [(conj xs y) ys] 11 | (nil? ys) 12 | nil 13 | :else 14 | (recur ys (conj xs y))))) 15 | 16 | (defn unquote-chars [s] 17 | (str/replace s #"\\(u[0-9a-fA-F]{4,}|.)" 18 | (fn [[_ m]] 19 | (case m 20 | "r" "\r" 21 | "n" "\n" 22 | "t" "\t" 23 | "f" "\f" 24 | "b" "\b" 25 | (if (= \u (first m)) 26 | (str (char (Long/parseLong (subs m 1) 16))) 27 | m))))) 28 | 29 | (defn expand-vars [s vars] 30 | (str/replace s #"\$\{([a-zA-Z_]+[a-zA-Z0-9_]*)\}" 31 | (fn [[_ k]] 32 | (str 33 | (or (get vars k) 34 | (System/getenv k) 35 | (throw (ex-info "Failed to expand variable reference" {:value s 36 | :unfound-var k}))))))) 37 | 38 | (defn parse-dotenv [contents {:keys [expand?]}] 39 | (let [lines (str/split contents #"\R")] 40 | (loop [vars {} 41 | [line & lines] lines] 42 | (cond 43 | (nil? line) 44 | vars 45 | (re-find #"^\s*(#.*)?$" line) 46 | (recur vars lines) 47 | :else 48 | (if-let [[_ _ k v] (re-find #"^\s*(export\s+)?([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)" line)] 49 | (case (first v) 50 | \' 51 | (let [v (subs v 1) 52 | [ql rl :as split] (split-at-pred #(re-find #"(^|[^\\])'" %) 53 | (cons v lines))] 54 | (when-not split 55 | (throw (ex-info "Unterminated quoted value" {:key k}))) 56 | (if-let [[_ ll] (re-find #"(.*)'\s*(#.*)?$" (last ql))] 57 | (recur (conj vars {k (str/join "\n" (concat (butlast ql) [ll]))}) 58 | rl) 59 | (throw (ex-info "Invalid single quoted value" {:key k})))) 60 | 61 | \" 62 | (let [v (subs v 1) 63 | [ql rl :as split] (split-at-pred #(re-find #"(^|[^\\])\"" %) 64 | (cons v lines))] 65 | (when-not split 66 | (throw (ex-info "Unterminated quoted value" {:key k}))) 67 | (if-let [[_ ll] (re-find #"(.*)\"\s*(#.*)?$" (last ql))] 68 | (recur (assoc vars k (cond-> (unquote-chars (str/join "\n" (concat (butlast ql) [ll]))) 69 | expand? 70 | (expand-vars vars))) 71 | rl) 72 | (throw (ex-info "Invalid double quoted value" {:key k})))) 73 | 74 | (recur (assoc vars k (cond-> (unquote-chars (str/replace v #"\s*#.*" "")) 75 | expand? 76 | (expand-vars vars))) lines)) 77 | (throw (ex-info "Invalid syntax" {:line line}))))))) 78 | -------------------------------------------------------------------------------- /src/lambdaisland/integreat/prod.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat.prod 2 | (:require [integrant.core :as ig] 3 | [lambdaisland.integreat :as igreat])) 4 | 5 | (defn go 6 | ([setup] 7 | (go setup nil)) 8 | ([setup aero-opts] 9 | (let [config (igreat/read-system-config setup (merge {:profile :prod} aero-opts)) 10 | system (ig/init config (:keys setup (keys config))) 11 | runtime (java.lang.Runtime/getRuntime)] 12 | (.addShutdownHook 13 | runtime 14 | (Thread. (fn [] 15 | (ig/halt! system) 16 | (shutdown-agents)))) 17 | @(promise)))) 18 | -------------------------------------------------------------------------------- /src/lambdaisland/integreat/test.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.integreat.test 2 | (:require [integrant.core :as ig] 3 | [lambdaisland.integreat :as igreat])) 4 | 5 | (def ^:dynamic *system* nil) 6 | 7 | (defn wrap-system 8 | "For use as fixture function" 9 | ([setup] 10 | (wrap-system setup nil)) 11 | ([setup opts] 12 | (fn [t] 13 | (let [ig-config (igreat/read-system-config setup 14 | (merge {:profile :test} 15 | opts)) 16 | ig-system (ig/init ig-config (:keys opts (:keys setup (keys ig-config))))] 17 | (binding [*system* ig-system] 18 | (t)) 19 | (ig/halt! ig-system))))) 20 | 21 | (defn init! 22 | "For use in REPL" 23 | ([setup] 24 | (init! setup nil)) 25 | ([setup opts] 26 | (alter-var-root 27 | #'*system* 28 | (fn [sys] 29 | (if sys 30 | sys 31 | (let [ig-config (igreat/read-system-config setup 32 | (merge {:profile :test} 33 | opts))] 34 | (ig/init ig-config (:keys opts (:keys setup (keys ig-config)))))))))) 35 | 36 | (defn halt! 37 | "For use in REPL" 38 | [] 39 | (alter-var-root 40 | #'*system* 41 | (fn [sys] 42 | (when sys 43 | (ig/halt! sys))))) 44 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plexus/integreat/e44496866cabae36d2fec1de587942caa058a337/test/.gitkeep -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:plugins [:notifier :print-invocations :profiling]} 3 | --------------------------------------------------------------------------------