简体   繁体   中英

Unable to understand import via __using__ macro Elixir/Phoenix

I am using Phoenix framework and trying to create a plug for authentication, but am stuck with an error. Below is this sample code.

defmodule ChhutiServer.GoogleAuthController do
  use ChhutiServer.Web, :controller
  use ChhutiServer.Plugs.GoogleAuth
end

# inside lib/plugs

defmodule ChhutiServer.Plugs.GoogleAuth do
  import Plug.Conn

  defmodule ChhutiServer.Behaviour.GoogleAuth do
    @callback callback(Plug.Conn.t, map) :: any
  end

  defmacro __using__(_) do
    quote do
      plug ChhutiServer.Plugs.GoogleAuth
    end
  end
end

Above code returns below error.

== Compilation error on file web/controllers/google_auth_controller.ex ==
** (UndefinedFunctionError) undefined function: ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Plugs.GoogleAuth.init/1 (module ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Plugs.GoogleAuth is not available)
    ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Plugs.GoogleAuth.init([])
    (plug) lib/plug/builder.ex:198: Plug.Builder.init_module_plug/3
    (plug) lib/plug/builder.ex:186: anonymous fn/4 in Plug.Builder.compile/3
    (elixir) lib/enum.ex:1387: Enum."-reduce/3-lists^foldl/2-0-"/3
    (plug) lib/plug/builder.ex:186: Plug.Builder.compile/3
    (phoenix) expanding macro: Phoenix.Controller.Pipeline.__before_compile__/1
    web/controllers/google_auth_controller.ex:1: ChhutiServer.GoogleAuthController (module)
    (elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8

I understand the error is due to elixir looking for ChhutiServe.Plugs.GoogleAuth.ChhuttiServer.Plugs.GoogleAuth instead of ChhuttiServe.Plugs.GoogleAuth . And I am also able to solve this issue by prepending modules with Elixir namespace. ie by using below code.

defmodule ChhutiServer.Plugs.GoogleAuth do
  import Plug.Conn

  defmodule ChhutiServer.Behaviour.GoogleAuth do
    @callback callback(Plug.Conn.t, map) :: any
  end

  defmacro __using__(_) do
    quote do
      plug Elixir.ChhutiServer.Plugs.GoogleAuth
    end
  end
end

But I am not able to understand why its behaving like this. Can anyone please help me out.

UPDATE

Add all required code.

Issue was because I have declared module ChhutiServer.Behaviour.GoogleAuth inside ChhutiServer.Plugs.GoogleAuth . But error reported is very strange and misleading. Moving ChhutiServer.Behaviour.GoogleAuth outside ChhutiServer.Behaviour.GoogleAuth solves the issue. One more strange thing that I noticed is moving ChhutiServer.Behaviour.GoogleAuth below the __using__ macro also removes the error. As error report looks strange , I raised an issue. Below is the issue

https://github.com/elixir-lang/elixir/issues/4120

Update

Jose Valim helped me to understand that it is not an issue and its due to alias created when we defined nested module. I will explain why we get above error so that other people facing similar issue may understand the concept.

Whenever we create a nested module in elixir we are creating aliase for the nested module. Below is the example.

defmodule A do
  defmodule B do
  end
end

Above code is equivalent to

defmodule A.B do
end

defmodule A do
  alias A.B, as: B
end

So nested module creates the alias so that it can be accessed inside the parent module without fully-qualified name ie we can use B instead of AB directly inside A to access B.

Now lets come to my question and check whats going on and understand the error generated.

defmodule ChhutiServer.Plugs.GoogleAuth do
  import Plug.Conn


  defmodule ChhutiServer.Behaviour.GoogleAuth do
    @callback callback(Plug.Conn.t, map) :: any
  end


  defmacro __using__(_) do
    quote do
      plug ChhutiServer.Plugs.GoogleAuth
    end
  end
end

In above code module ChhutiServer.Behaviour.GoogleAuth is nested within ChhutiServer.Plugs.GoogleAuth . So fully-qualified name of ChhutiServer.Behaviour.GoogleAuth is ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Behaviour.GoogleAuth . As we have seen nested module creates the alias, above code is same as

  defmodule ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Behaviour.GoogleAuth do
    @callback callback(Plug.Conn.t, map) :: any
  end

  defmodule ChhutiServer.Plugs.GoogleAuth do
    import Plug.Conn
    alias ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Behaviour.GoogleAuth, as: ChhutiServer.Behaviour.GoogleAuth   

    defmacro __using__(_) do
     quote do
       plug ChhutiServer.Plugs.GoogleAuth
     end
    end
  end

Due to this alias now ChhutiServer points to ChhutiServer.Plugs.GoogleAuth.ChhutiServer so that we can use ChhutiServer.Behaviour.GoogleAuth inside module ChhutiServer.Plugs.GoogleAuth instead of using full qualified name ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Behaviour.GoogleAuth . Then when we use plug ChhutiServer.Plugs.GoogleAuth inside __using__ macro it gets expanded to plug ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Plugs.GoogleAuth as now ChhutiServer points to ChhutiServer.Plugs.GoogleAuth.ChhutiServer . And there comes our error as elixir is not able to find the module ChhutiServer.Plugs.GoogleAuth.ChhutiServer.Plugs.GoogleAuth

Also I pointed out in question that when we define module ChhutiServer.Behaviour.GoogleAuth below __using__ macro then the above code runs without any error. This is because alias are lexically scoped in elixir. So if we bring ChhutiServer.Behaviour.GoogleAuth below __using__ macro the alias is defined below the macro. So plug ChhutiServer.Plugs.GoogleAuth is not expanded to anything and it remains as it is. And elixir is able to find ChhutiServer.Plugs.GoogleAuth . Small example to explain above thing.

defmodule AB do def b do end end

defmodule A do def a do # will return error "module B is not available" as AB is not aliased yet. # If we want to call b we have to write it as ABb Bb end alias AB, as: B end

If we use alias before using B it will work.

defmodule A do alias AB, as: B
def a do Bb end end

Hope this helps other to understand nested module and alias. Thanks Jose Valim for the explanation

References:

http://elixir-lang.org/getting-started/alias-require-and-import.html

https://github.com/elixir-lang/elixir/issues/4120

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM