How to include code into the current file from an external file in compile time?

I have a project which will be used as a template. I’ll then copy it and create a sub-project based on it. I want to be able to update it from the upstream without conflict.

All the *.ex files that are specific to a sub-project won’t be in git. Therefore, they’ll have to be created manually and loaded dynamically.

Question: how to load external .ex into the current one, to then compile it? Not at runtime, but at the compile-time.

This will be something similar to this:

# my_controller.ex
defmodule MyAppWeb.MyController do
  use MyAppWeb, :controller

  def page1(conn, params) do
    # ........
  end



  # HOW to implement this function?
  inject_external_file_if_exists("my_controller.personal.ex")
end

Code specific to a sub-project could look like this:

# my_controller.personal.ex
defmodule MyAppWeb.MyController do
  use MyAppWeb, :controller

  def specific_page1(conn, params) do
    # ........
  end


  def specific_page2(conn, params) do
    # ........
  end
end

Note that my_controller.personal.ex contains the definition of MyAppWeb.MyController once again. This isn’t a strict requirement. The idea is to merge at compile them rather than overwrite each other.

I come up with something like below (not cover all cases yet)

defmodule InjectFile do
  defmacro inject(path) do
    with {"code", {:ok, code}} <- {"code", File.read(path)},
         {"ast", {:ok, ast}} <- {"ast", Code.string_to_quoted(code)} do
      ast
      |> case do
        {:defmodule, _, [_, [do: {:__block__, _, defs}]]} -> defs
        {:defmodule, _, [_, [do: def]]} -> [def]
        _ -> {:error, "CAN_NOT_PARSE_CODE"}
      end
      |> Enum.map(fn def ->
        quote do
          unquote(def)
        end
      end)
    else
      {"code", {:error, _}} -> {:error, "CAN_NOT_READ_FILE"}
      {"ast", {:error, _}} -> {:error, "CAN_NOT_PARSE_CODE"}
    end
  end
end
defmodule ControllerA do
  require InjectFile

  def page1() do
    1
  end

  def page2() do
    2
  end

  InjectFile.inject("lib/controller_b.ex")
end
defmodule ControllerB do
  def page3() do
    3
  end

  def page4() do
    4
  end
end
iex> ControllerA.page3()
3
1 Like