使用 JSON::PullParser 正确移动类型

Correctly moving around types with JSON::PullParser

在我正在创建的 Crystal 分片中,需要从不同的 API 端点提取数据。收集端点将使用如下所示的数组进行响应:

json = %({
  "_embedded": [
    {"id":"item_1"},
    {"id":"item_2"}
  ]
})

为了解释数组并将其转换为对象数组,我准备了以下转换器:

struct ListConverter(T)
  def self.from_json(pull : JSON::PullParser)
    items = Array(T).new
    pull.read_array do
      items.push(Item.from_json(pull.read_raw))
    end
    items
  end
end

有两个抽象结构。一个用于数组中的项目,另一个用于列表本身,其中包括 Enumerable:

abstract struct Base
  include JSON::Serializable
end

abstract struct List(T) < Base
  include Enumerable(T)

  @[JSON::Field(key: "_embedded", converter: ListConverter(Item))]
  getter items : Array(T)

  def each(&block : T -> _)
  end
end

最后执行:

struct Item < Base
  getter id : String?
end

struct ItemList < List(Item)
end

list = ItemList.from_json(json)

这一切都很好,除了一件事。列表转换器需要传递准确的项目类型:

@[JSON::Field(key: "_embedded", converter: ListConverter(Item))]

我希望能够做到这一点,但当然,这不起作用,因为 T 未在运行时定义(我认为):

@[JSON::Field(key: "_embedded", converter: ListConverter(T))]

所以现在我必须在每个继承自 List 的结构中定义以下行:

@[JSON::Field(key: "_embedded", converter: ListConverter(Item))]
getter items : Array(T)

避免不必要重复的最佳方法是什么?

快步走后,我想出了一个可行的解决方案。通过使用宏,可以捕获 T 并且可以使用宏将转换器传递给 JSON::Field 注解:

abstract struct List(T) < Base
  include Enumerable(T)

  macro list_converter
    ListConverter({{ T.id }})
  end

  @[JSON::Field(key: "_embedded", converter: list_converter)]
  getter items : Array(T)

  def each(&block : T -> _)
  end
end

不确定这是否是 right/best 方法,但它按预期工作。