AWS の CloudFormation ではリソースの管理を YAML でできるんだけど、そこでは下記のような記法が使える。
# See: https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
InstanceId: !Ref ApplicationServer
この !Ref
のように !
で始まる文字列は YAML の仕様にあるタグと呼ばれるもので、値の型を示すために使われる。
しかし、このような YAML を Ruby の標準ライブラリで読み込むとタグの情報は抜け落ちてしまう。
require "yaml"
yaml = "InstanceId: !Ref ApplicationServer"
parsed = YAML.load(yaml)
pp parsed
# {"InstanceId"=>"ApplicationServer"}
puts parsed.to_yaml
# ---
# InstanceId: ApplicationServer
YAML でタグを扱う方法を調べてみた。
YAML.add_domain_type
タグ付きの値を特定のクラスのオブジェクトとして読み込むだけならこれが楽そう。
下記は !Ip
タグをつけた文字列を IPAddr オブジェクトとして読み込む例。
require "yaml"
require "ipaddr"
YAML.add_domain_type("", "Ip") do |type, value|
IPAddr.new(value)
end
yaml = "ip: !Ip '127.0.0.1'"
parsed = YAML.load(yaml)
p parsed["ip"]
# #<IPAddr: IPv4:127.0.0.1/255.255.255.255>
ただし、これを dump すると普通に Ruby オブジェクトを dump した場合と同じになり、元のかたちにはならない。
puts parsed.to_yaml
# ---
# ip: !ruby/object:IPAddr
# family: 2
# addr: 2130706433
# mask_addr: 4294967295
dump した場合の挙動を変更したい場合は値のクラスに encode_with
インスタンスメソッドを定義する。
class IPAddr
def encode_with(coder)
# 第 1 引数はタグ、第 2 引数は値
# 値がスカラーなので represent_scalar メソッドを使っている
# リストなら represent_seq、マップなら represent_map を使う
coder.represent_scalar "!Ip", to_s
end
end
puts parsed.to_yaml
# ---
# ip: !Ip 127.0.0.1
YAML.add_tag
YAML.add_domain_type
では YAML での値 -> オブジェクトの変換処理をブロックで指定していたが、値のクラスに持たせることもできる。
この場合は YAML.add_tag
でタグとクラスの対応付けを行う。
require "yaml"
require "ipaddr"
class IPAddr
# YAML の読み込み時に Class.allocate でオブジェクトが生成されたあと、このメソッドが呼ばれる。
# Class.allocate では initialize が呼ばれないので initialize を呼ぶのが楽。
def init_with(coder)
# coder.scalar でスカラー値が得られる。リストなら coder.seq、マップなら coder.map。
# coder.tag でタグを得ることもできる。
initialize(coder.scalar)
end
# 上例と同じ
def encode_with(coder)
coder.represent_scalar "!Ip", to_s
end
end
YAML.add_tag("!Ip", IPAddr)
yaml = "ip: !Ip '127.0.0.1'"
parsed = YAML.load(yaml)
p parsed["ip"]
# #<IPAddr: IPv4:127.0.0.1/255.255.255.255>
puts parsed.to_yaml
# ---
# ip: !Ip 127.0.0.1
CloudFormation のテンプレートでの実例
CloudFormation のテンプレートではタグを省略のために使っているだけなので、書き戻す必要がないのであれば、読み込み時に省略しないかたちに変換してしまうのが一番楽。
require "yaml"
YAML.add_domain_type("", "Ref") do |type, value|
{"Ref" => value}
end
yaml = "InstanceId: !Ref ApplicationServer"
parsed = YAML.load(yaml)
pp parsed
# {"InstanceId"=>{"Ref"=>"ApplicationServer"}}
puts parsed.to_yaml
# ---
# InstanceId:
# Ref: ApplicationServer
書き戻す必要があるならクラスを作って YAML.add_tag
使うのがよさそう。
require "yaml"
class Ref
attr_reader :value
def initialize(value)
@value = value
end
def init_with(coder)
initialize(coder.scalar)
end
def encode_with(coder)
coder.represent_scalar "!Ref", value
end
end
YAML.add_tag("!Ref", Ref)
yaml = "InstanceId: !Ref ApplicationServer"
parsed = YAML.load(yaml)
pp parsed
# {"InstanceId"=>#<Ref:0x00007ff04009b838 @value="ApplicationServer">}
puts parsed.to_yaml
# ---
# InstanceId: !Ref ApplicationServer