{"id":15010381,"url":"https://github.com/juxt/aero","last_synced_at":"2025-05-13T21:09:57.294Z","repository":{"id":32092549,"uuid":"35664663","full_name":"juxt/aero","owner":"juxt","description":"A small library for explicit, intentful configuration.","archived":false,"fork":false,"pushed_at":"2025-02-10T10:49:15.000Z","size":284,"stargazers_count":770,"open_issues_count":18,"forks_count":60,"subscribers_count":42,"default_branch":"master","last_synced_at":"2025-05-11T17:54:07.582Z","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":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/juxt.png","metadata":{"files":{"readme":"README.md","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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-05-15T09:08:54.000Z","updated_at":"2025-05-10T02:50:53.000Z","dependencies_parsed_at":"2025-04-09T19:11:13.006Z","dependency_job_id":"aedc051b-fa83-498b-b6f8-7ebaf1b2e4f4","html_url":"https://github.com/juxt/aero","commit_stats":{"total_commits":165,"total_committers":33,"mean_commits":5.0,"dds":0.5878787878787879,"last_synced_commit":"814b0006a1699e8149045e55c4e112e61b983fe9"},"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Faero","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Faero/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Faero/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juxt%2Faero/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/juxt","download_url":"https://codeload.github.com/juxt/aero/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254029002,"owners_count":22002283,"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":["clojure","configuration"],"created_at":"2024-09-24T19:33:53.478Z","updated_at":"2025-05-13T21:09:52.280Z","avatar_url":"https://github.com/juxt.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Aero\n\n![Workflow](https://github.com/juxt/aero/actions/workflows/tests.yml/badge.svg?branch=master)\n[![Join the chat at https://clojurians.slack.com/archives/C0CFGN25D](\nhttps://badgen.net/badge/chat/on%20slack/purple?icon=slack\n)](https://clojurians.slack.com/archives/C0CFGN25D)\n\n(\u003cb\u003ea\u003c/b\u003eero is \u003cb\u003ee\u003c/b\u003edn \u003cb\u003er\u003c/b\u003eeally, \u003cb\u003eo\u003c/b\u003ek?)\n\nA small library for explicit, intentful configuration.\n\n![Light and fluffy configuration](aero.jpg)\n\n## Installation\n\nAdd the following dependency to your `project.clj` file\n\n[![Clojars Project](http://clojars.org/aero/latest-version.svg)](http://clojars.org/aero)\n\n## Getting started\n\nCreate a file called `config.edn` containing the following\n\n```clojure\n{:greeting \"World!\"}\n```\n\nIn your code, read the configuration like this\n\n```clojure\n(require '[aero.core :refer [read-config]])\n(read-config \"config.edn\")\n```\n\nor to read from the classpath, like this\n\n```clojure\n(read-config (clojure.java.io/resource \"config.edn\"))\n```\n\nKeep in mind that even though `(read-config \"config.edn\")` will work in your Repl and when running tests, it's very likely to catastrophically fail if you run your application from the generated `.jar` file.\n\nSo to avoid surprises it's better to always use `io/resource` which works in all scenarios.\n\n## Design goals\n\n### Explicit and intentional\n\nConfiguration should be explicit, intentful, obvious, but not clever. It\nshould be easy to understand what the config is, and where it is\ndeclared.\n\nDetermining config in stressful situations, for example, while\ndiagnosing the cause of a production issue, should not be a\n[wild goose chase](http://en.wiktionary.org/wiki/wild-goose_chase).\n\n### Avoid duplication ...\n\nConfig files are often duplicated on a per-environment basis, attracting\nall the problems associated with duplication.\n\n### ... but allow for difference\n\nWhen looking at a config file, a reader will usually ask: \"Does the value differ from the default, and if so how?\". It's clearly better to answer that question in-place.\n\n### Allow config to be stored in the source code repository ...\n\nWhen config is left out of source code control it festers and diverges from the code base. Better to keep a single config file in source code control.\n\n### ... while hiding passwords\n\nWhile it is good to keep config in source code control, it is important to ensure passwords and other sensitive information remain hidden.\n\n### Config should be data\n\nWhile it can be very flexible to have 'clever' configuration 'programs', it can be [unsafe](http://www.learningclojure.com/2013/02/clojures-reader-is-unsafe.html), lead to exploits and compromise security. Configuration is a key input to a program. Always use data for configuration and [avoid turing-complete](http://langsec.org/occupy) languages!\n\n### Use environment variables sparingly\n\nWe suggest using environment variables judiciously and sparingly, the way Unix intends, and not [go mad](http://12factor.net/config). After all, we want to keep configuration explicit and intentional.\n\nAlso, see these arguments [against](https://gist.github.com/telent/9742059).\n\n### Use edn\n\nFortunately for Clojure developers like us, most of the tech to read configuration in a safe, secure and extensible way already exists in the Clojure core library (EDN).\n\n## Community\n\nIf you you have any questions, don't hesitate to write a message in [JUXT's Clojurians Slack channel](https://clojurians.slack.com/archives/C0CFGN25D)\n\n## Tag literals\n\nAero provides a small library of tag literals. If you need more options, check out [monkey-projects/aero-ext](https://github.com/monkey-projects/aero-ext)\n\n### env\n\nUse `#env` to reference an environment variable.\n\n```clojure\n{:database-uri #env DATABASE_URI}\n```\n\nIt is considered bad practice to use environment variables for passwords and other confidential information. This is because it is very easy to leak a process's environment (e.g. via `ps e -f` or to your application monitoring tool). Instead you should use `#include` - see [here](#hide-passwords-in-local-private-files).\n\n### envf\n\nUse `#envf` to insert environment variables into a formatted string.\n\n```clojure\n{:database #envf [\"protocol://%s:%s\" DATABASE_HOST DATABASE_NAME]}\n```\n\n### or\n\nUse `#or` when you want to provide a list of possibilities, perhaps with a default at the end.\n\n```clojure\n{:port #or [#env PORT 8080]}\n```\n\n### join\n\n`#join` is used as a string builder, useful in a variety of situations such as building up connection strings.\n\n``` clojure\n{:url #join [\"jdbc:postgresql://psq-prod/prod?user=\"\n             #env PROD_USER\n             \"\u0026password=\"\n             #env PROD_PASSWD]}\n```\n\n### profile\n\nUse profile as a kind of reader conditional.\n\n`#profile` expects a map, from which it extracts the entry corresponding to the __profile__.\n\n```clojure\n{:webserver\n  {:port #profile {:default 8000\n                   :dev 8001\n                   :test 8002}}}\n```\n\nYou can specify the value of __profile__ when you read the config.\n\n```clojure\n(read-config \"config.edn\" {:profile :dev})\n```\n\nwhich will return\n\n```clojure\n{:webserver\n  {:port 8001}}\n```\n\n(`#profile` replaces the now deprecated `#cond`, found in previous versions of Aero)\n\n### hostname\n\nUse when config has to differ from host to host, using the hostname. You\ncan specify multiple hostnames in a set.\n\n```clojure\n{:webserver\n  {:port #hostname {\"stone\" 8080\n                    #{\"emerald\" \"diamond\"} 8081\n                    :default 8082}}}\n```\n\n### long, double, keyword, boolean\n\nUse to parse a `String` value into a `Long`, `Double`, keyword or boolean.\n\n``` clojure\n{:debug #boolean #or [#env DEBUG \"true\"]\n :webserver\n  {:port #long #or [#env PORT 8080]\n   :factor #double #env FACTOR\n   :mode #keyword #env MODE}}\n```\n\n### user\n\n`#user` is like `#hostname`, but switches on the user.\n\n### include\n\nUse to include another config file. This allows you to split your config files\nto prevent them from getting too large.\n\n``` clojure\n{:webserver #include \"webserver.edn\"\n :analytics #include \"analytics.edn\"}\n```\n\nNOTE: By default `#include` will attempt to resolve the file to be included *relative* to the\nconfig file it's being included from. (this won't work for jars)\n\nYou can provide your own custom resolver to replace the default behaviour or use one that\naero provides (`resource-resolver`, `root-resolver`). For example\n\n```clojure\n(require '[aero.core :refer (read-config resource-resolver)])\n(read-config \"config.edn\" {:resolver resource-resolver})\n```\n\nYou can also provide a map as a resolver. For example\n\n```clojure\n(read-config \"config.edn\" {:resolver {\"webserver.edn\" \"resources/webserver/config.edn\"}})\n```\n\n### merge\n\nMerge multiple maps together\n\n```clojure\n#merge [{:foo :bar} {:foo :zip}]\n```\n\n### ref\n\nTo avoid duplication you can refer to other parts of your configuration file using the `#ref` tag.\n\nThe `#ref` value should be a vector resolveable by `get-in`. Take the following config map for example:\n\n```clojure\n{:db-connection \"datomic:dynamo://dynamodb\"\n :webserver\n  {:db #ref [:db-connection]}\n :analytics\n  {:db #ref [:db-connection]}}\n```\n\nBoth `:analytics` and `:webserver` will have their `:db` keys resolved\nto `\"datomic:dynamo://dynamodb\"`\n\nReferences are recursive. They can be used in `#include` files.\n\n### Define your own\n\nAero supports user-defined tag literals. Just extend the `reader` multimethod.\n\n```clojure\n(defmethod reader 'mytag\n [{:keys [profile] :as opts} tag value]\n  (if (= value :favorite)\n     :chocolate\n     :vanilla))\n```\n\n## Recommended usage patterns, tips and advice\n\n### Hide passwords in local private files\n\nPasswords and other confidential information should not be stored in version control, nor be specified in environment variables. One alternative option is to create a private file in the HOME directory that contains only the information that must be kept outside version control (it is good advice that everything else be subject to configuration management via version control).\n\nHere is how this can be achieved:\n\n```clojure\n{:secrets #include #join [#env HOME \"/.secrets.edn\"]\n\n :aws-secret-access-key\n  #profile {:test #ref [:secrets :aws-test-key]\n            :prod #ref [:secrets :aws-prod-key]}}\n```\n\n### Use functions to wrap access to your configuration.\n\nHere's some good advice on using Aero in your own programs.\n\nDefine a dedicated namespace for config that reads the config and provides functions to access it.\n\n```clojure\n(ns myproj.config\n  (:require [aero.core :as aero]))\n\n(defn config [profile]\n  (aero/read-config \"dev/config.edn\" {:profile profile}))\n\n(defn webserver-port [config]\n  (get-in config [:webserver :port]))\n```\n\nThis way, you build a simple layer of indirection to insulate the parts of your program that access configuration from the evolving structure of the configuration file. If your configuration structure changes, you only have to change the wrappers, rather than locate and update all the places in your code where configuration is accessed.\n\nYour program should call the `config` function, usually with an argument specifying the configuration profile. It then returned value passes the returned value through functions or via lexical scope (possibly components).\n\n### Using Aero with Plumatic schema\n\nAero has frictionless integration with [Plumatic Schema](https://github.com/plumatic/schema). If you wish, specify your configuration schemas and run `check` or `validate` against the data returned from `read-config`.\n\n### Using Aero with components\n\nIf you are using Stuart Sierra's\n[component](https://github.com/stuartsierra/component) library, here's how you might integrate Aero.\n\n```clojure\n(ns myproj.server\n  (:require [myproj.config :as config]))\n\n(defrecord MyServer [config]\n  Lifecycle\n  (start [component]\n    (assoc component :server (start-server :port (config/webserver-port config))))\n  (stop [component]\n    (when-let [server (:server component)] (stop-server server))))\n\n(defn new-server [config]\n  (-\u003eMyServer config))\n```\n\n```clojure\n(ns myproj.system\n  [com.stuartsierra.component :as component]\n  [myproj.server :refer [new-server]])\n\n(defn new-production-system []\n  (let [config (config/config :prod)]\n    (system-using\n      (component/system-map :server (new-server config))\n      {})))\n```\n\nHowever, another useful pattern you might consider is to keep your system map and configuration map aligned.\n\nFor example, imagine you have a config file:\n\n```clojure\n{:listener {:port 8080}\n :database {:uri \"datomic:mem://myapp/dev\"}}\n```\n\nHere we create a system as normal but with the key difference that we configure the system map after we have created using `merge-with merge`. This avoids all the boilerplate required in passing config around the various component constructors.\n\n```clojure\n(defrecord Listener [database port]\n  Lifecycle …)\n\n(defn new-listener []\n  (using (map-\u003eListener {}) [:database])\n\n(defrecord Database [uri]\n  Lifecycle …)\n\n(defn new-database []\n  (map-\u003eDatabase {}))\n\n(defn new-system-map\n  \"Create a configuration-free system\"\n  []\n  (system-map\n   :listener (new-listener)\n   :database (new-database)))\n\n(defn configure [system profile]\n  (let [config (aero/read-config \"config.edn\" {:profile profile})]\n    (merge-with merge system config)))\n\n(defn new-dependency-map [] {})\n\n(defn new-system\n  \"Create the production system\"\n  [profile]\n  (-\u003e (new-system-map)\n      (configure profile)\n      (system-using (new-dependency-map))))\n```\n\nAlso, if you follow the pattern described [here](https://juxt.pro/blog/posts/component-meet-schema.html) you can also ensure accurate configuration is given to each component without having to maintain explicit schemas. This way, you only verify the config that you are actually using.\n\n### Feature toggles\n\nAero is a great way to implement [feature toggles](http://martinfowler.com/articles/feature-toggles.html).\n\n### Use a single configuration file\n\nIf at all possible, try to avoid having lots of configuration files and stick with a single file. That way, you're encouraged to keep configuration down to a minimum. Having a single file is also useful because it can be more easily edited, published, emailed, [watched](https://github.com/juxt/dirwatch) for changes. It is generally better to surface complexity than hide it away.\n\n## (Alpha) Define macro tag literals\n\n`aero.alpha.core` defines a new experimental API for tagged literals.\nThis API allows you to define tagged literal \"macros\" similar to macros in Clojure.\nIt is intended for use in creating your own conditional constructs like `#profile` and `#or`.\n\n### case-like tag literal\n\nThe easiest kind of tagged literal to create is a case-like one.\nA case-like tagged literal is one which takes a map of possible paths to take.\nAn example of this in Aero is `#profile`.\n\nHere's how you can define your own version of `#profile`:\n\n```clojure\n(ns myns\n  (:require [aero.alpha.core :as aero.alpha]))\n\n(defmethod aero.alpha/eval-tagged-literal 'profile\n  [tagged-literal opts env ks]\n  (aero.alpha/expand-case (:profile opts) tagged-literal opts env ks))\n```\n\n`eval-tagged-literal` allows you to define macro tagged literals.\n`expand-case` is a function which forms the common behaviour beneath `#user`, `#profile`, etc.\n\n### Other conditional constructs\n\n`#or` is very different from `#profile` in implementation, and doesn't have a convenience function.\nThe source for `#or` in `aero.core` is a good example of doing custom partial expansion from a tagged literal.\n\nThe primitives you will need to understand are: `aero.alpha.core/expand`, `aero.alpha.core/expand-coll`, `aero.alpha.core/expand-scalar`.\nAnd helpers: `aero.alpha.core/expand-scalar-repeatedly`.\nThese vars have docstrings which explain their specific purpose.\n\nAll expand-* functions take parameters `opts`, `env`, and `ks`.\n`opts` are the same `opts` that are passed to `aero.core/read-config`.\n`env` is a map of `ks` to their resolved values in the config, being absent from this map means the value is not yet resolved.\n`ks` is a vector representing the current key path into the location of this tagged literal.\n\nYour implementation of eval-tagged-literal must `assoc` the `ks` into `env` if it is successfully resolved.\n\n## References\n\nAero is built on Clojure's [edn](https://github.com/edn-format/edn).\n\nAero is influenced by [nomad](https://github.com/james-henderson/nomad), but purposely avoids instance, environment and private config.\n\n## Acknowledgments\n\nThanks to the following people for inspiration, contributions, feedback and suggestions.\n\n* Gardner Vickers\n\n## Copyright \u0026 License\n\nThe MIT License (MIT)\n\nCopyright © 2015 JUXT LTD.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuxt%2Faero","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjuxt%2Faero","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuxt%2Faero/lists"}