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

Use PullParser to deserialize Range in Crystal

我正在尝试将一些 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}})

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

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

基于this sample I deduce that you're expected to call .begin_object? first. But actually that's just a nicety for error detection. The main thing is that you're also supposed to explicitly read ("consume") the values, 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, 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}})