absinthe
graphql
enum
elixir
macro
Absinthe 是 Elixir 语言的一个 GraphQL 工具库。即将发布的 1.5 版本经过了大量重构,该版本将不再支持动态定义 enum 类型的值。详情可以前往以下链接进一步了解。
https://github.com/absinthe-graphql/absinthe/issues/843
https://github.com/absinthe-graphql/absinthe/pull/859
hydrate
为了替代原来动态定义的枚举值,1.5 版本中引入了 hydrate/2
,请求时将会回调 hydrate/2
以处理枚举值的映射关系。
举个例子,假如邮箱的激活状态是由 MyApp.Email
中的两个函数进行定义
defmodule MyApp.Email do
def inactivated, do: 1
def activated, do: 2
end
那么 1.5 版本之前你可以这样定义这个枚举
alias MyApp.Email
enum :email_status do
value(:inactivated, as: Email.inactivated())
value(:activated, as: Email.activated())
end
但是在 1.5 版本中这么定义就会报错了,因为 as
之后不再支持函数调用,想要实现类似的效果就得用 hydrate/2
alias MyApp.Email
enum :email_status do
value(:inactivated)
value(:activated)
end
def hydrate(
%Absinthe.Blueprint.Schema.EnumValueDefinition{identifier: identifier},
[%Absinthe.Blueprint.Schema.EnumTypeDefinition{identifier: :email_status}]
) do
value =
case identifier do
:inactivated -> Email.inactivated()
:activated -> Email.activated()
end
{:as, value}
end
实在是有点难受。
编写 enum_dynamic 宏
不过不使用 hydrate/2
,直接硬编码还是可以的
enum :email_status do
value(:inactivated, as: 1)
value(:activated, as: 2)
end
但是这样会导致硬编码出现在两个地方,更让人难受了。
这个时候,就轮到强大的 Macro 登场!在编译期将函数调用替换为硬编码,以实现用最小的改动来兼容新版本。
先来设想这个宏的用法,设计的核心思路是尽量减少需要修改的代码
+ import MyApp.Schema.Helper.Enum
alias MyApp.Email
- enum :email_status do
+ enum_dynamic :email_status do
value(:inactivated, as: Email.inactivated())
value(:activated, as: Email.activated())
end
这样的话只须把所有的 enum 改名即可,很方便进行升级,接下来分析怎么去实现这个宏。
借助 Elixir 提供的 Macro.prewalk/2
或 Macro.postwalk/2
函数,就可以可以前序或者后续遍历 AST 并对 AST 中的节点进行修改,还是非常方便的。
在动手实现之前,先来用 quote
看看修改前的 AST 与修改后的 AST 有什么区别
这是修改前的
quote do
enum_dynamic :email_status do
value(:inactivated, as: Email.inactivated())
value(:activated, as: Email.activated())
end
end
{:enum_dynamic, [],
[
:email_status,
[
do: {:__block__, [],
[
{:value, [],
[
:inactivated,
[
as: {{:., [],
[{:__aliases__, [alias: false], [:Email]}, :inactivated]}, [],
[]}
]
]},
{:value, [],
[
:activated,
[
as: {{:., [],
[{:__aliases__, [alias: false], [:Email]}, :activated]}, [], []}
]
]}
]}
]
]}
我们希望修改后是这样一个结果
quote do
enum :email_status do
value(:inactivated, as: 1)
value(:activated, as: 2)
end
end
{:enum, [],
[
:email_status,
[
do: {:__block__, [],
[
{:value, [], [:inactivated, [as: 1]]},
{:value, [], [:activated, [as: 2]]}
]}
]
]}
对比可以看出来就是把 :as
后的内容给它执行了,并且把 :enum_dynamic
再换回 :enum
。
那么具体实现就可以这么写
# :email_status 会传给 name
# do block 会传给 values
defmacro enum_dynamic(name, do: values) do
values = Macro.postwalk(values, fn
{:as, value} ->
# 找到 :as 开头的 AST 节点,把这个节点修改为执行后的值
{actual_value, _} = Code.eval_quoted(value)
{:as, actual_value}
node ->
# 其余 AST node 保持不变
node
end)
{:enum, [], [name, [do: values]]}
end
但是仅仅这样写你会发现,这个宏无法处理 alias
模块名的调用,所以这里需要指定运行环境为 __CALLER__
以展开 alias
。
整理过后就是这样
defmacro enum_dynamic(name, do: values) do
eval_value = fn
{:as, value} ->
{actual_value, _} = Code.eval_quoted(value, [], __CALLER__)
{:as, actual_value}
node ->
node
end
values = Macro.postwalk(values, eval_value)
{:enum, [], [name, [do: values]]}
end
注意事项
宏是在编译期执行的,如果 as
后的函数在编译期无法执行,或者编译期的执行结果与运行时不同,使用 enum_dynamic
就会出现问题。