YAMLファイルからGoogle フォームを作成する

#はじめに
この記事は[LIFULL Advent Calendar2018](https://qiita.com/advent-calendar/2018/lifull)の12日目の記事です。
唐突ですがみなさん、個々の設定ファイルの管理やAnsibleなど、日々YAMLファイルを使う場面は多いのではないでしょうか。
ふとYAMLファイルからなんでも作成できるのでは？という衝動にかられ、ちょうど触る機会のあったGoogle フォームをYAMLから作成できないかということで作成してみました。
今回は、Google Apps Script(以降、GAS)を用いて作成を行います。

#概要
こんな感じのYAMLファイルからフォームを作成します。

```yaml:form.yml
title: フォームタイトル
description: |
  これはYAMLから作成したGoogleフォームです。
  概要は複数行書くことができます。
collectEmail: True
allowResponseEdits: True
confirmationMessage: ご協力あざす
sections:
  - title: Section1
    questions:
      - type: CHECKBOX
        title: 好きな動物は？
        required: True
        resource:
          choices:
            - title: いぬ
              goto: CONTINUE
            - title: ねこ
              goto: CONTINUE
            - title: いません
              goto: SUBMIT
  - title: Section2
    questions:
      - type: TEXT
        title: どんなところが好きですか？
        required: True
```

(厳密に作り込んでいませんので、抜けている機能などはご容赦ください。)

# 開発手順
1. GASを使って連想配列からGoogle フォームを作成する
2. ローカルのYAMLファイルを読み込めるようにする

# 1. GASを使って連想配列からGoogle フォームを作成する
GASを使ってGoogle フォームを作成します。
使用可能なGoogle フォーム APIは[公式ドキュメント](https://developers.google.com/apps-script/reference/forms/)に記載されています。

## フォームの基本設定
まずは、フォームの基本設定を行います。
ここでは、以下の設定を行います。

- フォームのタイトル
- フォームの概要
- 完了画面のメッセージ
- 回答済フォームの編集可否
- メールアドレスの収集有無


```js:form.gs
function createForm(data){
  // フォームを新規作成
  const form = FormApp.create(data.title);

  // フォームの基本設定
  form.setTitle(data.title)
      .setDescription(data.description)
      .setConfirmationMessage(data.confirmationMessage)
      .setAllowResponseEdits(data.allowResponseEdits)
      .setCollectEmail(data.collectEmail);
}
```

## セクションの追加
次にセクションを追加します。
Google フォームでは、セクションごとに質問を束ねることができ、回答ごとの質問の分岐を行うことができます。
ここでは、セクションの追加とタイトル設定のみを行います。

```js:form.gs
function createForm(data) {
  ...

  // フォームにセクションを追加
  data.sections.forEach(function(section) {
    addSection(form, section);
  });
}

function addSection(form, section) {
  // セクションを新規作成
  const sectionItem = form.addPageBreakItem();

  // セクションの基本設定
  sectionItem.setTitle(section.title);
}
```

## 質問の追加
最後に質問を追加します。
Google フォームでは、扱える質問のタイプがいくつかありますが、それぞれ設定できるオプションも異なるので少し無理矢理作ります。

```js:form.gs
function addSection(form, section) {
  ...

  // セクションに質問を追加
  section.questions.forEach(function(question) {
    addQuestion(form, question);
  });
}
```

```js:form.gs
function addQuestion(form, question) {
  const type = question.type
    , title = question.title
    , required = question.required
    , resource = question.resource
    ;
  var item;

  switch(type) {
  case 'CHOICE':
    item = resource.multiple ? form.addCheckboxItem() : form.addMultipleChoiceItem();
    var choices = resource.choices.map(function(choice) {
      return !resource.multiple && choice.goto ? item.createChoice(choice.title, FormApp.PageNavigationType[choice.goto]) : item.createChoice(choice.title);
    });
    item.setChoices(choices);
    break;
  case 'GRID':
    item = resource.multiple ? form.addCheckboxGridItem() : form.addGridItem();
    item.setRows(resource.rows)
        .setColumns(resource.columns);
    break;
  case 'LIST':
    item = form.addListItem();
    var choices = resource.choices.map(function(choice) {
      return choice.goto ? item.createChoice(choice.title, FormApp.PageNavigationType[choice.goto]) : item.createChoice(choice.title);
    });
    item.setChoices(choices);
    break;
  case 'SCALE':
    item = form.addScaleItem();
    item.setBounds(resource.lower.value, resource.upper.value)
        .setLabels(resource.lower.label, resource.upper.label);
    break;
  case 'TEXT':
    item = resource.multiple ? form.addParagraphTextItem() : form.addTextItem();
    break;
  case 'DATE':
    item = resource.includeTime ? form.addDateTimeItem() : form.addDateItem();
    item.setIncludesYear(resource.includeYear);
    break;
  case 'TIME':
    item = form.addTimeItem();
    break;
  case 'DURATION':
    item = form.addDurationItem();
    break;
  case 'IMAGE':
    const image = UrlFetchApp.fetch(resource.url);
    item = form.addImageItem();
    item.setImage(image);
    break;
  case 'VIDEO':
    item = form.addVideoItem();
    item.setVideoUrl(resource.url);
    break;
  default:
    return null;
  }

  item.setTitle(title);
  if (type != 'IMAGE' && type != 'VIDEO') {
    item.setRequired(required);
  }

  return item;
}
```

はい！あっという間にGASでGoogle フォームを作成できるようになりました。
createForm関数に以下のようなダミーデータを渡すとフォームが作成できることが確認できると思います。

```js:ダミーデータ
data = {
  title: 'フォームタイトル',
  description: '概要',
  collectEmail: true,
  allowResponseEdits: true,
  confirmationMessage: 'あざす',
  sections: [
    {
      title: 'Section',
      questions: [
        {
          type: 'TEXT',
          title: 'これは質問です',
          required: true
        }
      ]
    }
  ]
}            
```

ただ、これでは任意のデータを読み込ませることができませんので、
ローカルのYAMLファイルを読み込ませるようにします。

# 2. ローカルのYAMLファイルを読み込めるようにする
GASでは、[js-yaml](https://github.com/nodeca/js-yaml)のようなモジュールを読み込めないようなので、
今回はJavaScriptでYAMLのロードを行います。

まずはGAS側の実装を行います。
Google フォームにファイル読み込み用のメニューを新規作成し、
メニューボタンが押下されたら、upload関数を呼び出すことでファイル読み込み用のHTMLをロードします。

```js:form.gs
// Google フォームに独自のメニューを追加する
function onOpen() {
  FormApp.getUi()
         .createMenu('データからフォームを作成')
         .addItem('ファイルを追加', 'upload')
         .addToUi();
}

// ファイル読み込み用のHTMLをモーダルで表示する
function upload() {
  const html = HtmlService.createTemplateFromFile('upload')
                          .evaluate();
  FormApp.getUi()
         .showModalDialog(html, 'YAMLファイルを読み込み');
}
```

こんな感じのメニューが作成されます。
<img width="300" alt="menu" src="https://qiita-image-store.s3.amazonaws.com/0/210983/b8e40edb-5012-3542-14c0-5eaf69ac9943.png">

次にファイル読み込み用のHTMLを作成します。
ファイルをアップロードし、SubmitされたらYAMLファイルをjs-yamlを用いて連想配列に変換し、GASのcreateForm関数に渡します。

```html:upload.html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.12.0/js-yaml.min.js"></script>
    <script>
      function onSubmit(form) {
        const reader = new FileReader();
        reader.readAsText(form.file.files[0]);
        reader.onload = function () {
          const data = jsyaml.load(reader.result);
          google.script.run.createForm(data);
        }
      }
    </script>
  </head>
  <body>
    <form id="form" onsubmit="onSubmit(this)">
      <input name="file" type="file" /><br>
      <button type="submit">OK</button>
    </form>
  </body>
</html>
```

こんな感じのビューが作成されます。
<img width="400" alt="upload-view" src="https://qiita-image-store.s3.amazonaws.com/0/210983/89b72853-5951-e70b-71d0-0caf71c6b838.png">

できあがり。
<img width="726" alt="form-view" src="https://qiita-image-store.s3.amazonaws.com/0/210983/e32d7641-d49d-6583-0f44-6004d81d0920.png">

# おわりに
なんとなく最初からわかっていたかと思いますが、Google フォームの扱いやすさを考えると、YAMLでフォームを作成するほうが圧倒的に手間がかかりそうですね。。