Packages

Configuration options management powered by Rust NIFs. Merges feature-flagged JSON/YAML configs with deep-merge semantics.

Current section

Files

Jump to
optify lib optify.ex
Raw

lib/optify.ex

defmodule Optify do
@moduledoc """
High-level Elixir API for Optify feature providers.
Most applications interact with Optify through the default-provider helpers:
- `get_options/2` and `get_options!/2` for merged option lookup
- `get_features/0` and `get_aliases/0` for feature discovery
- `get_feature_metadata/1` for metadata lookup
When you need more control, you can build a provider explicitly with
`build!/1`, `build_with_schema!/2`, or `build_from_directories!/1` and then
call the provider-explicit APIs.
"""
alias Optify.DefaultProvider
alias Optify.GetOptionsPreferences
alias Optify.Native
alias Optify.OptionShapeCache
alias Optify.StructCaster
@type provider :: reference()
@type build_config_input :: keyword() | map() | nil
@doc """
Build a provider from one feature directory.
Returns `{:ok, provider}` on success or `{:error, reason}` when the directory
cannot be loaded.
"""
@spec build(String.t()) :: {:ok, provider()} | {:error, String.t()}
def build(directory), do: Native.build_provider(directory)
@doc """
Bang variant of `build/1`.
"""
@spec build!(String.t()) :: provider()
def build!(directory) do
case build(directory) do
{:ok, provider} -> provider
{:error, reason} -> raise ArgumentError, "Optify.build!/1 failed: #{reason}"
end
end
@doc """
Build a provider from one feature directory and a JSON schema path.
"""
@spec build_with_schema(String.t(), String.t()) :: {:ok, provider()} | {:error, String.t()}
def build_with_schema(directory, schema_path),
do: Native.build_provider_with_schema(directory, schema_path)
@doc """
Bang variant of `build_with_schema/2`.
"""
@spec build_with_schema!(String.t(), String.t()) :: provider()
def build_with_schema!(directory, schema_path) do
case build_with_schema(directory, schema_path) do
{:ok, provider} -> provider
{:error, reason} -> raise ArgumentError, "Optify.build_with_schema!/2 failed: #{reason}"
end
end
@doc """
Build a provider from multiple feature directories.
"""
@spec build_from_directories([String.t()]) :: {:ok, provider()} | {:error, String.t()}
def build_from_directories(directories), do: Native.build_provider_from_directories(directories)
@doc """
Bang variant of `build_from_directories/1`.
"""
@spec build_from_directories!([String.t()]) :: provider()
def build_from_directories!(directories) do
case build_from_directories(directories) do
{:ok, provider} ->
provider
{:error, reason} ->
raise ArgumentError, "Optify.build_from_directories!/1 failed: #{reason}"
end
end
@doc """
Build a provider from multiple feature directories and a JSON schema path.
"""
@spec build_from_directories_with_schema([String.t()], String.t()) ::
{:ok, provider()} | {:error, String.t()}
def build_from_directories_with_schema(directories, schema_path),
do: Native.build_provider_from_directories_with_schema(directories, schema_path)
@doc """
Bang variant of `build_from_directories_with_schema/2`.
"""
@spec build_from_directories_with_schema!([String.t()], String.t()) :: provider()
def build_from_directories_with_schema!(directories, schema_path) do
case build_from_directories_with_schema(directories, schema_path) do
{:ok, provider} ->
provider
{:error, reason} ->
raise ArgumentError,
"Optify.build_from_directories_with_schema!/2 failed: #{reason}"
end
end
@doc """
Build a provider from the `:optify, :provider` config shape.
Supported config keys:
- `:directory`
- `:directories`
- `:schema_path`
"""
@spec build_from_config(build_config_input()) :: {:ok, provider()} | {:error, String.t()}
def build_from_config(config \\ nil)
def build_from_config(nil), do: build_from_config(Application.get_env(:optify, :provider, []))
def build_from_config(config) when is_list(config) or is_map(config) do
case extract_build_config(config) do
{:directory, directory, nil} ->
build(directory)
{:directory, directory, schema_path} ->
build_with_schema(directory, schema_path)
{:directories, directories, nil} ->
build_from_directories(directories)
{:directories, directories, schema_path} ->
build_from_directories_with_schema(directories, schema_path)
:error ->
{:error, missing_provider_config_error()}
end
end
@doc """
Bang variant of `build_from_config/1`.
"""
@spec build_from_config!(build_config_input()) :: provider()
def build_from_config!(config \\ nil) do
case build_from_config(config) do
{:ok, provider} -> provider
{:error, reason} -> raise ArgumentError, "Optify.build_from_config!/1 failed: #{reason}"
end
end
@doc """
Set the process-global default provider used by the convenience APIs.
"""
@spec set_default_provider(provider()) :: :ok
def set_default_provider(provider), do: DefaultProvider.set(provider)
@doc """
Return the current default provider, or `nil` if one has not been loaded.
"""
@spec default_provider() :: provider() | nil
def default_provider, do: DefaultProvider.get()
@doc """
Return the current default provider.
Raises if no default provider has been loaded.
"""
@spec default_provider!() :: provider()
def default_provider!, do: DefaultProvider.get!()
@doc """
Build and store the default provider from the given config.
When `config` is omitted, this reads from `Application.get_env(:optify, :provider)`.
"""
@spec load_default_provider!(keyword() | map() | nil) :: provider()
def load_default_provider!(config \\ nil) do
provider = build_from_config!(config)
set_default_provider(provider)
provider
end
@doc false
@spec features() :: [String.t()]
def features, do: get_features()
@doc false
@spec features(provider()) :: [String.t()]
def features(provider), do: get_features(provider)
@doc """
Return all canonical feature names from the default provider.
"""
@spec get_features() :: [String.t()]
def get_features, do: get_features(default_provider!())
@doc """
Return all canonical feature names from the given provider.
"""
@spec get_features(provider()) :: [String.t()]
def get_features(provider), do: Native.features(provider)
@doc """
Return all configured aliases from the default provider.
"""
@spec get_aliases() :: [String.t()]
def get_aliases, do: get_aliases(default_provider!())
@doc """
Return all configured aliases from the given provider.
"""
@spec get_aliases(provider()) :: [String.t()]
def get_aliases(provider), do: Native.get_aliases(provider)
@doc """
Return canonical feature names and aliases from the default provider.
"""
@spec get_features_and_aliases() :: [String.t()]
def get_features_and_aliases, do: get_features_and_aliases(default_provider!())
@doc """
Return canonical feature names and aliases from the given provider.
"""
@spec get_features_and_aliases(provider()) :: [String.t()]
def get_features_and_aliases(provider), do: Native.get_features_and_aliases(provider)
@doc """
Resolve a feature name or alias to its canonical feature name using the default provider.
"""
@spec get_canonical_feature_name(String.t()) :: {:ok, String.t()} | {:error, String.t()}
def get_canonical_feature_name(feature_name),
do: get_canonical_feature_name(default_provider!(), feature_name)
@doc """
Resolve a feature name or alias to its canonical feature name using the given provider.
"""
@spec get_canonical_feature_name(provider(), String.t()) ::
{:ok, String.t()} | {:error, String.t()}
def get_canonical_feature_name(provider, feature_name),
do: Native.get_canonical_feature_name(provider, feature_name)
@doc """
Bang variant of `get_canonical_feature_name/1`.
"""
@spec get_canonical_feature_name!(String.t()) :: String.t()
def get_canonical_feature_name!(feature_name) do
case get_canonical_feature_name(feature_name) do
{:ok, canonical_feature_name} ->
canonical_feature_name
{:error, reason} ->
raise ArgumentError, "Optify.get_canonical_feature_name!/1 failed: #{reason}"
end
end
@doc """
Resolve multiple feature names or aliases using the default provider.
"""
@spec get_canonical_feature_names([String.t()]) :: {:ok, [String.t()]} | {:error, String.t()}
def get_canonical_feature_names(feature_names),
do: get_canonical_feature_names(default_provider!(), feature_names)
@doc """
Resolve multiple feature names or aliases using the given provider.
"""
@spec get_canonical_feature_names(provider(), [String.t()]) ::
{:ok, [String.t()]} | {:error, String.t()}
def get_canonical_feature_names(provider, feature_names),
do: Native.get_canonical_feature_names(provider, feature_names)
@doc """
Bang variant of `get_canonical_feature_names/1`.
"""
@spec get_canonical_feature_names!([String.t()]) :: [String.t()]
def get_canonical_feature_names!(feature_names) do
case get_canonical_feature_names(feature_names) do
{:ok, canonical_feature_names} ->
canonical_feature_names
{:error, reason} ->
raise ArgumentError, "Optify.get_canonical_feature_names!/1 failed: #{reason}"
end
end
@doc """
Return metadata for one canonical feature from the default provider.
"""
@spec get_feature_metadata(String.t()) :: map() | nil
def get_feature_metadata(canonical_feature_name),
do: get_feature_metadata(default_provider!(), canonical_feature_name)
@doc """
Return metadata for one canonical feature from the given provider.
"""
@spec get_feature_metadata(provider(), String.t()) :: map() | nil
def get_feature_metadata(provider, canonical_feature_name) do
case Native.get_feature_metadata_json(provider, canonical_feature_name) do
nil -> nil
json -> decode_json!(json)
end
end
@doc """
Return metadata for all features from the default provider.
"""
@spec get_features_with_metadata() :: map()
def get_features_with_metadata, do: get_features_with_metadata(default_provider!())
@doc """
Return metadata for all features from the given provider.
"""
@spec get_features_with_metadata(provider()) :: map()
def get_features_with_metadata(provider) do
provider
|> Native.get_features_with_metadata_json()
|> decode_json!()
end
@doc """
Filter feature names using the default provider and default preferences.
"""
@spec get_filtered_feature_names([String.t()]) :: {:ok, [String.t()]} | {:error, String.t()}
def get_filtered_feature_names(feature_names),
do: get_filtered_feature_names(feature_names, %GetOptionsPreferences{})
@doc """
Filter feature names using the default provider and the given preferences.
"""
@spec get_filtered_feature_names([String.t()], GetOptionsPreferences.input_t()) ::
{:ok, [String.t()]} | {:error, String.t()}
def get_filtered_feature_names(feature_names, preferences) when is_list(feature_names) do
get_filtered_feature_names(default_provider!(), feature_names, preferences)
end
@doc """
Filter feature names using the given provider and preferences.
"""
@spec get_filtered_feature_names(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
{:ok, [String.t()]} | {:error, String.t()}
def get_filtered_feature_names(provider, feature_names, preferences) do
Native.get_filtered_feature_names(
provider,
feature_names,
GetOptionsPreferences.to_nif_map(preferences)
)
end
@doc """
Bang variant of `get_filtered_feature_names/1`.
"""
@spec get_filtered_feature_names!([String.t()]) :: [String.t()]
def get_filtered_feature_names!(feature_names),
do: get_filtered_feature_names!(feature_names, %GetOptionsPreferences{})
@doc """
Bang variant of `get_filtered_feature_names/2`.
"""
@spec get_filtered_feature_names!([String.t()], GetOptionsPreferences.input_t()) ::
[String.t()]
def get_filtered_feature_names!(feature_names, preferences) when is_list(feature_names) do
case get_filtered_feature_names(feature_names, preferences) do
{:ok, resolved_feature_names} ->
resolved_feature_names
{:error, reason} ->
raise ArgumentError, "Optify.get_filtered_feature_names!/2 failed: #{reason}"
end
end
@doc """
Bang variant of `get_filtered_feature_names/3`.
"""
@spec get_filtered_feature_names!(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
[String.t()]
def get_filtered_feature_names!(provider, feature_names, preferences) do
case get_filtered_feature_names(provider, feature_names, preferences) do
{:ok, resolved_feature_names} ->
resolved_feature_names
{:error, reason} ->
raise ArgumentError, "Optify.get_filtered_feature_names!/3 failed: #{reason}"
end
end
@doc """
Return whether the canonical feature has conditions in the default provider.
"""
@spec has_conditions(String.t()) :: boolean()
def has_conditions(canonical_feature_name),
do: has_conditions(default_provider!(), canonical_feature_name)
@doc """
Return whether the canonical feature has conditions in the given provider.
"""
@spec has_conditions(provider(), String.t()) :: boolean()
def has_conditions(provider, canonical_feature_name),
do: Native.has_conditions(provider, canonical_feature_name)
@doc """
Get merged options using the default provider.
Returns atom-keyed maps by default so dot access works (e.g. `options.flow`).
Known nested option paths are hydrated with `nil` defaults so
`options.flow.handler` remains safe even when `flow` is absent from the
selected feature set.
"""
@spec get_options([String.t()], keyword()) ::
{:ok, map() | list() | String.t() | number() | boolean() | nil} | {:error, String.t()}
def get_options(feature_names, opts \\ []) when is_list(feature_names) and is_list(opts) do
provider = Keyword.get(opts, :provider, default_provider!())
preferences = Keyword.get(opts, :preferences, %GetOptionsPreferences{})
key_mode = Keyword.get(opts, :keys, :atoms)
as_module = Keyword.get(opts, :as)
with {:ok, options} <- get_all_options(provider, feature_names, preferences) do
options
|> hydrate_known_paths(provider)
|> transform_keys(key_mode)
|> cast_output(as_module)
end
end
@doc """
Bang variant of `get_options/2`.
"""
@spec get_options!([String.t()], keyword()) ::
map() | list() | String.t() | number() | boolean() | nil
def get_options!(feature_names, opts \\ []) when is_list(feature_names) and is_list(opts) do
case get_options(feature_names, opts) do
{:ok, options} -> options
{:error, reason} -> raise ArgumentError, "Optify.get_options!/2 failed: #{reason}"
end
end
@doc """
Get options for one top-level key from the given provider.
"""
@spec get_options(provider(), String.t(), [String.t()], GetOptionsPreferences.input_t()) ::
{:ok, map() | list() | String.t() | number() | boolean() | nil} | {:error, String.t()}
def get_options(provider, key, feature_names, preferences \\ %GetOptionsPreferences{}) do
with {:ok, json} <-
Native.get_options_json_with_preferences(
provider,
key,
feature_names,
GetOptionsPreferences.to_nif_map(preferences)
) do
decode_json(json)
end
end
@doc """
Bang variant of `get_options/4`.
"""
@spec get_options!(provider(), String.t(), [String.t()], GetOptionsPreferences.input_t()) ::
map() | list() | String.t() | number() | boolean() | nil
def get_options!(provider, key, feature_names, preferences \\ %GetOptionsPreferences{}) do
case get_options(provider, key, feature_names, preferences) do
{:ok, options} -> options
{:error, reason} -> raise ArgumentError, "Optify.get_options!/4 failed: #{reason}"
end
end
@doc false
@spec get_all_options(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
{:ok, map() | list() | String.t() | number() | boolean() | nil} | {:error, String.t()}
def get_all_options(provider, feature_names, preferences \\ %GetOptionsPreferences{}) do
with {:ok, json} <-
Native.get_all_options_json_with_preferences(
provider,
feature_names,
GetOptionsPreferences.to_nif_map(preferences)
) do
decode_json(json)
end
end
@doc false
@spec get_all_options!(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
map() | list() | String.t() | number() | boolean() | nil
def get_all_options!(provider, feature_names, preferences \\ %GetOptionsPreferences{}) do
case get_all_options(provider, feature_names, preferences) do
{:ok, options} -> options
{:error, reason} -> raise ArgumentError, "Optify.get_all_options!/3 failed: #{reason}"
end
end
@doc false
@spec get_options_json(provider(), String.t(), [String.t()], GetOptionsPreferences.input_t()) ::
{:ok, String.t()} | {:error, String.t()}
def get_options_json(provider, key, feature_names, preferences \\ %GetOptionsPreferences{}) do
Native.get_options_json_with_preferences(
provider,
key,
feature_names,
GetOptionsPreferences.to_nif_map(preferences)
)
end
@doc false
@spec get_all_options_json(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
{:ok, String.t()} | {:error, String.t()}
def get_all_options_json(provider, feature_names, preferences \\ %GetOptionsPreferences{}) do
Native.get_all_options_json_with_preferences(
provider,
feature_names,
GetOptionsPreferences.to_nif_map(preferences)
)
end
@doc false
@spec get_options_json!(provider(), String.t(), [String.t()], GetOptionsPreferences.input_t()) ::
String.t()
def get_options_json!(provider, key, feature_names, preferences \\ %GetOptionsPreferences{}) do
case get_options_json(provider, key, feature_names, preferences) do
{:ok, json} -> json
{:error, reason} -> raise ArgumentError, "Optify.get_options_json!/4 failed: #{reason}"
end
end
@doc false
@spec get_all_options_json!(provider(), [String.t()], GetOptionsPreferences.input_t()) ::
String.t()
def get_all_options_json!(provider, feature_names, preferences \\ %GetOptionsPreferences{}) do
case get_all_options_json(provider, feature_names, preferences) do
{:ok, json} -> json
{:error, reason} -> raise ArgumentError, "Optify.get_all_options_json!/3 failed: #{reason}"
end
end
@doc false
@spec warm_option_shape(provider()) :: :ok
def warm_option_shape(provider) do
_shape = option_shape(provider)
:ok
end
defp cast_output(value, nil), do: {:ok, value}
defp cast_output(value, module) when is_atom(module), do: StructCaster.cast(module, value)
defp hydrate_known_paths(nil, provider), do: default_for_shape(option_shape(provider))
defp hydrate_known_paths(value, provider) when is_map(value) do
fill_missing_paths(value, option_shape(provider))
end
defp hydrate_known_paths(value, _provider), do: value
defp transform_keys(value, :strings), do: value
defp transform_keys(value, :atoms) when is_map(value) do
value
|> Enum.map(fn {k, v} -> {to_atom_key(k), transform_keys(v, :atoms)} end)
|> Map.new()
end
defp transform_keys(value, :atoms) when is_list(value),
do: Enum.map(value, &transform_keys(&1, :atoms))
defp transform_keys(value, _), do: value
defp to_atom_key(key) when is_atom(key), do: key
defp to_atom_key(key) when is_binary(key), do: String.to_atom(key)
defp to_atom_key(key), do: key
defp option_shape(provider) do
OptionShapeCache.fetch(provider, fn -> build_option_shape(provider) end)
end
defp build_option_shape(provider) do
provider
|> get_features()
|> Enum.reduce(%{}, fn feature_name, acc ->
feature_shape =
provider
|> get_all_options!([feature_name])
|> build_shape()
merge_shapes(acc, feature_shape)
end)
end
defp build_shape(value) when is_map(value) do
Map.new(value, fn {key, nested_value} -> {key, build_shape(nested_value)} end)
end
defp build_shape(_value), do: nil
defp merge_shapes(left, right) when is_map(left) and is_map(right) do
Map.merge(left, right, fn _key, left_value, right_value ->
merge_shapes(left_value, right_value)
end)
end
defp merge_shapes(left, right) when is_map(left) and not is_map(right), do: left
defp merge_shapes(left, right) when not is_map(left) and is_map(right), do: right
defp merge_shapes(_left, right), do: right
defp fill_missing_paths(value, shape) when is_map(value) and is_map(shape) do
Enum.reduce(shape, value, fn {key, nested_shape}, acc ->
case Map.fetch(acc, key) do
{:ok, nested_value} ->
Map.put(acc, key, fill_missing_value(nested_value, nested_shape))
:error ->
Map.put(acc, key, default_for_shape(nested_shape))
end
end)
end
defp fill_missing_value(nil, nested_shape) when is_map(nested_shape),
do: default_for_shape(nested_shape)
defp fill_missing_value(value, nested_shape) when is_map(value) and is_map(nested_shape) do
fill_missing_paths(value, nested_shape)
end
defp fill_missing_value(value, _nested_shape), do: value
defp default_for_shape(shape) when is_map(shape), do: fill_missing_paths(%{}, shape)
defp default_for_shape(_shape), do: nil
defp extract_build_config(config) when is_list(config) do
schema_path = Keyword.get(config, :schema_path) || Keyword.get(config, :schema)
cond do
directory = Keyword.get(config, :directory) ->
{:directory, directory, schema_path}
directories = Keyword.get(config, :directories) ->
{:directories, directories, schema_path}
true ->
:error
end
end
defp extract_build_config(config) when is_map(config) do
schema_path =
Map.get(config, :schema_path) ||
Map.get(config, "schema_path") ||
Map.get(config, :schema) ||
Map.get(config, "schema")
cond do
directory = Map.get(config, :directory) || Map.get(config, "directory") ->
{:directory, directory, schema_path}
directories = Map.get(config, :directories) || Map.get(config, "directories") ->
{:directories, directories, schema_path}
true ->
:error
end
end
defp missing_provider_config_error do
"Missing provider config. Set :directory or :directories in config :optify, :provider"
end
defp decode_json(json) do
case Jason.decode(json) do
{:ok, decoded} -> {:ok, decoded}
{:error, %Jason.DecodeError{} = err} -> {:error, Exception.message(err)}
end
end
defp decode_json!(json) do
case decode_json(json) do
{:ok, decoded} -> decoded
{:error, reason} -> raise ArgumentError, "Optify JSON decode failed: #{reason}"
end
end
end