Aero

repository·master·Indexed 21 days ago

https://github.com/juxt/aero

A small Clojure library for explicit, intentional, and data-driven configuration using EDN. It provides safe tag literals for common tasks such as environment variable access (#env, #envf), profiling (#profile), file inclusion (#include), and value referencing (#ref), avoiding the security risks of complex configuration programs.

Tokens
2.5K
Snippets
15
Records
16
Agent score
24%

What's inside Aero

  1. Include other configuration files with #include

    master

    Use #include to split large configuration files into smaller, manageable pieces.

    By default, #include resolves files relative to the file they are being included from. This behavior may not work inside JARs. To ensure reliability in all environments, provide a custom resolver (like resource-resolver) or a map-based resolver to read-config.

    ;; config.edn
    {:webserver #include "webserver.edn"
     :analytics #include "analytics.edn"}
    
    ;; Using a resolver for JAR compatibility
    (require '[aero.core :refer (read-config resource-resolver)])
    (read-config "config.edn" {:resolver resource-resolver})
    
    ;; Using a map as a resolver
    (read-config "config.edn" {:resolver {"webserver.edn" "resources/webserver/config.edn"}})
  2. Reference other parts of config with #ref

    master

    To avoid duplication, use the #ref tag to point to other parts of your configuration. The value provided to #ref should be a vector that can be resolved using get-in.

    {:db-connection "datomic:dynamo://dynamodb"
     :webserver
      {:db #ref [:db-connection]}
     :analytics
      {:db #ref [:db-connection]}}
  3. Use #profile for environment-specific configuration

    master

    The #profile tag acts as a reader conditional. It expects a map and extracts the entry corresponding to the __profile__ provided during the read-config call.

    ;; config.edn
    {:webserver
      {:port #profile {:default 8000
                       :dev 8001
                       :test 8002}}}
    
    ;; In your code
    (read-config "config.edn" {:profile :dev})
    ;; Returns: {:webserver {:port 8001}}
  4. Wrap configuration access with dedicated functions

    master

    Instead of accessing the configuration map directly throughout your application, define a dedicated namespace that reads the config and provides helper functions. This creates a layer of indirection: if your configuration structure changes, you only need to update the wrapper functions rather than searching through your entire codebase.

    (ns myproj.config
      (:require [aero.core :as aero]))
    
    (defn config [profile]
      (aero/read-config "dev/config.edn" {:profile profile}))
    
    (defn webserver-port [config]
      (get-in config [:webserver :port]))
  5. Integrate Aero with Stuart Sierra's component library

    master

    When using the component library, you can integrate Aero by passing the configuration map into your component constructors.

    Alternatively, a highly effective pattern is to keep your system map and configuration map aligned. You can create a 'configuration-free' system map and then use merge-with merge to apply the Aero configuration to the system map. This avoids the boilerplate of passing configuration objects through every component constructor.

    (defn configure [system profile]
      (let [config (aero/read-config "config.edn" {:profile profile})]
        (merge-with merge system config)))
    
    (defn new-system
      """Create the production system"""
      [profile]
      (-> (new-system-map)
          (configure profile)
          (system-using (new-dependency-map))))
  6. Hide passwords in local private files

    master

    To avoid storing sensitive information in version control or environment variables, you can use Aero's #include and #join tags to pull secrets from a private file located in your HOME directory. This allows you to keep most configuration in version control while keeping passwords in a local, unmanaged file.

    {:secrets #include #join [#env HOME "/.secrets.edn"]
    
     :aws-secret-access-key
      #profile {:test #ref [:secrets :aws-test-key]
                :prod #ref [:secrets :aws-prod-key]}}
  7. Read configuration with read-config

    master

    Use aero.core/read-config to load configuration from an EDN file.

    Important: When running applications from a generated .jar file, do not use relative file paths like (read-config "config.edn"). Instead, always use clojure.java.io/resource to ensure the file is correctly located on the classpath.

    (require '[aero.core :refer [read-config]])
    (require '[clojure.java.io :as io])
    
    ;; Recommended: Read from classpath to ensure JAR compatibility
    (read-config (io/resource "config.edn"))
  8. Define custom macro tag literals (Alpha)

    master

    The aero.alpha.core namespace provides an experimental API for defining custom tagged literal 'macros' (like #profile or #or). This allows you to implement custom conditional logic within your EDN configuration files.

    To create a 'case-like' tagged literal (which takes a map of paths to follow), use aero.alpha/eval-tagged-literal in conjunction with aero.alpha/expand-case.

    (ns myns
      (:require [aero.alpha.core :as aero.alpha]))
    
    (defmethod aero.alpha/eval-tagged-literal 'profile
      [tagged-literal opts env ks]
      (aero.alpha/expand-case (:profile opts) tagged-literal opts env ks))
  9. Define custom tag literals

    master

    You can extend Aero by defining your own tag literals using the reader multimethod.

    (defmethod reader 'mytag
     [{:keys [profile] :as opts} tag value]
      (if (= value :favorite)
         :chocolate
         :vanilla))
  10. Use tag literals for environment variables and strings

    master

    Aero provides several tag literals to handle environment variables and string manipulation within your EDN config:

    • #env: References an environment variable.
    • #envf: Inserts an environment variable into a formatted string.
    • #join: A string builder used to concatenate values (e.g., building connection strings).

    Note: It is considered bad practice to use #env for passwords or sensitive information to avoid leaking them via process environment inspection.

    {:database-uri #env DATABASE_URI
     :database #envf ["protocol://%s:%s" DATABASE_HOST DATABASE_NAME]
     :url #join ["jdbc:postgresql://psq-prod/prod?user=" #env PROD_USER "&password=" #env PROD_PASSWD]}