Absinthe 1.5 动态定义枚举值

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/2Macro.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 就会出现问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容