lib/uniform.ex

defmodule Uniform do
  @moduledoc """
  > Write less boilerplate and reuse more code in your portfolio of Elixir apps

  With Uniform, developers maintain multiple Elixir apps inside a Base Project:
  a "monolith" containing every app. Before deployment, the apps are "ejected"
  into separate codebases containing only the code needed by each app.

  The entire process is automated, so there's much less work required to start
  a new app or share capabilities between apps.

  ## Recommended Guides

  In order to understand and use this library, we heavily recommend reading the
  following guides:

  - [How It Works](how-it-works.html)
  - [Uniform Manifests](uniform-manifests-uniform-exs.html)
  - [Dependencies](dependencies.html)
  - [Code Transformations](code-transformations.html)

  The [Setting up a Phoenix project](setting-up-a-phoenix-project.html) guide
  is recommended if you're building Phoenix apps.

  ## Usage

  ```bash
  mix uniform.eject tweeter
  ```

  ## Installation

  Consult the [Getting Started](getting-started.html) guide to add Uniform to
  an Elixir application. In summary, you'll need to:

  1. Add the dep in `mix.exs`
  2. Add a [Blueprint](Uniform.Blueprint.html) module to your project
  3. Add configuration to identify the Blueprint module
  4. Add `uniform.exs` manifests to each Ejectable App
  5. Fill out the Blueprint so all the necessary files get ejected

  > #### Uniform ❤️ Automation {: .tip}
  >
  > Uniform is about powering up developers by automating repetitive work.
  >
  > With that in mind, we recommend using Continuous Integration (CI) tools to
  > [automate the process of committing code to ejected
  > repos](auto-updating-ejected-codebases.html).

  """

  @typep prepare_opt :: {:destination, String.t()}

  @doc """
  Returns a list of all [Ejectable App](how-it-works.html#ejectable-apps) names
  in your Base Project.

  ### Examples

  ```bash
  $ fd uniform.exs
  lib/tweeter/uniform.exs
  lib/trillo/uniform.exs
  lib/hatmail/uniform.exs
  ```

      iex> ejectable_app_names()
      ["tweeter", "trillo", "hatmail"]

  """
  @spec ejectable_app_names :: [String.t()]
  def ejectable_app_names do
    "lib/*/uniform.exs"
    |> Path.wildcard()
    |> Enum.map(&(&1 |> Path.dirname() |> Path.basename()))
    |> Enum.sort()
  end

  @doc """
  Return a list of all [Ejectable Apps](how-it-works.html#ejectable-apps) in
  your Base Project as `Uniform.App` structs.

  ### Example

  ```bash
  $ fd uniform.exs
  lib/tweeter/uniform.exs
  lib/trillo/uniform.exs
  lib/hatmail/uniform.exs
  ```

      iex> ejectable_apps()
      [
        #Uniform.App<
          extra: [...],
          name: %{camel: "Tweeter", hyphen: "tweeter", module: Tweeter, underscore: "tweeter"},
          ...
        >,
        #Uniform.App<
          extra: [...],
          name: %{camel: "Trillo", hyphen: "trillo", module: Trillo, underscore: "trillo"},
          ...
        >,
        #Uniform.App<
          extra: [...],
          name: %{camel: "Hatmail", hyphen: "hatmail", module: Hatmail, underscore: "hatmail"},
          ...
        >
      ]

  """
  @spec ejectable_apps :: [Uniform.App.t()]
  def ejectable_apps do
    for name <- ejectable_app_names() do
      prepare(%{name: name, opts: []})
    end
  end

  @doc """
       Prepares the `t:Uniform.App.t/0` struct with all information needed for ejection.

       When ejecting an app, this step runs prior to the actual `eject/1` process,
       allowing the user to see pertinent information about what decisions will be made
       during ejection: (e.g. which dependencies will be included, where on
       disk the ejected app will be written, etc.). If there is a mistake, the user will
       have a chance to abort before performing a potentially destructive action.
       """ && false
  @spec prepare(init :: %{name: atom, opts: [prepare_opt]}) :: Uniform.App.t()
  def prepare(%{name: name, opts: opts}) do
    # ensure the name was passed in under_score format; otherwise subtle bugs happen
    unless name in Uniform.ejectable_app_names() do
      raise ArgumentError,
        message: """
        The name must correspond to a directory in lib, in under_score format.

        For example, to eject `lib/my_app`, do:

            mix uniform.eject my_app

        Did you forget to run this command?

            mix uniform.gen.app #{name}

        """
    end

    Mix.Task.run("compile", [])
    config = Uniform.Config.build()
    manifest = Uniform.Manifest.eval_and_parse(config, Macro.underscore(name))
    Uniform.App.new!(config, manifest, name, opts)
  end
end