エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

aws-sdkで取得できるタグを扱いやすくするための覚書

動機

AWS のリソースの多くは key-value の組みをタグとして設定できるようになっているのですが。

例えば EC2 インスタンスを取得する aws-sdk のメソッド Aws::EC2::Client#describe_instances のレスポンスは次のようになっています。

resp.reservations[0].instances[0].tags #=> Array
resp.reservations[0].instances[0].tags[0].key #=> String
resp.reservations[0].instances[0].tags[0].value #=> String

docs.aws.amazon.com

つまり辞書形式になっているわけではなく、単に key と value のペアを格納するクラスの配列にすぎません。

実装を紐解いてみると、次のような構造になっていました。

class Tag < Struct.new(:key, :value)
end

key から value を引きたい場合、たとえば次のようなコードを書くことになります。

tags = [
  Tag.new("foo", 123),
  Tag.new("bar", 456),
  Tag.new("baz", 789)
]

pp tags.find { |tag| tag.key == "foo" }&.value  # => 123
pp tags.find { |tag| tag.key == "bar" }&.value  # => 456
pp tags.find { |tag| tag.key == "baz" }&.value  # => 789
pp tags.find { |tag| tag.key == "hoge" }&.value # => nil

これがもう少しどうにかならないか、というのが今回のお題です。

クラスで包む

Tag の配列を内部に持ち、key-value アクセスを容易にするメソッドを用意するパタンです。

class Tags
  def self.[](tags)
    new(tags)
  end

  def initialize(tags)
    @tags = tags
  end

  def [](key)
    @tags.find { |tag| tag.key == key }&.value
  end
end

pp Tags[tags]["foo"]  # => 123
pp Tags[tags]["bar"]  # => 456
pp Tags[tags]["baz"]  # => 789
pp Tags[tags]["hoge"] # => nil

これは C++ のときに割と好んで利用していた方法です。

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Tag {
    std::string key;
    int value;
};

class Tags {
public:
    Tags(const std::vector<Tag>& tags) : tags_(tags) {}

    int operator [] (const std::string& key) {
        auto tag = std::find_if(tags_.begin(), tags_.end(), [&](const Tag& tag) { return tag.key == key; });
        return tag->value;
    }

private:
    const std::vector<Tag>& tags_;
};

int main(int, char*[]) {
    auto tags = {
        Tag { "foo", 123 },
        Tag { "bar", 456 },
        Tag { "baz", 789 }
    };

    std::cout << Tags(tags)["foo"] << std::endl;
    std::cout << Tags(tags)["bar"] << std::endl;
    std::cout << Tags(tags)["baz"] << std::endl;
}

これは、動的にクラスのメソッドを変更できない環境ではよくある方法と思いますが、Ruby では個々のオブジェクトにも固有のメソッドを追加できることを利用して、tags オブジェクトにメソッドを追加してみようと思います。

オブジェクトを拡張する (1)

今回はメソッドを定義する Tags をモジュールとして定義し、Object#extend を利用して tags オブジェクトにメソッドを組み込んでいます。

module Tags
  def [](key)
    find { |tag| tag.key == key }&.value
  end
end

tags.extend(Tags)

pp tags["foo"]  # => 123
pp tags["bar"]  # => 456
pp tags["baz"]  # => 789
pp tags["hoge"] # => nil

ただし #[] を上書きしてしまうため Array として機能しなくなるというかなり重度な欠点を抱えます。 Array と被らない名前を選べばよい話ではあるのですが。

pp tags[0] # => nil
pp tags[1] # => nil
pp tags[2] # => nil

オブジェクトを拡張する (2)

より Ruby ならではの方法といえば、やはり BasicObject#method_missing のオーバーライド。 アクセスできるキーがメソッドとして記述できる識別子に限られるという制約がありますが、オブジェクトの属性のように扱うことができるのでとても強力です。

module Tags
  def method_missing(name)
    key = name.to_s
    find { |tag| tag.key == key }&.value
  end
end

tags.extend(Tags)

pp tags.foo  # => 123
pp tags.bar  # => 456
pp tags.baz  # => 789
pp tags.hoge # => nil