{"id":13801137,"url":"https://github.com/juxt/clip","last_synced_at":"2025-12-12T01:10:42.941Z","repository":{"id":38080517,"uuid":"214666312","full_name":"juxt/clip","owner":"juxt","description":"Light structure and support for dependency injection","archived":false,"fork":false,"pushed_at":"2023-10-28T16:51:08.000Z","size":142,"stargazers_count":233,"open_issues_count":9,"forks_count":15,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-05-11T17:54:07.608Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/juxt.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-10-12T15:03:37.000Z","updated_at":"2025-05-11T08:54:56.000Z","dependencies_parsed_at":"2024-01-08T18:03:34.778Z","dependency_job_id":"e7f7d53b-fa9a-4f97-867d-2e10baa1ed78","html_url":"https://github.com/juxt/clip","commit_stats":{"total_commits":169,"total_committers":8,"mean_commits":21.125,"dds":"0.33136094674556216","last_synced_commit":"c93d5142ee8d6f0cf0b6a2354eb27d5b77c5dfc8"},"previous_names":["severeoverfl0w/high"],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Fclip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Fclip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Fclip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Fclip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/juxt","download_url":"https://codeload.github.com/juxt/clip/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253932770,"owners_count":21986445,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T00:01:19.919Z","updated_at":"2025-12-12T01:10:42.901Z","avatar_url":"https://github.com/juxt.png","language":"Clojure","funding_links":[],"categories":["Dependency injection"],"sub_categories":[],"readme":"= Clip\nifdef::env-github[]\n:toc:\n\nimage:https://img.shields.io/clojars/v/juxt/clip.svg[Clojars Project, link=https://clojars.org/juxt/clip]\nimage:https://cljdoc.org/badge/juxt/clip[cljdoc badge, link=https://cljdoc.org/d/juxt/clip/CURRENT]\nendif::[]\n\nClip is an inversion of control API which minimizes impact on your code.\nIt is an alternative to integrant, mount or component.\n\n== Background\n\n=== Project Status\n\n*Alpha*.\nThe core API is unlikely to change significantly, but there may be some small changes to the system-config format.\nDesign feedback is still welcome, there is still space for the rationale to change/expand.\nWhile bugs are avoided, the immaturity of the project makes them more likely.\nPlease provide design feedback and report bugs on the link:https://github.com/juxt/clip/issues/new[issue tracker].\n\n=== Rationale\n\n==== System as data\n\nSystems may fail during startup, which can leave you in a state where a port is in use, but you have no handle to that partially-started system in order to close it.\n\n[source,clojure]\n----\n(let [db (get-db)]\n      http (start-http-server db)\n      ;; What happens if make-queue-listener throws an exception?\n      queue-listener (make-queue-listener)]\n  …)\n----\n\nThis can lead to re-starting the REPL, which is a major interruption to flow.\nClip provides a data model for your system, and will rethrow exceptions with the partially-started components attached in order to allow automatic recovery in the REPL or during tests.\n\nYou may choose to make your system very granular.\nFor example, you may choose to provide each web request handler with it's dependencies directly, rather than passing them via the router.\nIn these cases, you will find your system quickly grows large, having your system as data reduces the effort to maintain \u0026 understand the relationships between components.\n\nEDN is a natural way to express data in Clojure.\nA data-driven system should naturally work with EDN without any magic.\n\n==== Boilerplate\n\nExisting dependency injection libraries require boilerplate for defining your components.\nThis either comes in the form of multi-methods which must have their namespaces required for side-effects, or the creation of records in order to implement a protocol.\nThese extension mechanisms are used to extend existing var calls in libraries.\nInstead of doing that, we can take a reference to a function (e.g. `ring.adapter.jetty/run-jetty`) and call it directly.\nThis means that effort to wrap an existing library is minimal, you simply have to specify what to call.\nThis also removes the problem of inventing new strategies for tying a keyword back to a namespace as Integrant has to, by directly using the fully qualified symbol which already includes the namespace as required. \nIn addition, normal functions support doc-strings, making for easy documentation.\nFinally, this use of vars means that your library is not coupled to Clip in any way, yet it's easy to use directly from Clip.\n\n==== Transitions\n\nSide-effects are part of a startup process.\nThe most common example is seeding a database.\nBefore other components can function (such as a health check) the migration must have run.\nExisting approaches require us to either taint our component's `start` with additional side-effect code (complecting connection with migration) or to create side-effecting components which others must then depend upon.\n\nClip by default, provides a :pre and :post phase for start-up, enabling you to run setup before/after starting the component.\nFor example, you may need to call `datomic.api/create-database` before connecting to it, and you may want to call `my.app/seed-db` after starting it, but before passing it around.\n\nClip is also simple, it separates out running many actions on your system.\nThis allows you to define custom phases for components, if you need them.\n\n==== Async\n\nAsync is a significant part of systems when doing ClojureScript applications.\nSomething as simple as reading data from disk or fetching the user from a remote endpoint will cause your entire system to be aware of the callback.\n\nInterceptors have shown that async can be a layered-on concern, rather than being intrinsically present in all consumption of the result.\nClip provides multiple \"executors\" for running actions against your system, providing out of the box support for sync (no async), promesa, and manifold.\nIf you need an additional executor, they are fairly simple to write.\n\n==== Obvious\n\nObvious connections between actions are easier to understand than obscure ones.\nUse of inheritance for references or relying on implicit dependencies increases the obscurity of your API.\nComponent and Integrant allow you to spread out your references through the use of `using` and `prep-key`.\nInstead Clip encourages you to make references live in the system, making it always obvious how components connect together.\n\n=== Comparison with other libraries\n\n|===\n|Name      |Need/has wrappers for libs |Extension mechanism |Suspension  |System location |Multiple parallel systems? |Transparent async between components?\n\n|Component |Yes                        |Protocol            |Via library |Map             |Yes                        |No\n|Integrant |Yes                        |Multi-method        |Yes         |Map             |Yes                        |No\n|Clip      |No                         |Code/code as data   |Coming      |Map             |Yes                        |Yes\n|Mount     |No                         |Code                |No          |Namespace       |No                         |No\n\n|===\n\n=== Example Applications\n\n* link:https://github.com/juxt/clip-example[Official clip-example repo]\n* link:https://github.com/PrestanceDesign/todo-backend-clojure-reitit/tree/clip[Implementation of the Todo-Backend API spec,using Ring/Reitit, Clip and next-jdbc]\n* link:https://github.com/dharrigan/startrek[StarTrek, Clojure REST example project using reitit, next-jdbc and malli]\n\nWant to add one? Open an issue or pull request.\n\n== Usage\n\n=== Defining a system configuration\n\nYou define a system configuration with data.\nA system configuration contains a `:components` key and an optional `:executor` key.\n`:components` is a map from anything to a map of properties about that component.\nNote the use of ``` in the example below, this is to prevent execution, you might find it easier to use \u003c\u003cEDN\u003e\u003e to define your system configuration.\n\n[source,clojure]\n----\n(def system-config\n  {:components\n   {:db {:pre-start `(d/create-database \"datomic:mem://newdb\") ;; \u003c1\u003e\n         :start `(d/connect \"datomic:mem://newdb\") ;; \u003c2\u003e\n         :post-start `seed-conn} ;; \u003c3\u003e\n    :handler {:start `(find-seed-resource (clip/ref :db))} ;; \u003c4\u003e\n    :http {:start `(yada/listener (clip/ref :handler))\n           :stop '((:close this)) ;; \u003c5\u003e\n           :resolve :server} ;; \u003c6\u003e\n    :foo {:start '(clip/ref :http)}}})\n----\n\u003c1\u003e `:pre-start` will be run before `:start` for your component.  Here we use it to run the required `create-database` in datomic.\n\u003c2\u003e `:start` is run and returns the value that other components will refer to.\n\u003c3\u003e `:post-start` is run before passing the component to any other components.  Here, we use it to seed the connection.  Because we provided a symbol, it will be resolved and called like so `(seed-conn conn)` where `conn` is the started component.\n\u003c4\u003e Here we use `(clip/ref)` to refer to the `:db` component.  This will be provided positionally to the function.\n\u003c5\u003e `:stop` has access to the variable `this` to refer to the started component.\n\u003c6\u003e You can control how a component is referenced by other components.  Here the `:server` key will be passed to other components referencing it (e.g. `:foo`).\n\n==== `:components` reference\n\nOut of the box, there are a handful of keys supported for a component.\nIn the future this may be more extensible (please open an issue if you have a use-case!).\n\nMany of the values of these keys take code as data.\nThis means that if you were to create them programmatically you have to create them using either `list` or quotes.\nSupporting code as data means that EDN-based systems can be defined, but also that there's special execution rules.\n\nCode as data means that you don't need to use actual function references, you can use symbols and these will be required \u0026 resolved by Clip.\nRequiring and resolving isn't supported in ClojureScript, see workaround in \u003c\u003cClojureScript\u003e\u003e.\n\nCode is either executed with an \"implicit target\" or not.\nAn example of an implicit target is the started instance.\nIf an implicit target is available, you can provide a symbol or function without a list and it will be called with an argument which is the implicit target.\nIf the function to call has multiple arguments, then you can use `this` to change where the implicit target will be placed.\n\n|===\n| Key | Implicit Target | Description\n \n| `:pre-start` | No | Run before starting the component\n| `:start` | No | Run to start the component, this will be what ends up in the system\n| `:post-start` | Started instance | Run with the started component, a useful place to perform migrations or seeding\n| `:stop` | Started instance (to stop) | Run with the started component, should be used to shut down the component.  Optional to add.  If not specified and value is AutoCloseable, then .close will be run on it\n| `:resolve` | Started instance | Run with the started component used by other components to get the value for this component when using `(clip/ref)` \n\n|===\n\nSupported values for code as data with implicit target:\n\n|===\n| Type | Description\n\n| Symbol | Resolved to function then called with target\n| Function | Resolved to function then called with target\n| Keyword | Used to get the key out of the target\n\n|===\n\nSupported values for code as data without implicit target:\n\n|===\n| Type | Description | Example(s)\n\n| Symbol | Resolved to function and called with no arguments | `'myapp.core/start-web-server`\n| Function | Called with no arguments | `myapp.core/start-web-server`\n| Vector | Recursed into, with arguments resolved | `[(clip/ref :foo)]`\n| List | Called as if code | `(list 'myapp.core/start-web-server {:port 8000})` `'(myapp.core/start-web-server {:port 8000})`\n| :else | Returned unchanged | \n\n|===\n\n=== Async Components\n\nIn Clip, async is achieved by using alternative executors.\nOut of the box support is provided for link:https://github.com/funcool/promesa[promesa] and link:https://github.com/ztellman/manifold[manifold].\nOpen an issue if you'd like to see support for another popular library.\n\nExecutors are specified on your system and must be a function.\n`juxt.clip.edn/load` will convert your `:executor` from a symbol to a function.\n\n.Promesa Async Example\n====\n\n[source,clojure]\n----\n{:executor juxt.clip.promesa/exec\n :components\n {:a {:start `(promesa.core/resolved 10)}\n  :b {:start `(inc (clip/ref :a))}}}\n----\n\nNote that `:b` does not need to be aware that `:a` returns an async value.\nIt will be called at the appropriate time with the resolved value.\n\n====\n\n.Manifold Example\n====\n\n[source,clojure]\n----\n(require '[manifold.deferred :as d])\n\n{:executor juxt.clip.manifold/exec\n :components\n {:a {:start `(d/chain 10)}\n  :b {:start `(inc (clip/ref :a))}}}\n----\n\n====\n\n=== Reloaded Workflow\n\nClip provides a namespace for easily setting up a reloaded workflow.\nYou will need to add a dependency on link:https://github.com/clojure/tools.namespace[tools.namespace] to your project.\n\nYou should call `set-init!` with a function which will return your system-config.\nUsually you will have such a function defined in another namespace that takes a \"profile\" or \"config\" in order to be parameterized to development or production.\nIf you are using \u003c\u003cEDN\u003e\u003e to load your system, ensure your function calls `clip.edn/load`.\n\n\n[source,clojure]\n----\n(ns dev\n  (:require\n    [app.system]\n    [juxt.clip.repl :refer [start stop reset set-init! system]]))\n\n(set-init! #(app.system/system-config :dev))\n----\n\n==== Async executors\n\nIf you're using an async executor with the repl namespace, you may need to make it sync.\nOut of the box, the repl namespace will do it's best to work with anything supported by `deref`.\nIf you need to override the deref that the repl namespace uses, you can supply a symbol or function in the key `:juxt.clip.repl/deref`.\nIt should take one argument: the system to deref.\n\nYou won't need to tweak this for Promesa or Manifold.\n\n[source,clojure]\n----\n(set-init!\n  (constantly {:executor juxt.clip.awkward-async/exec\n               :juxt.clip.repl/deref some.ns.awkard-async/deref\n               …}))\n----\n\n=== ClojureScript\n\nClojureScript has limitations with taking code-forms as data.\nThis will continue to be an active research topic, but until resolved the usage is still reasonably concise.\nYou must use `list` to create a list-form.\n\n.ClojureScript Usage\n====\n\n[source,clojure]\n----\n(ns frontend.core\n  (:require [juxt.clip.core :as clip]))\n\n(def system-config\n  {:components\n    {:foo {:start 200}\n     :bar {:start (list inc (clip/ref :foo))}}})\n----\n\n====\n\nCAUTION: The following macro is experimental, feedback on use is welcome. However, of the following experimental options it is currently the forerunner.\n\nThere is a macro called `with-deps` that allows you to write a code-form and bind the dependencies required.\nThis is useful when using Clip from a code (rather than data) context.\nIt's also particularly useful in ClojureScript where symbols cannot be resolved back to functions.\n\n`with-deps` takes `bindings` and a `body`, much like `fn`.\nThe first of the bindings must be to the deps you want.\nYou _must_ use link:https://clojure.org/guides/destructuring#_associative_destructuring[associative destructuring].\n\n.`with-deps` Usage\n====\n\n[source,clojure]\n----\n(ns frontend.core\n  (:require [juxt.clip.core :as clip :include-macros true]))\n\n(def system-config\n  {:components\n    {:foo {:start 200}\n     :bar {:start 300}\n     :baz {:start (clip/with-deps [{:keys [foo bar]}]\n                    (+ foo bar))}}})\n----\n\n====\n\nCAUTION: The following macro is extremely experimental, feed-back on use is welcome.\n\nYou can also bring in the `deval` macro.\nThis macro will convert lists of code it finds into non-evaluated lists, which can later be interpreted by Clip.\n\n.Deval Usage\n====\n\n[source,clojure]\n----\n(ns frontend.core\n  (require '[juxt.clip.core :as clip :include-macros true]))\n\n(def system-config\n  (clip/deval\n    {:components\n      {:foo {:start 200}\n       :bar {:start (inc (clip/ref :foo))}}}))\n----\n\n====\n\n== How to\n\n[[EDN]]\n=== Use with EDN\n\nClip works very well with EDN.\nFirst, you must call `juxt.clip.edn/load` on your EDN system to produce a system Clip can understand.\nThis will convert your EDN structure into a system compatible with juxt.clip.core functions by, e.g. converting `:executor` from a symbol to a function.\n\nIt was designed to be used with a library such as link:https://github.com/juxt/aero[aero] in order to make dev/prod changes to your system.\nHere's a minimal example system-config configured with aero:\n\n.config.edn\n[source,clojure]\n----\n{:system-config\n {:components\n  {:db {:start (hikari-cp.core/make-datasource\n                 #profile\n                 {:dev\n                  {:adapter \"h2\"\n                   :url \"jdbc:h2:~/test\"}\n                  :prod\n                  {:jdbc-url \"jdbc:sqlite:db/database.db\"}})\n        :stop (hikari-cp.core/close-datasource this)}}}}\n----\n\n=== Use from -main\n\nWhen starting your application from -main there's a few considerations:\n\n* Blocking forever (Use `@(promise)` to do this)\n* Storing the system for REPLing in later\n* Whether to shutdown the system or not\n\n.Simplest version, blocking forever\n[source,clojure]\n----\n(ns myapp.main\n  (:require\n    [myapp.system]\n    [juxt.clip.core :as clip]))\n\n(defn -main\n  [\u0026 _]\n  (clip/start (myapp.system/system-config :prod))\n  @(promise))\n----\n\n.Storing system for later\n[source,clojure]\n----\n(ns myapp.main\n  (:require\n    [myapp.system]\n    [juxt.clip.core :as clip]))\n\n(def system nil)\n\n(defn -main\n  [\u0026 _]\n  (let [system (clip/start (myapp.system/system-config :prod))]\n    (alter-var-root #'system (constantly system)))\n  @(promise))\n----\n\n.Stopping system on shutdown\n[source,clojure]\n----\n(ns myapp.main\n  (:require\n    [myapp.system]\n    [juxt.clip.core :as clip]))\n\n(def system nil)\n\n(defn -main\n  [\u0026 _]\n  (let [system-config (myapp.system/system-config :prod)\n        system (clip/start system-config)]\n    (alter-var-root #'system (constantly system))\n    (.addShutdownHook\n     (Runtime/getRuntime)\n     (Thread. #(clip/stop system-config system))))\n  @(promise))\n----\n\n=== AOT compile namespaces\n\nYou need to involve Clip in the AOT process in order to require any namespaces required by your system.\n\n[source,clojure]\n----\n(binding [*compile-files* true] ;; \u003c1\u003e\n  (-\u003e ((requiring-resolve 'myapp.system/get-system)) ;; \u003c2\u003e\n      clip.edn/load ;; \u003c3\u003e\n      clip/require ;; \u003c4\u003e\n      ))\n----\n\u003c1\u003e Tell Clojure that when we require code, we'd like to AOT it.\n\u003c2\u003e We must require myapp.system inside of the `binding` otherwise it won't be AOT'd.\n\u003c3\u003e Optional, only do this if your system is in EDN.\n\u003c4\u003e This is what requires the namespaces used by your system.\n\n=== Test systems\n\nClip provides 2 useful mechanisms for testing:\n\n. `select` to get a subset of a system-config with only the specified components.  `start` also takes a list of components to start as a convenience.\n. `with-system` macro which can start a system, and tries very hard to close it.\n\nFor example, you may wish to test a single ring handler, and pass it any required arguments to start:\n\n[source,clojure]\n----\n(ns my.app.handler-test\n  (:require\n    [clojure.test :refer [deftest is]]\n    [juxt.clip.core :as clip]\n    [my.app.system :refer [get-system]]\n    [ring.mock.request :as mock]))\n\n(deftest handler-test\n  (clip/with-system [system (clip/select (get-system) [:my-handler])]\n    (try\n      (is (= {} (:my-handler system)))\n      (finally\n        (clip/stop system)))))\n----\n\n=== Custom reloaded workflow\n\nAlternatively you can roll your own Reloaded workflow quite easily, but you will miss out on convenient features in the built-in one like auto-cleanup.\n\n[source,clojure]\n----\n(ns dev\n  (:require [juxt.clip.core :as clip]\n            [clojure.tools.namespace.repl :refer [refresh]]))\n\n(def system-config {:a {:start 1}})\n(def system nil)\n\n(defn go []\n  (alter-var-root #'system (constantly (clip/start system-config))))\n\n(defn stop []\n  (alter-var-root #'system\n    (fn [s] (when s (clip/stop system-config s)))))\n\n(defn reset []\n  (stop)\n  (refresh :after 'dev/go))\n----\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuxt%2Fclip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjuxt%2Fclip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuxt%2Fclip/lists"}