OpenAPI 定義と Aspida で型付きの Symbol API レスポンスを取り扱う



# Symbol SDK 2.x系と3.x系

Symbol SDK は3.x系からガラッと実装が変わっています。というか作り直されています。

大まかな点で変化をまとめると、

| 2.x | 3.x |
|:-:|:-:|
| WEB APIアクセスクラスがある | WEB APIに関与しない |
| RxJSでストリームを扱える | RxJSに依存しない |
| TypeScript | **JavaScript** |

（全部隅々まで見たわけではないので若干間違っているかもしれませんが）

SDK 2.xの大部分はAPIアクセスクラスを占めていたと言っても過言ではなく、その部分がごっそりなくなりました。
また、全体的な実装ボリュームも削減されたので、機能としても大幅に削減されています。
RxJSはWeb APIクラスが依存していましたが、それが無くなったので依存から無くなりました。

理由はコアデブなどに確認したわけではないので、憶測ですが、

- 規模が大きくなりすぎていて、メンテが行き届かなくなり、必要な機能だけに絞り込んだ小さなSDKを目指している
- 他の言語と同様のインターフェイスと使い心地を維持するため、リアクティブを止めた

という狙いがあるように思えます。

参考までに公開されているパッケージのリンクを張っておきますが、この記事においては、SDKの利用については触れません。


## SDK 2.x

https://www.npmjs.com/package/symbol-sdk/v/2.0.0

https://github.com/symbol/symbol-sdk-typescript-javascript


## SDK 3.x

https://www.npmjs.com/package/symbol-sdk/v/3.0.0

https://github.com/symbol/symbol/tree/dev/sdk/javascript


## WEB API エンドポイント

エンドポイントの定義はここで確認できます。

https://symbol.github.io/symbol-openapi/v1.0.4/

https://docs.symbol.dev/ja/api.html#endpoints

そして、API定義はOpen APIフォーマットで定義されており、リポジトリはこちらです。

https://github.com/symbol/symbol-openapi

例えば、次のようにアクセスします。

```curl
$ curl -k https://001-joey-dual.symboltest.net:3001/node/info | jq .
{
  "version": 16777987,
  "publicKey": "AAA1922FA60DB681092CBE70A9A1BAB85745025310AE7567F95EA7FD05B3D3FC",
  "networkGenerationHashSeed": "7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836",
  "roles": 7,
  "port": 7900,
  "networkIdentifier": 152,
  "host": "001-joey-dual.symboltest.net",
  "friendlyName": "001-joey-dual",
  "nodePublicKey": "05E5C0841720AE9DA737C184429002E917F74FF7A3BF8E11AC2862C4875B3D7E"
}
```

よくある`JSON`を返却するWEB APIです。
ここでは`curl`でアクセスしてみましたが、要するにSymbolのWEB APIは単なるHTTPアクセスで扱えるということです。


## API アクセスの悩み

2.x系のAPIアクセスクラスでは、リクエストやそれに関するパラメタの処理に加え、レスポンスには型を付けて(RxJSのObservalなので)ストリームに流してくれていました。

```ts
const factory = new RepositoryFactoryHttp(NODE_GATEWAY_URL)
const nodeRepo = factory.createNodeRepository()
nodeRepo.getNodeInfo()
    .subscribe((resp: NodeInfo) => console.info(resp))

> NodeInfo {
>   publicKey: 'AAA1922FA60DB681092CBE70A9A1BAB85745025310AE7567F95EA7FD05B3D3FC',
>   networkGenerationHashSeed: '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836',
>   port: 7900,
>   networkIdentifier: 152,
>   version: 16777987,
>   roles: [ 1, 2, 4 ],
>   host: '001-joey-dual.symboltest.net',
>   friendlyName: '001-joey-dual',
>   nodePublicKey: '05E5C0841720AE9DA737C184429002E917F74FF7A3BF8E11AC2862C4875B3D7E'
> }
```

しかし、3.xではそれらが無くなってしまったので、WEB APIアクセスは実装者に委ねられるようになります。

HTTPアクセスについては`node-fetch`、`axios`、`superagent`など、好きなライブラリを使えばいいでしょう。
でも、レスポンスオブジェクトの型はどうしましょう…。


# Aspida で解決するアプローチ

自分も最近ちょっと触っただけなので、あまり語れないので、他の解説記事を確認してもらいたいんですが、簡単に言うと、WEB APIに型付きでアクセスするできるようになるライブラリです。

https://github.com/aspida/aspida

結論から書くと、こんな感じでAPIレスポンスを得られるようになります。

