stephanos / rewire

Dependency injection for Elixir. Zero code changes required.

Home Page:https://hex.pm/packages/rewire

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rewire nested module support?

edisonywh opened this issue · comments

Thanks a lot for the library, it's a very cool idea to utilise aliasing and I really enjoy not having to deal with Application.fetch!

However, I am facing an issue with nested modules right now.

In my example,

defmodule Messaging do
  def send, do: Courier.send()
end

defmodule Courier do
  def send, do: Pigeon.send()
end

Pigeon is the mock that I want to implement, now, if I do:

defmock(PigeonMock, for: PigeonBehaviour)
rewire Messaging, Pigeon: PigeonMock

This compiler gives a nice helpful message and says it can't find Pigeon in Messaging, which is only available in Courier.

My actual use case is a little more complicated (with protocol dispatch), but this should illustrate the point just fine.

I can see that it is mentioned in the README,

Only the dependencies of the rewired module will be replaced. Any modules defined around the rewired module will be ignored. All references of the rewired module to them will be pointing to the original. You're always able to rewire them separately yourself.

But is this due to a technical reason, or will it be possible to do it? I'd gladly take a look at it if that's the case.

Since rewire works by replacing the module with a new version with alias, perhaps we could just recursively walk and replace the modules?

I haven't really looked into where it would work or not, but the proposed idea could look something like:

rewire Messaging, [Courier: [Pigeon: PigeonMock]]

This generates a new Messaging module that uses a rewired version of Courier, which uses a rewired version of Pigeon

Actually, I'm using protocol which means this would likely never work for me anyway, but nested rewiring could still be nice!

Here is my exact protocol version:

defprotocol Courier do
  def push(mod)
end

defmodule Push do
  defimpl Courier do
     def push(_), do: Pigeon.send()
  end
end

defmodule Messaging do
   def send, do: Courier.push(%Push{})
end

I'm not sure if protocol would impact it (I'd probably need to disable consolidation), I think what I really need is to rewire the Push module in Messaging to a Push that rewired?

EDIT: While writing the message I tested it out and it seems to work, here's my code if anyone is interested to get rewire to work with Protocols :)

rewire(Push, Pigeon: PigeonMock, as: NewPush)
rewire(Messaging, Push: NewPush)

You would also need to disable protocol consolidation (https://hexdocs.pm/elixir/Protocol.html#module-consolidation),

def projects
  ..
  consolidate_protocols: Mix.env() != :test
  ..
end

Disabling protocol consolidating is apparently not recommended, but I'm not sure if there's a better way (article for reference, https://www.synopsys.com/blogs/software-security/tinfoil-define-protocol-implementations/)

Hey Edison, thank you!

Since rewire works by replacing the module with a new version with alias, perhaps we could just recursively walk and replace the modules?

You're right, rewire only looks one level deep and could support a recursive strategy but I hope we can avoid that. Rewire can actually support nested modules already, the syntax is just not as nice as the one you posted (good idea!): see https://github.com/stephanos/rewire/blob/master/test/rewire_alias_test.exs#L17. The secret is just to have multiple rewire.

But it seems like you already found that our yourself!

defprotocol

That's interesting! I didn't think about that at all, yet. Could you explain how disabling consolidate_protocols fixed the issue for you? What was the error message you saw?

rewire only looks one level deep and could support a recursive strategy but I hope we can avoid that.

Any particular reason why we're trying to avoid that? Just trying to understand if it would introduce too much noise into the codebase or something?

The secret is just to have multiple rewire

That's exactly what I'm doing now which is great! I'm actually kinda surprised that it worked, but I guess since it's just aliasing it works pretty similar to nested modules?

The consolidate_protocol error message was something described here:

https://elixirforum.com/t/protocol-has-already-been-consolidated-in-tests/11308

I suppose it's because Rewire tries to reload the file in memory after aliasing, but reloading a file with protocol definition won't have an effect because mix had already "consolidated" them, so the "fix" is to disable it.

Any particular reason why we're trying to avoid that? Just trying to understand if it would introduce too much noise into the codebase or something?

Yes, exactly. I think it would be the code base larger and only add more problems. The way it is now it does exactly one thing, no hidden magic.

That's exactly what I'm doing now which is great! I'm actually kinda surprised that it worked

I was, too, when I tried it first :D it's really just because the output of the first rewire is still a simple, ordinary module that itself can be used to rewire something else. There's nothing inherently different about the rewired module.