In a Crystal shard I am creating, data needs to be pulled from different API endpoints. Collection endpoints will respond with a array like the one below:
json = %({
"_embedded": [
{"id":"item_1"},
{"id":"item_2"}
]
})
To interpret the array and cast it to an array of objects, I prepared the following converter:
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
There are two abstract structs. One for the items in the array and another for the list itself, which includes 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
Finally, the implementation:
struct Item < Base
getter id : String?
end
struct ItemList < List(Item)
end
list = ItemList.from_json(json)
This all works fine, except for one thing. The list converter needs to be passed the exact item type:
@[JSON::Field(key: "_embedded", converter: ListConverter(Item))]
I would like to be able to do this, but of course, that does not work because T
is not defined at runtime (I think):
@[JSON::Field(key: "_embedded", converter: ListConverter(T))]
So now I will have to define the following lines in every struct inheriting from List
:
@[JSON::Field(key: "_embedded", converter: ListConverter(Item))]
getter items : Array(T)
What would be the best approach to avoid unnecessary duplication?
After a brisk walk, I came up with a solution that works. By using a macro, T
can be captured and the macro can be used to pass the converter to the JSON::Field
annotation:
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
Not sure if this is the right/best approach, but it works as expected.