```ts
const info: NodeInfoDTO = await client.node.info.$get()
console.info(server)

> {
>   version: 16777987,
>   publicKey: 'AAA1922FA60DB681092CBE70A9A1BAB85745025310AE7567F95EA7FD05B3D3FC',
>   networkGenerationHashSeed: '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836',
>   roles: 7,
>   port: 7900,
>   networkIdentifier: 152,
>   host: '001-joey-dual.symboltest.net',
>   friendlyName: '001-joey-dual',
>   nodePublicKey: '05E5C0841720AE9DA737C184429002E917F74FF7A3BF8E11AC2862C4875B3D7E'
> }
```

めちゃくちゃいいですね…。
レスポンスだけでなく、呼び出しも型補完できるので、型の力の恩恵を存分に受けることができます。


## openapi2aspida

APIの定義はOpenAPIドキュメントから生成することができます。

https://github.com/aspida/openapi2aspida

`openapi2aspida`を使って、定義からクラスを生成します。

```shell
$ npx openapi2aspida -i openapi3.yml
```


### Symbol APIのOpenAPI定義の修正

公式で公開されている定義ではエラーになってしまうので、動作するように自分の方で修正を施しました。
一応自分なりにOpenAPIの仕様を確認した上で修正をしましたが、そんなに真面目に定義を書いたことがないので、これが正解かまではわかりません。

https://github.com/44uk/symbol-openapi/tree/fix

こちらから`clone`して、`npm run build`して、`openapi3.yml`を生成してください。

```shell
$ git clone -b fix https://github.com/44uk/symbol-openapi.git
$ npm install && npm run build

> symbol-openapi@1.0.4 build
> swagger-cli bundle ./spec/openapi.yml --outfile _build/openapi3.yml --type yaml

Created _build/openapi3.yml from ./spec/openapi.yml
```


## Aspida API ソース生成

ビルドしたAPI定義を指定して、クラスを生成します。

```shell
$ npx openapi2aspida -i _build/openapi3.yml
api/$api.ts was built successfully.
api/account/$api.ts was built successfully.
.
.
api/transactions/partial/$api.ts was built successfully.
api/transactions/unconfirmed/$api.ts was built successfully.
```

`api/`にクラスが生成されます。


## 実装

```ts
import axios from 'axios'
import axiosClient from '@aspida/axios'
import api from './api/$api'
import { NodeInfoDTO } from './api/@types'

;(async () => {
  const client = api(
    axiosClient(axios, {
      baseURL: 'https://001-joey-dual.symboltest.net:3001',
    })
  )
  const info: NodeInfoDTO = await client.node.info.$get()
  console.info(server)
})()

> {
>   version: 16777987,
>   publicKey: 'AAA1922FA60DB681092CBE70A9A1BAB85745025310AE7567F95EA7FD05B3D3FC',
>   networkGenerationHashSeed: '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836',
>   roles: 7,
>   port: 7900,
>   networkIdentifier: 152,
>   host: '001-joey-dual.symboltest.net',
>   friendlyName: '001-joey-dual',
>   nodePublicKey: '05E5C0841720AE9DA737C184429002E917F74FF7A3BF8E11AC2862C4875B3D7E'
> }
```

# おまけ（OpenAPI定義からモックサーバを立てる）

せっかくなので、モックサーバも立ててみましょう。

```shell
docker run --rm -p 3000:4010 stoplight/prism:4 mock -h 0.0.0.0 https://symbol.github.io/symbol-openapi/v1.0.4/openapi3.yml
```

色々とモックサーバを立てるDockerイメージが公開されていましたが、定評がありそうな`stoplight/prism`にしてみました。

直接URLを指定して動かすことができたので、お手軽でいいですね。

立ち上がったら`http://localhost:3000/node/info`にアクセスしてみましょう。

`symbol-bootstrap`を使えば簡単にローカルにノードを立ち上げたり、プライベートネットワークを立ち上げることもできますが、かなりの容量やスペックを必要とします。

ダミーレスポンスで用が足りる場合や、開発時・テスト時に活用して見てください。


# まとめ

WEB APIアクセスクラスが無くなったことで、`symbol-sdk`単体ではAPIからデータの取得をできなくなってしまいました。
その分だけハードルが上がってしまったかもしれませんが、`RxJS`の学習もなかなかに難儀でした。

むしろより一般的な知識で実装すればいいので、プログラミング初学者としてはSymbol APIを扱うために学習したことの応用が効きやすく、習熟者であれば既存の知識で実装することができるでしょう。

今回は`Aspida`を用いた一例の記事でしたが、OpenAPIを利用した型付与の方法は他にもあるようですし、型が必要ないならどうぞ任意のHTTPクライアントをご利用ください。

キミだけのWEB APIアクセスノウハウを構築しよう！