Packages

ConfigCat SDK for Elixir. Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages.

Current section

Files

Jump to
configcat lib config_cat local_file_data_source.ex
Raw

lib/config_cat/local_file_data_source.ex

defmodule ConfigCat.LocalFileDataSource do
@moduledoc """
Load flag overrides from a file.
See `ConfigCat.OverrideDataSource` for more details.
"""
alias ConfigCat.OverrideDataSource
require Logger
defmodule FileCache do
@moduledoc false
use Agent
defstruct cached_timestamp: 0, settings: nil
def start_link(_opts) do
Agent.start_link(fn -> %__MODULE__{} end)
end
def cached_settings(cache) do
case Agent.get(cache, fn %__MODULE__{settings: settings} -> settings end) do
nil -> {:error, :not_found}
settings -> {:ok, settings}
end
end
def cached_timestamp(cache) do
Agent.get(cache, fn %__MODULE__{cached_timestamp: timestamp} -> timestamp end)
end
def update(cache, settings, timestamp) do
Agent.update(cache, fn %__MODULE__{} = state ->
%{state | cached_timestamp: timestamp, settings: settings}
end)
end
end
defstruct [:cache, :filename, :override_behaviour]
@type t :: %__MODULE__{
cache: pid,
filename: String.t(),
override_behaviour: OverrideDataSource.behaviour()
}
@doc """
Create a `ConfigCat.OverrideDataSource` that loads overrides from a file.
"""
@spec new(String.t(), OverrideDataSource.behaviour()) :: t
def new(filename, override_behaviour) do
unless File.exists?(filename) do
Logger.error("The file #{filename} does not exist.")
end
{:ok, pid} = FileCache.start_link([])
%__MODULE__{cache: pid, filename: filename, override_behaviour: override_behaviour}
end
defimpl OverrideDataSource do
alias ConfigCat.Constants
require ConfigCat.Constants
def behaviour(data_source), do: data_source.override_behaviour
def overrides(%{cache: cache} = data_source) do
refresh_cache(cache, data_source.filename)
FileCache.cached_settings(cache)
end
defp refresh_cache(cache, filename) do
with {:ok, %{mtime: timestamp}} <- File.stat(filename, time: :posix) do
unless FileCache.cached_timestamp(cache) == timestamp do
with {:ok, contents} <- File.read(filename),
{:ok, data} <- Jason.decode(contents),
settings <- normalize(data) do
FileCache.update(cache, settings, timestamp)
else
error ->
log_error(error, filename)
:ok
end
end
end
end
defp log_error({:error, %Jason.DecodeError{} = error}, filename) do
Logger.error("Could not decode json from file #{filename}. #{Exception.message(error)}")
end
defp log_error({:error, error}, filename) do
Logger.error(
"Could not read the content of the file #{filename}. #{:file.format_error(error)}"
)
end
defp normalize(%{"flags" => source} = _data) do
flags =
source
|> Enum.map(fn {key, value} -> {key, %{Constants.value() => value}} end)
|> Map.new()
%{Constants.feature_flags() => flags}
end
defp normalize(source), do: source
end
end