{"id":19204620,"url":"https://github.com/walmartlabs/schematic","last_synced_at":"2026-03-10T02:33:09.741Z","repository":{"id":57713567,"uuid":"126353144","full_name":"walmartlabs/schematic","owner":"walmartlabs","description":"Combine configuration with building a Component system","archived":false,"fork":false,"pushed_at":"2020-05-22T17:14:51.000Z","size":48,"stargazers_count":106,"open_issues_count":0,"forks_count":3,"subscribers_count":11,"default_branch":"master","last_synced_at":"2026-01-06T18:56:12.786Z","etag":null,"topics":["clojure","configuration"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/walmartlabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-22T15:07:31.000Z","updated_at":"2025-12-23T05:35:09.000Z","dependencies_parsed_at":"2022-09-26T17:41:06.876Z","dependency_job_id":null,"html_url":"https://github.com/walmartlabs/schematic","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/walmartlabs/schematic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walmartlabs%2Fschematic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walmartlabs%2Fschematic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walmartlabs%2Fschematic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walmartlabs%2Fschematic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/walmartlabs","download_url":"https://codeload.github.com/walmartlabs/schematic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walmartlabs%2Fschematic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30322645,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T01:36:58.598Z","status":"online","status_checked_at":"2026-03-10T02:00:06.579Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["clojure","configuration"],"created_at":"2024-11-09T13:08:57.610Z","updated_at":"2026-03-10T02:33:09.706Z","avatar_url":"https://github.com/walmartlabs.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# walmartlabs/schematic\n\n[![Clojars Project](https://img.shields.io/clojars/v/com.walmartlabs/schematic.svg)](https://clojars.org/com.walmartlabs/schematic)\n\n[![CircleCI](https://circleci.com/gh/walmartlabs/schematic.svg?style=svg)](https://circleci.com/gh/walmartlabs/schematic)\n\nSchematic is a Clojure library which aids in assembling [Component][] systems\nfrom configuration data. That configuration data is often read from an `edn` file.\nThe structure of the configuration data was inspired by [Integrant][].\nThe goal of Schematic is to reduce the complexity of programmatic\nconstruction of Components, while continuing to use existing Component workflows \nand tools. \n\nThe primary features provided by Schematic are:\n* specifying an initialization function for a Component\n* declaring Component dependencies (for use in `component/using`)\n* declaring merge definitions to enable configuration sharing among Components\n* assembling a System using only a subset of the available Components\n\n[component]: https://github.com/stuartsierra/component\n[integrant]: https://github.com/weavejester/integrant\n\n[API Documentation](http://walmartlabs.github.io/apidocs/schematic/)\n\n[Blog Post](https://medium.com/@hlship/schematic-92b0b6ffdb26)\n\n## Overview\n\nSchematic starts with a configuration map.\nFrom this map, Schematic will assemble a Component system map, ready to\nbe started.\n\nEach key/value pair in the configuration map will become\na system key and component in the Component system map.\n\nSchematic uses some special keys to identify\n\n- The constructor for the component (`:sc/create-fn`)\n- The Component dependencies for the components (`:sc/refs`)\n- Additional rules to manipulate the component's configuration (`:sc/merge`)\n\nThe component's configuration is passed to its constructor as part of\nbuilding the Component system map.\n\nIt is ok to omit the `:sc/create-fn` constructor when a plain map is sufficient.\nThis may occur when the component doesn't need to implement any protocols,\nor when the component exists as a source of configuration for other components.\n\n## Usage\n\nThe simplest example is:\n\n```clojure\n(require '[com.walmartlabs.schematic :as sc]\n         '[com.stuartsierra.component :as component])\n \n(def config {:app {:host \"localhost\"}})  \n       \n(-\u003e\u003e (sc/assemble-system config)\n\n     (component/start-system)\n     (into {}) ;; put it into a map just so it prints in the REPL\n     )\n     \n;; =\u003e {:app {:host \"localhost\"}}\n```\nIn this case, Schematic didn't really do any work, \nsince there were no declared refs, merge-defs, or create-fns.\n\nA more complete example is:\n```clojure\n(require '[com.walmartlabs.schematic :as sc]\n         '[com.stuartsierra.component :as component])\n\n(defrecord App [server api-keys]\n  component/Lifecycle\n  (start [this]\n   (assoc this :started true))\n  \n  (stop [this]\n   (assoc this :started nil)))\n\n(def config\n  {:app {:sc/create-fn `map-\u003eApp\n         :sc/refs {:server :webserver}\n         :sc/merge [{:to [:api-keys] :from [:api :keys]}]}\n  \n   :webserver {:sc/merge [{:from [:host-config]}]\n               :port 8080}\n   \n   :host-config {:host \"localhost\"}\n  \n   :api {:username \"user\"\n         :keys [1 2 3 4]}})\n   \n(-\u003e\u003e (sc/assemble-system config)\n     (component/start-system)\n     (into {}) ;; put it into a map just so it prints in the REPL\n     )\n\n;; =\u003e\n;; {:api {:username \"user\", :keys [1 2 3 4]},\n;;  :host-config {:host 8080},\n;;  :webserver {:host \"localhost\", :port 8080},\n;;  :app #user.App{:server {:host \"localhost\", :port 8080}, \n;;                 :api-keys [1 2 3 4], \n;;                 :started true}}\n```\n\nNotice, in the above, that the Schematic keys (`:sc/create-fn`, etc.) have\nbeen removed, and the configuration for the `:app` component has been\nextended via `:sc/merge`.\n\nOf course, in a more realistic example, the configuration data would be read from \nan EDN file, rather than hard-coded into the application.\n\nThe following sections will cover aspects seen in the above example.\n\n### Declaring configuration components\n\nExample (edn):\n```clojure\n{:app {:sc/create-fn user/map-\u003eApp\n       :sc/refs {:server :webserver\n                 :api-keys :api-keys}}\n\n :webserver {:sc/refs [:host]\n             :port 8080}\n             \n :host 8080\n\n :api-keys [1 2 3 4]}\n```\n\nIn this example, `:app`, `:webserver`, `host` and `:api-keys` are all top-level items, \nand will be used to create components and/or be injected into components.\nTop-level items can be any type of data, but only `associative` data structures will\nreceive dependency injection metadata for their references.\n\n### Declaring dependencies\n\nComponent dependencies are declared via a key of `:sc/refs` with the value being\na map or vector having the same semantics as `component/using`.\n\nExample:\n```clojure\n:app {:sc/create-fn user/map-\u003eApp\n      :sc/refs {:server :webserver\n                :api-keys :api-keys}}\n                \n:webserver {:sc/refs [:host]}\n```\nIn `:app`, the refs are a map of component-ids, where the key is the local id of the component,\nand the value is the global id of the component.\nIn `:webserver`, a vector is provided, indicating that the local id and global id of the \ncomponent are the same.\n\n### Creating Components\n\nTo specify that a particular configuration item can participate in `component/Lifecycle` functions (start/stop),\nthe special key -- `:sc/create-fn` -- is used to declare the namespace-qualified name of the function which\nwhich will be called to create the Component instance. This will usually be the default `map-\u003eRecord` function which `defrecord`\ncreates for us. But it can also be any regular function which returns an object which implements\nthe `Lifecycle` interface, and accepts a single map as an argument.\n\nExample (using a `map-\u003eRecord` constructor function):\n```clojure\n(ns com.business.system\n  (:require [com.walmartlabs.schematic :as sc]\n            [com.stuartsierra.component :as component]))\n\n(defrecord App [server api-keys thread-pool-size]\n  component/Lifecycle\n  (start [this] this)\n  (stop [this] this))\n  \n(def config {:app {:sc/create-fn 'com.business.system/map-\u003eApp\n                   :sc/refs {:server :webserver\n                             :api-keys :api-keys}\n                   :thread-pool-size 20}\n             ;; remaining config omitted     \n             })\n```\nNote `:sc/create-fn 'com.business.system/map-\u003eApp` which will cause the `App` record to be created\nwhen the system is assembled.\n\nExample (using plain function):\n```clojure\n(ns com.business.system\n  (:require [com.walmartlabs.schematic :as sc]\n            [com.stuartsierra.component :as component]))\n\n(defrecord App [server api-keys thread-pool-size]\n  component/Lifecycle\n  (start [this] this)\n  (stop [this] this))\n  \n(defn make-app [{:keys [thread-pool-size] :or {thread-pool-size 10}}]\n  (map-\u003eApp {:thread-pool-size thread-pool-size}))\n  \n(def config {:app {:sc/create-fn 'com.business.system/make-app\n                   :sc/refs {:server :webserver\n                             :api-keys :api-keys}\n                   :thread-pool-size 20}\n             ;; remaining config omitted     \n             })\n```\nIn the 'plain function' example, you can see that it is possible to set a default value\nfor `thread-pool-size` in the `make-app` function if a value were not to be provided in the config.\nThat is just one possible use case for using a plain function constructor.\n \n### Dependency injection\n\nFor each top-level, `map?` item in the config, Schematic will extract a list of the declared \n`:sc/refs` and attach the needed Component metadata by calling `(component/using component ref-map)`.\nThe resulting value will be a Component which can be started and stopped.\n\n### Merging common configuration\n\nIn complex applications, there may be configuration that is shared or duplicated across components,\nthe `:sc/merge` key is a set of rules for selecting, renaming, and injecting global configuration\ninto a component's specific configuration.\n\nThe merge happens before `:sc/refs` are processed.\n\n```clojure\n:host-config {:host \"localhost\"\n              :port 8080}\n\n:app {:sc/merge [{:from [:host-config]}]}\n```\nIn this example, `:host-config` map will be merged into the `:app` map. \n\n* `:sc/merge` accepts a vector of merge-defs.\n* a merge-def can provide `:to`, `:from`, and `:select` keys\n* `:to` indicates the key/key-path in the component config where the results will be merged. It defaults to `[]`. \n* `:from` indicates the key/key-path from the global config where values will be copied from\n* `:select` is either a vector of key-names or a map of local-key-names to from-key-names (names of fields in `:from`). \nIt defaults to the special keyword `:all`, which indicates to copy the entire `:from` data object.\n\n### Assembling the system\n \nThe actual work of processing merge-defs, analyzing the `:sc/refs`, and adding the dependency metadata happens in the\n`com.walmartlabs.schematic/assemble-system` function. That function takes a config map\nand returns a System map which can be started and stopped.\n\nExample:\n```clojure\n(-\u003e\u003e {:webserver {:sc/refs [:host :port]}\n      :host \"localhost\"\n      :port 8080}\n     (sc/assemble-system)\n     (component/start-system))\n\n;; =\u003e #\u003cSystemMap\u003e\n```\n\n`assemble-system` optionally takes a configuration map\n and an optional list of component-ids to include in the final system.\n\n#### Assembling a sub-system using component-ids\n\nIn some cases it might be desirable to include only a subset of the available config \nwhen assembling a system, so that only the needed components are started and stopped.\nTwo possible such scenarios are: \n* The config is shared among multiple applications, which \neach need a portion of the components at runtime, but not all of them.\n* At the REPL, it might be useful to get a particular component and start it, such as a database connection.\n\nIn such cases, the two argument version of `assemble-system` can be invoked, passing\nthe list of component ids as the second parameter.\nThere are the top-level components which must be included in the final system.\nOnly these top-level components and all of their transitive dependencies will be included in\nthe final system map.\n\nExample (include single app):\n```clojure\n(-\u003e {:app-1 {:sc/refs {:conn :db-conn\n                       :product-api :product-api}}\n     :app-2 {:sc/refs {:conn :db-conn\n                       :customer-api :customer-api}}\n     :db-conn {:sc/refs {:host :db-host}\n               :username \"user\"\n               :password \"secret\"}\n     :product-api {}\n     :customer-api {}\n     :db-host \"localhost\"}\n    (sc/assemble-system [:app-1])\n    (component/start-system)\n    ((partial into {})))\n;; =\u003e\n;; {:product-api {},\n;;  :app-1 {:conn {:host \"localhost\", :username \"user\", :password \"secret\"}, :product-api {}},\n;;  :db-host \"localhost\",\n;;  :db-conn {:host \"localhost\", :username \"user\", :password \"secret\"}}\n```\nNotice that `:app-2` and `:customer-api` have not been included in the final system.\n\nThis filtering of components occurs before components are properly instantiated (via the :sc/create-fn function).\n\nExample (development at the REPL):\n```clojure\n(let [config {:app-1 {:sc/refs {:conn :db-conn\n                                :product-api :product-api}}\n              :app-2 {:sc/refs {:conn :db-conn\n                                :customer-api :customer-api}}\n              :db-conn {:sc/refs {:host :db-host}\n                        :username \"user\"\n                        :password \"secret\"}\n              :product-api {}\n              :customer-api {}\n              :db-host \"localhost\"}\n      system (-\u003e (sc/assemble-system config [:db-conn :product-api])\n                 (component/start-system))\n      {:keys [db-conn product-api]} system]\n  (println (into {} system))\n  ;; do something here with db-conn or product-api\n  (component/stop system)\n  nil)\n\n;; {:product-api {}, \n;;  :db-host localhost, \n;;  :db-conn {:host localhost, :username user, :password secret}}    \n;; =\u003e nil \n```\nIn this example, we were able to use just the database component and product-api to do some\ntesting in the REPL.\n\n## License\n\nCopyright (c) 2017-present, Walmart Inc.\n\nDistributed under the Apache Software License 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwalmartlabs%2Fschematic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwalmartlabs%2Fschematic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwalmartlabs%2Fschematic/lists"}