毎回 API 叩かない中間キャッシュ API をつくる

たまにこんなものが欲しい時があります。

* Web ページなどで外部 API 叩かせたい。
* (かつ) OAuth 認証などしてもらわずに特定のデータを外部 API で取りたい。
* (かつ) token はもちろん client に置けない。
* (かつ) **毎度 API を叩かずに、時間が空いた場合のみ fetch したデータを返したい**

つまり間に 1ホップ必要なのですが、アプリケーションとストレージが必要になります。
こんなサービスないかなと探し方もわからず見つけられてないです。

なのでとりあえず Google Apps Script での実装したものについて書きます。

# 具体的に何をしたいのかの例

* Tumblr API で特定の Feed を Web に設置する。

* Steam API で特定のデータを監視したい。

API 制限を消費したくないし、token も置けません。

**=> つまりキャッシュが欲しい**



# Google Apps Script による実装

Google Apps Script には書いた関数を Web API 化する機能があります。そして SpreadSheet へのアクセスが出来るのでデータストアとして使えます。(API 化の手順は調べるとたくさん出てくるので見て下さい。)


Steam API で自分のユーザの `persona state` を取ってくるものを例に作ります。
https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerSummaries_.28v0002.29
> personastate
The user's current status. 0 - Offline, 1 - Online, 2 - Busy, 3 - Away, 4 - Snooze, 5 - looking to trade, 6 - looking to play.


## 実装内容
* Steam の `persona state` を返す APIを作る
* 取得したデータは SpreadSheet に保存(キャッシュ)しておく。
* リクエスト時から 5分以内に更新したキャッシュがある場合はそれを返す。


# コード

```js
function doGet() {
  const cell = new CellManager("A2", "B2", getSteamState)
  const s = cell.getSmartValue();
  return jsonResponse({ state: s, updatedAt: cell.getTime() });
}

//  Steam API でユーザの状態を取る
function getSteamState() {
  const url = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=xxxxxxxxxx&steamids=76561198129612698";
  const response = UrlFetchApp.fetch(url);
  const player = JSON.parse(response).response.players[0]
  return player.personastate
}

CellManager = function(valuePos, timePos, fetchFunc) {
  this.sheet = getSheet();
  this.valueRow = this.sheet.getRange(valuePos);
  this.timeRow = this.sheet.getRange(timePos);
  this.fetchFunc = fetchFunc;
}

CellManager.prototype.getValue = function() { return this.valueRow.getValue(); }
CellManager.prototype.getTime = function() { return this.timeRow.getValue(); }

CellManager.prototype.setValue = function(v) { this.valueRow.setValue(v); }
CellManager.prototype.refreshTime = function() {
  this.timeRow.setValue(new Date());
}

CellManager.prototype.fetchValue = function() {
  const s = this.fetchFunc()
  this.refreshTime();
  this.setValue(s)
  return s
}

CellManager.prototype.isOld = function() {
  return this.getTime() < date5MinutesAgo();
}

// 前回のリクエストが5分以内だったら保存したデータを使う
CellManager.prototype.getSmartValue = function() { 
  if (!this.isOld()) {
    return this.getValue();
  } else {
    return this.fetchValue();
  }
}


// global funcs

function getSheet() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  return spreadsheet.getSheetByName('db');
}

function jsonResponse(payload) {
  ContentService.createTextOutput()
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify(payload));
  // return response-data
  return output;
}

function date5MinutesAgo() {
  const date = new Date();
  date.setMinutes(date.getMinutes() - 5);
  return date;
}
```


![Cropper_and_elzup-steam.png](https://qiita-image-store.s3.amazonaws.com/0/40717/31b79aae-2d1b-808c-e0a7-d2b68c9dfa87.png)

URL を叩くと `doGet` が実行されてレスポンスが帰ってきます。

```json
{"state":0,"updatedAt":"2018-02-06T11:59:46.582Z"}
```

データは更新されています。
![elzup-steam_-_Google_スプレッドシート.png](https://qiita-image-store.s3.amazonaws.com/0/40717/fdba6eb2-0bd6-d571-baf0-e930cb2a82eb.png)


# ちょっとコードの解説
CellManager クラスが SpreadSheet への I/O をしています。
CellManager.isOld() では記録されているタイムスタンプが 5分前より古いか確認しています。

別の API を叩きたい場合は getSteamState 関数を置き換えるだけです。
SpreadSheet の記録座標も new CellManager の引数で指定します。

date5Minutes (キャッシュ時間) のロジックも汎用にすればよかったかもしれません。

# 終わり
もっとスマートなやりかたとかあれば

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">知りたい</p>&mdash; えるざっぷ (@_elzup_) <a href="https://twitter.com/_elzup_/status/960850696655454209?ref_src=twsrc%5Etfw">February 6, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

コード処理が挟まると限られそうなのでレスポンス丸々流すのでもいいので。