TypeScript を使って Clova Custom Extension を作ろう - AWS Lambda

# はじめに

[TypeScript を使って Clova Custom Extension を作ろう - Express](https://qiita.com/daisukeArk/items/78b82bb97d4b76d7ca08)では、公式サンプルに従いExpressを利用しました。
本投稿では、前回作成したソースを元に`API Gateway + Lambda`で Clova Custom Extension を動かしてみましょう。

API Gateway, Lambda の構築については、他に良記事があるので本投稿では割愛します。

## 目次

* 拡張クライアントをさらに拡張した `Lambda` を呼び出す 拡張クライアント2の作成
* Lambda エントリポイントの作成
* テスト

## 拡張クライアントをさらに拡張した `Lambda` を呼び出す 拡張クライアント2の作成

さっそくコード。

```typescript:extension/clova-extension-lambda.ts
import * as Clova from '@line/clova-cek-sdk-nodejs';
import * as Handlers from '../handlers';

export class ClovaExtensionLambda {
  /**
   * AWS Lambda 呼出
   * @param body リクエスト本文
   */
  public async lambda(body: string): Promise<Clova.Clova.ResponseBody> {
    // ペイロード本文パース
    const eventBody = JSON.parse(body);

    // コンテキスト作成
    const context = new Clova.Context(eventBody);

    let requestHandler: any;

    switch (context.requestObject.request.type) {
      case 'LaunchRequest':
        requestHandler = Handlers.LaunchRequestHandler.handle;
        break;
      case 'IntentRequest':
        requestHandler = Handlers.intentHandlers;
        break;
      case 'SessionEndedRequest':
      default:
        requestHandler = Handlers.SessionEndedRequestHandler.handle;
        break;
    }
    if (requestHandler) {
      // ハンドラ呼出
      await requestHandler.call(context, context);

      return context.responseObject;
    } else {
      const error = new Error(`Unable to find requestHandler for '${context.requestObject.request.type}'`);

      throw error;
    }
  }
}
```

[\[clova-cek-sdk-nodejs - GitHub\]](https://github.com/line/clova-cek-sdk-nodejs/blob/master/src/client.ts#L62-L88)の62〜88行目で書かれている処理を行ってレスポンスオブジェクトを返すようにしてあげます。

## Lambda エントリポイントの作成

Expressの場合は、エントリポイントは以下のコードでした。

```typescript:index.ts
import * as Clova from '@line/clova-cek-sdk-nodejs';
import * as bodyParser from 'body-parser';
import * as Express from 'express';
import * as Handlers from './handlers';

const app = Express();

const clovaHandler = Clova.Client
  .configureSkill()
  .onLaunchRequest(Handlers.LaunchRequestHandler.handle)
  .onIntentRequest(Handlers.intentHandlers)
  .onSessionEndedRequest(Handlers.SessionEndedRequestHandler.handle)
  .handle();

app.post('/clova', bodyParser.json(), <Express.RequestHandler>clovaHandler);

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on ${port}`);
});
```

これを Lambda に合わせて以下のように書き換えます。

* 先程作成した `extensionHandler.lambda()`にリクエスト本文（文字列）を渡す
* レスポンスが取得できればその結果を、取得出来なければエラーを返す

```typescript:index.ts
import * as Clova from '@line/clova-cek-sdk-nodejs';
import * as Lambda from 'aws-lambda';
import * as Util from 'util';
import { ClovaExtensionLambda } from './extension/clova-extension-lambda';
import { LoggerFactory } from './helpers/logger-factory';

/**
 * エントリポイント
 * @param event イベントソース
 * @param context コンテキスト
 * @param callback コールバック
 */
export const handler = async (
  event: Lambda.APIGatewayEvent,
  context: Lambda.Context,
  callback: Lambda.APIGatewayProxyCallback
) => {
  // ハンドラ作成(Lambda用)
  const extensionHandler = new ClovaExtensionLambda();

  // レスポンス本文
  let responseBody: Clova.Clova.ResponseBody | null = null;

  try {
    // ハンドラ呼出
    responseBody = await extensionHandler.lambda(String(event.body));
  } catch (error) {
    LoggerFactory.instance.trace(error.message);
  }

  // レスポンス
  let response: Lambda.APIGatewayProxyResult;

  if (responseBody !== null) {
    // トレースログ
    LoggerFactory.instance.trace(Util.inspect(responseBody, { depth: null }));

    response = {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(responseBody)
    };
  } else {
    response = {
      statusCode: 500,
      body: 'Internal Server Error'
    };
  }

  // コールバック
  callback(null, response);
};
```

## テスト

Lambda エントリポイントを呼出して結果が返ってくることを確認しましょう。
テストクラスを作成します。

### テストクラスの作成

```typescript:test/src/index.test.ts
import * as Lambda from 'aws-lambda';
import 'mocha';
import { IMock, It, Mock } from 'moq.ts';
import * as Assert from 'power-assert';
import * as Util from 'util';
import { handler } from '../../src/';
import { LoggerFactory } from '../../src/helpers/logger-factory';
import { helloWorldIntentRequest } from '../fixtures/hello-world-intent-request';

describe('index.handler のテスト', () => {
  let eventMock: IMock<Lambda.APIGatewayProxyEvent>;
  let contextMock: IMock<Lambda.Context>;

  /**
   * 前処理
   */
  before(async () => {
    // イベントモック
    eventMock = new Mock<Lambda.APIGatewayProxyEvent>('eventMock')
      .setup((self: Lambda.APIGatewayProxyEvent) => self.body)
      .returns(JSON.stringify(helloWorldIntentRequest));
    // コンテキストモック
    contextMock = new Mock<Lambda.Context>('contextMock');
  });

  // 検証
  it('正常終了すること', async () => {
    try {
      // 実行
      await handler(
        eventMock.object(),
        contextMock.object(),
        (error?: string | Error | null | undefined, result?: Lambda.APIGatewayProxyResult | undefined) => {
          if (error) {
            Assert.fail(error);
          } else {
            if (result) {
              // トレースログ
              LoggerFactory.instance.info(Util.inspect(JSON.parse(result.body), { depth: null }));
            }

            Assert.ok('正常終了');
          }
        }
      );
    } catch (error) {
      Assert.fail(error);
    }
  });
});
```

### テスト実行

トランスパイル後テスト実行し、以下のように結果が返ってくれば成功です。

```bash
# テスト実行
$ NODE_ENV=production npm test

index.handler のテスト
[2018-07-24T23:59:46.523] [INFO] default - { response:
   { card: {},
     directives: [],
     outputSpeech:
      { type: 'SimpleSpeech',
        values: { lang: 'ja', type: 'PlainText', value: 'こんにちは、東京都' } },
     shouldEndSession: false,
     reprompt:
      { outputSpeech:
         { type: 'SimpleSpeech',
           values: { lang: 'ja', type: 'PlainText', value: '他に何かご用ですか？' } } } },
  version: '1.0' }
    ✓ 正常終了すること


  1 passing (21ms)
```

## まとめ

`API Gateway + Lambda`で動いている体で Clova Custom Extension を動かしてみました。
REST APIのバックエンドとしてLambdaを選択することでAlexaと同じ感覚で実装が可能になりますね。

今回のソースは以下にあります。

[daisukeArk/clova-extension-sample-node-lambda - GitHub](https://github.com/daisukeArk/clova-extension-sample-node-lambda)