ども、ゴリラです。
みなさんjq
を使っていますか?とても便利なのでおそらく多くの方はjq
を使っていると思います。
ぼくもその一人ですが、最近JSONをツリー状にして編集、フィルタリング、保存できたら便利では?と思い立ってtson
っていうTUIツールを作りました。
意外と便利だったので、紹介していきます。
どんな感じ?
対応OS
Mac/Linuxのみになります。(Windowsだと画面が崩れます)
将来的にはWindowsにも対応するつもりです。
機能
便利だろうと思った機能を実装しました。次になります。
- ファイル、URLからJSONを読み込み、ツリー化
- フィルタリング
- 編集、追加、削除
- ファイルに保存
- 外部エディタを使ってJSONを編集する
使い方
とてもシンプルです。以下の3つの方法があります。
- ファイルから直接読み込む:
tson < file.json
- パイプ
|
で標準出力から読み込む:curl http://gorilla/api | tson
-
-url
引数を使ってURLのレスポンスから読み込む:tson -url http://gorilla/api
URLから読み込みについて
-url
で指定したURLからJSONを読み込むことができますが、これは簡易なHTTP GETなため複雑なことはできません。
GET以外のPOSTやヘッダーをつけてリクエストを発行したいときはcurl
といったコマンドを使用して|
でtsonに流し込むと良いでしょう。
具体的な使い方について
tson
の便利なところはインタラクティブにJSONを編集できるところです。TUIツールならではですね。
個人的におすすめなキーバインドをいくつか紹介します。より詳細なキーバインドはこちらを参照してください。
絞り込み
/
で検索ダイアログが表示されるので、検索したい文字を入力してください。
入力するたび結果が即反映されます。特に長いJSONの場合キー名を入力して絞り込みするときに便利でしょう。
折りたたみ
H
でvalueノードをすべて折りたたみ、Keyノードだけ残します。これは長いJSONの全体像を把握するのに役に立ちます。
現在のkeyノードのみ折りたたみたい場合はh
を使用します。
valueを知りたいときはl
で現在のノードを展開できます。また全体を展開したいときはL
を使用します。
外部エディタを使ってJSONを編集
e
で$EDITOR
に設定されているエディタを使用してJSONを編集することができます。
エディタで編集したJSONを保存して終了すると、制御はtsonに戻り編集した結果も反映されます。
ただ、エディタを使用してtson
も戻ったあとに1キーストロークが反応しなくなります。
つまり1回目に入力したキーが効かないです。これはtview
が使用しているtcell
のバグのようです。
現在まだ修正されていないのとtcell
作者があんまり直す気がないので、修正されるのを待つよりもこちらが直したほう早いと思いますが、tcellナニモワカラナイので、時間はかかりますが自分が頑張って治そうと思います。
ノード間ジャンプ
ctrl-j
/ctrl-k
でノード間のジャンプができます。子ノードの数が多く、次のノードに移動したいときに便利です。
例えば、次のようにrange
ノードからindex
ノードにジャンプするときに使用したりします。
ファイルに保存
s
で現在のツリー状態をファイルに保存できます。もちろん出力もJSONです。
例えばtson -url https://gorilla/api
でレスポンスを確認したあとにそれを出力したいときに役たちます。
実装
すこし実装について書いていきます。実際ソースコードを読んでもらった方がわかりやすいのかもしれません。
まず、JSONは以下の型が定義されています。
false/null/true/object/array/number/string
これはRFC 8259で詳しく定義されているので、興味ある方は読んでみてください。
ちなみに、numberはint
とfloat
が含まれています。
JSONの読み込みからツリー生成
JSONからツリーを作成するときの大まかの処理の流れは次になります。
- JSONをGoの型にパース(
json.Unmarshal()
を使用) - objectかarrayかliteralかで処理を分岐し、objectとarrayの中身を再帰的に処理していく
objectとarrayはarray、object、literalを持つことができるので、
どのように型を判定して適切な処理を行うのか、この再帰処理がかなり難航しました。
JSONの読み込み
JSONを読み込むときに、空インターフェイスを用意します。なぜならJSONがどんな形でも対応できるようにするためです。
b, err := ioutil.ReadAll(in)
if err != nil {
log.Println(err)
return nil, err
}
var i interface{}
if err := json.Unmarshal(b, &i); err != nil {
return nil, err
}
あとはjson.Unmarshal
がJSONをよしなにGoの型にしてくれます。
JSONの型に対応するGoの型は次になります。
JSON | Go |
---|---|
string |
string |
number |
int/float64 |
object |
[]map[string]interface{} |
array |
[]interface{} |
true |
true |
false |
false |
null |
nil |
ちなみに、ツリーを作成するときにどのノードがどの型なのか、という情報を持たせる必要があります。
なぜなら、この情報はツリーをJSONに書き出すときに使用するからです。
ツリー生成
パース後のinterfaceをtype
を使って型ごとに処理を行います
switch node := node.(type) {
case map[string]interface{}:
// objectの場合の処理
case []interface{}:
// arrayの場合の処理
default:
// literalの場合の処理
}
objectとarrayは基本的にt.AddNode()
を使って再帰処理を行います。
それと同時に親ノードがarray
なのかobject
なのかといった情報をReferrence
というオブジェクトにセットしています。
なぜノードの型情報が必要かというと、JSONに書き出すときも再帰的にノードを辿っていきますが、
array
かobject
かによって処理が変わるので、その判定に使用します。
ちなみに、次のコードはarray
の場合の処理です。
case []interface{}:
for _, v := range node {
id := uuid.Must(uuid.NewV4()).String()
switch v.(type) {
case map[string]interface{}:
objectNode := tview.NewTreeNode("{object}").
SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Object})
nodes = append(nodes, objectNode)
case []interface{}:
arrayNode := tview.NewTreeNode("{array}").
SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Array})
nodes = append(nodes, arrayNode)
default:
nodes = append(nodes, t.AddNode(v)...)
}
}
literalの場合、型チェックはreflect
パッケージを使用して次のようにチェックします。
ValueType
はJSONを書き出すときにnull
で書き出すのか"null"
で書き出すのかといった判定に使用します。
JSONの書き出しはこうった情報がないと正しく書き出せないので、ここがキモになります。
default:
ref := reflect.ValueOf(node)
var valueType ValueType
switch ref.Kind() {
case reflect.Int:
valueType = Int
case reflect.Float64:
valueType = Float
case reflect.Bool:
valueType = Boolean
default:
if node == nil {
valueType = Null
} else {
valueType = String
}
}
id := uuid.Must(uuid.NewV4()).String()
nodes = append(nodes, t.NewNodeWithLiteral(node).
SetReference(Reference{ID: id, JSONType: Value, ValueType: valueType}))
ちなみに、ValueType
の定義は以下のようになっています。
type ValueType int
const (
Int ValueType = iota + 1
String
Float
Boolean
Null
)
JSONの書き出し
JSONの書き出しですが、こちらはとてもシンプルです。
JSONType
でarray
かobject
かそれ以外かで処理を分けます。
こちらも再帰的にノードを辿りながらJSONを組み上げていきます。
parseValue
でValueType
を見てint
なのかstring
なのかといった値の種類別で変換処理を行います。
func (g *Gui) makeJSON(node *tview.TreeNode) interface{} {
ref := node.GetReference().(Reference)
children := node.GetChildren()
switch ref.JSONType {
case Object:
i := make(map[string]interface{})
for _, n := range children {
i[n.GetText()] = g.makeJSON(n)
}
return i
case Array:
var i []interface{}
for _, n := range children {
i = append(i, g.makeJSON(n))
}
return i
case Key:
v := node.GetChildren()[0]
if v.GetReference().(Reference).JSONType == Value {
return g.parseValue(v)
}
return map[string]interface{}{
node.GetText(): g.makeJSON(v),
}
}
return g.parseValue(node)
}
func (g *Gui) parseValue(node *tview.TreeNode) interface{} {
v := node.GetText()
ref := node.GetReference().(Reference)
switch ref.ValueType {
case Int:
i, _ := strconv.Atoi(v)
return i
case Float:
f, _ := strconv.ParseFloat(v, 64)
return f
case Boolean:
b, _ := strconv.ParseBool(v)
return b
case Null:
return nil
}
return v
}
ざっくりですが、以上がtson
の処理の一部になります。
今後について
tson
は今後も機能を追加していきます。
具体的に言うとPostmanのような、ヘッダーやリクエストボディなどを自由に編集してHTTPリクエストを発行できる機能を追加する予定です。
おまけ
デモの中でgjoというツールを使いましたが、これは画像のようにkey=value
の組み合わせで簡単にJSON文字列を生成できるツールです。
curl -X POST -H "Content-Type: application/json" https://gorilla/api -d $(gjo name=gorilla)
って感じでcurlと組み合わせて使用できるので、よかったら使ってみてください。
最後に
tson
はまだまだ使いにくいところがあります。
まだまだ改善の余地があるので、この記事を見てGo詳しくなくても、
OSSを作るのをチャレンジしてみたい方や一緒に作ってみたい方はぜひ声かけてください。お待ちしています。