使用PullParser反序列化Crystal中的Range



我正在尝试将一些json{ "range": {"start": 1, "stop": 10} }转换为等效于Range.new(1,10)Range对象。

似乎如果我想在Foo结构中这样做,我需要一个自定义转换器(见下文(,它使用JSON::PullParser来消费每个令牌。我尝试了下面这样的东西,看看我是否能理解pull解析器应该如何使用。但看起来它希望所有东西都是一根绳子,并在找到的第一个Int上窒息。因此,以下内容没有帮助,但说明了我的困惑:

require "json"
module RangeConverter
def self.from_json(pull : JSON::PullParser)
pull.read_object do |key, key_location|
puts key # => puts `start` then chokes on the `int`
# Expected String but was Int at 1:22
end
Range.new(1,2)
end
end

struct Foo
include JSON::Serializable

@[JSON::Field(converter: RangeConverter)]
property range : Range(Int32, Int32)
end
Foo.from_json %({"range": {"start": 1, "stop": 10}})

我唯一能弄清楚这一点的方法是读取原始json字符串并直接使用它,但感觉我在回避解析器,因为我不理解它

require "json"
module RangeConverter
def self.from_json(pull : JSON::PullParser)
h = Hash(String, Int32).from_json(pull.read_raw)
Range.new(h["start"],h["stop"])
end
end

struct Foo
include JSON::Serializable

@[JSON::Field(converter: RangeConverter)]
property range : Range(Int32, Int32)
end
Foo.from_json %({"range": {"start": 1, "stop": 10}})

那么,我应该如何在这里使用Parser呢?

Oleh Prypin的回答很好。正如他所说,第二种方法很好,只是它分配了一个Hash,这样它就消耗了额外的内存。

您可以使用在堆栈上分配的NamedTuple来代替Hash,这样效率会高得多。对于这样的类型,这是一个很好的用例:

require "json"
module RangeConverter
def self.from_json(pull : JSON::PullParser)
tuple = NamedTuple(start: Int32, stop: Int32).new(pull)
tuple[:start]..tuple[:stop]
end
end
struct Foo
include JSON::Serializable
@[JSON::Field(converter: RangeConverter)]
property range : Range(Int32, Int32)
end
p Foo.from_json %({"range": {"start": 1, "stop": 10}})

NamedTuple的一个替代方案是使用带有getter的普通结构,这就是record的作用:

require "json"
record JSONRange, start : Int32, stop : Int32 do
include JSON::Serializable
def to_range
start..stop
end
end
module RangeConverter
def self.from_json(pull : JSON::PullParser)
JSONRange.new(pull).to_range
end
end
struct Foo
include JSON::Serializable
@[JSON::Field(converter: RangeConverter)]
property range : Range(Int32, Int32)
end
p Foo.from_json %({"range": {"start": 1, "stop": 10}})

后一个选项一点也不坏。它只是重用Hash的实现,但它是完全可行和可组合的。唯一的缺点是它需要分配然后丢弃Hash

基于这个示例,我推断您应该首先调用.begin_object?。但实际上,这只是错误检测的一个细节。主要的是,您还应该基于这个示例显式读取("消费"(值。在下面的代码中,这用Int32.new(pull)表示。

require "json"
module RangeConverter
def self.from_json(pull : JSON::PullParser)
start = stop = nil
unless pull.kind.begin_object?
raise JSON::ParseException.new("Unexpected pull kind: #{pull.kind}", *pull.location)
end
pull.read_object do |key, key_location|
case key
when "start"
start = Int32.new(pull)
when "stop"
stop = Int32.new(pull)
else
raise JSON::ParseException.new("Unexpected key: #{key}", *key_location)
end
end
raise JSON::ParseException.new("No start", *pull.location) unless start
raise JSON::ParseException.new("No stop", *pull.location) unless stop
Range.new(start, stop)
end
end

struct Foo
include JSON::Serializable

@[JSON::Field(converter: RangeConverter)]
property range : Range(Int32, Int32)
end
p Foo.from_json %({"range": {"start": 1, "stop": 10}})

相关内容

  • 没有找到相关文章

最新更新