JSONをツリー形式にしてほしい。型ごとにまとめて。

![Screenshot from 2022-11-19 08-35-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/2edb8ed3-4172-f8a2-f219-3155b42bfd64.png)

# はじめに
この記事は自作のJSON解析CLIツール`jtr`の紹介です。

## 開発経緯
近年GoやNim, TypeScriptといった静的型付け言語を扱うようになって、PythonやJavaScriptと違って型が強く意識した開発が求められるようになりました。
提供されているWebAPIからJSONを抜き出したり、逆に個人制作のWebアプリからJSONを提供する側に回るとき、typeを定義して各言語のオブジェクトとして扱う機会が多くあります。
特に、別のWebAPIからJSONを利用する際に、長ったらしいJSONを眺めて概要把握し、typeを定義してJSONを各言語のオブジェクトに落とし込む作業に疲れたので、もっと効率化できないかと考えました。


## JSONをパースする際に型が意識されるケース
JSONをパースする際に型が意識されるケースをコード例を示しながら説明します。

### JSONパース@Go
例として、Goはtypeを定義するとき、タグというメタ情報をtypeに定義しておいて、JSONパースする作業を楽にしてくれます。

引用: [u1and0/pnsearch](https://github.com/u1and0/pnsearch)

```go:Go言語で型の定義
// Option : ソート列の選択、昇順/降順、AND検索OR検索切り替え
Option struct {
  SortOrder string `form:"orderby"`
  SortAsc   bool   `form:"asc"`
  OR        bool   `form:"or"`
}
// Filter : 発注日、納入日フィルター
Filter struct {
  Order    string `form:"発注"`
  Delivery string `form:"納入"`
}
```

```go:Go言語でJSONのマッピング
import "github.com/gin-gonic/gin"

func ReturnTempl(c *gin.Context) {
  //
  // --snip--
  //
	// Extract query
	q := newQuery()
	if err := c.ShouldBind(q); err != nil {
		msg := fmt.Sprintf("%#v Bad Query", q)
    c.JSON(http.StatusBadRequest, gin.H{"msg": msg, "query": q})
		return
	}
```

フォームの文字列を`c.ShouldBind(q)`でtypeで定義したQuery構造体に変換します。
このとき、`form:"orderby"`のようなメタ情報を手がかりにフレームワーク側がいい感じに割り当ててくれます。


### JSONパース@Nim
次の例では、配信されたJSONをNim言語のオブジェクトとして扱います。

引用: [u1and0/growiapi](https://github.com/u1and0/growiapi)


```nim:Nim言語で型の定義
  ## _api/v3/page で取得できるJSONオブジェクトとページの存在、エラーメッセージ
  MetaPage* = object
    page*: Page
    limit: int
    exist: bool
    error: string

```

```nim:Nim言語でJSONのマッピング
import json

proc initMetaPage*(path: string, limit = 50): MetaPage =
  # --snip--
  let res: Response = result.get()
  case res.status:
    of $Http200:
      # --snip--
      result.page = to(parseJson(jsonStr)["page"], Page)
```

`parseJson(jsonStr)`で文字列をJSON(Nimの型定義としてはJsonNode型)に変換してくれます。
更に、`to(JsonNode, T)`テンプレートでJsonNodeを型Tにいい感じに割り当ててくれます。


以上の例から見られるような **いい感じに割当ててくれる** ようにするためには、事前にtypeで構造体、オブジェクト、そして各プロパティの型を決めておいてあげないといけないわけです。


## jqというコマンドがある
短いJSONならいいけど
長いJSONは構造を把握するだけで疲れます。

```json:解読に疲れるJSONの例
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
  {
    "name": "passy",
    "url": "https://github.com/infinitybeond1/passy",
    "method": "git",
    "tags": [
      "password",
      "generator",
      "cryptography",
      "security"
    ],
    "description": "A fast little password generator",
    "license": "GPL3",
    "web": "https://github.com/infinitybeond1/passy"
  },
  {
    "name": "entgrep",
    "url": "https://github.com/srozb/entgrep",
    "method": "git",
    "tags": [
      "command-line",
      "crypto",
      "cryptography",
      "security"
    ],
    "description": "A grep but for secrets (based on entropy).",
    "license": "MIT",
    "web": "https://github.com/srozb/entgrep"
  },
  {
    ...他 27000行ほど
]
```


# jtrの紹介

ここからようやく`jtr`コマンドの説明です。
使い方はシンプルで、`jq`コマンドの使い方をリスペクトしています。

> 名前の由来が`jq` + `tree`[^1] => `jtr` ですので、使い方も`jq`に似せています。

[^1]:`tree`コマンドの実装例として[DenoでCLIツールを作ってみる：treeコマンドの実装](https://zenn.dev/kawarimidoll/articles/0f5bc71ae633f3)を参考にさせていただきました。コードができあがっていく過程が見えて大変参考になりました。ここでも再帰処理を使って実装しています。


## 使用例

標準入力をJSONとしてパースし、型をオブジェクト名の右に添えて標準出力に出力します。
オブジェクトに階層があれば

```shell-session
$ echo {"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}},],"login":true} | jtr
.
├── foo <float>
├── baz [].
│     └── foo
│         ├── bar <int>
│         ├── baz <string>
│         └── cat <null>
└── login <bool>
```

### プロパティアクセス
`jq`と同様に、プロパティを下ってそこからツリー表示します。
下記の例ではbazオブジェクト以下をツリー表示しています。

```shell-session
$ echo '{"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}}],"login":true}' | jtr '.baz'
[].
  └── foo
      ├── bar <int>
      ├── baz <string>
      └── cat <null>
```

### オプション
今のところ、使えるオプションは`--jq`だけ。
`jq`表示を同時に出力します。

```jqのオプション
options:
  --jq, -q          Display jq view
  --help, -h        Show help message
  --version, -v     Show version
```


### 巨大なJSONに使ってみた
使用例で見る感じ、どんな結果になるかは想像つくかと思いますが、実際に取り扱う巨大なJSONファイルに対して実行してみた結果を書きます。

#### test case 1
[NimのパッケージリポジトリのJSON](https://github.com/nim-lang/packages/blob/master/packages.json)です。

実行コマンドを`jq`, `jtr` の順に実行します。

```json:packages.jsonのjq出力(省略あり)
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
  {
    "name": "passy",
    "url": "https://github.com/infinitybeond1/passy",
    "method": "git",
    "tags": [
      "password",
      "generator",
      "cryptography",
      "security"
    ],
    "description": "A fast little password generator",
    "license": "GPL3",
    "web": "https://github.com/infinitybeond1/passy"
  },
  {
    "name": "entgrep",
    "url": "https://github.com/srozb/entgrep",
    "method": "git",
    "tags": [
      "command-line",
      "crypto",
      "cryptography",
      "security"
    ],
    "description": "A grep but for secrets (based on entropy).",
    "license": "MIT",
    "web": "https://github.com/srozb/entgrep"
  },
  {
    ...他 27000行ほど
]
```

```shell-session:packages.jsonのjtr出力(省略なし)
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jtr
[].
  ├── name <string>
  ├── url <string>
  ├── method <string>
  ├── tags []string
  ├── description <string>
  ├── license <string>
  ├── web <string>
  └── doc <string>
```


まだこれならarrayの各要素が12個なので、`jq`でもいけるか。
しかし、`jq`と`jtr`の出力をよく見比べてください。3要素目以降で"web"要素の次に"doc"という要素があることは、この`jq`の2要素だけではわからなかったでしょう。
`jtr`ならarrayをすべて走査して、最も大きい要素の解析結果を返してくれます。そのため、すべてのJSONを読み込まなくても、ここでは出てきていない3要素目以降に"doc"というオブジェクトが登場することに気づくことができます。

> 細かいこと言うと、**最初に出てきた要素数最大のオブジェクト**を解析します。そのため、例えば3要素目に"doc"を持つオブジェクトが登場して、4要素目以降に"doc"の代わりに"misc"という要素に置き換わっていても、"misc"というプロパティが出てきたことには気づくことがありません。
> なお、"doc"オブジェクトが出てくるのは、調べた時点で全2055要素中の147要素だけでした。これは見逃してしまいそうですね。


#### test case 2
24.9MBの巨大なJSONを扱ってみます。

```json:large-file.jsonのjq出力(省略あり)
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jq
[
  {
    "id": "2489651045",
    "type": "CreateEvent",
    "actor": {
      "id": 665991,
      "login": "petroav",
      "gravatar_id": "",
      "url": "https://api.github.com/users/petroav",
      "avatar_url": "https://avatars.githubusercontent.com/u/665991?"
    },
    "repo": {
      "id": 28688495,
      "name": "petroav/6.828",
      "url": "https://api.github.com/repos/petroav/6.828"
    },
    "payload": {
      "ref": "master",
      "ref_type": "branch",
      "master_branch": "master",
      "description": "Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.",
      "pusher_type": "user"
    },
    "public": true,
    "created_at": "2015-01-01T15:00:00Z"
  },
  {
    "id": "2489651051",
    "type": "PushEvent",
    "actor": {
      "id": 3854017,
      "login": "rspt",
      "gravatar_id": "",
      "url": "https://api.github.com/users/rspt",
      "avatar_url": "https://avatars.githubusercontent.com/u/3854017?"
    },
    "repo": {
      "id": 28671719,
      "name": "rspt/rspt-theme",
      "url": "https://api.github.com/repos/rspt/rspt-theme"
    },
    "payload": {
      "push_id": 536863970,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
      "before": "437c03652caa0bc4a7554b18d5c0a394c2f3d326",
      "commits": [
        {
          "sha": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
          "author": {
            "email": "5c682c2d1ec4073e277f9ba9f4bdf07e5794dabe@rspt.ch",
            "name": "rspt"
          },
          "message": "Fix main header height on mobile",
          "distinct": true,
          "url": "https://api.github.com/repos/rspt/rspt-theme/commits/6b089eb4a43f728f0a594388092f480f2ecacfcd"
        }
      ]
    },
    "public": true,
    "created_at": "2015-01-01T15:00:01Z"
  },
  {
    "id": "2489651053",
    "type": "PushEvent",
    "actor": {
      "id": 6339799,
      "login": "izuzero",
      "gravatar_id": "",
      "url": "https://api.github.com/users/izuzero",
      "avatar_url": "https://avatars.githubusercontent.com/u/6339799?"
    },
    "repo": {
      "id": 28270952,
      "name": "izuzero/xe-module-ajaxboard",
      "url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard"
    },
    "payload": {
      "push_id": 536863972,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/develop",
      "head": "ec819b9df4fe612bb35bf562f96810bf991f9975",
      "before": "590433109f221a96cf19ea7a7d9a43ca333e3b3e",
      "commits": [
        {
          "sha": "ec819b9df4fe612bb35bf562f96810bf991f9975",
          "author": {
            "email": "df05f55543db3c62cf64f7438018ec37f3605d3c@gmail.com",
            "name": "Eunsoo Lee"
          },
          "message": "#20 게시글 및 댓글 삭제 시 새로고침이 되는 문제 해결\n\n원래 의도는 새로고침이 되지 않고 확인창만으로 해결되어야 함.\n기본 게시판 대응 플러그인에서 발생한 이슈.",
          "distinct": true,
          "url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard/commits/ec819b9df4fe612bb35bf562f96810bf991f9975"
        }
      ]
    },
    "public": true,
    ...他に 11251行
```

```shell-session:large-file.jsonのjtr出力(省略なし)
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jtr
[].
  ├── id <string>
  ├── type <string>
  ├── actor
  │   ├── id <int>
  │   ├── login <string>
  │   ├── gravatar_id <string>
  │   ├── url <string>
  │   └── avatar_url <string>
  ├── repo
  │   ├── id <int>
  │   ├── name <string>
  │   └── url <string>
  ├── payload
  │   ├── action <string>
  │   ├── comment
  │   │   ├── url <string>
  │   │   ├── id <int>
  │   │   ├── diff_hunk <string>
  │   │   ├── path <string>
  │   │   ├── position <int>
  │   │   ├── original_position <int>
  │   │   ├── commit_id <string>
  │   │   ├── original_commit_id <string>
  │   │   ├── user
  │   │   │   ├── login <string>
  │   │   │   ├── id <int>
  │   │   │   ├── avatar_url <string>
  │   │   │   ├── gravatar_id <string>
  │   │   │   ├── url <string>
  │   │   │   ├── html_url <string>
  │   │   │   ├── followers_url <string>
  │   │   │   ├── following_url <string>
  │   │   │   ├── gists_url <string>
  │   │   │   ├── starred_url <string>
  │   │   │   ├── subscriptions_url <string>
  │   │   │   ├── organizations_url <string>
  │   │   │   ├── repos_url <string>
  │   │   │   ├── events_url <string>
  │   │   │   ├── received_events_url <string>
  │   │   │   ├── type <string>
  │   │   │   └── site_admin <bool>
  │   │   ├── body <string>
  │   │   ├── created_at <string>
  │   │   ├── updated_at <string>
  │   │   ├── html_url <string>
  │   │   ├── pull_request_url <string>
  │   │   └── _links
  │   │       ├── self
  │   │       │   └── href <string>
  │   │       ├── html
  │   │       │   └── href <string>
  │   │       └── pull_request
  │   │           └── href <string>
  │   └── pull_request
  │       ├── url <string>
  │       ├── id <int>
  │       ├── html_url <string>
  │       ├── diff_url <string>
  │       ├── patch_url <string>
  │       ├── issue_url <string>
  │       ├── number <int>
  │       ├── state <string>
  │       ├── locked <bool>
  │       ├── title <string>
  │       ├── user
  │       │   ├── login <string>
  │       │   ├── id <int>
  │       │   ├── avatar_url <string>
  │       │   ├── gravatar_id <string>
  │       │   ├── url <string>
  │       │   ├── html_url <string>
  │       │   ├── followers_url <string>
  │       │   ├── following_url <string>
  │       │   ├── gists_url <string>
  │       │   ├── starred_url <string>
  │       │   ├── subscriptions_url <string>
  │       │   ├── organizations_url <string>
  │       │   ├── repos_url <string>
  │       │   ├── events_url <string>
  │       │   ├── received_events_url <string>
  │       │   ├── type <string>
  │       │   └── site_admin <bool>
  │       ├── body <string>
  │       ├── created_at <string>
  │       ├── updated_at <string>
  │       ├── closed_at <null>
  │       ├── merged_at <null>
  │       ├── merge_commit_sha <string>
  │       ├── assignee
  │       │   ├── login <string>
  │       │   ├── id <int>
  │       │   ├── avatar_url <string>
  │       │   ├── gravatar_id <string>
  │       │   ├── url <string>
  │       │   ├── html_url <string>
  │       │   ├── followers_url <string>
  │       │   ├── following_url <string>
  │       │   ├── gists_url <string>
  │       │   ├── starred_url <string>
  │       │   ├── subscriptions_url <string>
  │       │   ├── organizations_url <string>
  │       │   ├── repos_url <string>
  │       │   ├── events_url <string>
  │       │   ├── received_events_url <string>
  │       │   ├── type <string>
  │       │   └── site_admin <bool>
  │       ├── milestone
  │       │   ├── url <string>
  │       │   ├── labels_url <string>
  │       │   ├── id <int>
  │       │   ├── number <int>
  │       │   ├── title <string>
  │       │   ├── description <null>
  │       │   ├── creator
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   ├── open_issues <int>
  │       │   ├── closed_issues <int>
  │       │   ├── state <string>
  │       │   ├── created_at <string>
  │       │   ├── updated_at <string>
  │       │   ├── due_on <null>
  │       │   └── closed_at <null>
  │       ├── commits_url <string>
  │       ├── review_comments_url <string>
  │       ├── review_comment_url <string>
  │       ├── comments_url <string>
  │       ├── statuses_url <string>
  │       ├── head
  │       │   ├── label <string>
  │       │   ├── ref <string>
  │       │   ├── sha <string>
  │       │   ├── user
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   └── repo
  │       │       ├── id <int>
  │       │       ├── name <string>
  │       │       ├── full_name <string>
  │       │       ├── owner
  │       │       │   ├── login <string>
  │       │       │   ├── id <int>
  │       │       │   ├── avatar_url <string>
  │       │       │   ├── gravatar_id <string>
  │       │       │   ├── url <string>
  │       │       │   ├── html_url <string>
  │       │       │   ├── followers_url <string>
  │       │       │   ├── following_url <string>
  │       │       │   ├── gists_url <string>
  │       │       │   ├── starred_url <string>
  │       │       │   ├── subscriptions_url <string>
  │       │       │   ├── organizations_url <string>
  │       │       │   ├── repos_url <string>
  │       │       │   ├── events_url <string>
  │       │       │   ├── received_events_url <string>
  │       │       │   ├── type <string>
  │       │       │   └── site_admin <bool>
  │       │       ├── private <bool>
  │       │       ├── html_url <string>
  │       │       ├── description <string>
  │       │       ├── fork <bool>
  │       │       ├── url <string>
  │       │       ├── forks_url <string>
  │       │       ├── keys_url <string>
  │       │       ├── collaborators_url <string>
  │       │       ├── teams_url <string>
  │       │       ├── hooks_url <string>
  │       │       ├── issue_events_url <string>
  │       │       ├── events_url <string>
  │       │       ├── assignees_url <string>
  │       │       ├── branches_url <string>
  │       │       ├── tags_url <string>
  │       │       ├── blobs_url <string>
  │       │       ├── git_tags_url <string>
  │       │       ├── git_refs_url <string>
  │       │       ├── trees_url <string>
  │       │       ├── statuses_url <string>
  │       │       ├── languages_url <string>
  │       │       ├── stargazers_url <string>
  │       │       ├── contributors_url <string>
  │       │       ├── subscribers_url <string>
  │       │       ├── subscription_url <string>
  │       │       ├── commits_url <string>
  │       │       ├── git_commits_url <string>
  │       │       ├── comments_url <string>
  │       │       ├── issue_comment_url <string>
  │       │       ├── contents_url <string>
  │       │       ├── compare_url <string>
  │       │       ├── merges_url <string>
  │       │       ├── archive_url <string>
  │       │       ├── downloads_url <string>
  │       │       ├── issues_url <string>
  │       │       ├── pulls_url <string>
  │       │       ├── milestones_url <string>
  │       │       ├── notifications_url <string>
  │       │       ├── labels_url <string>
  │       │       ├── releases_url <string>
  │       │       ├── created_at <string>
  │       │       ├── updated_at <string>
  │       │       ├── pushed_at <string>
  │       │       ├── git_url <string>
  │       │       ├── ssh_url <string>
  │       │       ├── clone_url <string>
  │       │       ├── svn_url <string>
  │       │       ├── homepage <string>
  │       │       ├── size <int>
  │       │       ├── stargazers_count <int>
  │       │       ├── watchers_count <int>
  │       │       ├── language <string>
  │       │       ├── has_issues <bool>
  │       │       ├── has_downloads <bool>
  │       │       ├── has_wiki <bool>
  │       │       ├── has_pages <bool>
  │       │       ├── forks_count <int>
  │       │       ├── mirror_url <null>
  │       │       ├── open_issues_count <int>
  │       │       ├── forks <int>
  │       │       ├── open_issues <int>
  │       │       ├── watchers <int>
  │       │       └── default_branch <string>
  │       ├── base
  │       │   ├── label <string>
  │       │   ├── ref <string>
  │       │   ├── sha <string>
  │       │   ├── user
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   └── repo
  │       │       ├── id <int>
  │       │       ├── name <string>
  │       │       ├── full_name <string>
  │       │       ├── owner
  │       │       │   ├── login <string>
  │       │       │   ├── id <int>
  │       │       │   ├── avatar_url <string>
  │       │       │   ├── gravatar_id <string>
  │       │       │   ├── url <string>
  │       │       │   ├── html_url <string>
  │       │       │   ├── followers_url <string>
  │       │       │   ├── following_url <string>
  │       │       │   ├── gists_url <string>
  │       │       │   ├── starred_url <string>
  │       │       │   ├── subscriptions_url <string>
  │       │       │   ├── organizations_url <string>
  │       │       │   ├── repos_url <string>
  │       │       │   ├── events_url <string>
  │       │       │   ├── received_events_url <string>
  │       │       │   ├── type <string>
  │       │       │   └── site_admin <bool>
  │       │       ├── private <bool>
  │       │       ├── html_url <string>
  │       │       ├── description <string>
  │       │       ├── fork <bool>
  │       │       ├── url <string>
  │       │       ├── forks_url <string>
  │       │       ├── keys_url <string>
  │       │       ├── collaborators_url <string>
  │       │       ├── teams_url <string>
  │       │       ├── hooks_url <string>
  │       │       ├── issue_events_url <string>
  │       │       ├── events_url <string>
  │       │       ├── assignees_url <string>
  │       │       ├── branches_url <string>
  │       │       ├── tags_url <string>
  │       │       ├── blobs_url <string>
  │       │       ├── git_tags_url <string>
  │       │       ├── git_refs_url <string>
  │       │       ├── trees_url <string>
  │       │       ├── statuses_url <string>
  │       │       ├── languages_url <string>
  │       │       ├── stargazers_url <string>
  │       │       ├── contributors_url <string>
  │       │       ├── subscribers_url <string>
  │       │       ├── subscription_url <string>
  │       │       ├── commits_url <string>
  │       │       ├── git_commits_url <string>
  │       │       ├── comments_url <string>
  │       │       ├── issue_comment_url <string>
  │       │       ├── contents_url <string>
  │       │       ├── compare_url <string>
  │       │       ├── merges_url <string>
  │       │       ├── archive_url <string>
  │       │       ├── downloads_url <string>
  │       │       ├── issues_url <string>
  │       │       ├── pulls_url <string>
  │       │       ├── milestones_url <string>
  │       │       ├── notifications_url <string>
  │       │       ├── labels_url <string>
  │       │       ├── releases_url <string>
  │       │       ├── created_at <string>
  │       │       ├── updated_at <string>
  │       │       ├── pushed_at <string>
  │       │       ├── git_url <string>
  │       │       ├── ssh_url <string>
  │       │       ├── clone_url <string>
  │       │       ├── svn_url <string>
  │       │       ├── homepage <string>
  │       │       ├── size <int>
  │       │       ├── stargazers_count <int>
  │       │       ├── watchers_count <int>
  │       │       ├── language <string>
  │       │       ├── has_issues <bool>
  │       │       ├── has_downloads <bool>
  │       │       ├── has_wiki <bool>
  │       │       ├── has_pages <bool>
  │       │       ├── forks_count <int>
  │       │       ├── mirror_url <null>
  │       │       ├── open_issues_count <int>
  │       │       ├── forks <int>
  │       │       ├── open_issues <int>
  │       │       ├── watchers <int>
  │       │       └── default_branch <string>
  │       └── _links
  │           ├── self
  │           │   └── href <string>
  │           ├── html
  │           │   └── href <string>
  │           ├── issue
  │           │   └── href <string>
  │           ├── comments
  │           │   └── href <string>
  │           ├── review_comments
  │           │   └── href <string>
  │           ├── review_comment
  │           │   └── href <string>
  │           ├── commits
  │           │   └── href <string>
  │           └── statuses
  │               └── href <string>
  ├── public <bool>
  ├── created_at <string>
  └── org
      ├── id <int>
      ├── login <string>
      ├── gravatar_id <string>
      ├── url <string>
      └── avatar_url <string>
```

ものすごい量の要素数だ...。
しかし、`jq`の10000行も見なくても、`jtr`の383行を見れば、どんな感じの構造のJSONなのかはわかりますね。


### パフォーマンス

ネットワーク速度に依存しないように[large-file.json](https://github.com/json-iterator/test-data/blob/master/large-file.json)のrawcontentをローカルに保存してから実行しました。

行数はwcコマンドで数えます。
実行コマンドは下記です。

```shell-session
$ cat large-file.json | wc -l
$ cat large-file.json | jq | wc -l
$ cat large-file.json | jtr | wc -l
```


実行速度は、コマンド実行から標準出力に表示され終わるまでの時間です。
実行コマンドは下記です。

```shell-session
$ time cat large-file.json
$ time cat large-file.json | jq
$ time cat large-file.json | jtr
```


結果を示します。

|                    | cat | cat+jq | cat+jtr |
|--------------------|-----|--------|---------|
| 行数(行)           |11351  | 732457  | 383  |
| 実行速度(秒)       | 3.273 | 11.216  | 2.882 |

出力行が少なければ実行速度が早くなるのはまぁ当然な結果なわけですが、実行速度も申し分なく、型まで判別してくれるのは便利なコマンドではないかな、と思う次第です。


## インストール
github releaseからダウンロードしてください。

```shell-session
$ curl -fLO https://github.com/u1and0/jtr/releases/download/v0.2.8/jtr-linux.zip
$ unzip jtr-linux.zip
$ chmod 755 ./jtr
$ ./jtr -v
```

または、Nimbleを使ってください。
https://nimble.directory/pkg/jtr

```shell-session
$ nimble install jtr
```


## ビルド
Githubからフォークしてください。
[u1and0/jtr](https://github.com/u1and0/jtr)



## 実装
実装のコアのコードを示します。
詳細が知りたければ[Github上のソース](https://github.com/u1and0/jtr/src/jtr.nim)を見てください。

### 概要
実装はNimの標準パッケージ[std/json](https://nim-lang.org/docs/json.html)が強力ですのでそこまで苦労しませんでした。

`parseJson()`を使うと`JsonNodeKind`という型でそのオブジェクトの型を判別できるので、switch-case分岐してタイプを表示するか、Objectを再帰的に探索するか、Arrayを再帰的に探索します。

探索の結果はすべてstring型でつなげていって、最終的にstringを標準出力に`echo()`するだけです。


### プリミティブ型の探索
要はnullかboolかstringかfloatかintかを判定してくれます。

```nim:プリミティブ型解析rootTree()の実装
func rootTree*(jnode: JsonNode): string =
  ## Tree view for type of plain JSON recursively
  ##
  ## ```nim
  ## doAssert rootTree(parseJson("null")) == "<null>"
  ## doAssert rootTree(parseJson("true")) == "<bool>"
  ## doAssert rootTree(parseJson("\"5\"")) == "<string>"
  ## doAssert rootTree(parseJson($5.0)) == "<float>"
  ## doAssert rootTree(parseJson($5)) == "<int>"
  ## ```
  case jnode.kind
    of JNull: "<null>"
    of JBool: "<bool>"
    of JString: "<string>"
    of JInt: "<int>"
    of JFloat: "<float>"
    of JArray: arrayTree(jnode)
    of JObject: ".\n" & objectTree(jnode)
```

### Objectの探索
Objectを再帰的に潜って解析します。

```nim:オブジェクト型の解析objectTree()の実装
func objectTree*(jobj: JsonNode, indent = ""): string =
  ## Tree view for type of object elemnts recursively
  ##
  ## ```nim
  ## let obj = """
  ## {"foo": "0", "obj": {"bar":1, "baz":"2"}, "name": "ken"}
  ## """.parseJson
  ## doAssert objectTree(obj) == """├── foo <string>
  ## ├── obj
  ## │   ├── bar <int>
  ## │   └── baz <string>
  ## └── name <string>"""
  ## ```

  var
    res: seq[string]
    i: int
    branch, nextIndent: string
  let isLastOne = func(i: int): bool = i >= len(jobj)-1
  for key, val in jobj.pairs:
    if isLastOne(i):
      branch = "└── "
      nextIndent = indent & " ".repeat(4)
    else:
      branch = "├── "
      nextIndent = indent & "│" & " ".repeat(3)
    let types: string = case val.kind
      of JNull: " <null>"
      of JBool: " <bool>"
      of JString: " <string>"
      of JInt: " <int>"
      of JFloat: " <float>"
      of JArray: " " & arrayTree(val, nextIndent)
      of JObject: "\n" & objectTree(val, nextIndent)
    res.add(&"{indent}{branch}{key}{types}")
    i += 1
  return res.join("\n")
```

### Arrayの探索
Array型のすべての要素をまとめて表示します。

```nim:Array型解析arrayTree()の実装
func arrayTree*(jarray: JsonNode, indent = ""): string =
  ## Tree view for type of array elemnts recursively
  ##
  ## ```nim
  ## doAssert arrayTree(parseJson("[null,null,null]")) == "[]null"
  ## doAssert arrayTree(parseJson("[true,false,false]")) == "[]bool"
  ## doAssert arrayTree(parseJson("[\"cat\", \"dog\"]")) == "[]string"
  ## doAssert arrayTree(parseJson("[1,2,3]")) == "[]int"
  ## doAssert arrayTree(parseJson("[1.0,10.0,100.0]")) == "[]float"
  ## doAssert arrayTree(parseJson("[1,\"2\",3]")) == "[]any"
  ## doAssert arrayTree(parseJson("[[1,2,3]]")) == "[][]int"
  ## doAssert arrayTree(parseJson("""
  ## [
  ##   {
  ##     "foo":{
  ##       "bar": 1,
  ##       "baz": "key"
  ##     }
  ##   },
  ##   {
  ##     "foo":{
  ##       "bar": 2,
  ##       "baz": "signal",
  ##       "cat": null
  ##     }
  ##   }
  ## ]
  ## """)) == """[].
  ##   └── foo
  ##       ├── bar <int>
  ##       ├── baz <string>
  ##       └── cat <null>"""
  ## ```

  let typesArr: seq[JsonNodeKind] = collect:
    for val in jarray:
      val.kind
  if all(typesArr, func(x: JsonNodeKind): bool = x == JNull):
    return "[]null"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JBool):
    return "[]bool"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JString):
    return "[]string"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JInt):
    return "[]int"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JFloat):
    return "[]float"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JArray):
    return "[]" & arrayTree(jarray[0]) # TODO 本当にjarray[0]だけでいいのか？seekLargestObject通すべきでは
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JObject):
    let nextIndent = indent & " ".repeat(2)
    let largestObj = seekLargestObject(jarray)
    return "[].\n" & objectTree(largestObj, nextIndent)
  else: # 全ての型が一致しない場合
    return "[]any"
```