Google Apps Script(GAS)1で都道府県マスタをjsonで返すAPI
概要
Googleアカウントさえあれば無料でAWS LambdaのようなAPIが作れるということなので、使い方の備忘録として都道府県マスタを返すAPIでも作ってみる。
方針
- GET /prefectures
- 都道府県一覧を北から南の順で返す
- webサイトではアクセスの多い東京都を一番上に置いて選択させるようなものも見るけど、東京だけ配置順じゃないのは気持ち悪いので、そういう場合は東京は真ん中らへんに置いたままデフォルトで東京が選ばれてればいいと思った。
- GET /prefectures/:ID
- IDは
tokyo
,kyoto
など。数値にしない理由はレガシーアンチパターンを参照。
データを保存しないのでPOST/PUT無し。市区町村や祭日マスタなども将来的にはやりたい。
実装
環境
GAS APIを利用することでGoやJavaなどで開発したものをアップできる。ひとまずブラウザで完結させようとするとレガシーなJavascriptを強制され、letやアロー表記も使えない。つらい。
Google SpreadsheetsなどのGoogle Docsのファイルも参照できるので簡易データベースにできるのではないか?こちらもひとまずソースに直書きして動作確認してから検討する。
→ GACにロックインしてしまうので記事を分けることにした。
Web APIとしての挙動
まずは https://script.google.com/home から新規スクリプトを作成。表示された関数を以下のように変更する。
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...
を選択すると下記のようなダイアログがでる。
導入
を押すと公開用のURLが表示される。
https://script.google.com/macros/s/xxxxxxxxx-xxxx/exec
アクセスしてみるとdoGet()に渡された引数が表示されている。
{"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{},"pathInfo":""}
パスやパラメータが取得できそうなので、URLのexec
の後ろに追加して.../exec/prefectures/aomori
のようにすると
{"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{},"pathInfo":"prefectures/aomori"}
pathInfoからexec/
より後ろの部分が取得できることがわかる。
一覧処理の仮実装
ではこのpathInfoを分解し、prefectures
だったらindexを返すようにする。
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で取得できた。
[{"id":"hokkaido","title":"北海道","sort_order":1},{"id":"aomori","title":"青森","sort_order":2}]
個別表示の実装処理
routeにid指定がある場合の条件分岐を追加する。都道府県データがindex()の中にあると再利用しづらいので外にvarで宣言する。
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を作る。
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に用意されているクラスで(多分)、ブラウザエディタでは実行後にCtrl+Enterを押すとログを表示してくれる。
/* 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
-
GET /prefectures/hokkaido
-
GET /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
-
Google AppsはG Suiteに名称変更されました。Google Apps Scriptは置いてけぼりを喰らったいらない子、、なのかな、、、わたし><。 ↩