lib/mix/tasks/nbx.gen.cache.ex

defmodule Mix.Tasks.Nbx.Gen.Cache do
  @shortdoc "Generates a new cache"

  @moduledoc """
  Generates a new cache.

  The cache will be placed in the `lib` directory.

  For generating a cache with the config, you can run:

      mix nbx.gen.cache -c MyApp.Cache

  Besides, you can also specify the adapter you want to use, like so:

      mix nbx.gen.cache -c MyApp.PartitionedCache -a Nebulex.Adapters.Local
      mix nbx.gen.cache -c MyApp.PartitionedCache -a Nebulex.Adapters.Partitioned
      mix nbx.gen.cache -c MyApp.ReplicatedCache -a Nebulex.Adapters.Replicated
      mix nbx.gen.cache -c MyApp.MultilevelCache -a Nebulex.Adapters.Multilevel

  ## Command line options

    * `-c`, `--cache` - The cache to generate.

    * `-a`, `--adapter` - The cache adapter to use (optional).
      Defaults to `Nebulex.Adapters.Local`.

  """

  use Mix.Task

  import Mix.Nebulex
  import Mix.Generator

  alias Mix.Project

  @switches [
    cache: [:string, :keep],
    adapter: [:string, :keep]
  ]

  @aliases [
    c: :cache,
    a: :adapter
  ]

  @doc false
  def run(args) do
    no_umbrella!("nbx.gen.cache")
    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)

    {cache, adapter} = get_cache_and_adapter(opts)

    config = Project.config()
    underscored = Macro.underscore(inspect(cache))

    base = Path.basename(underscored)
    file = Path.join("lib", underscored) <> ".ex"
    app = config[:app] || :YOUR_APP_NAME
    opts = [mod: cache, app: app, base: base, adapter: adapter]

    {cache_template, config_template} = resolve_template(opts)

    create_directory(Path.dirname(file))
    create_file(file, cache_template)
    config_path = config[:config_path] || "config/config.exs"

    case File.read(config_path) do
      {:ok, contents} ->
        check = String.contains?(contents, "import Config")
        config_first_line = get_first_config_line(check) <> "\n"
        new_contents = config_first_line <> "\n" <> config_template
        Mix.shell().info([:green, "* updating ", :reset, config_path])
        File.write!(config_path, String.replace(contents, config_first_line, new_contents))

      {:error, _} ->
        config_first_line = Config |> Code.ensure_loaded?() |> get_first_config_line()
        create_file(config_path, config_first_line <> "\n\n" <> config_template)
    end

    Mix.shell().info("""
    Don't forget to add your new cache to your supervision tree
    (typically in lib/#{app}/application.ex):

        {#{inspect(cache)}, []}

    And for more information about configuration options, check
    adapters documentation and Nebulex.Cache shared options.
    """)
  end

  defp get_cache_and_adapter(opts) do
    cache =
      case Keyword.get_values(opts, :cache) do
        [] -> Mix.raise("nbx.gen.cache expects the cache to be given as -c MyApp.Cache")
        [cache] -> Module.concat([cache])
        [_ | _] -> Mix.raise("nbx.gen.cache expects a single cache to be given")
      end

    adapter =
      case Keyword.get_values(opts, :adapter) do
        [] -> Nebulex.Adapters.Local
        [adapter] -> Module.concat([adapter])
        [_ | _] -> Mix.raise("nbx.gen.cache expects a single adapter to be given")
      end

    {cache, adapter}
  end

  defp get_first_config_line(true), do: "import Config"
  defp get_first_config_line(false), do: "use Mix.Config"

  defp resolve_template(opts) do
    case opts[:adapter] do
      Nebulex.Adapters.Local ->
        {cache_template(opts), local_config_template(opts)}

      Nebulex.Adapters.Partitioned ->
        {dist_cache_template(opts), dist_config_template(opts)}

      Nebulex.Adapters.Replicated ->
        {dist_cache_template(opts), dist_config_template(opts)}

      Nebulex.Adapters.Multilevel ->
        {ml_cache_template(opts), ml_config_template(opts)}

      _ ->
        {cache_template(opts), config_template(opts)}
    end
  end

  embed_template(:cache, """
  defmodule <%= inspect @mod %> do
    use Nebulex.Cache,
      otp_app: <%= inspect @app %>,
      adapter: <%= inspect @adapter %>
  end
  """)

  embed_template(:dist_cache, """
  defmodule <%= inspect @mod %> do
    use Nebulex.Cache,
      otp_app: <%= inspect @app %>,
      adapter: <%= inspect @adapter %>,
      primary_storage_adapter: Nebulex.Adapters.Local
  end
  """)

  embed_template(:ml_cache, """
  defmodule <%= inspect @mod %> do
    use Nebulex.Cache,
      otp_app: <%= inspect @app %>,
      adapter: Nebulex.Adapters.Multilevel

    ## Cache Levels

    # Default auto-generated L1 cache (local)
    defmodule L1 do
      use Nebulex.Cache,
        otp_app: <%= inspect @app %>,
        adapter: Nebulex.Adapters.Local
    end

    # Default auto-generated L2 cache (partitioned cache)
    defmodule L2 do
      use Nebulex.Cache,
        otp_app: <%= inspect @app %>,
        adapter: Nebulex.Adapters.Partitioned
    end

    ## TODO: Add, remove or modify the auto-generated cache levels above
  end
  """)

  embed_template(:config, """
  # See the adapter's documentation for configuration options
  # config <%= inspect @app %>, <%= inspect @mod %>,
  #   key: :value
  """)

  embed_template(:local_config, """
  config <%= inspect @app %>, <%= inspect @mod %>,
    # When using :shards as backend
    # backend: :shards,
    # GC interval for pushing new generation: 12 hrs
    gc_interval: :timer.hours(12),
    # Max 1 million entries in cache
    max_size: 1_00_000,
    # Max 2 GB of memory
    allocated_memory: 2_000_000_000,
    # GC min timeout: 10 sec
    gc_cleanup_min_timeout: :timer.seconds(10),
    # GC min timeout: 10 min
    gc_cleanup_max_timeout: :timer.minutes(10)
  """)

  embed_template(:dist_config, """
  config <%= inspect @app %>, <%= inspect @mod %>,
    primary: [
      # When using :shards as backend
      # backend: :shards,
      # GC interval for pushing new generation: 12 hrs
      gc_interval: :timer.hours(12),
      # Max 1 million entries in cache
      max_size: 1_00_000,
      # Max 2 GB of memory
      allocated_memory: 2_000_000_000,
      # GC min timeout: 10 sec
      gc_cleanup_min_timeout: :timer.seconds(10),
      # GC min timeout: 10 min
      gc_cleanup_max_timeout: :timer.minutes(10)
    ]
  """)

  embed_template(:ml_config, """
  config <%= inspect @app %>, <%= inspect @mod %>,
    model: :inclusive,
    levels: [
      # Default auto-generated L1 cache (local)
      {
        <%= inspect @mod %>.L1,
        # GC interval for pushing new generation: 12 hrs
        gc_interval: :timer.hours(12),
        # Max 1 million entries in cache
        max_size: 1_00_000
      },
      # Default auto-generated L2 cache (partitioned cache)
      {
        <%= inspect @mod %>.L2,
        primary: [
          # GC interval for pushing new generation: 12 hrs
          gc_interval: :timer.hours(12),
          # Max 1 million entries in cache
          max_size: 1_00_000
        ]
      }
    ]
  """)
end