lib/config_cat.ex

defmodule ConfigCat do
  @moduledoc """
  The ConfigCat Elixir SDK.

  `ConfigCat` provides a `Supervisor` that must be added to your applications
  supervision tree and an API for accessing your ConfigCat settings.

  ## Add ConfigCat to Your Supervision Tree

  Your application's supervision tree might need to be different, but the most
  basic approach is to add `ConfigCat` as a child of your top-most supervisor.

  ```elixir
  # lib/my_app/application.ex
  def start(_type, _args) do
    children = [
      # ... other children ...
      {ConfigCat, [sdk_key: "YOUR SDK KEY"]}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ```

  ### Options

  `ConfigCat` takes a number of keyword arguments:

  - `sdk_key`: **REQUIRED** The SDK key for accessing your ConfigCat settings.
    Go to the [Connect your application](https://app.configcat.com/sdkkey) tab
    to get your SDK key.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY"]}
    ```

  - `base_url`: **OPTIONAL** Allows you to specify a custom URL for fetching
    your ConfigCat settings.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", base_url: "https://my-cdn.example.com"]}
    ```

  - `cache`: **OPTIONAL** Custom cache implementation. By default, `ConfigCat`
    uses its own in-memory cache, but you can also provide the name of a module
    that implements the `ConfigCat.ConfigCache` behaviour if you want to provide
    your own cache (e.g. based on Redis). If your cache implementation requires
    supervision, it is your application's responsibility to provide that.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", cache: MyCustomCacheModule]}
    ```

  - `cache_policy`: **OPTIONAL** Specifies the [polling
    mode](https://configcat.com/docs/sdk-reference/elixir#polling-modes) used by
    `ConfigCat`. Defaults to auto-polling mode with a 60 second poll interval.
    You can specify a different polling mode or polling interval using
    `ConfigCat.CachePolicy.auto/1`, `ConfigCat.CachePolicy.lazy/1`, or
    `ConfigCat.CachePolicy.manual/0`.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", cache_policy: ConfigCat.CachePolicy.manual()]}
    ```

  - `connect_timeout_milliseconds`: **OPTIONAL** timeout for establishing a TCP
    or SSL connection, in milliseconds. Default is 8000.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", connect_timeout_milliseconds: 8000]}
    ```

  - `data_governance`: **OPTIONAL** Describes the location of your feature flag
    and setting data within the ConfigCat CDN. This parameter needs to be in
    sync with your Data Governance preferences.  Defaults to `:global`. [More
    about Data Governance](https://configcat.com/docs/advanced/data-governance).

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", data_governance: :eu_only]}
    ```

  - `default_user`: **OPTIONAL** user object that will be used as fallback when
    there's no user parameter is passed to the getValue() method.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", default_user: User.new("test@test.com")]}
    ```

  - `flag_overrides`: **OPTIONAL** Specify a data source to use for [local flag
    overrides](https://configcat.com/docs/sdk-reference/elixir#flag-overrides).
    The data source must implement the `ConfigCat.OverrideDataSource` protocol.
    `ConfigCat.LocalFileDataSource` and `ConfigCat.LocalMapDataSource` are
    provided for you to use.

  - `hooks`: **OPTIONAL** Specify callback functions to be called when
    particular events are fired by the SDK. See `ConfigCat.Hooks`.

  - `http_proxy`: **OPTIONAL** Specify this option if you need to use a proxy
    server to access your ConfigCat settings. You can provide a simple URL, like
    `https://my_proxy.example.com` or include authentication information, like
    `https://user:password@my_proxy.example.com/`.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", http_proxy: "https://my_proxy.example.com"]}
    ```

  - `name`: **OPTIONAL** A unique identifier for this instance of `ConfigCat`.
    Defaults to `ConfigCat`.  Must be provided if you need to run more than one
    instance of `ConfigCat` in the same application. If you provide a `name`,
    you must then pass that name to all of the API functions using the `client`
    option. See `Multiple Instances` below.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", name: :unique_name]}
    ```

    ```elixir
    ConfigCat.get_value("setting", "default", client: :unique_name)
    ```

  - `offline`: **OPTIONAL**  # Indicates whether the SDK should be initialized
    in offline mode or not.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", offline: true]}
    ```

  - `read_timeout_milliseconds`: **OPTIONAL** timeout for receiving an HTTP
    response from the socket, in milliseconds. Default is 5000.

    ```elixir
    {ConfigCat, [sdk_key: "YOUR SDK KEY", read_timeout_milliseconds: 5000]}
    ```

  ### Multiple Instances

  If you need to run more than one instance of `ConfigCat`, there are two ways
  you can do it.

  #### Module-Based

  You can create a module that `use`s `ConfigCat` and then call the ConfigCat
  API functions on that module. This is the recommended option, as it makes the
  calling code a bit clearer and simpler.

  You can pass any of the options listed above as arguments to `use ConfigCat`
  or specify them in your supervisor. Arguments specified by the supervisor take
  precedence over those provided to `use ConfigCat`.

  ```elixir
  # lib/my_app/first_flags.ex
  defmodule MyApp.FirstFlags do
    use ConfigCat, sdk_key: "sdk_key_1"
  end

  # lib/my_app/second_flags.ex
  defmodule MyApp.SecondFlags do
    use ConfigCat, sdk_key: "sdk_key_2"
  end

  # lib/my_app/application.ex
  def start(_type, _args) do
    children = [
      # ... other children ...
      FirstFlags,
      SecondFlags,
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end


  # Calling code:
  FirstFlags.get_value("someKey", "default value")
  SecondFlags.get_value("otherKey", "other default")
  ```

  #### Explicit Client

  If you prefer not to use the module-based solution, you can instead add
  multiple `ConfigCat` children to your application's supervision tree. You will
  need to give `ConfigCat` a unique `name` option for each, as well as using
  `Supervisor.child_spec/2` to provide a unique `id` for each instance.

  When calling the ConfigCat API functions, you'll pass a `client:` keyword
  argument with the unique `name` you gave to that instance.

  ```elixir
  # lib/my_app/application.ex
  def start(_type, _args) do
    children = [
      # ... other children ...
      Supervisor.child_spec({ConfigCat, [sdk_key: "sdk_key_1", name: :first]}, id: :config_cat_1),
      Supervisor.child_spec({ConfigCat, [sdk_key: "sdk_key_2", name: :second]}, id: :config_cat_2),
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Calling code:
  ConfigCat.get_value("someKey", "default value", client: :first)
  ConfigCat.get_value("otherKey", "other default", client: :second)
  ```

  ## Use the API

  Once `ConfigCat` has been started as part of your application's supervision
  tree, you can use its API to access your settings.

  ```elixir
  ConfigCat.get_value("isMyAwesomeFeatureEnabled", false)
  ```

  By default, all of the public API functions will communicate with the default
  instance of the `ConfigCat` application.

  If you are running multiple instances of `ConfigCat`, you must provide the
  `client` option to the functions, passing along the unique name you specified
  above.

  ```elixir
  ConfigCat.get_value("isMyAwesomeFeatureEnabled", false, client: :second)
  ```
  """

  alias ConfigCat.CachePolicy
  alias ConfigCat.Client
  alias ConfigCat.Config
  alias ConfigCat.EvaluationDetails
  alias ConfigCat.Hooks
  alias ConfigCat.OverrideDataSource
  alias ConfigCat.User

  require ConfigCat.Constants, as: Constants

  @typedoc "Options that can be passed to all API functions."
  @type api_option :: {:client, instance_id()}

  @typedoc """
  Data Governance mode

  [More about Data Governance](https://configcat.com/docs/advanced/data-governance)
  """
  @type data_governance :: :eu_only | :global

  @typedoc "Identifier of a specific instance of `ConfigCat`."
  @type instance_id :: atom()

  @typedoc "The name of a configuration setting."
  @type key :: Config.key()

  @typedoc "An option that can be provided when starting `ConfigCat`."
  @type option ::
          {:base_url, String.t()}
          | {:cache, module()}
          | {:cache_policy, CachePolicy.t()}
          | {:connect_timeout_milliseconds, non_neg_integer()}
          | {:data_governance, data_governance()}
          | {:default_user, User.t()}
          | {:flag_overrides, OverrideDataSource.t()}
          | {:hooks, [Hooks.option()]}
          | {:http_proxy, String.t()}
          | {:name, instance_id()}
          | {:offline, boolean()}
          | {:read_timeout_milliseconds, non_neg_integer()}
          | {:sdk_key, String.t()}

  @type options :: [option()]

  @typedoc "The return value of the `force_refresh/1` function."
  @type refresh_result :: :ok | {:error, String.t()}

  @typedoc "The actual value of a configuration setting."
  @type value :: Config.value()

  @typedoc "The name of a variation being tested."
  @type variation_id :: Config.variation_id()

  @doc """
  Builds a child specification to use in a Supervisor.

  Normally not called directly by your code. Instead, it will be
  called by your application's Supervisor once you add `ConfigCat`
  to its supervision tree.
  """
  @spec child_spec(options()) :: Supervisor.child_spec()
  defdelegate child_spec(options), to: ConfigCat.Supervisor

  @doc """
  Queries all settings keys in your configuration.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`,
    provide the `client: :unique_name` option, specifying the name you
    configured for the instance you want to access.
  """
  @spec get_all_keys([api_option()]) :: [key()]
  def get_all_keys(options \\ []) do
    options
    |> client()
    |> GenServer.call(:get_all_keys, Constants.fetch_timeout())
  end

  @doc "See `get_value/4`."
  @spec get_value(key(), value(), User.t() | [api_option()]) :: value()
  def get_value(key, default_value, user_or_options \\ []) do
    if Keyword.keyword?(user_or_options) do
      get_value(key, default_value, nil, user_or_options)
    else
      get_value(key, default_value, user_or_options, [])
    end
  end

  @doc """
  Retrieves a setting value from your configuration.

  Retrieves the setting named `key` from your configuration. To use ConfigCat's
  [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a
  `ConfigCat.User` struct containing the information used by the targeting
  rules.

  Returns the value of the setting, or `default_value` if an error occurs.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec get_value(key(), value(), User.t() | nil, [api_option()]) :: value()
  def get_value(key, default_value, user, options) do
    options
    |> client()
    |> GenServer.call({:get_value, key, default_value, user}, Constants.fetch_timeout())
  end

  @doc "See `get_value_details/4`."
  @spec get_value_details(key(), value(), User.t() | [api_option()]) :: EvaluationDetails.t()
  def get_value_details(key, default_value, user_or_options \\ []) do
    if Keyword.keyword?(user_or_options) do
      get_value_details(key, default_value, nil, user_or_options)
    else
      get_value_details(key, default_value, user_or_options, [])
    end
  end

  @doc """
  Fetches the value and evaluation details of a feature flag or setting.

  Retrieves the setting named `key` from your configuration. To use ConfigCat's
  [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a
  `ConfigCat.User` struct containing the information used by the targeting
  rules.

  Returns the evaluation details for the setting, including the value. If an
  error occurs while performing the evaluation, it will be captured in the
  `:error` field of the `ConfigCat.EvaluationDetails` struct.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec get_value_details(key(), value(), User.t() | nil, [api_option()]) :: EvaluationDetails.t()
  def get_value_details(key, default_value, user, options) do
    options
    |> client()
    |> GenServer.call({:get_value_details, key, default_value, user}, Constants.fetch_timeout())
  end

  @doc "See `get_all_value_details/2`."
  @spec get_all_value_details(User.t() | [api_option()]) :: [EvaluationDetails.t()]
  def get_all_value_details(user_or_options \\ []) do
    if Keyword.keyword?(user_or_options) do
      get_all_value_details(nil, user_or_options)
    else
      get_all_value_details(user_or_options, [])
    end
  end

  @doc """
  Fetches the values and evaluation details of all feature flags and settings.

  To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting)
  feature, provide a `ConfigCat.User` struct containing the information used by
  the targeting rules.

  Returns evaluation details for all settings and feature flags, including their
  values. If an error occurs while performing the evaluation, it will be
  captured in the `:error` field of the individual `ConfigCat.EvaluationDetails`
  structs.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec get_all_value_details(User.t() | nil, [api_option()]) :: [EvaluationDetails.t()]
  def get_all_value_details(user, options) do
    options
    |> client()
    |> GenServer.call({:get_all_value_details, user}, Constants.fetch_timeout())
  end

  @doc """
  Fetches the name and value of the setting corresponding to a variation id.

  Returns a tuple containing the setting name and value, or `nil` if an error
  occurs.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec get_key_and_value(variation_id(), [api_option()]) :: {key(), value()} | nil
  def get_key_and_value(variation_id, options \\ []) do
    options
    |> client()
    |> GenServer.call({:get_key_and_value, variation_id}, Constants.fetch_timeout())
  end

  @doc """
  Fetches the values of all feature flags or settings from your configuration.

  To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting)
  feature, provide a `ConfigCat.User` struct containing the information used by
  the targeting rules.

  Returns a map of all key value pairs.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec get_all_values(User.t() | nil, [api_option()]) :: %{key() => value()}
  def get_all_values(user, options \\ []) do
    options
    |> client()
    |> GenServer.call({:get_all_values, user}, Constants.fetch_timeout())
  end

  @doc """
  Force a refresh of the configuration from ConfigCat's CDN.

  Depending on the polling mode you're using, `ConfigCat` may automatically
  fetch your configuration during normal operation. Call this function to
  force a manual refresh when you want one.

  If you are using manual polling mode (`ConfigCat.CachePolicy.manual/0`),
  this is the only way to fetch your configuration.

  Returns `:ok`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec force_refresh([api_option()]) :: refresh_result()
  def force_refresh(options \\ []) do
    options
    |> client()
    |> GenServer.call(:force_refresh, Constants.fetch_timeout())
  end

  @doc """
  Sets the default user.

  Returns `:ok`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec set_default_user(User.t(), [api_option()]) :: :ok
  def set_default_user(user, options \\ []) do
    options
    |> client()
    |> GenServer.call({:set_default_user, user}, Constants.fetch_timeout())
  end

  @doc """
  Clears the default user.

  Returns `:ok`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec clear_default_user([api_option()]) :: :ok
  def clear_default_user(options \\ []) do
    options
    |> client()
    |> GenServer.call(:clear_default_user, Constants.fetch_timeout())
  end

  @doc """
  Configures the SDK to allow HTTP requests.

  Returns `:ok`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec set_online([api_option()]) :: :ok
  def set_online(options \\ []) do
    options
    |> client()
    |> GenServer.call(:set_online, Constants.fetch_timeout())
  end

  @doc """
  Configures the SDK to not initiate HTTP requests and work only from its cache.

  Returns `:ok`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec set_offline([api_option()]) :: :ok
  def set_offline(options \\ []) do
    options
    |> client()
    |> GenServer.call(:set_offline, Constants.fetch_timeout())
  end

  @doc """
  Returns `true` when the SDK is configured not to initiate HTTP requests, otherwise `false`.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec offline?([api_option()]) :: boolean()
  def offline?(options \\ []) do
    options
    |> client()
    |> GenServer.call(:offline?, Constants.fetch_timeout())
  end

  @doc """
  Return the current hook callbacks.

  ### Options

  - `client`: If you are running multiple instances of `ConfigCat`, provide the
    `client: :unique_name` option, specifying the name you configured for the
    instance you want to access.
  """
  @spec hooks([api_option()]) :: Hooks.t()
  def hooks(options \\ []) do
    Keyword.get(options, :client, __MODULE__)
  end

  defp client(options) do
    options
    |> Keyword.get(:client, __MODULE__)
    |> Client.via_tuple()
  end

  defmacro __using__(default_options) do
    quote do
      @client Keyword.get(unquote(default_options), :name, __MODULE__)

      @spec child_spec(ConfigCat.options()) :: Supervisor.child_spec()
      def child_spec(options) do
        options =
          unquote(default_options)
          |> Keyword.merge(options)
          |> Keyword.put_new(:name, @client)

        Supervisor.child_spec({ConfigCat, options}, id: @client)
      end

      @spec get_all_keys :: [ConfigCat.key()]
      def get_all_keys do
        ConfigCat.get_all_keys(client: @client)
      end

      @spec get_value(ConfigCat.key(), ConfigCat.value(), ConfigCat.User.t() | nil) ::
              ConfigCat.value()
      def get_value(key, default_value, user \\ nil) do
        ConfigCat.get_value(key, default_value, user, client: @client)
      end

      @spec get_value_details(ConfigCat.key(), ConfigCat.value(), ConfigCat.User.t() | nil) ::
              ConfigCat.EvaluationDetails.t()
      def get_value_details(key, default_value, user \\ nil) do
        ConfigCat.get_value_details(key, default_value, user, client: @client)
      end

      @spec get_all_value_details(ConfigCat.User.t() | nil) :: [EvaluationDetails.t()]
      def get_all_value_details(user \\ nil) do
        ConfigCat.get_all_value_details(user, client: @client)
      end

      @spec get_key_and_value(ConfigCat.variation_id()) ::
              {ConfigCat.key(), ConfigCat.value()} | nil
      def get_key_and_value(variation_id) do
        ConfigCat.get_key_and_value(variation_id, client: @client)
      end

      @spec get_all_values(ConfigCat.User.t() | nil) :: %{ConfigCat.key() => ConfigCat.value()}
      def get_all_values(user \\ nil) do
        ConfigCat.get_all_values(user, client: @client)
      end

      @spec force_refresh :: ConfigCat.refresh_result()
      def force_refresh do
        ConfigCat.force_refresh(client: @client)
      end

      @spec set_default_user(ConfigCat.User.t()) :: :ok
      def set_default_user(user) do
        ConfigCat.set_default_user(user, client: @client)
      end

      @spec clear_default_user :: :ok
      def clear_default_user do
        ConfigCat.clear_default_user(client: @client)
      end

      @spec set_online :: :ok
      def set_online do
        ConfigCat.set_online(client: @client)
      end

      @spec set_offline :: :ok
      def set_offline do
        ConfigCat.set_offline(client: @client)
      end

      @spec offline? :: boolean()
      def offline? do
        ConfigCat.offline?(client: @client)
      end

      @spec hooks :: ConfigCat.Hooks.t()
      def hooks do
        ConfigCat.hooks(client: @client)
      end
    end
  end
end