Current section
Files
Jump to
Current section
Files
lib/settings.ex
defmodule Settings do
@moduledoc """
"""
alias Settings.EtsHelper
@none :__none
@default :__default
@global :__global
@all_but_default :__all_but_default
@global_defaults :global_defaults
@error_result {:error, :bad_key}
@initial_defaults [app: @none, backend: @none]
##############################
# API
##############################
@doc """
`set_defaults(opts)` accepts a keword list with the following valid keys:
- `:backend` specify a default backend for all settings.
- `:app` specify a default app for all settings.
This call's effect is scoped to the current Elixir node, and provides default
values when the option is not specified in `get/2` or `set/3`. The function
returns the defaults after the change is made.
"""
def set_defaults(opts) do
new_defaults = get_defaults()
|> Keyword.merge(opts)
EtsHelper.set(@global_defaults, new_defaults)
new_defaults
end
@doc """
returns the current default app and backend. The initial values are `:__none`.
## Examples:
```elixir
iex> Settings.get_defaults
[app: :__none, backend: :__none]
...> Settings.set_defaults(app: :myapp)
[app: :myapp, backend: :__none]
...> Settings.set_defaults(backend: MyBackend)
[app: :myapp, backend: MyBackend]
"""
def get_defaults do
case EtsHelper.get(@global_defaults) do
nil -> @initial_defaults
other -> other
end
end
@doc """
`create(name, default_value, opts)` sets up a new setting, providing the name
and default value. If `:backend` or `:app` are not specified in `opts`, they
will default to those set using `set_defaults/1`
When a persistent backend is used, `create` will update the default value for
the setting, and will not impact overriding scopes already set.
This function **must** be called each time the app is started.
This function returns the setting name.
#### Examples:
```elixir
iex> Settings.set_defaults(app: :myapp, backend: MyBackend)
[app: :myapp, backend: MyBackend]
...> Settings.create(:setting_1, "value")
:setting_1
...> Settings.create(:setting_2, "value2", backend: OtherBackend)
:setting_2
...> Settings.get(:setting_2) # <= retrieves from OtherBackend
"value2"
```
"""
@default_create_opts [app: @default, backend: @default]
def create(name, default_value, opts \\ @default_create_opts) do
opts = Keyword.merge(@default_create_opts, opts)
backend = get_backend(Keyword.get(opts, :backend))
app = get_app(Keyword.get(opts, :app))
EtsHelper.set({:backend, app, name}, backend)
apply(backend, :set, [app, name, @default, default_value])
name
end
@doc """
`get(name, opts)` retrieves a value for a setting. Valid opts:
- `:app` The application which this setting is part of. Default is
`:__default`, which is replaced with `get_defaults[:app]`
- `:scope` The scope of the setting. Default: `:__global`
- `:backend` The backend to retrieve from. Default is :__default, which is
replaced with `get_defaults[:backend]`
`get` observes the following semantics: Settings first searches for a value
matching `{app, name, scope}` using the scope passed in to `opts`. If nothing is
found, Settings then searches for `{app, name, :__global}`, and finally for
`{app, name, :__default}`
### Examples:
```elixir
iex> Settings.create(:setting, "default_value")
...> Settings.set(:setting, "global_override")
...> Settings.set(:setting, "specific_scope", scope: node())
...> Settings.get(:setting, scope: node())
"specific_scope"
...> Settings.get(:setting, scope: :other scope)
"global_override"
```
"""
@default_get_opts [app: @default, scope: @global, backend: @default]
def get(name, opts \\ @default_get_opts) do
opts = Keyword.merge(@default_get_opts, opts)
app = get_app(Keyword.get(opts, :app))
backend = get_backend_for(app, name, Keyword.get(opts, :backend))
if exists?(backend, app, name) do
scope = Keyword.get(opts, :scope)
scopes = case scope do
@global -> [@default, @global]
other -> [@default, @global, other]
end
available_scopes = apply(backend, :get, [app, name, scopes])
|> Map.new(fn(map) -> {map.scope, map} end)
do_get(backend, app, name, scope, available_scopes)
else
@error_result
end
end
@doc """
`set(name, value, opts)` sets a value for a setting. Valid opts:
- `:app` The application which this setting is part of. Default is
`:__default`, which is replaced with `get_defaults[:app]`
- `:scope` The scope to set. Default: `:__global`
- `:backend` The backend to assign to. Default is :__default, which is
replaced with `get_defaults[:backend]`
This function returns the value that was set.
"""
@default_set_opts [app: @default, scope: @global, backend: @default]
def set(name, value, opts \\ @default_set_opts) do
opts = Keyword.merge(@default_set_opts, opts)
app = get_app(Keyword.get(opts, :app))
scope = Keyword.get(opts, :scope)
backend = get_backend_for(app, name, Keyword.get(opts, :backend))
if exists?(backend, app, name) do
apply(backend, :set, [app, name, scope, value])
value
else
@error_result
end
end
@doc """
`all(opts)` retrieves all settings matching the options passed in. Valid opts:
- `:app` Limit the query to just this application's settings.
- `:backend` Retrieve from this backend
`all` returns a list of maps, each having the following keys:
- `:app`
- `:name`
- `:scope`
- `:value`
"""
@default_all_opts [app: @none, backend: @default]
def all(opts \\ @default_all_opts) do
opts = Keyword.merge(@default_all_opts, opts)
app = Keyword.get(opts, :app)
backend = get_backend(Keyword.get(opts, :backend))
case app do
@none -> apply(backend, :get, [])
_ -> apply(backend, :get, [app])
end
end
@doc """
`delete(name, opts)` removes an entire setting from the backend.
"""
@default_delete_opts [app: @default, backend: @default]
def delete(name, opts \\ @default_delete_opts) do
opts = Keyword.merge(@default_delete_opts, opts)
app = get_app(Keyword.get(opts, :app))
backend = get_backend_for(app, name, Keyword.get(opts, :backend))
if exists?(backend, app, name) do
apply(backend, :del, [app, name])
EtsHelper.del({:backend, app, name})
else
@error_result
end
end
@doc """
`clear(name, opts)` removes overrides from the backend, without modifying the
default value specified in `create/3`.
If `:scope` is specified in `opts`, then clear will eliminate just that scope.
Otherwise, `clear` will remove all overrides, restoring the setting to just
the default value.
"""
@default_clear_opts [app: @default, scope: @all_but_default, backend: @default]
def clear(name, opts \\ @default_clear_opts) do
opts = Keyword.merge(@default_clear_opts, opts)
app = get_app(Keyword.get(opts, :app))
backend = get_backend_for(app, name, Keyword.get(opts, :backend))
scope = Keyword.get(opts, :scope)
if exists?(backend, app, name) do
do_clear(backend, app, name, scope)
else
@error_result
end
end
##############################
# Internal Calls
##############################
defp exists?(nil, _app, _name), do: false
defp exists?(@none, _app, _name), do: false
defp exists?(backend, app, name) do
Enum.any?(apply(backend, :get, [app, name]))
end
defp get_backend(:__default) do
EtsHelper.get(@global_defaults)[:backend]
end
defp get_backend(other), do: other
defp get_backend_for(app, name, @default) do
EtsHelper.get({:backend, app, name})
end
defp get_backend_for(_app, _name, other), do: other
defp get_app(:__default) do
EtsHelper.get(@global_defaults)[:app]
end
defp get_app(other), do: other
defp do_get(_backend, _app, _name, @global, scopes) do
Map.get(scopes, @global, scopes[@default])
|> Map.get(:value)
end
defp do_get(backend, app, name, scope, scopes) do
case Map.get(scopes, scope) do
nil -> do_get(backend, app, name, @global, scopes)
other -> Map.get(other, :value)
end
end
defp do_clear(_backend, _app, _name, @default), do: :ok
defp do_clear(backend, app, name, @all_but_default) do
apply(backend, :keep_only, [app, name, [@default]])
end
defp do_clear(backend, app, name, scope) do
apply(backend, :del, [app, name, [scope]])
end
end