DNSに登録する文字列から Resolv::DNS::Resource のインスタンスを作成

ネームサーバーの動作確認をRubyでしたい件の続き。[以前の記事](/HMMNRST/items/9e701cd6896c27e5ed54)で、Rubyのresolvライブラリを用いて名前解決できるようになった。しかし、取得した結果が期待値と一致するか調べるには、**手元でも文字列からインスタンスを作成**しなければならない。

> 初めは取得した結果を文字列化して比較しようとしたが、AAAAレコード（IPv6）の比較で早速つまづいた。他のレコードも文字列化すると、空白の数など表記揺れで失敗しかねない。

コンストラクタに引数を与えればいいだけではあるが、文字列のまま与えると**適切なクラスに変換されず比較失敗する**ことがあるので、適宜対処しなければならない。

（ちなみにTTLはインスタンス同士の比較に使われない）

## コード
扱ったことのあるレコード種別のみ。文字列も規定されたパターンを全て解釈できるわけではなく、よく使うパターンだけ対応させている。

```ruby
require 'resolv'

def create_resource(type, value)
	type = type.upcase.to_sym
	klass = Resolv::DNS::Resource::IN.const_get(type)

	case type
	when :A, :AAAA
		klass.new(value)
	when :CNAME, :NS, :PTR
		klass.new(Resolv::DNS::Name.create(value))
	when :MX, :SRV
		*num_list, name = value.split
		klass.new(*num_list.map!(&:to_i),
		          Resolv::DNS::Name.create(name))
	when :SOA
		mname, rname, *num_list = value.tr("()", " ").split
		klass.new(Resolv::DNS::Name.create(mname),
		          Resolv::DNS::Name.create(rname),
		          *num_list.map!(&:to_i))
	when :TXT, :SPF
		klass.new(value.gsub(/^"|"$/, ""))
	else
		raise ArgumentError, "not implemented yet"
	end
end
```

```ruby:実験
create_resource(:A, '192.0.2.1')
#=> #<Resolv::DNS::Resource::IN::A:0x... @address=#<Resolv::IPv4 192.0.2.1>>
create_resource(:AAAA, '2001:0db8:0000:0000:0000:0000:0000:0001')
#=> #<Resolv::DNS::Resource::IN::AAAA:0x... @address=#<Resolv::IPv6 2001:DB8::1>>

create_resource(:CNAME, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::CNAME:0x... @name=#<Resolv::DNS::Name: example.com.>>
create_resource(:NS, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::NS:0x... @name=#<Resolv::DNS::Name: example.com.>>
create_resource(:PTR, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::PTR:0x... @name=#<Resolv::DNS::Name: example.com.>>

create_resource(:MX, '10 example.com.')
#=> #<Resolv::DNS::Resource::IN::MX:0x... @preference=10, @exchange=#<Resolv::DNS::Name: example.com.>>
create_resource(:SRV, '1 0 21 example.com.')
#=> #<Resolv::DNS::Resource::IN::SRV:0x... @priority=1, @weight=0, @port=21, @target=#<Resolv::DNS::Name: example.com.>>

create_resource(:SOA, <<EOS)
ns1.example.com. root.example.com. (
         1
      7200
       900
   1209600
     86400 )
EOS
#=> #<Resolv::DNS::Resource::IN::SOA:0x... @mname=#<Resolv::DNS::Name: ns1.example.com.>, @rname=#<Resolv::DNS::Name: root.example.com.>, @serial=1, @refresh=7200, @retry=900, @expire=1209600, @minimum=86400>

create_resource(:TXT, '"v=spf1 +ip4:192.168.100.0/24 ~all"')
#=> #<Resolv::DNS::Resource::IN::TXT:0x... @strings=["v=spf1 +ip4:192.168.100.0/24 ~all"]>
create_resource(:SPF, '"v=spf1 +ip4:192.168.100.0/24 ~all"')
#=> NameError: uninitialized constant Resolv::DNS::Resource::IN::SPF
```

最後のSPFレコードについては次節を参照。

## 詳細
`case` でレコード毎に処理を変えているが、それぞれ何のための処理なのかを簡単に記す。

### A, AAAA レコード
IPアドレス（IPv4, IPv6）を表すレコード。コンストラクタに文字列のまま与えても、内部で `Resolv::IPv[46]` に変換してくれる。親切。

IPアドレスくらいなら文字列比較でもいけそうに思えるが、IPv6には様々な省略ルールがあるため文字列比較は非常に難しい。上記の実験では結果が最大限に短縮されている上、アルファベットも大文字に変わっている。

### CNAME, NS, PTR レコード
ドメイン名を表すレコード。これらのクラスは `Resolv::DNS::Resource::DomainName` を親に持つ。コンストラクタに `Resolv::DNS::Name` を与えないといけない（文字列から自動変換されない）。

### MX, SRV レコード
ドメイン名だけでなく数値も持つレコード。これも、ドメイン名は `Resolv::DNS::Name` で、数値は `Integer` で与える必要がある。引数の順序はDNSに設定する文字列と同じなので、コードではsplatを利用して簡略化している。

### SOA レコード
括弧を用いて複数行で書くことがあるので、事前にスペースに変換してから区切っている。引数の数は多いけれども、これも文字列の順番通りなのでSRVレコードと似た対応でいい。

### TXT, SPF レコード
任意の文字列を持てるレコード。DNSに設定するときはダブルクォートで囲むが、 `Resolv::DNS` の結果ではダブルクォートが含まれないので、事前に取っておく。

なお、**resolvライブラリはまだSPFレコードのクラスを持たない**ので、上の実験ではエラーになった。issueにパッチが書かれているので、それを適用すればひとまず使えるようになる。
https://bugs.ruby-lang.org/issues/11312

## 参考

* Rubyリファレンスマニュアル https://docs.ruby-lang.org/ja/latest/library/resolv.html
* ソースコード https://github.com/ruby/ruby/blob/trunk/lib/resolv.rb
* SPFレコード対応 https://bugs.ruby-lang.org/issues/11312