{"id":15010447,"url":"https://github.com/dryewo/cyrus-config","last_synced_at":"2025-04-09T22:41:05.047Z","repository":{"id":62432235,"uuid":"116310180","full_name":"dryewo/cyrus-config","owner":"dryewo","description":"Almost statically typed REPL-friendly configuration library.","archived":false,"fork":false,"pushed_at":"2019-09-27T15:51:57.000Z","size":41,"stargazers_count":4,"open_issues_count":2,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-24T00:38:13.415Z","etag":null,"topics":["12factor","12factorapp","clj","clojure","configuration","cyrus","repl","repl-support"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dryewo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.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-01-04T21:38:35.000Z","updated_at":"2020-02-20T13:32:47.000Z","dependencies_parsed_at":"2022-11-01T20:46:56.953Z","dependency_job_id":null,"html_url":"https://github.com/dryewo/cyrus-config","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dryewo%2Fcyrus-config","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dryewo%2Fcyrus-config/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dryewo%2Fcyrus-config/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dryewo%2Fcyrus-config/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dryewo","download_url":"https://codeload.github.com/dryewo/cyrus-config/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247629682,"owners_count":20969940,"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":["12factor","12factorapp","clj","clojure","configuration","cyrus","repl","repl-support"],"created_at":"2024-09-24T19:34:15.057Z","updated_at":"2025-04-09T22:41:05.025Z","avatar_url":"https://github.com/dryewo.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cyrus-config\n[![Build Status](https://travis-ci.org/dryewo/cyrus-config.svg?branch=master)](https://travis-ci.org/dryewo/cyrus-config)\n[![codecov](https://codecov.io/gh/dryewo/cyrus-config/branch/master/graph/badge.svg)](https://codecov.io/gh/dryewo/cyrus-config)\n[![Clojars Project](https://img.shields.io/clojars/v/cyrus/config.svg)](https://clojars.org/cyrus/config)\n\nAlmost statically typed REPL-friendly configuration library.\n\n```clj\n[cyrus/config \"0.3.1\"]\n```\n\nMany other configuration libraries just give you a map from keyword to string:\n\n```clj\n{:http-port   \"8090\"\n :db-user     \"master\"\n :db-password \"foo123\"}\n```\n\nThis map is collected from various sources, but there is no support for you to actually check if these values\nare present, have correct format, etc.\n\n* What if a value is missing?\n* What if it's not convertible to number or boolean?\n* What if you want to pass a list in a variable?\n* Can you remember all the configuration parameters that your application supports?\n* How can you log all the configuration values when the application starts?\n* How can you reload the configuration when you `(refresh)` your project in REPL?\n* How can you unit-test application behavior with different configuration values?\n \nThis is what **cyrus-config** helps you with. Following [12 Factor App] manifesto, it only reads configuration \nfrom environment (not files, not command line arguments, not network services),\nand then transforms, validates and summarizes it.\n\n## Usage\n\n*http.clj:*\n```clj\n(ns my.http\n  (:require [cyrus-config.core :as cfg]))\n\n;; Introduce a configuration constant that will contain validated and transformed value from the environment\n;; By default uses variable name transformed from the defined name: \"HTTP_PORT\"\n(cfg/def HTTP_PORT \"Port to listen on\" {:spec     int?\n                                        :default  8080})\n\n;; Available immediately, without additional loading commands, but can contain a special value indicating an error.\n\n(defn start-server []\n  (server/start-server {:port http-port}))\n```\n\n*core.clj:*\n```clj\n(ns my.core\n  (:require [cyrus-config.core :as cfg]\n            [my.http :as http]))\n\n(defn -main [\u0026 args]\n  ;; Will throw if some configuration variables are invalid\n  (cfg/validate!)\n  (println \"Config loaded:\\n\" (cfg/show))\n  (http/start-server))\n```\n\nWhen started, it will print something like:\n\n```\nConfig loaded:\n#'my.http/HTTP_PORT: 8080 from HTTP_PORT in :enironment // Port to listen on\n```\n\nThis library is also a part of [cyrus] Leiningen template:\n\n```\n$ lein new cyrus org.example/my-project +all\n```\n\n### Reference\n\n#### Defining\n\n`(cfg/def FOO_BAR \u003coptional-docstring\u003e \u003coptional-parameter-map\u003e)` — defines a configuration constant (which is an ordinary var,\nlike normal `def` does), assigns its value according to parameters.\nAdditionally, metadata is set that contains all parameters, raw value, source (`:environment`, `:override`, `:default`) and error.\nThis metadata is used in `(cfg/show)`.\n\n```clj\n;; Assuming that HTTP_PORT environment variable contains \"8080\"\n(cfg/def HTTP_PORT {:spec int? :required true})\nHTTP_PORT\n=\u003e 8080\n(meta #'HTTP_PORT)\n=\u003e {::cfg/user-spec      {:spec #object[clojure.core$int_QMARK___5132 ...]}\n    ::cfg/effective-spec {:required true\n                          :default  nil\n                          :secret   false\n                          :var-name \"HTTP_PORT\"\n                          :spec     #object[clojure.core$int_QMARK___5132 ...]}\n    ::cfg/source         :environment\n    ::cfg/raw-value      \"8080\"\n    ::cfg/error          nil\n    ...}\n\n;; Like with normal def, only the name is mandatory\n(cfd/def DB_USERNAME)\n\n;; Docstring behaves the same way as for normal def\n(cfg/def DB_PASSWORD \"DB password\" {:secret true})\n\n;; Parameter map is optional\n(cfg/def DB_URL \"DB URL\")\n\n;; Variable name can be explicitly specified\n(cfg/def DESC {:var-name \"DESCRIPTION\"}) \n```\n\nParameters (all are optional):\n\n* `:var-name` — string or keyword, environment variable name to get the value from. Automatically converted to ENV_CASE string.\n  Defaults to `\"FOO_BAR\"` (according to the constant's name).\n* `:required` — boolean, if the environment variable is not set, an error will be thrown during `(cfg/validate!)`. The constant\nwill silently get a special value that indicates an error, for example:\n    ```clj\n    (cfg/def REQUIRED_1 {:required true})\n    REQUIRED_1\n    =\u003e #=(cyrus_config.core.ConfigNotLoaded. {:code    :cyrus-config.core/required-not-present\n                                              :message \"Required not present\"})\n\n    ```\n* `:default` — default value to use if the variable is not set. Cannot be used together with `:required true`. Defaults to `nil`.\n* `:spec` — Clojure Spec to conform the value to. Defaults to `string?`, can also be `int?`, `keyword?`, `double?` and \n  any complex spec, in which case the original value will be parsed as EDN and then conformed. See Conforming/Coercing section below. \n* `:schema` — Prismatic Schema to coerce the value to (same as `:spec`, but for Prismatic). Complex schemas first parse the value as YAML.\n* `:secret` — boolean, if true, the value will not be displayed in the overview returned by `(cfg/show)`:\n    ```\n    #'my.db/DB_PASSWORD: \u003cSECRET\u003e from DB_PASSWORD in :environment // Database password\n    ```\n\nYou can also use existing configuration constants' values when defining configuration constants:\n\n```clj\n(cfg/def SERVER_URL)\n\n;; This constant will only be required if SERVER_URL is set\n(cfg/def SERVER_POLLING_INTERVAL {:required (some? SERVER_URL) :spec int?})\n\n;; This will get default value from SERVER_POLLING_INTERVAL, when it's set (it also has a different type)\n(cfg/def SERVER_POLLING_DELAY {:default SERVER_POLLING_INTERVAL})\n```\n\n#### Validation\n\n`(cfg/validate!)` — checks if there were any errors during config loading, throws an `ex-info` with their description.\nIf everything is ok, does nothing.\nThe output looks like this:\n\n```\n               my.core.main                \n                        ...                \n              my.core/-main  core.clj:   21\n              my.core/-main  core.clj:   32\ncyrus-config.core/validate!  core.clj:  146\n       clojure.core/ex-info  core.clj: 4739\nclojure.lang.ExceptionInfo: Errors found when loading config:\n                            #'my.http/HTTP_PORT: \u003cERROR\u003e because HTTP_PORT contains \"abcd\" in :environment - java.lang.NumberFormatException: For input string: \"abcd\" // Port to listen on\n```\n\n#### Summary\n\n`(cfg/show)` — returns a formatted string containing information about all defined and loaded configuration constants.\nThe return value looks like this:\n\n```\n#'my.nrepl/NREPL_BIND: \"0.0.0.0\" from NREPL_BIND in :default \"0.0.0.0\" // NREPL network interface\n#'my.nrepl/NREPL_PORT: 55000 from NREPL_PORT in :environment // NREPL port\n#'my.db/DB_PASSWORD: \u003cSECRET\u003e because DB_PASSWORD is not set // Password\n#'my.db/DB_USERNAME: \"postgres\" from DB_USERNAME in :default \"postgres\" // Username\n#'my.db/JDBC_URL: \"jdbc:postgresql://localhost:5432/postgres\" from DB_JDBC_URL in :default \"jdbc:postgresql://localhost:5432/postgres\" // Coordinates of the DB\n#'my.authenticator/TOKENINFO_URL: nil because TOKENINFO_URL is not set // URL to check access tokens against. If not set, tokens won't be checked.\n#'my.http/HTTP_PORT: 8090 from HTTP_PORT in :environment // Port for HTTP server to listen on\n```\n\nIt's recommended to print it from `-main`:\n\n```clj\n(defn -main [\u0026 args]\n  ;; By the time -main starts, all the config is already loaded. Here we only find out about errors. \n  (cfg/validate!)\n  (println \"Config loaded:\\n\" (cfg/show))\n  ...)\n\n```\n\n#### REPL support\n\n`(reload-with-override! \u003cenv-map\u003e)` — reloads all configuration constants from a merged source:\n\n    (merge (System/getenv) \u003cenv-map\u003e)\n\n\nFor REPL-driven development it's recommended to have it in a wrapper for `(refresh)`:\n\n*dev/user.clj:*\n```clj\n(defn load-dev-env []\n  (edn/read-string (slurp \"./dev-env.edn\")))\n\n(defn refresh []\n  (cfg/reload-with-override! (load-dev-env))\n  (cfg/validate!)\n  (clojure.tools.namespace.repl/refresh))\n```\n\nThis will ensure that every time the code is reloaded, the overrides file `dev-env.edn` is also re-read.\n\n### Conforming/Coercion\n\nThe library supports two ways of conforming (a.k.a. coercing) environment values (which are always string) to\nvarious types: integer, keyword, double, etc. The ways of defining targer types are Clojure Spec and Prismatic Schema.\nThey are mutually exclusive, i.e. only one of `:spec` and `:schema` keys are possible at the same time for each configuration constant.\n\n#### Clojure Spec\n\n\u003e [spec] is a Clojure library to describe the structure of data and functions.\n\nIt is enabled by setting `:spec` key in the parameters:\n\n```clj\n(cfg/def HTTP_PORT {:spec int?})\n```\n\nImplicit coercion is in place for basic types: `int?`, `double?`, `boolean?`, keyword?`, `string?` (the default one, does nothing). \n\n##### Custom coercions\n\n`:cyrus-config.coerce/nonblank-string` spec is included, it conforms blank string to `nil`:\n\n    EXTERNAL_SERVICE_URL=\"\"\n\n```clj\n(require '[cyrus-config.coerce :as cfgc])\n\n(cfg/def EXTERNAL_SERVICE_URL {:spec ::cfgc/nonblank-string})\n\n(when-not EXTERNAL_SERVICE_URL\n  (log/warn \"EXTERNAL_SERVICE_URL is not set, will not try to access!\"))\n```\n\nWithout `::cfgc/nonblank-string` you would need to check for blankness of the string every time you use it:\n\n```clj\n(when (str/blank? EXTERNAL_SERVICE_URL)\n  (log/warn \"EXTERNAL_SERVICE_URL is not set, will not try to access!\"))\n```\n\nAdditionally, you can put a complex value in EDN format into the variable:\n\n    IP_WHITELIST='[\"1.2.3.4\" \"4.3.2.1\"]'\n\nand then conform it:\n\n```clj\n(cfg/def IP_WHITELIST {:spec (cfgc/from-edn (s/coll-of string?))})\nIP_WHITELIST\n=\u003e [\"1.2.3.4\" \"4.3.2.1\"]\n;; ^ not a string, a Clojure data structure\n```\n\nAlternatively, you can use JSON format:\n\n    IP_WHITELIST='[\"1.2.3.4\", \"4.3.2.1\"]'\n\n```clj\n(cfg/def IP_WHITELIST {:spec (cfgc/from-custom-parser json/parse-string (s/coll-of string?))})\n```\n\nOr any other custom conversion:\n\n    IP_WHITELIST='1.2.3.4, 4.3.2.1'\n\n```clj\n(defn parse-csv [csv]\n  (if (sequential? csv)\n    csv\n    (-\u003e\u003e (str/split (str csv) #\",\")\n         (map str/trim))))\n\n(cfg/def IP_WHITELIST {:spec (s/conformer parse-csv)})\n```\n\nIn this case, conversion is considered successful if it does not throw an exception.\n\n`if (sequential? csv)` condition is important, it allows to provide `:default` not only as string, but also as target type:\n\n```clj\n(cfg/def IP_WHITELIST {:spec (s/conformer parse-csv) :default [\"one\" \"two\"]})\n```\n\n\n#### Prismatic Schema\n\n\u003e [Prismatic Schema]: A Clojure(Script) library for declarative data description and validation.\n\nIt is enabled by setting `:schema` key in the parameters:\n\n```clj\n(cfg/def HTTP_PORT {:schema s/Int})\n```\n\nAlso, it's necessary to include `[squeeze \"0.3.2]` library in project dependencies to use `:schema` key.\n\nBesides supporting all atomic types (`s/Int`, `s/Num`, `s/Keyword`, etc.), complex types can be coerced:\n\n```\nIP_WHITELIST=$(cat \u003c\u003cEOF\n- 1.2.3.4\n- 4.3.2.1\nEOF\n)\nFOO='foo:\\n  bar: 1\\n  baz: 42'\n```\n\nAnd then define the config constants as following:\n\n```clj\n(cfg/def IP_WHITELIST {:spec [s/Str]})\nIP_WHITELIST\n=\u003e [\"1.2.3.4\" \"4.3.2.1\"]\n\n(cfg/def FOO {:schema {:foo {:bar                  s/Str \n                             :baz                  s/Int\n                             (s/optional-key :opt) s/Keyword}}})\nFOO\n=\u003e {:foo {:bar \"1\" \n          :baz 42}}\n\n```\n\nAdditionally, when using REPL-driven development, you can provide unstringified values in the overrides:\n\n```clj\n(cfg/reload-with-override! {\"IP_WHITELIST\" [\"1.2.3.4\" \"4.3.2.1\"]\n                            \"FOO\"          {:foo {:bar \"1\" \n                                                  :baz 42}}})\n```\n\n## Rationale\n\nLet's assume we are following [12 Factor App] manifesto and read the configuration only from environment variables.\n\nImagine you have a HTTP server that expects a port:\n\n```clj\n(defn start-server []\n  (server/start-server {:port (System/getenv \"HTTP_PORT\")}))\n```\n\nBut the HTTP server library expects an integer, not a string:\n\n```clj\n(defn start-server []\n  (server/start-server {:port (Integer/parseInt (System/getenv \"HTTP_PORT\"))}))\n```\n\nWe need a default value in case when `HTTP_PORT` is not set:\n\n```clj\n(defn start-server []\n  (server/start-server {:port (or (some-\u003e (System/getenv \"HTTP_PORT\") (Integer/parseInt)) 8080)}))\n```\n\nEnvironment variables cannot be changed when the process is running. In order to make our application testable\n (for example, to try starting the server on different ports), we need an abstraction layer.\nFor example, we can read the entire environment to a map and then redefine it for tests:\n\n```clj\n(def ^:dynamic environment (System/getenv))\n\n(defn start-server []\n  (server/start-server {:port (Integer/parseInt (get environment \"HTTP_PORT\"))}))\n\n(deftest test-start-server\n  (alter-var-root #'environment (constantly {\"HTTP_PORT\" \"7777\"})))\n  (start-server)\n  ...\n  (stop-server)\n```\n\nThis can be further improved by keeping the original environment intact and changing only the overriding:\n\n```clj\n(def environment (System/getenv))\n(def ^:dynamic env-override {})\n\n(defn reload-overrides! [overrides]\n  (alter-var-root #'env-override (constantly overrides)))\n\n(defn get-config [var-name]\n  (get (merge environment env-override) var-name))\n\n(defn start-server []\n  (server/start-server {:port (Integer/parseInt (get-config \"HTTP_PORT\")}))\n```\n\nIf you do REPL-driven development, overrides can be read from a file and applied without restarting the REPL:\n\n```clj\n(reload-overrides! (clojure.edn/read-string (slurp \"dev-env.edn\")))\n```\n\nIt is a lot of hassle already, while we just wanted to read one simple value and have some basic REPL support.\nThis solution still has drawbacks:\n\n* If the variable is used in many places, its name has to be repeated, which adds risk of typos (\"HTPP_PORT\").\n* Errors are found late, only when first accessing the value.\n* Errors are only given one at a time: if the application fails to start because of one invalid variable, it does not check\n  other values which are needed later.\n* We might want to have a print a summary of configuration values when the application starts.\n    * Some variables should not be printed (passwords, keys, etc.)\n\nThis list can be continued, and the requirements are common to many projects, hence this library.\n\n## License\n\nCopyright © 2017 Dmitrii Balakhonskii\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n\n[12 Factor App]: https://12factor.net/\n[spec]: https://github.com/clojure/spec.alpha\n[Prismatic Schema]: https://github.com/plumatic/schema\n[cyrus]: https://github.com/dryewo/cyrus\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdryewo%2Fcyrus-config","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdryewo%2Fcyrus-config","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdryewo%2Fcyrus-config/lists"}