AWS SDKのラッパーを作るとき用の便利concern

追記：EC2上でSDKを利用する場合は、IAMロールを利用しましょう。

RubyでAWS SDK（[aws/aws-sdk-ruby](https://github.com/aws/aws-sdk-ruby)）を使うときに、
そのままアプリケーションのコードに組み込んで使う人はそんなにいないと思う。
SDKのコードを直接組み込むと、ユニットテストしづらくなるし設定用のコードが各所に散らばることになる。
なので、簡単なラッパー＆クライアントのファクトリクラスを書くと良い感じに手軽にコードも書けるし
テストも書けるという望ましい状態になるかと思います。

今回は、SDK使う際に自分が書いた簡単なコードを紹介します。

注：AWS SDKはもうすぐv2がリリースされそうな感じですがここではv1のコードを元に記事を書いています。

まず、便利concern。これをファクトリクラスにincludeして使う。
そうすると、config/initializers/aws.rbで使う用の.configureメソッドと、
設定済みクライアントの取得ができる.clientメソッドが生えてくる。
実装が若干おまじないっぽい。

```ruby:lib/my_app/aws/configurable.rb
module MyApp::AWS::Configurable
  extend ActiveSupport::Concern

  def client
    self.class.client
  end

  included do
    class << self
      # cache client object until re-configuring
      def client
        @client ||= service_class.new(configuration.to_h)
      end

      def configure(&block)
        # remove client cache at first
        clear_client_cache!
        options = Options.new(AWS::Core::Configuration.accepted_options)
        yield options
        @aws_core_configuration = AWS::Core::Configuration.new(aws_options.to_h)
      end

      def configuration
        stub_configuration || @aws_core_configuration
      end

      def stub_configuration
        # override at your spec_helper
      end

      def clear_client_cache!
        @client = nil
      end

      protected
      def service_class
        matched = self.to_s.match(/\AMyApp\:\:([a-zA-Z_\:]+)/).captures[0]
        (matched + '::Client').constantize
      end
    end
  end

  class Options < BasicObject
    def initialize(accepted_options)
      @accepted_options = accepted_options
      @config = {}
    end

    def to_h
      @config
    end

    def method_missing(method, *args)
      return super unless setter?(method.to_s)

      name = extract_name(method.to_s)
      return super unless accept?(name)

      @config[name] = args[0]
    end

    def setter?(method)
      method =~ /\A\w*=\z/
    end

    def extract_name(method)
      method.match(/\A(\w*)=\z/).captures[0].to_sym
    end

    def accept?(name)
      @accepted_options.include?(name)
    end
  end
end
```

ファクトリクラスの定義

```ruby:lib/my_app/aws/sns.rb
module MyApp
  module AWS
    class SNS
      include MyApp::AWS::Configurable
      
      # 以下に必要なコードがあれば書いてく
    end
  end
end
```

初期化処理。よくあるアクセスキーの設定がアプリケーションのコードから分離できる

```ruby:config/initializers/aws.rb
MyApp::AWS::SNS.configure do |conf|
  conf.access_key_id = Rails.application.secrets.aws_access_key_id
  conf.secret_access_key = Rails.application.secrets.aws_secret_access_key
  conf.region = Rails.application.secrets.aws_region_sns
end
```

ファクトリクラスの使い方。ファクトリクラスのclientメソッドから
初期化済みのクライアントのインスタンスが取得できるので、設定処理が散らかることがない。
これを元にラッパークラスを書いていく。

```ruby:lib/my_app/hoge.rb
class MyApp::Hoge
  def create(xxx)
    sns = MyApp::AWS::SNS.client
    sns.create_platform_endpoint(xxx)
  end
end
```

テストを書くときは、AWS::Core::Configurationの設定にstub_requestsをtrueで渡してやる。
すると、requestがstubのresponseを返すようになる。
しかしこのままだと、空のAWS::Core::Responseが返ってくるのみ。

```ruby:spec/support/aws.rb
module MyApp
  module AWS
  
    class SNS
      def self.stub_configuration
        AWS::Core::Configuration.new(
          access_key_id: 'ACCESS_KEY_ID',
          secret_access_key: 'SECRET_ACCESS_KEY',
          stub_requests: true,
        )
      end
    end
    
  end
end
```

このようにstub_forでレスポンスの値を書くと実際のレスポンスを好きな値にすることができる。
ただし、SDKだけではモック化には対応してない。
参考： [Stubbing AWS Responses - AWS Developer Blog - Ruby](http://ruby.awsblog.com/post/Tx2SU6TYJWQQLC3/Stubbing-AWS-Responses)

```ruby:spec/lib/my_app/xxx.rb
before do
  sns = MyApp::AWS::SNS.client
  resp = sns.stub_for(:create_platform_endpoint)
  resp.data[:endpoint_arn] = 'arn:aws....'
end

after do
  MyApp::AWS::SNS.clear_client_cache!
end

it 'xxxx xxx xxx' do
  resp = MyApp::Hoge.create(xxx)
  expect(resp[:endpoint_arn]).to eq('arn:aws....')
end
```

こんな感じでサクッとテストも書けるようになりました。便利。