使用PullParser反序列化Crystal中的范围

问题描述

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

似乎,如果我想在自己的Foo结构中执行此操作,则需要一个自定义转换器(请参见下文),该转换器使用JSON::PullParser来消耗每个令牌。我尝试了以下类似的操作,以了解是否可以理解应该使用提取解析器。但是看起来它希望所有内容都是字符串,并且在找到的第一个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,"stop": 10}})

那么我实际上应该如何在这里使用解析器?

解决方法

您的后一种选择一点也不差。它只是重用了Hash中的实现,但是它是完全可行且可组合的。唯一的缺点是它需要分配然后丢弃该Hash

基于this sample,我推断您应该首先致电.begin_object?。但这实际上只是一个错误检测的好地方。最主要的是,您还应该显式读取(“消费”)值based on this sample。在下面的代码中,用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}})
,

Oleh Prypin 的回答很棒。正如他所说,第二种方法很好,只是它分配了一个哈希,因此会消耗额外的内存。

您可以使用在堆栈上分配的 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,"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,"stop": 10}})

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...