Google Apps Scriptを使ってサーバーレスファンクションを作る

# Google Apps Script(GAS)[^1]で都道府県マスタをjsonで返すAPI

[^1]: Google AppsはG Suiteに名称変更されました。Google Apps Scriptは置いてけぼりを喰らったいらない子、、なのかな、、、わたし＞＜。

## 概要

Googleアカウントさえあれば無料でAWS LambdaのようなAPIが作れるということなので、使い方の備忘録として都道府県マスタを返すAPIでも作ってみる。

## 方針

- GET /prefectures
 - 都道府県一覧を北から南の順で返す
 - webサイトではアクセスの多い東京都を一番上に置いて選択させるようなものも見るけど、東京だけ配置順じゃないのは気持ち悪いので、そういう場合は東京は真ん中らへんに置いたままデフォルトで東京が選ばれてればいいと思った。
- GET /prefectures/:ID
 - IDは`tokyo`,`kyoto`など。数値にしない理由は[レガシーアンチパターン](https://qiita.com/hakaicode/items/94db16ed99d071225ae8)を参照。

データを保存しないのでPOST/PUT無し。市区町村や祭日マスタなども将来的にはやりたい。

## 実装

### 環境
GAS APIを利用することでGoやJavaなどで開発したものをアップできる。ひとまずブラウザで完結させようとするとレガシーなJavascriptを強制され、letやアロー表記も使えない。つらい。

Google SpreadsheetsなどのGoogle Docsのファイルも参照できるので簡易データベースにできるのではないか？こちらもひとまずソースに直書きして動作確認してから検討する。

→ GACにロックインしてしまうので記事を分けることにした。

### Web APIとしての挙動

まずは https://script.google.com/home から新規スクリプトを作成。表示された関数を以下のように変更する。

```javascript
function doGet(e){
  return toOutput(JSON.stringify(e));
}
  
function toOutput(value){
  ContentService.createTextOutput()
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(value);
  return output;
}
```

Web APIとして実装するにはdoGet()関数を用意し、ContentServiceでtextOutputにしてあげればよさそう。メニューから`Publish` -> `Deploy as web app...`を選択すると下記のようなダイアログがでる。

<img width="348" alt="gas.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16962/7d8fe998-6438-cbb9-748a-1796a752b186.png">

`導入`を押すと公開用のURLが表示される。

`https://script.google.com/macros/s/xxxxxxxxx-xxxx/exec`

アクセスしてみるとdoGet()に渡された引数が表示されている。

```json
{"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{},"pathInfo":""}
```

パスやパラメータが取得できそうなので、URLの`exec`の後ろに追加して`.../exec/prefectures/aomori`のようにすると

```json
{"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{},"pathInfo":"prefectures/aomori"}
```

pathInfoから`exec/`より後ろの部分が取得できることがわかる。

### 一覧処理の仮実装

ではこのpathInfoを分解し、`prefectures`だったらindexを返すようにする。

```javascript
function doGet(e){
  return toOutput(JSON.stringify(route(e)));
}
...
function route(e){
  var pathes = new String(e.pathInfo).split('/');
  if (pathes.length==1 && e.queryString=='') return index();
  else return {'message': 'no route' }
}

function index() {
  return [
    { id: 'hokkaido', title: '北海道', sort_order: 1},
    { id: 'aomori', title: '青森', sort_order: 2},
  ];
}
```

queryStringが空文字列かどうかを見ているのは、`GET /prefectures?ids=hokkaido,aomori`のように検索条件を指定できるようにしておきたいから(それはindex()の中でqueryStringに応じて処理を変更すればいいか)。

これで再度公開する。この時プロジェクトバージョンは新規を選ばないといけないのかな、デバッグしてるとバージョン増えまくってうざい。

パスに`/prefectures`をつけてURLにアクセスすると都道府県の配列がjsonで取得できた。

https://script.google.com/macros/s/AKfycbx2hdlUcIZoHo7KDxTK5UDfGHs3Ljlvy84-tiZVTC5NHixmYa0/exec/prefectures

```json
[{"id":"hokkaido","title":"北海道","sort_order":1},{"id":"aomori","title":"青森","sort_order":2}]
```

### 個別表示の実装処理

routeにid指定がある場合の条件分岐を追加する。都道府県データがindex()の中にあると再利用しづらいので外にvarで宣言する。


```javascript

function route(e){
  var pathes = new String(e.pathInfo).split('/');
  if (pathes.length==1 && e.queryString=='') return index();
  if (pathes.length==2) return show(pathes[1]);
  else return {'message': 'no route' }
}

var prefectures = ...
function index() {
  return prefectures;
}

function show(id) {
  return findById(id, prefectures);
}

// find by id from array.
function findById(id, array) {
  Logger.log('findById: '+ id);
  for(var i=0; i<array.length; i++){
    if(array[i].id == id) return array[i];
  }
}
```

これを公開して新しいURL+`/prefectures/aomori`でアクセスすると青森県の情報だけ表示される。んー素晴らしい。

### 機能追加に向けリファクタリング
さてそろそろ全部グローバルだとどの関数を使うべきか把握しておくのが面倒になってきた。Prefecture処理をいい感じに切り出しておきたい。メインの`route()`では`pathes[0]`が`prefectures`かどうかだけを判断してPrefectureControllerに投げよう。ファイルメニューから`新規スクリプトファイル`を指定してPrefectureController.jsを作る。

```javascript:PrefectureController.gs
function PrefectureController() {}

PrefectureController = {
  route: function(e) {
    Logger.log('controller: '+JSON.stringify(e));
    var prefectures = getPrefectures();
    var pathes = new String(e.pathInfo).split('/');
    if (pathes.length==1 && e.queryString=='') return index();
    else return show(pathes[1]);
  
    // GET /prefectures
    function index() {
      Logger.log('index');
      return prefectures;
    }
    
    // GET /prefectures/:id
    function show(id) {
      Logger.log('show: '+ id);
      return findById(id, prefectures)
    }
    
    /* Prefectures of Japan */
    function getPrefectures() { 
      return [
        { id: 'hokkaido', title: '北海道', sort_order: 1},
        { id: 'aomori', title: '青森', sort_order: 2},
      ];
    }
  }
}
```

外部からはroute() だけしか見えないようになっている。他のものはすべてrouteメソッドの中のローカルスコープ。findByIdはメインコントローラーのファイルにグローバルのまま残してるのでどこからでもアクセス可能。この分け方なら複数ID(?ids='hyogo','gifu','hyogifu')のリクエストが処理するような機能拡張も容易である。

### メインの方の変更
```
function route(e){
  var pathes = new String(e.pathInfo).split('/');
  Logger.log(pathes);
  switch( pathes[0] ) {
    case 'prefectures': return PrefectureController.route(e);
    default: return { message: 'no route' }; // to be 404
  }
}
```
メインのrouteではpathes[0]だけを見てどのコントローラーに割り振るか判断するだけで、実装詳細には関知しない。こうすることで次の市区町村マスタCityControllerが追加しやすくなった。


### テスト

一覧と個別のテストを書く。ひとまず`Logger.log`で吐いて目で確認する。`Logger`はGASに用意されているクラスで(多分)、ブラウザエディタでは実行後に<kbd>Ctrl+Enter</kbd>を押すとログを表示してくれる。

```javascript

/* test functions */
function testIndex(){
  _testCall({ 'pathInfo': 'prefectures', 'queryString': ''});
}
function testShow(){
  _testCall({ 'pathInfo': 'prefectures/aomori' });
}
function _testCall(s){
  Logger.log( doGet( s ).getContent() );
}
```
これでそれぞれ一覧とテストを実行し結果を画面に表示する。_testCallが文字列で返すようになれば、その中に期待する言葉(たとえば'青森')が入っているかどうかでassertを投げればよい。

### スプレッドシート操作等

GACの最大の特徴であるGoogle Docsとの連携であるが、これをやるとG Suiteにロックインされてしまう。ので、この記事はここで区切りDocs連携は別記事で行う。

## Appendix

<!-- ### 全ソース -->

### アクセスURL

- GET /prefectures
 - https://script.google.com/macros/s/AKfycbx2hdlUcIZoHo7KDxTK5UDfGHs3Ljlvy84-tiZVTC5NHixmYa0/exec/prefectures

- GET /prefectures/hokkaido
 - https://script.google.com/macros/s/AKfycbx2hdlUcIZoHo7KDxTK5UDfGHs3Ljlvy84-tiZVTC5NHixmYa0/exec/prefectures/hokkaido

- GET /prefectures/aomori
 - https://script.google.com/macros/s/AKfycbx2hdlUcIZoHo7KDxTK5UDfGHs3Ljlvy84-tiZVTC5NHixmYa0/exec/prefectures/aomori

ちゃんと使うときは独自ドメインや短縮URLを利用する。

### 感想
レガシーJavascriptとブラウザのへぼIDEに疲れたよ。でもスプレッドシートはそのうちやるよ。市区町村データは量が多いからスプレッドシートじゃないと厳しいし。レガシーJavascriptでクラスメソッドとインスタンスメソッドの定義の仕方の違いがわかるようになったよ。他人のjs読むときに役に立つかも。


### 参考サイト

- GAS 利用制限 https://cartman0.hatenablog.com/entry/2017/12/17/Google_Apps_Script%E3%81%AE%E7%84%A1%E6%96%99%E5%88%B6%E9%99%90%E3%83%A1%E3%83%A2
- JS ClassMethod https://so-zou.jp/web-app/tech/programming/javascript/grammar/function/class/#class-method
- JS Array https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/array/slice