Library for runtime application configuration—interested?

After some development, I’m interested in your thoughts with my prototype API. Today it looks akin to:

# config/config.exs

config :runtime, values: [
  SIGNING_SALT: :string,
  DATABASE_URL: :uri,
  DATABASE_POOL: {:integer, min: 1, max: 100}
]

config :runtime, sources: [
  json: [file: "priv/config.json"],
  exs: [file: "priv/config.exs"],
  dotenv: [file: "priv/config.env"],
  env: []
]

While I’d love to distance myself even further from the config/*.exs world, having the library know these things in advance at compile-time lets me make very strong guarantees—for example:

  • generating compile-time warnings/errors on typo’d keys
    • ie. Runtime.Config.get(:DATABAS_URL)
  • meta-programming clauses with typespecs for specific keys
    • ie. letting dialyzer know that Runtime.Config.get(:DATABASE_URL) will always return a %URI{} struct and contrary usage should emit a warning

This API is Keyword based, so plays well with Config’s deep-merging strategy. This means theoretically you could still do stuff like:

# config/dev.exs
config :runtime, values: [
  DATABASE_URL: [
    type: :uri,
    required: false,
    default: {:value, "http://localhost:5432/my_app_dev"}
  ]
]
# config/prod.exs
config :runtime, values: [
  DATABASE_URL: [type: :uri, required: true]
]

or:

# config/dev.exs
config :runtime, sources: [json: [file: "priv/config/dev.json"]]
# config/prod.exs
config :runtime, sources: [json: [file: "priv/config/prod.json"]]

However, the library would generally discourage such an approach, in favor of a more robust and consolidated “context” DSL. The above examples can be written as:

# config/config.exs

config :runtime, values: [
  DATABASE_URL: [
    type: :uri,
    required: [in: :prod],
    default: [in: [dev: {:value, "http://localhost:5432/my_app_dev"}]]
  ]
]

config :runtime, sources: [
  json: [file: "priv/config/dev.json", in: :dev],
  json: [file: "priv/config/prod.json", in: :prod]
]

The current context DSL further supports things like specifying that a value is required to have been provided in contexts:

  • at: :compile_time | :boot_time | :runtime
  • in: [:list, :of, :mix, :envs],
  • for: [:list, :of, :mix, :targets]

in any permutation. Similarly for default values, when sources are loaded, and if a source is required to exist at: a certain time in: a certain env for: a certain target.

Thoughts? As far as I can tell, this DSL pretty much allows all compile-time config that actually impacts how a project’s dependencies generate source code to be provided exclusively by traditional config/*.exs mechanisms, and have all other current runtime-config usecases to be described by two single entries for config :runtime. This lets all actual runtime values live outside the config/*.exs world entirely, removing the need for any config/runtime.exs stuff, while preserving compile-time goodies furnished by the library itself.

1 Like