Rubyでスプレッドシートからデータを取得するAPIを作成してみた(with サービスアカウント)


## 概要

スプレッドシートからデータを取得してjsonに整形するみたいなAPIを作成してみました。
意外とサービスアカウントを使っている記事が少なかったので少しでも参考になればと思います。

## 環境

Ruby: 2.5.7
Ruby on Jets: 2.3.18
(Railsライクなフレームワークなので、Railsでもできます)

## サービスアカウントについて

ユーザではなくアプリケーションレベルで使用するアカウント。ユーザで認証する場合はそのユーザがアプリケーションの管理からはずれた場合などに認証情報を変えなければならなくなるため不便。

サービスアカウントはユーザに依存しないのでユーザなど関係なくアプリケーションからAPIを叩く場合にはこちらが便利。

ちなみに今回はバックエンドからユーザなど関係なくAPIを叩くために使うのでサービスアカウントを利用しました。

## サービスアカウントの作成

作成していない場合は以下を参考に作成してください。ここでは省きます

[https://support.google.com/a/answer/7378726?hl=ja](https://support.google.com/a/answer/7378726?hl=ja)

### サービスアカウントからcredentialファイルをjsonファイルで取得

認証情報 > サービスアカウントを管理 > 作成したアカウントの操作 > 鍵を作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/923148/a52cd956-10c6-2822-ddb2-8be8869c9b12.png)

json形式で鍵を作成して、
ダウンロードしたらアプリのトップディレクトリに`google-sa-credential.json`という名前で保存

## APIを有効にする

[https://console.developers.google.com/apis/dashboard](https://console.developers.google.com/apis/dashboard)

GCPコンソールの「APIとサービス」から

- Google Drive API
- Google Sheets API

を検索してそれぞれ有効にする。

### サービスアカウントでフォルダ、スプレッドシートを作成する

サービスアカウントでスプレッドシートを操作する際には、以下のいずれかの必要がある。

- サービスアカウントによって作成されている
- サービスアカウントが編集者として共有されている
- 公開されている

([gem google driveの記述](https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md#on-behalf-of-no-existing-users-service-account))

スプレッドシートの「共有」でサービスアカウントを許可できるならば、共有した時点でこのタスクを必要としないので飛ばしてもOKです。

以下に2つのrakeタスクを記述しました

- 指定したフォルダID配下にフォルダを作成する(指定しなければ最上階にフォルダを作成する)
- 指定したフォルダID配下にスプレッドシートを作成する

```ruby

# lib/tasks/google_drive.rake

namespace :google_drive do
  desc 'Create folder with Service account'
  task :create_folder, [:title, :email_address, :collection_id] => :environment do |_, args|
    session = create_session
    folder = if args[:collection_id]
               session.file_by_id(args[:collection_id]).create_subcollection(args[:title])
             else
               session.root_collection.create_subcollection(args[:title])
             end

    folder.acl.push(type: 'user', email_address: args[:email_address], role: 'writer')
    p "Created folder: #{folder.human_url}"
  end
  
  desc 'Create sheet with Service account'
  task :create_sheet, [:title, :email_address, :collection_id] => :environment do |_, args|    
    session = create_session
    sheet = session.file_by_id(args[:collection_id]).create_spreadsheet(args[:title])
    sheet.acl.push(type: 'user', email_address: args[:email_address], role: 'writer')
    p "Created sheet: #{sheet.human_url}"
  end
  
  def create_session
    ::GoogleDrive::Session.from_service_account_key('google-sa-credential.json')
  end
end

```

```shell

$ bundle exec rake 'google_drive:create_folder[<フォルダタイトル>,<共有したいユーザのメールアドレス>]'
$ bundle exec rake 'google_drive:create_sheet[<シートタイトル>,<共有したいユーザのメールアドレス>]'
```

### 補足

collection: folderの意味

ACL: アクセス制御リスト。IAMと似て非なるもの。バケットやオブジェクトに柔軟に権限を与えるっぽい。

## その後の実装方針

1. gem google_driveを使用する
    - spreadsheet → worksheet → json
2. gem google_api_clientを使用する
    - 柔軟性がない(jsonで出力できてもどちらにせよしたい形に変換しなければならない)

json構造を柔軟に変更できるように1の方針で実装しました。

所々moduleに切り分けていますが必須ではないです
また、routingは省略します

## Gemfile

```
gem 'google_drive'
```

## controller

```ruby
# controller

session = GoogleDriveSession.create_session

service = SpreadSheetToHashService.new(session)
service.run!

render json: JSON.dump(service.records)
```

## session作成module

```ruby
# google_drive_session.rb

module GoogleDriveSession
  CREDENTIAL_PATH = 'google-sa-credential.json'

  def self.create_session
    return ::GoogleDrive::Session.from_service_account_key(CREDENTIAL_PATH)
  end
end
```

ちなみに自分の場合は開発環境以外ではSecrets Managerから取得する(gem 'aws-sdk-secretsmanager'を使用)

```ruby
# google_drive_session.rb

module GoogleDriveSession
  CREDENTIAL_PATH = 'google-sa-credential.json'

  def self.create_session
    return ::GoogleDrive::Session.from_service_account_key(CREDENTIAL_PATH) if Jets.env.development?

    credential_json = RequestSecretsManager.request('/<project_name>/google-sa-credential')
    credential_hash = JSON.parse(credential_json)
    File.open("/tmp/#{CREDENTIAL_PATH}", 'w') do |f|
      JSON.dump(credential_hash, f)
    end
    ::GoogleDrive::Session.from_service_account_key("/tmp/#{CREDENTIAL_PATH}")
  end
end

# request_secrets_manager.rb
require 'aws-sdk-secretsmanager'

module RequestSecretsManager
  def self.request(secret_name)
    client = Aws::SecretsManager::Client.new(region: Jets.aws.region)
    get_secret_value_response = client.get_secret_value(secret_id: secret_name)
    get_secret_value_response.secret_string
  end
end
```

## service

```ruby
# spread_sheet_to_hash_service.rb

class SpreadSheetToHashService
  attr_reader :records

  SPREADSHEET_ID = '<スプレッドシートのID>'
  WORKSHEET_ID = '<ワークシートのID(1ページ目は0)>'
  HEADER_COUNT = 1

  def initialize(session)
    @session = session
  end

  def run!
    worksheet = SpreadSheet.identify_worksheet_by_id(@session, SPREADSHEET_ID, WORKSHEET_ID)
    convert_worksheet_to_hash(worksheet)
  end

  private

  def convert_worksheet_to_hash(worksheet)
    # ヘッダをスキップする
    @records = worksheet.rows(HEADER_COUNT).map { |row|
      # ここでセル単位で好きな形にする
      # rowは行のこと、一行ずつのイテレータ
      # row[0]: 1列目の値
      # row[1]: 2列目の値
    }
  end
end
```

## spread sheet操作用module

```ruby
# spread_sheet.rb

module SpreadSheet
  def self.identify_worksheet_by_id(session, spread_sheet_id, work_sheet_id)
    spreadsheet = session.file_by_id(spread_sheet_id)
    spreadsheet.worksheet_by_sheet_id(work_sheet_id)
  end
end
```

## まとめ
これでスプレッドシートの内容を柔軟にjsonへ変更してレスポンスとして返すことができます！

## 参考
[gem google-drive](https://github.com/gimite/google-drive-ruby)