Current section

Files

Jump to
ex_esdb lib options_simplified.ex
Raw

lib/options_simplified.ex

defmodule ExESDB.Options.Simplified do
@moduledoc """
Simplified ExESDB configuration module using automatic OTP app discovery.
This module dramatically reduces complexity by:
1. Automatically discovering the calling application
2. Using a macro to generate config functions with less duplication
3. Providing sensible defaults and environment variable overrides
Usage:
# Automatic discovery (recommended)
ExESDB.Options.data_dir() # Discovers calling app automatically
# Explicit app specification
ExESDB.Options.data_dir(:my_app)
# Context-based (for umbrella apps)
ExESDB.Options.with_context(:my_app, fn ->
ExESDB.Options.data_dir() # Uses :my_app context
end)
"""
# Environment variable mappings
@env_mappings %{
data_dir: "EX_ESDB_DATA_DIR",
store_id: "EX_ESDB_STORE_ID",
timeout: "EX_ESDB_TIMEOUT",
db_type: "EX_ESDB_DB_TYPE",
pub_sub: "EX_ESDB_PUB_SUB",
writer_idle_ms: "EX_ESDB_WRITER_IDLE_MS",
reader_idle_ms: "EX_ESDB_READER_IDLE_MS",
store_description: "EX_ESDB_STORE_DESCRIPTION",
store_tags: "EX_ESDB_STORE_TAGS",
persistence_interval: "EX_ESDB_PERSISTENCE_INTERVAL",
persistence_enabled: "EX_ESDB_PERSISTENCE_ENABLED"
}
# Configuration defaults
@defaults %{
data_dir: "/data",
store_id: :undefined,
timeout: 10_000,
db_type: :single,
pub_sub: :ex_esdb_pubsub,
writer_idle_ms: 10_000,
reader_idle_ms: 10_000,
store_description: "undefined!",
store_tags: [],
persistence_interval: 5_000,
persistence_enabled: true
}
#
# App Discovery Functions
#
@doc """
Discovers the current OTP application name using multiple strategies.
"""
def discover_app_name do
Process.get(:ex_esdb_otp_app) ||
try_mix_project() ||
System.get_env("OTP_APP") |> maybe_atom() ||
try_loaded_applications() ||
:ex_esdb
end
defp try_mix_project do
try do
Mix.Project.config()[:app]
rescue
_ -> nil
catch
:exit, _ -> nil
end
end
defp try_loaded_applications do
Application.loaded_applications()
|> Enum.find(fn {app, _desc, _vsn} ->
# Look for user applications (not system ones)
app_str = Atom.to_string(app)
not String.starts_with?(app_str, "kernel") and
not String.starts_with?(app_str, "stdlib") and
app not in [:logger, :runtime_tools, :crypto, :compiler]
end)
|> case do
{app, _, _} -> app
nil -> nil
end
end
defp maybe_atom(nil), do: nil
defp maybe_atom(str) when is_binary(str) do
try do
String.to_existing_atom(str)
rescue
_ -> String.to_atom(str)
end
end
#
# Context Management
#
@doc "Set application context for configuration lookup"
def set_context(otp_app) when is_atom(otp_app) do
Process.put(:ex_esdb_otp_app, otp_app)
end
@doc "Get current application context"
def get_context, do: Process.get(:ex_esdb_otp_app)
@doc "Execute function within specific application context"
def with_context(otp_app, fun) when is_atom(otp_app) and is_function(fun, 0) do
previous_context = Process.get(:ex_esdb_otp_app)
Process.put(:ex_esdb_otp_app, otp_app)
try do
fun.()
after
if previous_context do
Process.put(:ex_esdb_otp_app, previous_context)
else
Process.delete(:ex_esdb_otp_app)
end
end
end
#
# Configuration Access Functions
#
@doc "Get complete configuration for an OTP app"
def app_env(otp_app \\ nil) do
app = otp_app || discover_app_name()
config = Application.get_env(app, :ex_esdb, [])
Keyword.put(config, :otp_app, app)
end
# Note: Individual config functions are defined manually below
# to avoid macro complexity while still reducing duplication
@doc """
Generic configuration value getter with automatic app discovery.
Priority order:
1. Environment variable
2. Application configuration
3. Default value
"""
def get_config_value(key, otp_app \\ nil) do
app = otp_app || get_context() || discover_app_name()
case get_env_value(key) do
nil -> get_app_config_value(app, key)
env_value -> transform_env_value(key, env_value)
end
end
defp get_env_value(key) do
env_var = Map.get(@env_mappings, key)
env_var && System.get_env(env_var)
end
defp get_app_config_value(app, key) do
default = Map.get(@defaults, key)
Application.get_env(app, :ex_esdb, [])
|> Keyword.get(key, default)
end
# Transform environment string values to appropriate types
defp transform_env_value(:timeout, value), do: String.to_integer(value)
defp transform_env_value(:writer_idle_ms, value), do: String.to_integer(value)
defp transform_env_value(:reader_idle_ms, value), do: String.to_integer(value)
defp transform_env_value(:persistence_interval, value), do: String.to_integer(value)
defp transform_env_value(:persistence_enabled, value), do: String.to_atom(value)
defp transform_env_value(:db_type, value), do: String.to_atom(value)
defp transform_env_value(:store_id, value), do: to_unique_atom(value)
defp transform_env_value(:pub_sub, value), do: to_unique_atom(value)
defp transform_env_value(:store_tags, value) do
value
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp transform_env_value(_key, value), do: value
defp to_unique_atom(candidate) do
try do
String.to_existing_atom(candidate)
rescue
_ -> String.to_atom(candidate)
end
end
#
# Individual Configuration Functions
#
def data_dir(otp_app \\ nil), do: get_config_value(:data_dir, otp_app)
def store_id(otp_app \\ nil), do: get_config_value(:store_id, otp_app)
def timeout(otp_app \\ nil), do: get_config_value(:timeout, otp_app)
def db_type(otp_app \\ nil), do: get_config_value(:db_type, otp_app)
def pub_sub(otp_app \\ nil), do: get_config_value(:pub_sub, otp_app)
def writer_idle_ms(otp_app \\ nil), do: get_config_value(:writer_idle_ms, otp_app)
def reader_idle_ms(otp_app \\ nil), do: get_config_value(:reader_idle_ms, otp_app)
def store_description(otp_app \\ nil), do: get_config_value(:store_description, otp_app)
def store_tags(otp_app \\ nil), do: get_config_value(:store_tags, otp_app)
def persistence_interval(otp_app \\ nil), do: get_config_value(:persistence_interval, otp_app)
def persistence_enabled(otp_app \\ nil), do: get_config_value(:persistence_enabled, otp_app)
# Additional convenience functions
def topologies, do: Application.get_env(:libcluster, :topologies)
@doc """
Get a specific config value from app environment with default.
"""
def app_env(otp_app, key, default) when is_atom(otp_app) and is_atom(key) do
Keyword.get(app_env(otp_app), key, default)
end
end