读取和写入YAML文件而不破坏锚点和别名?

d11*_*wtq 17 ruby yaml

我需要打开一个YAML文件,其中包含别名:

defaults: &defaults
  foo: bar
  zip: button

node:
  <<: *defaults
  foo: other
Run Code Online (Sandbox Code Playgroud)

这显然扩展到了一个等效的YAML文档:

defaults:
  foo: bar
  zip: button

node:
  foo: other
  zip: button
Run Code Online (Sandbox Code Playgroud)

其中YAML::load把它读成.

我需要在这个YAML文档中设置新密钥,然后将其写回磁盘,尽可能保留原始结构.

我看过YAML :: Store,但这完全破坏了别名和锚点.

是否有任何可用的东西:

thing = Thing.load("config.yml")
thing[:node][:foo] = "yet another"
Run Code Online (Sandbox Code Playgroud)

将文档保存为:

defaults: &defaults
  foo: bar
  zip: button

node:
  <<: *defaults
  foo: yet another
Run Code Online (Sandbox Code Playgroud)

我选择使用YAML,因为它很好地处理了这个别名,但是编写包含别名的YAML在现实中看起来有点像一个看起来很暗淡的游戏场.

mat*_*att 12

使用<<表示别名映射应该合并到当前映射不是核心Yaml规范的一部分,但它是标记库的一部分.

精极度紧张- -由红宝石提供的电流YAML库提供了dumpload其允许Ruby对象的容易序列化和反序列化和使用的各种隐式类型转换在标签信息库包括方法<<合并散列.它还提供了在需要时进行更多低级Yaml处理的工具.不幸的是,它不容易选择性地禁用或启用标签存储库的特定部分 - 这是一个全有或全无的事情.特别是处理<<散列的处理非常好.

实现你想要的东西的一种方法是提供你自己的Psych ToRuby类的子类并覆盖这个方法,这样它就可以将映射键<<视为文字.这涉及到在Psych中覆盖私有方法,所以你需要小心一点:

require 'psych'

class ToRubyNoMerge < Psych::Visitors::ToRuby
  def revive_hash hash, o
    @st[o.anchor] = hash if o.anchor

    o.children.each_slice(2) { |k,v|
      key = accept(k)
      hash[key] = accept(v)
    }
    hash
  end
end
Run Code Online (Sandbox Code Playgroud)

然后你会像这样使用它:

tree = Psych.parse your_data
data = ToRubyNoMerge.new.accept tree
Run Code Online (Sandbox Code Playgroud)

从你的例子中的Yaml,data然后看起来像

{"defaults"=>{"foo"=>"bar", "zip"=>"button"},
 "node"=>{"<<"=>{"foo"=>"bar", "zip"=>"button"}, "foo"=>"other"}}
Run Code Online (Sandbox Code Playgroud)

请注意<<作为文字键.另外,下哈希data["defaults"]键是相同的哈希作为下一个data["node"]["<<"]重点,即它们具有相同的object_id.您现在可以根据需要操作数据,当您将其写为Yaml时,锚点和别名仍将存在,尽管锚名称已更改:

data['node']['foo'] = "yet another"
puts Yaml.dump data
Run Code Online (Sandbox Code Playgroud)

产生(Psych使用object_id哈希值来确保唯一的锚名称(当前版本的Psych现在使用连续数字而不是object_id)):

---
defaults: &2151922820
  foo: bar
  zip: button
node:
  <<: *2151922820
  foo: yet another
Run Code Online (Sandbox Code Playgroud)

如果您想控制锚名称,可以提供自己的名称Psych::Visitors::Emitter.这是一个基于您的示例的简单示例,并假设只有一个锚点:

class MyEmitter < Psych::Visitors::Emitter
  def visit_Psych_Nodes_Mapping o
    o.anchor = 'defaults' if o.anchor
    super
  end

  def visit_Psych_Nodes_Alias o
    o.anchor = 'defaults' if o.anchor
    super
  end
end
Run Code Online (Sandbox Code Playgroud)

data上面的修改哈希一起使用时:

#create an AST based on the Ruby data structure
builder = Psych::Visitors::YAMLTree.new
builder << data
ast = builder.tree

# write out the tree using the custom emitter
MyEmitter.new($stdout).accept ast
Run Code Online (Sandbox Code Playgroud)

输出是:

---
defaults: &defaults
  foo: bar
  zip: button
node:
  <<: *defaults
  foo: yet another
Run Code Online (Sandbox Code Playgroud)

(更新: 另一个问题是如何使用多个锚点来解决这个问题,在这里我提出了一种在序列化时保留锚名称的更好方法.)