1. demand
Writing a memcache-based cache module requires a specific prefix before the key, so the original store function of user cache should be written as
# user.ex
def store(user_id, value) do key = Cache.key_encode(user_id, :user)
... end
Since the prefixed operation (key_encode/1) is all that must be done before caching, we can consider using metaprogramming to define a behavior called before_store/2 to do this, and then hook before_store before put ting, which makes the code very difficult to understand.
I think it's better to check that the prefixed encode function has been executed at the beginning of the store/2 compilation to make the code easier to understand.
So our underlying rule is that the first line of each function in the module must be Cache.key_encode/2.
2. Use @on_definition to check that the first line of each function in the module must call Cache.key_encode/2
We're going to use it next. @on_definition Check in the compiler whether the specified module conforms to this customized latent rule.
mix new on_definition_play
cd on_definition_play
# lib/user.ex defmodule User do @on_definition {Cache.Enforcement, :on_def} def store_user(user_id, user) do key = Cache.key_encode(user_id, :user) Cache.put(key, user) end # This is not done. key_encode For example, it should be compiled but def store_comment(user_id, comment) do Cache.put(user_id, comment) end end
Look at the definition above. on_definition Attribute, and then we'll implement this on_def/6
defmodule Cache do # Here's just an example of memcache_client. You can use other backend s. def put(key, value) do Memcache.Client.put(key, value) end def get(key) do Memcache.Client.get(key) end def key_encode(key, prefix) do "#{prefix}:#{inspect key}" end defmodule Enforcement do def on_def(env, _kind, _name, args, _guards, body) do check_start_with_key_encode(env, args, body) end defp check_start_with_key_encode(_env, [{_, meta, _} | _args], body) do line = Keyword.get(meta, :line)
# Take the first line out of the body and check its format expr = get_first_line(body) IO.inspect expr case expr do :print_to_see_this_struct-> # We don't know what it is now, so let's use it first. IO.inspect/1 Type it out and see, and then format it. :ok _ -> raise Cache.LacksEncodeError, message: "Function line#{line} must begin with a Cache.key_encode/2" end end
# Define the schema def func used in the function, do: defp get_first_line({:__block__, _, expr_list}) do List.first(expr_list) end defp get_first_line(expr) do expr end end defmodule LacksEncodeError do defexception [:message] end end
We don't know what the first line will look like in AST, so let's give IO.inspect the correct format first. Then match it up:)
So based on inspect's results, we can finally write check_start_with_key_encode/3 as follows:
defp check_start_with_key_encode(_env, [{_, meta, _} | _args], body) do line = Keyword.get(meta, :line) expr = get_first_line(body) case expr do {:=, _, [{_, _, _}, {{:., _, [{:__aliases__, _, [:Cache]},#That's it! :key_encode]}, _,#That's it! _}]} -> :ok _ -> raise Cache.LacksEncodeError, message: "Function line#{line} must begin with a Cache.key_encode/2" end end
If you run mix compile here, you will get
> mix compile == Compilation error on file lib/user.ex == ** (Cache.LacksEncodeError) Function line9 must begin with a Cache.key_encode/2 lib/cache.ex:31: Cache.Enforcement.check_start_with_key_encode/3 (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
Be accomplished!
3. conclusion:
@ on_definition calls on_def/6, so we can customize any underlying rules you need for each function during compilation (but don't abuse them either.)
4. Resources
Module docs There are other compile callback functions and options that deserve a good look