Confispex (confispex v1.2.0)

A tool which allows defining specs for runtime configuration, cast values according to specified types and inspect them.

Workflow

  1. Define a schema - Create a module with defvariables/1 describing your configuration
  2. Initialize - Call init/1 in config/runtime.exs with your schema and context
  3. Get values - Use get/1 to retrieve typed and validated configuration values
  4. Inspect - Run mix confispex.report to see all variables and their status

Key Concepts

  • Schema - Defines variables with types, defaults, validation rules, and grouping
  • Context - Runtime environment info (e.g., %{env: :prod, target: :host})
  • Store - Source of raw configuration values (default: System.get_env/0)
  • Groups - Logical organization of related variables for reporting
  • Invocations - Tracked variable access for comprehensive error reporting

Example

# 1. Define schema
defmodule MyApp.ConfigSchema do
  import Confispex.Schema
  @behaviour Confispex.Schema

  defvariables(%{
    "DATABASE_URL" => %{
      cast: Confispex.Type.URL,
      required: [:database],
      groups: [:database]
    }
  })
end

# 2. Initialize in config/runtime.exs
Confispex.init(%{
  schema: MyApp.ConfigSchema,
  context: %{env: config_env()}
})

# 3. Use values
config :my_app, MyApp.Repo,
  url: Confispex.get("DATABASE_URL")

# 4. Inspect (command line)
# $ mix confispex.report --mode=detailed

See the Getting Started guide for more details.

Summary

Types

Runtime context information used for conditional defaults and requirements.

A map containing configuration values, typically environment variables.

Functions

Returns true if all required variables in specified group are present in store.

Returns true if any required variable in specified group is present in store.

Get a value from store by specified variable name (key) and cast it according to schema.

Initialize or reinitialize a state in server

Initialize a state in server if it hasn't already been initialized.

Print report with variables usage to STDOUT.

Update the store at runtime by applying a function to the current store.

Types

context()

@type context() :: %{required(atom()) => atom()}

Runtime context information used for conditional defaults and requirements.

Commonly includes :env (:dev, :test, :prod) and :target (:host, :docker).

store()

@type store() :: map()

A map containing configuration values, typically environment variables.

Keys are variable names (usually strings) and values are their string representations.

Functions

all_required_touched?(group_name, server \\ Confispex.Server)

@spec all_required_touched?(group_name :: atom(), GenServer.server()) :: boolean()

Returns true if all required variables in specified group are present in store.

Use this when you want to ensure ALL required variables are configured before enabling a feature. This is stricter than any_required_touched?/1.

Example

# Only configure database if ALL required variables are provided
if Confispex.all_required_touched?(:database) do
  config :my_app, MyApp.Repo,
    url: Confispex.get("DATABASE_URL"),
    pool_size: Confispex.get("DATABASE_POOL_SIZE"),
    ssl: Confispex.get("DATABASE_SSL")
end

Compare with any_required_touched?/1: if even ONE required variable is present, any_required_touched?/1 returns true. This function requires ALL of them.

any_required_touched?(group_name, server \\ Confispex.Server)

@spec any_required_touched?(group_name :: atom(), GenServer.server()) :: boolean()

Returns true if any required variable in specified group is present in store.

Use this to detect if the user is trying to configure a group, even if the configuration is incomplete. This is useful for conditional configuration of services that crash on invalid config (better to skip than crash).

Example

# Configure APNS only if user provided at least one APNS variable
if Confispex.any_required_touched?(:apns) do
  config :pigeon, :apns,
    sandbox: %{
      cert: Confispex.get("APNS_CERT"),
      key: Confispex.get("APNS_KEY"),
      mode: :dev
    }
end

Difference from all_required_touched?/1

  • any_required_touched?/1 - returns true if at least one required variable is present (user is trying to configure this group)
  • all_required_touched?/1 - returns true only if all required variables are present (configuration is complete)

Use any_required_touched?/1 to decide "should I configure this at all?" and all_required_touched?/1 to decide "is configuration complete and valid?"

get(variable_name, server \\ Confispex.Server)

Get a value from store by specified variable name (key) and cast it according to schema.

Returns the casted value on success, or nil on any error (variable not found, type casting failed, etc.). Errors are collected and can be viewed with report/1.

Example

config :my_app, MyApp.Repo, url: Confispex.get("DATABASE_URL")

Behavior

  • Variable found and valid: returns the casted value
  • Variable found via alias: returns the casted value (e.g., DB_URL when DATABASE_URL not found)
  • Variable not found but has default: returns the default value (casted)
  • Type casting fails: returns nil and saves error for later reporting
  • Variable not in schema: returns nil and shows warning in report

To see all errors, run:

mix confispex.report --mode=detailed
# or
Confispex.report(:detailed)

This design allows your application to start even with configuration errors, so you can see ALL problems at once in the report, rather than fixing them one-at-a-time.

init(params, server \\ Confispex.Server)

@spec init(
  %{
    :schema => module(),
    :context => context(),
    optional(:store) => store() | (-> store())
  },
  GenServer.server()
) :: :ok

Initialize or reinitialize a state in server

Example

Confispex.init(%{
  schema: MyApp.RuntimeConfigSchema,
  context: %{env: config_env(), target: config_target()}
})

By default, Confispex uses System.get_env/0 to setup the store.

init_once(params, server \\ Confispex.Server)

@spec init_once(
  %{
    :schema => module(),
    :context => context(),
    optional(:store) => store() | (-> store())
  },
  GenServer.server()
) :: :ok

Initialize a state in server if it hasn't already been initialized.

Use this instead of init/1 when you want to initialize only once, ignoring subsequent calls. This is useful when configuration file is re-read with Config.Reader.read!/1 to prevent overwriting the existing state.

Example

# In config/runtime.exs - initializes only on first call
Confispex.init_once(%{
  schema: MyApp.RuntimeConfigSchema,
  context: %{env: config_env(), target: config_target()}
})

By default, Confispex uses System.get_env/0 to setup the store.

report(mode, opts \\ [])

@spec report(
  :detailed | :brief,
  keyword()
) :: :ok

Print report with variables usage to STDOUT.

The report shows all variables organized by groups with color-coded status:

  • Green groups: all required variables present and valid
  • Red groups: required variables missing or invalid
  • Blue groups: functional (no required variables or all have defaults)

Modes

  • :detailed - shows actual values from the store (may contain sensitive data)
  • :brief - hides values, only shows variable status (safe for logs)

Options

  • :server (atom/0) - The GenServer to query for report data. Defaults to the internal server.

  • :emit_ansi? (boolean/0) - Whether to emit ANSI color codes. Defaults to IO.ANSI.enabled?().

Examples

# Show full report with values
Confispex.report(:detailed)

# Show report without values (safe for CI/logs)
Confispex.report(:brief)

# Force colors on remote shell
Confispex.report(:detailed, emit_ansi?: true)

# Custom server with colors disabled
Confispex.report(:brief, server: MyApp.ConfigServer, emit_ansi?: false)

You can also use the mix task:

mix confispex.report --mode=detailed
mix confispex.report --mode=brief

update_store(update_fn, server \\ Confispex.Server)

@spec update_store((store() -> store()), GenServer.server()) :: :ok

Update the store at runtime by applying a function to the current store.

Example

# Add or override specific variables
new_values = %{"FEATURE_X_ENABLED" => "true", "API_KEY" => "new-key"}
Confispex.update_store(&Map.merge(&1, new_values))

# Remove a variable
Confispex.update_store(&Map.delete(&1, "TEMP_CONFIG"))