TypeScript のソースコードから JSON のバリデーションをするツール

TypeScript のインターフェイスやクラスの定義を使ってJSONのバリデーションをしたいという場合のため、[TypedGate](https://github.com/twogate/typedgate)というツールを作りました。
このツールでは JSON Schema などのスキーマを別途定義することなく、TypeScriptソースコードそのもので型をバリデーションすることを目的としています。

https://github.com/twogate/typedgate/

例えば、以下のようなTypeScriptの定義がある場合、

```typescript
export type Gender = 'female' | 'male' | 'other';

// ↓ これが ControlComment です
//   この場合 JSON の people プロパティを指しています
// @TG:path .people
export interface People {
  name: string;
  age: number;
  gender: Gender;
  isProgrammer: boolean;
}
```

上記のように `// @TG:path .people` という **"*ControlComment*" をインターフェイスの直前に書くことで、JSONの該当する場所と比較して、バリデーションをします**。この場合、以下のJSONは `Valid` となります。

```json
{
  "people": {
    "name": "Linus Torvalds",
    "age": 49,
    "gender": "male",
    "isProgrammer": true
  }
}
```

動作の仕組みとしては、インターフェイスの定義の直前の ControlComment に `// @TG:path .people` と書いてあるため、 JSONの `people` というプロパティの

```json
{
    "name": "Linus Torvalds",
    "age": 49,
    "gender": "male",
    "isProgrammer": true
}
```

の部分が `interface People` と比較されたのです。

## 正しくないJSONの場合
下記の画像の左がJSON、右がインターフェイスの定義とすると、JSONには `available` の定義がないためこのJSONは `Not Valid` であると出力されます。

右記 `interface HomeConfig` の ControlComment では左記JSONの `.home` というプロパティと比較せよ、と指示しており、TypedGate は JSON の `home` プロパティの部分と比較しますが、JSON には `.home.contents[].available` プロパティが欠如しているため、この JSON は `Not Valid` になるのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520325/8878aac5-4367-278c-3bf4-9cf1d772123d.png)

また、エラーが発生したソースファイルの場所とJSONのパスが表示されます。

上記の例では、`HomeContent` の定義の中で `HomeContentName` やら `LatestSchedumeMeta` や他複数の型を参照していることがわかると思います。**TypedGateはこれらの定義を全て辿るため、 ControlComment はルートの部分だけに書けば動きます**(この場合は`HomeConfig`に書く)。

# 使い方
このツールはコマンドラインツールとなっており、動作させるには `tsconfig.json` へのパス(`--tsconfig`)とソースのファイル名(`--src`)、JSONのファイルパス(`--json`)を引数に指定する必要があります。

`-v` を指定すると詳細なデバッグログを出力します。

## インストールなしで使う
以下のコマンドを実行すると、`npx` の機能によりお手持ちのPCにインストールすることなくオンザフライで動作できます。

```sh
npx typedgate --tsconfig ./tsconfig.json --src ./src/index.ts --json ./some-json-file.json
```

## インストールする
もしインストールをしたいなら、

```sh
npm install -g typedgate
```

とすると

```sh
typedgate --tsconfig ./tsconfig.json --src ./src/index.ts --json ./some-json-file.json
```

で実行できます。

# Tips
## 巨大なプロジェクトで使う
`--src` には1つのファイルしか指定できませんが、このツールは内部的に TypeScript Compiler API を利用しているため、`import` `export` などで指定されたファイルもたどることができます。

そのため、例えば複数のファイルにインターフェイス定義がまたがっている場合、

```typescript
export * from './user';
export * from './product';
export * from './login-history';
// other exports ...
```

などのように `export` をただ記載したファイルへのパスを指定して動作させることで、**それぞれのファイルを辿ってバリデーションすることができます**。

上のファイルを `index.ts` とした場合、`--src` に指定するものは `index.ts` へのパスだけです。

## 複数の ControlComment 定義

例えば、以下のようなJSONがあった場合、

```json
{
  "login":{
    "providers":{
      "twitter":{
        "url":"https://twitter.com/twogateinc",
        "follower":123
      },
      "facebook":{
        "url":"https://www.facebook.com/TwoGate-inc-646638978844172/",
        "follower":321
      },
      "instagram":{
        "url":"https://www.instagram.com/twogateinc/",
        "follower":987
      }
    }
  }
}
```

以下のような複数の ControlComment を利用して、interface 定義を複数箇所でバリデーションすることができます。

```typescript
// @TG:path .login.providers.twitter
// @TG:path .login.providers.facebook
// @TG:path .login.providers.instagram
export interface ProviderItem {
  url: string;
  follower: number;
}
```

※本来であれば以下のように定義できれば良いのですが、Mapped型に未対応のため、現在はできません。

```typescript
export type ProviderName = 'twitter' | 'facebook' | 'instagram'

// @TG:path .login <<-- こう書きたいところですが、動きません
export class SocialMedium {
  providers: { [key in ProviderName]: ProviderItem };
}

export interface ProviderItem {
  url: string;
  follower: number;
}
```

## 配列
このようなJSONがあった場合、

```json
[
  {
    "num": 123,
  },
  {
    "num": 321,
  },
  {
    "num": 987,
  },
  {
    "num": 456,
  }
]
```

以下のように **`[]` を指定することで配列を示すことができます**。

```typescript
// @TG:path .[]
export interface ArrayInterface {
  num: number;
}
```

また、応用として例えば、

```json
{
  "array":
    [
      {
        "num": 123,
      },
      {
        "num": 321,
      }
    ]
}
```

というようなJSONがあった場合、

```typescript
// @TG:path .array[]
export interface ArrayInterface {
  num: number;
}
```

というように指定できます。この記法は、[jq](https://stedolan.github.io/jq/) を参考にしています。

# API
現在、本プログラムはCLIツールだけで、**Validの場合は exit code = `0`、 Not Valid の場合は exit code = `1` で終了するようになっていて、CIなどで利用できるようにPOSIXの流儀に則った挙動をするようになっています**。
ただ、CLIツールだと他のプログラムから利用しづらいため、他のnodeプログラムから `import` して使えるような API を準備しています。

# 制限
TypeScript Compiler API を利用していますが、TypeScript Compiler API で利用できるのがあくまで TypeScript の抽象構文木にアクセスすることまでで、[実際の型のチェックは TypeScript の言語処理系を通している訳ではありません](https://github.com/twogate/typedgate/blob/df4d16ae9e3ebadf3ccb5ee569e855bb3cb0bd6c/src/abstract-syntax-tree.ts#L116-L161)。

プリミティブな型 (`string`,`number`,`boolean`,`null`) とそれのArray型はもちろん、 Union型 (`'female' | 'male'`) 、Enum型には対応していますが、 Intersection型、Mapped型など複雑な型には対応していません。

ControlComment を付与できるものは Interface と Class 定義です。