Import all modules that use X module

Hi, i’d like to know if there’s a way to list modules searching for a specific “import”, like the Ecto.Schema, or another module of my choice.

Let’s say that i want to list all Schemas that ecto uses to create tables in my db.

i want to dynamically search/list/load modules that uses “use Ecto.Schema”, so i can do whatever i want with them.

It depends. It might be easier if you explain the specific thing you are trying to achieve.

For example, if you do want to do something with all ecto schemas, a better approach might be to wrap the use Ecto.Schema with your own macro like use MyApp.Schema. That will give you a seam to be able to do whatever you need before calling Ecto’s usual schema stuff.

But it really depends what you atually want to achieve in the end

1 Like

If you are interested specifically in ecto schemas, the best bet is to iterate over all modules in your project and check for ecto schema metadata.

Here is how to list all application’s modules:

with {:ok, list} <- :application.get_key(:my_app, :modules) do
  list
  |> Enum.filter(& &1 |> Module.split |> Enum.take(1) == ~w|UserHelpers|)
  |> Enum.reduce(user_data, fn m, acc -> apply(m, :create, acc) end)
end

An then simply check for something unique from the schema module that is present in all of them. For example functions:

MySchemaModule.__info__(:functions)

[
  __changeset__: 0,
  __schema__: 1,
  __schema__: 2,
  __struct__: 0,
  __struct__: 1,
  changeset: 1,
  changeset: 2
]
2 Likes

Either have them all be in a certain namespace e.g. YourApp.Schemas. or do as @D4no0 recommends: look for the functions that use Ecto.Schema injects in the modules. I’d go for the latter, it’s more reliable, though a bit slower to scan and find them all (but likely not by a lot).

AFAIK there’s no explicit “this module said use Ecto.Schema” indication; you could produce a module that behaves exactly the same way without the macros at all, by manually writing all the needed clauses of the __schema__/1 function:

Another key bit of info: do you want to know this at compile time or runtime?

If at compile time, you need to do what @Adzz is talking about. If at runtime, then what @D4no0/@dimitarvp said, though from a design perspective this is less than ideal, though in some situations it could probably be ok.

imported functions just get rewritten to their fully qualified versions when compiled so, as @al2o3cr said, there’s no (sane) way of knowing that a module definition ever declared any imports.

2 Likes

Thanks guys, managed to get something working like this:

defmodule YourApp.SchemaDiscover do
  def has_schema_and_changeset?(module) do
    functions = module.__info__(:functions)
    Enum.any?(functions, fn {name, _} -> name in [:__schema__, :__changeset__] end)
  end

  def get_modules_using_ecto_schema() do
    with {:ok, list} <- :application.get_key(:yourapp, :modules) do
      list
      |> Enum.filter(fn module ->
        not String.contains?(inspect(module), "YourAppWeb") # ignores Web folder
      end)
      |> Enum.filter(&has_schema_and_changeset?/1)
    end
  end

  def list_module_fields(schema_module) do
    for field <- schema_module.__schema__(:fields) do
      {field, schema_module.__schema__(:type, field)}
    end
  end

  def get_modules_and_fields() do
    modules = get_modules_using_ecto_schema()
    Enum.map(modules, fn module ->
      {module, list_module_fields(module)}
    end)
  end

end

get_modules_and_fields Returns:

[
  {YourApp.Accounts.User,
   [
     id: :id,
     email: :string,
     hashed_password: :string,
     confirmed_at: :naive_datetime,
     inserted_at: :naive_datetime,
     updated_at: :naive_datetime
   ]},
  {YourApp.Accounts.UserToken,
   [
     id: :id,
     token: :binary,
     context: :string,
     sent_to: :string,
     user_id: :id,
     inserted_at: :naive_datetime
   ]}
]

Do not rely on inspect for programmatic logic. It can be overridden to output something entirely different than what you’d expect. For reference: the Inspect protocol.

Make use of the Module.split/1 function instead:

      |> Enum.filter(fn module ->
        [prefix | _] = Module.split(module)
        prefix != "YourAppWeb"
      end)

Or if you prefer a one-liner:

      |> Enum.filter(fn module ->
        not match?(["YourAppWeb" | _], Module.split(module))
      end)
1 Like