Modkit.Mount (modkit v0.10.0)

Copy Markdown View Source

Maps module name prefixes to directory paths.

A mount is a list of mount points, each one associating a module prefix with the directory where the modules under that prefix belong. In a Mix project it is defined by the :mount entry of the :modkit configuration in mix.exs:

def project do
  [
    # ...
    modkit: [
      mount: [
        {MyApp, "lib/my_app"},
        {Mix.Tasks, "lib/mix/tasks", flavor: :mix_task}
      ]
    ]
  ]
end

Once defined, a mount gives the preferred file path of any module it covers:

iex> mount = Modkit.Mount.define!([{MyApp, "lib/my_app"}])
iex> Modkit.Mount.preferred_path(mount, MyApp.Some.Worker)
{:ok, "lib/my_app/some/worker.ex"}

Mount points are tried from the most precise prefix to the least precise one, so a module always resolves to the deepest prefix that covers it.

Summary

Functions

Derives a mount for test modules from the given mount.

Builds a mount from a list of mount points.

Builds a mount from a list of mount points. Raises an ArgumentError when a point is invalid.

Transforms a module file path (without extension) into the corresponding test file path (also without extension).

Returns the file path where the given module belongs according to the mount.

Returns whether the given mount point covers the module, that is whether the point prefix is a prefix of the module name. The module is given as a list of segments as returned by Module.split/1.

Returns the mount point covering the given module.

Functions

as_test(mount)

Derives a mount for test modules from the given mount.

Each mount point is replaced by two points targeting the test directory: one keeping the original prefix, and one with a Test suffix on the prefix. For instance {MyApp, "lib/my_app"} produces {MyApp, "test/my_app"} and {MyAppTest, "test/my_app_test"}. Paths resolved from the derived mount use the .exs extension:

iex> mount = Modkit.Mount.define!([{MyApp, "lib/my_app"}])
iex> test_mount = Modkit.Mount.as_test(mount)
iex> Modkit.Mount.preferred_path(test_mount, MyApp.WorkerTest)
{:ok, "test/my_app/worker_test.exs"}

Points mounted on :ignore are kept as they are.

define(raw_points, opts \\ [])

Builds a mount from a list of mount points.

Accepts the same points and options as define!/2 but returns {:ok, mount}, or {:error, exception} when a point is invalid.

define!(points, opts \\ [])

Builds a mount from a list of mount points. Raises an ArgumentError when a point is invalid.

Each point is a {prefix, path} or {prefix, path, opts} tuple:

  • prefix - a module whose namespace is mounted on the path.
  • path - the directory for the modules under the prefix, or :ignore to exclude those modules from path resolution.
  • opts - accepts a :flavor option with one of the following values:
    • :elixir (the default) - path segments are the snake cased module segments, as in lib/my_app/some/worker.ex.
    • :mix_task - segments are joined with dots, as in lib/mix/tasks/mod.new.ex.
    • :phoenix - controllers, views, components, live views, channels and sockets are placed in the conventional Phoenix sub-directories, as in lib/my_app_web/controllers/user_controller.ex. Namespaces are nested under that sub-directory, as in lib/my_app_web/controllers/admin/user_controller.ex.

Options

  • :names - custom snake case forms for words found in module names, given as {word, snake_form} pairs in a keyword list or a map. For instance with names: [RabbitMQ: "rabbitmq"], the module MyApp.RabbitMQConsumer resolves to my_app/rabbitmq_consumer.ex instead of my_app/rabbit_mq_consumer.ex.

path_as_test(path)

Transforms a module file path (without extension) into the corresponding test file path (also without extension).

The transformation replaces a leading lib/ or test/ segment with test/, or prepends test/ otherwise, then appends _test to the last segment.

iex> Modkit.Mount.path_as_test("lib/my_app/foo")
"test/my_app/foo_test"

iex> Modkit.Mount.path_as_test("dev/my_app/foo")
"test/dev/my_app/foo_test"

iex> Modkit.Mount.path_as_test("test/support/my_app/foo")
"test/support/my_app/foo_test"

preferred_path(mount, module)

Returns the file path where the given module belongs according to the mount.

The path joins the mount point directory with the snake cased module segments that follow the point prefix. The extension is .ex, or .exs for mounts derived with as_test/1.

Returns {:ok, path} on success, :ignore when the module resolves to an :ignore point, {:error, :not_mounted} when no point covers the module, and {:error, :not_elixir} for module names without the Elixir. namespace, such as Erlang module names.

Examples

iex> mount = Modkit.Mount.define!([{MyApp, "lib/my_app"}, {MyApp.Schemas, "lib/schemas"}])
iex> Modkit.Mount.preferred_path(mount, MyApp.Some.Worker)
{:ok, "lib/my_app/some/worker.ex"}
iex> Modkit.Mount.preferred_path(mount, MyApp.Schemas.User)
{:ok, "lib/schemas/user.ex"}
iex> Modkit.Mount.preferred_path(mount, Other.Module)
{:error, :not_mounted}

prefix_of?(map, mod_split)

Returns whether the given mount point covers the module, that is whether the point prefix is a prefix of the module name. The module is given as a list of segments as returned by Module.split/1.

resolve(mount, mod_split)

Returns the mount point covering the given module.

The mount can be given as a Modkit.Mount struct or a list of Modkit.Mount.Point structs, and the module as a module name or a list of segments as returned by Module.split/1.

Returns {:ok, point} for the first point whose prefix covers the module, :ignore when that point is mounted on :ignore, and {:error, :not_mounted} when no point matches.

Examples

iex> mount = Modkit.Mount.define!([{MyApp, "lib/my_app"}, {MyApp.Ecto, :ignore}])
iex> {:ok, point} = Modkit.Mount.resolve(mount, MyApp.Worker)
iex> point.path
"lib/my_app"
iex> Modkit.Mount.resolve(mount, MyApp.Ecto.Repo)
:ignore
iex> Modkit.Mount.resolve(mount, Other.Worker)
{:error, :not_mounted}