Repository: simeji/jid Branch: master Commit: 2968365762bd Files: 17 Total size: 71.7 KB Directory structure: gitextract_eacgd7ye/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── .goreleaser.yml ├── ChangeLog ├── LICENSE ├── README.md ├── engine.go ├── engine_test.go ├── go.mod ├── go.sum ├── json_manager.go ├── json_manager_test.go ├── query.go ├── query_test.go ├── suggestion.go ├── suggestion_test.go └── terminal.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 jobs: test: docker: - image: circleci/golang:latest steps: - checkout - run: go test -v ./ - run: go test -v ./cmd/jid release: docker: - image: circleci/golang:latest steps: - checkout - run: name: build jid using goreleaser command: curl -sL https://git.io/goreleaser | bash workflows: version: 2.1 test_and_release: jobs: - test: filters: tags: only: /v[0-9]+(\.[0-9]+)*(-.*)*/ - release: requires: - test filters: branches: ignore: /.*/ tags: only: /v[0-9]+(\.[0-9]+)*(-.*)*/ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof *.swp *.test test.* # jid package jid *.out *.log dist .idea ================================================ FILE: .goreleaser.yml ================================================ # This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: # you may remove this if you don't use vgo - go mod download # you may remove this if you don't need go generate - go generate ./... builds: - env: - CGO_ENABLED=0 main: ./cmd/jid/jid.go ldflags: - -s -w goos: - windows - openbsd - netbsd - linux - freebsd - darwin goarch: - arm64 - amd64 - 386 archive: name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" format: zip files: - none* checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' # for Validation #release: # disable: true ================================================ FILE: ChangeLog ================================================ ChangeLog ======= ### 0.7.2 - Jan 23 2017 BugFix: * fix version number ### 0.7.1 - Jan 5 2017 Features: * scroll to bottom and top ### 0.7.0 - Jan 4 2017 Features: * output with color * monochrome mode option BugFix: * multibyte Query & Json key/value Modified README ### 0.6.3 - 15 Dec 2016 Features: * Add `-help` and `-h` command for show a help * Add Ctrl-U command and a query behavior Change Behaviors: * Force insert `.` ### 0.6.2 - 9 Dec 2016 Features: * Add --version flag for homebrew test ### 0.6.1 - 5 Dec 2016 Features: * Get first argument of `jid` for initial query Change Behaviors: * Auto input . at first character of query ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 simeji Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # jid [![Circle CI](https://circleci.com/gh/simeji/jid/tree/master.svg?style=shield)](https://circleci.com/gh/simeji/jid/tree/master) Json Incremental Digger It's a very simple tool. You can drill down JSON interactively by using filtering queries like [jq](https://stedolan.github.io/jq/). **Suggestion** and **Auto completion** of this tool will provide you a very comfortable JSON drill down. ## Demo ![demo-jid-main](https://github.com/simeji/jid/wiki/images/demo-jid-main-640-colorize.gif) ## Installation * [With HomeBrew (for macOS)](#with-homebrew-for-macos) * [With MacPorts (for macOS)](#with-macports-for-macos) * [With pkg (for FreeBSD)](#with-pkg-for-freebsd) * [With scoop (for Windows)](#with-scoop-for-windows) * [Other package management system](#other-package-management-systems) * [Simply use "jid" command](#simply-use-jid-command) * [Build](#build) ### With HomeBrew (for macOS) ``` brew install jid ``` ### With MacPorts (for macOS) ``` sudo port install jid ``` ### With pkg (for FreeBSD) ``` pkg install jid ``` ### With scoop (for Windows) ``` scoop install jid ``` ### Other package management systems Jid can install by package management systems of below OS. [![Packaging status](https://repology.org/badge/vertical-allrepos/jid.svg)](https://repology.org/metapackage/jid/versions) ### Simply use "jid" command If you simply want to use `jid` command, please download binary from below. https://github.com/simeji/jid/releases ## Build ``` go install github.com/simeji/jid/cmd/jid@latest ``` ## Usage ### Quick start * [simple json example](#simple-json-example) * [simple json example2](#simple-json-example2) * [with initial query](#with-initial-query) * [with curl](#with-curl) #### simple json example Please execute the below command. ``` echo '{"aa":"2AA2","bb":{"aaa":[123,"cccc",[1,2]],"c":321}}'| jid ``` then, jid will be running. You can dig JSON data incrementally. When you enter `.bb.aaa[2]`, you will see the following. ``` [Filter]> .bb.aaa[2] [ 1, 2 ] ``` Then, you press Enter key and output `[1,2]` and exit. #### simple json example2 This json is used by [demo section](https://github.com/simeji/jid#demo). ``` echo '{"info":{"date":"2016-10-23","version":1.0},"users":[{"name":"simeji","uri":"https://github.com/simeji","id":1},{"name":"simeji2","uri":"https://example.com/simeji","id":2},{"name":"simeji3","uri":"https://example.com/simeji3","id":3}],"userCount":3}}'|jid ``` #### With a initial query First argument of `jid` is initial query. (Use JSON same as [Demo](#demo)) ![demo-jid-with-query](https://github.com/simeji/jid/wiki/images/demo-jid-with-query-640.gif) #### with curl Sample for using [RDAP](https://datatracker.ietf.org/wg/weirds/documents/) data. ``` curl -s http://rdg.afilias.info/rdap/domain/example.info | jid ``` #### Load JSON from a file ``` jid < file.json ``` ## Keymaps |key|description| |:-----------|:----------| |`TAB` / `CTRL` + `I` |Show available items and choice them| |`CTRL` + `W` |Delete from the cursor to the start of the word| |`CTRL` + `U` |Delete whole query| |`CTRL` + `F` / Right Arrow (:arrow_right:)|Move cursor a character to the right| |`CTRL` + `B` / Left Arrow (:arrow_left:)|Move cursor a character to the left| |`CTRL` + `A`|To the first character of the 'Filter'| |`CTRL` + `E`|To the end of the 'Filter'| |`CTRL` + `J`|Scroll json buffer 1 line downwards| |`CTRL` + `K`|Scroll json buffer 1 line upwards| |`CTRL` + `G`|Scroll json buffer to bottom| |`CTRL` + `T`|Scroll json buffer to top| |`CTRL` + `N`|Scroll json buffer 'Page Down'| |`CTRL` + `P`|Scroll json buffer 'Page Up'| |`CTRL` + `L`|Change view mode whole json or keys (only object)| |`ESC`|Hide a candidate box| ### Option |option|description| |:-----------|:----------| |First argument ($1) | Initial query| |-h | print a help| |-help | print a help| |-version | print the version and exit| |-q | Output query mode (for jq)| |-M | monochrome output mode| ================================================ FILE: engine.go ================================================ package jid import ( "io" "strings" termbox "github.com/nsf/termbox-go" ) const ( DefaultY int = 1 FilterPrompt string = "[Filter]> " ) type EngineInterface interface { Run() EngineResultInterface GetQuery() QueryInterface } type EngineResultInterface interface { GetQueryString() string GetContent() string GetError() error } type Engine struct { manager *JsonManager query QueryInterface queryCursorIdx int term *Terminal complete []string keymode bool candidates []string candidatemode bool candidateidx int contentOffset int queryConfirm bool prettyResult bool } type EngineAttribute struct { DefaultQuery string Monochrome bool PrettyResult bool } func NewEngine(s io.Reader, ea *EngineAttribute) (EngineInterface, error) { j, err := NewJsonManager(s) if err != nil { return nil, err } e := &Engine{ manager: j, term: NewTerminal(FilterPrompt, DefaultY, ea.Monochrome), query: NewQuery([]rune(ea.DefaultQuery)), complete: []string{"", ""}, keymode: false, candidates: []string{}, candidatemode: false, candidateidx: 0, contentOffset: 0, queryConfirm: false, prettyResult: ea.PrettyResult, } e.queryCursorIdx = e.query.Length() return e, nil } type EngineResult struct { content string qs string err error } func (er *EngineResult) GetQueryString() string { return er.qs } func (er *EngineResult) GetContent() string { return er.content } func (er *EngineResult) GetError() error { return er.err } func (e *Engine) GetQuery() QueryInterface { return e.query } func (e *Engine) Run() EngineResultInterface { err := termbox.Init() if err != nil { panic(err) } defer termbox.Close() var contents []string for { if e.query.StringGet() == "" { e.query.StringSet(".") e.queryCursorIdx = e.query.Length() } bl := len(contents) contents = e.getContents() e.setCandidateData() e.queryConfirm = false if bl != len(contents) { e.contentOffset = 0 } ta := &TerminalDrawAttributes{ Query: e.query.StringGet(), Contents: contents, CandidateIndex: e.candidateidx, ContentsOffsetY: e.contentOffset, Complete: e.complete[0], Candidates: e.candidates, CursorOffset: e.query.IndexOffset(e.queryCursorIdx), } err = e.term.Draw(ta) if err != nil { panic(err) } switch ev := termbox.PollEvent(); ev.Type { case termbox.EventKey: switch ev.Key { case 0: e.inputChar(ev.Ch) case termbox.KeyBackspace, termbox.KeyBackspace2: e.deleteChar() case termbox.KeyTab: e.tabAction() case termbox.KeyArrowLeft, termbox.KeyCtrlB: e.moveCursorBackward() case termbox.KeyArrowRight, termbox.KeyCtrlF: e.moveCursorForward() case termbox.KeyHome, termbox.KeyCtrlA: e.moveCursorToTop() case termbox.KeyEnd, termbox.KeyCtrlE: e.moveCursorToEnd() case termbox.KeyCtrlK: e.scrollToAbove() case termbox.KeyCtrlJ: e.scrollToBelow() case termbox.KeyCtrlG: e.scrollToBottom(len(contents)) case termbox.KeyCtrlT: e.scrollToTop() case termbox.KeyCtrlN: _, h := termbox.Size() e.scrollPageDown(len(contents), h) case termbox.KeyCtrlP: _, h := termbox.Size() e.scrollPageUp(h) case termbox.KeyCtrlL: e.toggleKeymode() case termbox.KeyCtrlU: e.deleteLineQuery() case termbox.KeyCtrlW: e.deleteWordBackward() case termbox.KeyEsc: e.escapeCandidateMode() case termbox.KeyEnter: if !e.candidatemode { var cc string var err error if e.prettyResult { cc, _, _, err = e.manager.GetPretty(e.query, true) } else { cc, _, _, err = e.manager.Get(e.query, true) } return &EngineResult{ content: cc, qs: e.query.StringGet(), err: err, } } e.confirmCandidate() case termbox.KeyCtrlC: return &EngineResult{} default: } case termbox.EventError: panic(ev.Err) break default: } } } func (e *Engine) getContents() []string { var c string var contents []string c, e.complete, e.candidates, _ = e.manager.GetPretty(e.query, e.queryConfirm) if e.keymode { contents = e.candidates } else { contents = strings.Split(c, "\n") } return contents } func (e *Engine) setCandidateData() { if l := len(e.candidates); e.complete[0] == "" && l > 1 { if e.candidateidx >= l { e.candidateidx = 0 } } else { e.candidatemode = false } if !e.candidatemode { e.candidateidx = 0 e.candidates = []string{} } } func (e *Engine) confirmCandidate() { _, _ = e.query.PopKeyword() _ = e.query.StringAdd(".") _ = e.query.StringAdd(e.candidates[e.candidateidx]) e.queryCursorIdx = e.query.Length() e.queryConfirm = true } func (e *Engine) deleteChar() { if i := e.queryCursorIdx - 1; i > 0 { _ = e.query.Delete(i) e.queryCursorIdx-- } } func (e *Engine) deleteLineQuery() { _ = e.query.StringSet("") e.queryCursorIdx = 0 } func (e *Engine) scrollToBelow() { e.contentOffset++ } func (e *Engine) scrollToAbove() { if o := e.contentOffset - 1; o >= 0 { e.contentOffset = o } } func (e *Engine) scrollToBottom(rownum int) { e.contentOffset = rownum - 1 } func (e *Engine) scrollToTop() { e.contentOffset = 0 } func (e *Engine) scrollPageDown(rownum int, height int) { co := rownum - 1 if o := rownum - e.contentOffset; o > height { co = e.contentOffset + (height - DefaultY) } e.contentOffset = co } func (e *Engine) scrollPageUp(height int) { co := 0 if o := e.contentOffset - (height - DefaultY); o > 0 { co = o } e.contentOffset = co } func (e *Engine) toggleKeymode() { e.keymode = !e.keymode } func (e *Engine) deleteWordBackward() { if k, _ := e.query.StringPopKeyword(); k != "" && !strings.Contains(k, "[") { _ = e.query.StringAdd(".") } e.queryCursorIdx = e.query.Length() } func (e *Engine) tabAction() { if !e.candidatemode { e.candidatemode = true if e.complete[0] != e.complete[1] && e.complete[0] != "" { if k, _ := e.query.StringPopKeyword(); !strings.Contains(k, "[") { _ = e.query.StringAdd(".") } _ = e.query.StringAdd(e.complete[1]) } else { _ = e.query.StringAdd(e.complete[0]) } } else { e.candidateidx = e.candidateidx + 1 } e.queryCursorIdx = e.query.Length() } func (e *Engine) escapeCandidateMode() { e.candidatemode = false } func (e *Engine) inputChar(ch rune) { _ = e.query.Insert([]rune{ch}, e.queryCursorIdx) e.queryCursorIdx++ } func (e *Engine) moveCursorBackward() { if i := e.queryCursorIdx - 1; i >= 0 { e.queryCursorIdx-- } } func (e *Engine) moveCursorForward() { if e.query.Length() > e.queryCursorIdx { e.queryCursorIdx++ } } func (e *Engine) moveCursorWordBackwark() { } func (e *Engine) moveCursorWordForward() { } func (e *Engine) moveCursorToTop() { e.queryCursorIdx = 0 } func (e *Engine) moveCursorToEnd() { e.queryCursorIdx = e.query.Length() } ================================================ FILE: engine_test.go ================================================ package jid import ( "bytes" "os" "testing" "github.com/stretchr/testify/assert" ) func TestNewEngine(t *testing.T) { var assert = assert.New(t) f, _ := os.Create("/dev/null") e, err := NewEngine(f, &EngineAttribute{ DefaultQuery: "", Monochrome: false, }) assert.Nil(e) assert.NotNil(err) ee := getEngine(`{"name":"go"}`, "") assert.NotNil(ee) assert.Equal("", ee.query.StringGet()) assert.Equal(0, ee.queryCursorIdx) } func TestNewEngineWithQuery(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, ".nam") assert.Equal(".nam", e.query.StringGet()) assert.Equal(4, e.queryCursorIdx) e = getEngine(`{"name":"go"}`, "nam") assert.Equal("", e.query.StringGet()) assert.Equal(0, e.queryCursorIdx) e = getEngine(`{"name":"go"}`, ".nam..") assert.Equal("", e.query.StringGet()) assert.Equal(0, e.queryCursorIdx) } func TestDeleteChar(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") e.query.StringSet(".name") e.queryCursorIdx = e.query.Length() e.deleteChar() assert.Equal(".nam", e.query.StringGet()) assert.Equal(4, e.queryCursorIdx) } func TestDeleteWordBackward(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") e.query.StringSet(".name") e.deleteWordBackward() assert.Equal(".", e.query.StringGet()) assert.Equal(1, e.queryCursorIdx) e.query.StringSet(".name[1]") e.deleteWordBackward() assert.Equal(".name", e.query.StringGet()) assert.Equal(5, e.queryCursorIdx) e.query.StringSet(".name[") e.deleteWordBackward() assert.Equal(".name", e.query.StringGet()) assert.Equal(5, e.queryCursorIdx) } func TestDeleteLineQuery(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") e.query.StringSet(".name") e.deleteLineQuery() assert.Equal("", e.query.StringGet()) assert.Equal(0, e.queryCursorIdx) } func TestScrollToAbove(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") assert.Equal(0, e.contentOffset) e.scrollToAbove() assert.Equal(0, e.contentOffset) e.contentOffset = 5 e.scrollToAbove() assert.Equal(4, e.contentOffset) e.scrollToAbove() assert.Equal(3, e.contentOffset) } func TestScrollToBelow(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") e.scrollToBelow() assert.Equal(1, e.contentOffset) e.scrollToBelow() e.scrollToBelow() assert.Equal(3, e.contentOffset) } func TestScrollToBottomAndTop(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") e.scrollToBottom(5) assert.Equal(4, e.contentOffset) e.scrollToTop() assert.Equal(0, e.contentOffset) } func TestScrollPageUpDown(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") cl := len(e.getContents()) // Then DefaultY = 1 e.scrollPageDown(cl, 3) assert.Equal(2, e.contentOffset) e.scrollPageDown(cl, 3) assert.Equal(4, e.contentOffset) e.scrollPageUp(3) assert.Equal(2, e.contentOffset) // term height changed e.scrollPageDown(cl, 5) assert.Equal(6, e.contentOffset) e.scrollPageDown(cl, 5) assert.Equal(7, e.contentOffset) e.scrollPageDown(cl, 5) assert.Equal(7, e.contentOffset) e.scrollPageUp(5) assert.Equal(3, e.contentOffset) e.scrollPageUp(5) assert.Equal(0, e.contentOffset) e.scrollPageUp(5) assert.Equal(0, e.contentOffset) // term height > content size + default Y (a filter query line) e.scrollPageDown(cl, 10) assert.Equal(7, e.contentOffset) } func TestGetContents(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") c := e.getContents() assert.Equal([]string{`{`, ` "name": "go"`, "}"}, c) assert.Equal([]string{}, e.candidates) assert.Equal([]string{"", ""}, e.complete) e = getEngine(`{"name":"go", "naming":"simeji", "foo":"bar"}`, "") e.query.StringSet(".n") c = e.getContents() assert.Equal([]string{`{`, ` "foo": "bar",`, ` "name": "go",`, ` "naming": "simeji"`, "}"}, c) assert.Equal([]string{"name", "naming"}, e.candidates) assert.Equal([]string{"am", "nam"}, e.complete) e.keymode = true c = e.getContents() assert.Equal([]string{"name", "naming"}, c) assert.Equal([]string{"name", "naming"}, e.candidates) assert.Equal([]string{"am", "nam"}, e.complete) } func TestSetCandidateData(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") // case 1 e.candidates = []string{"test", "testing"} e.complete = []string{"est", "test"} e.candidatemode = true e.candidateidx = 1 e.setCandidateData() assert.False(e.candidatemode) assert.Zero(e.candidateidx) assert.Equal([]string{}, e.candidates) // case 2 e.candidates = []string{"test"} e.complete = []string{"", "test"} e.candidatemode = true e.candidateidx = 1 e.setCandidateData() assert.False(e.candidatemode) assert.Zero(e.candidateidx) assert.Equal([]string{}, e.candidates) // case 3 e.candidates = []string{"test", "testing"} e.complete = []string{"", "test"} e.candidatemode = true e.candidateidx = 2 e.setCandidateData() assert.True(e.candidatemode) assert.Zero(e.candidateidx) assert.Equal([]string{"test", "testing"}, e.candidates) // case 4 e.candidates = []string{"test", "testing"} e.complete = []string{"", "test"} e.candidatemode = true e.candidateidx = 1 e.setCandidateData() assert.True(e.candidatemode) assert.Equal(1, e.candidateidx) assert.Equal([]string{"test", "testing"}, e.candidates) // case 4 e.candidates = []string{"test", "testing"} e.complete = []string{"", "test"} e.candidatemode = false e.candidateidx = 1 e.setCandidateData() assert.False(e.candidatemode) assert.Equal(0, e.candidateidx) assert.Equal([]string{}, e.candidates) } func TestConfirmCandidate(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") e.query.StringSet(".") e.queryConfirm = false e.candidates = []string{"test", "testing", "foo"} e.candidateidx = 0 e.confirmCandidate() assert.Equal(".test", e.query.StringGet()) assert.True(e.queryConfirm) assert.Equal(5, e.queryCursorIdx) e.candidateidx = 2 e.confirmCandidate() assert.Equal(".foo", e.query.StringGet()) assert.True(e.queryConfirm) assert.Equal(4, e.queryCursorIdx) e = getEngine(`{"name":"go"}`, "") e.query.StringSet(".name.hoge") e.candidates = []string{"aaa", "bbb", "ccc"} e.candidateidx = 1 e.confirmCandidate() assert.True(e.queryConfirm) assert.Equal(9, e.queryCursorIdx) assert.Equal(".name.bbb", e.query.StringGet()) } func TestCtrllAction(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") assert.False(e.keymode) e.toggleKeymode() assert.True(e.keymode) e.toggleKeymode() assert.False(e.keymode) } func TestTabAction(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") e.query.StringSet(".namet") e.complete = []string{"est", "NameTest"} e.candidatemode = false e.tabAction() assert.Equal(".NameTest", e.query.StringGet()) _, e.complete, _, _ = e.manager.GetPretty(e.query, true) e.candidatemode = false e.tabAction() assert.Equal(".NameTest[", e.query.StringGet()) _, e.complete, _, _ = e.manager.GetPretty(e.query, true) e.candidatemode = false e.tabAction() assert.Equal(".NameTest[", e.query.StringGet()) } func TestEscAction(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") assert.False(e.candidatemode) e.escapeCandidateMode() assert.False(e.candidatemode) e.candidatemode = true e.escapeCandidateMode() assert.False(e.candidatemode) } func TestInputChar(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"go"}`, "") e.query.StringSet(".name") e.queryCursorIdx = e.query.Length() assert.Equal(5, e.queryCursorIdx) e.inputChar('n') assert.Equal(".namen", e.query.StringGet()) assert.Equal(6, e.queryCursorIdx) e.inputChar('.') assert.Equal(".namen.", e.query.StringGet()) assert.Equal(7, e.queryCursorIdx) } func TestMoveCursorForwardAndBackward(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"simeji"}`, "") e.query.StringSet(".ne") e.moveCursorForward() assert.Equal(1, e.queryCursorIdx) e.moveCursorForward() assert.Equal(2, e.queryCursorIdx) e.moveCursorForward() assert.Equal(3, e.queryCursorIdx) e.moveCursorForward() assert.Equal(3, e.queryCursorIdx) e.moveCursorBackward() assert.Equal(2, e.queryCursorIdx) e.moveCursorBackward() assert.Equal(1, e.queryCursorIdx) e.moveCursorBackward() assert.Equal(0, e.queryCursorIdx) e.moveCursorBackward() assert.Equal(0, e.queryCursorIdx) } func TestMoveCursorToTopAndEnd(t *testing.T) { var assert = assert.New(t) e := getEngine(`{"name":"simeji"}`, "") e.query.StringSet(".ne") e.moveCursorToTop() assert.Zero(e.queryCursorIdx) e.moveCursorToEnd() assert.Equal(3, e.queryCursorIdx) } func getEngine(j string, qs string) *Engine { r := bytes.NewBufferString(j) e, _ := NewEngine(r, &EngineAttribute{ DefaultQuery: qs, Monochrome: false, }) ee := e.(*Engine) return ee } ================================================ FILE: go.mod ================================================ module github.com/simeji/jid go 1.20 require ( github.com/bitly/go-simplejson v0.5.0 github.com/mattn/go-runewidth v0.0.4 github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5 github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.8.1 ) require ( github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.10.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed h1:bAVGG6B+R5qpSylrrA+BAMrzYkdAoiTaKPVxRB+4cyM= github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5 h1:d+C3xJdxZT7wNlxqEwbXn3R355CwAhYBL9raVNfSnK0= github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5/go.mod h1:GYFm0zZgTNeoK1QxuIofRDasy2ibmaJZhZLzwsMXUF4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: json_manager.go ================================================ package jid import ( "github.com/bitly/go-simplejson" "github.com/pkg/errors" "io" "regexp" "strconv" //"strings" ) type JsonManager struct { current *simplejson.Json origin *simplejson.Json suggestion *Suggestion } func NewJsonManager(reader io.Reader) (*JsonManager, error) { buf, err := io.ReadAll(reader) if err != nil { return nil, errors.Wrap(err, "invalid data") } j, err2 := simplejson.NewJson(buf) if err2 != nil { return nil, errors.Wrap(err2, "invalid json format") } json := &JsonManager{ origin: j, current: j, suggestion: NewSuggestion(), } return json, nil } func (jm *JsonManager) Get(q QueryInterface, confirm bool) (string, []string, []string, error) { json, suggestion, candidates, _ := jm.GetFilteredData(q, confirm) data, enc_err := json.Encode() if enc_err != nil { return "", []string{"", ""}, []string{"", ""}, errors.Wrap(enc_err, "failure json encode") } return string(data), suggestion, candidates, nil } func (jm *JsonManager) GetPretty(q QueryInterface, confirm bool) (string, []string, []string, error) { json, suggestion, candidates, _ := jm.GetFilteredData(q, confirm) s, err := json.EncodePretty() if err != nil { return "", []string{"", ""}, []string{"", ""}, errors.Wrap(err, "failure json encode") } return string(s), suggestion, candidates, nil } func (jm *JsonManager) GetFilteredData(q QueryInterface, confirm bool) (*simplejson.Json, []string, []string, error) { json := jm.origin lastKeyword := q.StringGetLastKeyword() keywords := q.StringGetKeywords() idx := 0 if l := len(keywords); l == 0 { return json, []string{"", ""}, []string{}, nil } else if l > 0 { idx = l - 1 } for _, keyword := range keywords[0:idx] { json, _ = getItem(json, keyword) } reg := regexp.MustCompile(`\[[0-9]*$`) suggest := jm.suggestion.Get(json, lastKeyword) candidateKeys := jm.suggestion.GetCandidateKeys(json, lastKeyword) // hash if len(reg.FindString(lastKeyword)) < 1 { candidateNum := len(candidateKeys) if j, exist := getItem(json, lastKeyword); exist && (confirm || candidateNum == 1) { json = j candidateKeys = []string{} if _, err := json.Array(); err == nil { suggest = jm.suggestion.Get(json, "") } else { suggest = []string{"", ""} } } else if candidateNum < 1 { json = j suggest = jm.suggestion.Get(json, "") } } return json, suggest, candidateKeys, nil } func (jm *JsonManager) GetCandidateKeys(q QueryInterface) []string { return jm.suggestion.GetCandidateKeys(jm.current, q.StringGetLastKeyword()) } func getItem(json *simplejson.Json, s string) (*simplejson.Json, bool) { var result *simplejson.Json var exist bool re := regexp.MustCompile(`\[([0-9]+)\]`) matches := re.FindStringSubmatch(s) if s == "" { return json, false } // Query include [ if len(matches) > 0 { index, _ := strconv.Atoi(matches[1]) if a, err := json.Array(); err != nil { exist = false } else if len(a) < index { exist = false } result = json.GetIndex(index) } else { result, exist = json.CheckGet(s) if result == nil { result = &simplejson.Json{} } } return result, exist } func isEmptyJson(j *simplejson.Json) bool { switch j.Interface().(type) { case nil: return true default: return false } } ================================================ FILE: json_manager_test.go ================================================ package jid import ( "bytes" "io" "testing" simplejson "github.com/bitly/go-simplejson" "github.com/stretchr/testify/assert" ) func TestNewJson(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString("{\"name\":\"go\"}") jm, e := NewJsonManager(r) rr := bytes.NewBufferString("{\"name\":\"go\"}") buf, _ := io.ReadAll(rr) sj, _ := simplejson.NewJson(buf) assert.Equal(jm, &JsonManager{ current: sj, origin: sj, suggestion: NewSuggestion(), }) assert.Nil(e) assert.Equal("go", jm.current.Get("name").MustString()) } func TestNewJsonWithError(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString("{\"name\":\"go\"") jm, e := NewJsonManager(r) assert.Nil(jm) assert.Regexp("invalid json format", e.Error()) } func TestGet(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString("{\"name\":\"go\"}") jm, _ := NewJsonManager(r) q := NewQueryWithString(".name") result, suggest, candidateKeys, err := jm.Get(q, false) assert.Nil(err) assert.Equal(`"go"`, result) assert.Equal([]string{``, ``}, suggest) assert.Equal([]string{}, candidateKeys) // data data := `{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}}` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) // case 2 q = NewQueryWithString(".abcde") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) //assert.Equal(`"2AA2"`, result) assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}}`, result) assert.Equal([]string{``, "abcde"}, suggest) // case 3 q = NewQueryWithString(".abcde_fgh") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, result) assert.Equal([]string{``, ``}, suggest) // case 4 q = NewQueryWithString(".abcde_fgh.aaa[2]") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Equal(`[1,2]`, result) // case 5 q = NewQueryWithString(".abcde_fgh.aaa[3]") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) assert.Equal(`null`, result) // case 6 q = NewQueryWithString(".abcde_fgh.aa") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, result) assert.Equal([]string{`a`, `aaa`}, suggest) // case 7 q = NewQueryWithString(".abcde_fgh.ac") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) assert.Equal(`null`, result) assert.Equal([]string{``, ``}, suggest) // data data = `{"abc":"2AA2","def":{"aaa":"bbb"}}` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) // case 2 q = NewQueryWithString(".def") result, suggest, candidateKeys, err = jm.Get(q, false) assert.Nil(err) assert.Equal(`{"aaa":"bbb"}`, result) assert.Equal([]string{``, ``}, suggest) } func TestGetPretty(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString("{\"name\":\"go\"}") jm, _ := NewJsonManager(r) q := NewQueryWithString(".name") result, _, _, err := jm.GetPretty(q, true) assert.Nil(err) assert.Equal(`"go"`, result) } func TestGetItem(t *testing.T) { var assert = assert.New(t) rr := bytes.NewBufferString(`{"name":"go"}`) buf, _ := io.ReadAll(rr) sj, _ := simplejson.NewJson(buf) d, _ := getItem(sj, "") result, _ := d.Encode() assert.Equal(`{"name":"go"}`, string(result)) d, _ = getItem(sj, "name") result, _ = d.Encode() assert.Equal(`"go"`, string(result)) // case 2 rr = bytes.NewBufferString(`{"name":"go","age":20}`) buf, _ = io.ReadAll(rr) sj, _ = simplejson.NewJson(buf) d, _ = getItem(sj, "age") result, _ = d.Encode() assert.Equal("20", string(result)) // case 3 rr = bytes.NewBufferString(`{"data":{"name":"go","age":20}}`) buf, _ = io.ReadAll(rr) sj, _ = simplejson.NewJson(buf) d, _ = getItem(sj, "data") d2, _ := getItem(d, "name") d3, _ := getItem(d, "age") result2, _ := d2.Encode() result3, _ := d3.Encode() assert.Equal(`"go"`, string(result2)) assert.Equal(`20`, string(result3)) // case 4 rr = bytes.NewBufferString(`{"data":[{"name":"test","age":30},{"name":"go","age":20}]}`) buf, _ = io.ReadAll(rr) sj, _ = simplejson.NewJson(buf) d, _ = getItem(sj, "data") d2, _ = getItem(d, "[1]") d3, _ = getItem(d2, "name") result, _ = d3.Encode() assert.Equal(`"go"`, string(result)) // case 5 rr = bytes.NewBufferString(`[{"name":"go","age":20}]`) buf, _ = io.ReadAll(rr) sj, _ = simplejson.NewJson(buf) d, _ = getItem(sj, "") result, _ = d.Encode() assert.Equal(`[{"age":20,"name":"go"}]`, string(result)) // case 6 d, _ = getItem(sj, "[0]") result, _ = d.Encode() assert.Equal(`{"age":20,"name":"go"}`, string(result)) // case 7 key contains '.' rr = bytes.NewBufferString(`{"na.me":"go","age":20}`) buf, _ = io.ReadAll(rr) sj, _ = simplejson.NewJson(buf) d, _ = getItem(sj, "na.me") result, _ = d.Encode() assert.Equal(`"go"`, string(result)) } func TestGetFilteredData(t *testing.T) { var assert = assert.New(t) // data data := `{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}` r := bytes.NewBufferString(data) jm, _ := NewJsonManager(r) // case 1 q := NewQueryWithString(".abcde") result, s, c, err := jm.GetFilteredData(q, false) assert.Nil(err) d, _ := result.Encode() assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}`, string(d)) //assert.Equal(`"2AA2"`, string(d)) assert.Equal([]string{``, `abcde`}, s) assert.Equal([]string{"abcde", "abcde_fgh"}, c) // case 2 q = NewQueryWithString(".abcde_fgh") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 3 q = NewQueryWithString(".abcde_fgh.aaa[2]") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`[1,2]`, string(d)) assert.Equal([]string{`[`, `[`}, s) // case 4 q = NewQueryWithString(".abcde_fgh.aaa[3]") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`null`, string(d)) assert.Equal([]string{``, ``}, s) // case 5 q = NewQueryWithString(".abcde_fgh.aaa") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`[123,"cccc",[1,2]]`, string(d)) assert.Equal([]string{`[`, `[`}, s) // case 6 q = NewQueryWithString(".abcde_fgh.aa") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, string(d)) assert.Equal([]string{`a`, `aaa`}, s) // case 7 q = NewQueryWithString(".abcde_fgh.aaa[") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`[123,"cccc",[1,2]]`, string(d)) assert.Equal([]string{``, `[`}, s) // case 8 q = NewQueryWithString(".") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}`, string(d)) assert.Equal([]string{``, ``}, s) // case 9 q = NewQueryWithString(".cc.") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"a":[3,4]}`, string(d)) assert.Equal([]string{`a`, `a`}, s) assert.Equal([]string{"a"}, c) // case 2-1 data = `{"arraytest":[{"aaa":123,"aab":234},[1,2]]}` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) q = NewQueryWithString(".arraytest[0]") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"aaa":123,"aab":234}`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 3-1 data = `{"aa":"abcde","bb":{"foo":"bar"}}` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) q = NewQueryWithString(".bb") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"foo":"bar"}`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 4-1 data = `[{"name": "simeji"},{"name": "simeji2"}]` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) q = NewQueryWithString("") result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`[{"name":"simeji"},{"name":"simeji2"}]`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 5-1 data = `{"PrivateName":"simei", "PrivateAlias": "simeji2"}` r = bytes.NewBufferString(data) jm, _ = NewJsonManager(r) q = NewQueryWithString(".Private") result, s, c, err = jm.GetFilteredData(q, false) d, _ = result.Encode() assert.Equal([]string{``, `Private`}, s) assert.Equal([]string{"PrivateAlias", "PrivateName"}, c) } func TestGetFilteredDataWithMatchQuery(t *testing.T) { var assert = assert.New(t) data := `{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}` r := bytes.NewBufferString(data) jm, _ := NewJsonManager(r) q := NewQueryWithString(`.name`) result, s, c, err := jm.GetFilteredData(q, false) assert.Nil(err) d, _ := result.Encode() assert.Equal(`[1,2,3]`, string(d)) assert.Equal([]string{"[", "["}, s) assert.Equal([]string{}, c) q = NewQueryWithString(`.naming`) result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"account":"simeji"}`, string(d)) assert.Equal([]string{"", ""}, s) assert.Equal([]string{}, c) q = NewQueryWithString(`.naming.`) result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"account":"simeji"}`, string(d)) assert.Equal([]string{"account", "account"}, s) assert.Equal([]string{"account"}, c) q = NewQueryWithString(`.test`) result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`{"name":[1,2,3],"naming":{"account":"simeji"},"test":"simeji","testing":"ok"}`, string(d)) assert.Equal([]string{"", "test"}, s) assert.Equal([]string{"test", "testing"}, c) } func TestGetFilteredDataWithContainDots(t *testing.T) { var assert = assert.New(t) // data data := `{"abc.de":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}` r := bytes.NewBufferString(data) jm, _ := NewJsonManager(r) // case 1 q := NewQueryWithString(`.\"abc.de\"`) result, s, c, err := jm.GetFilteredData(q, false) assert.Nil(err) d, _ := result.Encode() assert.Equal(`"2AA2"`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 2 q = NewQueryWithString(`."abc.de"`) result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`null`, string(d)) assert.Equal([]string{``, ``}, s) assert.Equal([]string{}, c) // case 3 q = NewQueryWithString(`.abc.de`) result, s, c, err = jm.GetFilteredData(q, false) assert.Nil(err) d, _ = result.Encode() assert.Equal(`null`, string(d)) assert.Equal([]string{"", ""}, s) assert.Equal([]string{}, c) } func TestGetCandidateKeys(t *testing.T) { var assert = assert.New(t) data := `{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}` r := bytes.NewBufferString(data) jm, _ := NewJsonManager(r) q := NewQueryWithString(`.n`) keys := jm.GetCandidateKeys(q) assert.Equal([]string{"name", "naming"}, keys) q = NewQueryWithString(`.`) keys = jm.GetCandidateKeys(q) assert.Equal([]string{"name", "naming", "test", "testing"}, keys) q = NewQueryWithString(`.test`) keys = jm.GetCandidateKeys(q) assert.Equal([]string{"test", "testing"}, keys) q = NewQueryWithString(`.testi`) keys = jm.GetCandidateKeys(q) assert.Equal([]string{"testing"}, keys) q = NewQueryWithString(`.testia`) keys = jm.GetCandidateKeys(q) assert.Equal([]string{}, keys) } func TestGetCurrentKeys(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString(`{"name":"go","age":20,"weight":60}`) buf, _ := io.ReadAll(r) sj, _ := simplejson.NewJson(buf) keys := getCurrentKeys(sj) assert.Equal([]string{"age", "name", "weight"}, keys) r = bytes.NewBufferString(`[2,3,"aa"]`) buf, _ = io.ReadAll(r) sj, _ = simplejson.NewJson(buf) keys = getCurrentKeys(sj) assert.Equal([]string{}, keys) } func TestIsEmptyJson(t *testing.T) { var assert = assert.New(t) r := bytes.NewBufferString(`{"name":"go"}`) buf, _ := io.ReadAll(r) sj, _ := simplejson.NewJson(buf) assert.Equal(false, isEmptyJson(sj)) assert.Equal(true, isEmptyJson(&simplejson.Json{})) } ================================================ FILE: query.go ================================================ package jid import ( "regexp" "strings" runewidth "github.com/mattn/go-runewidth" ) type QueryInterface interface { Get() []rune Set(query []rune) []rune Insert(query []rune, idx int) []rune Add(query []rune) []rune Delete(i int) []rune Clear() []rune Length() int IndexOffset(int) int GetChar(int) rune GetKeywords() [][]rune GetLastKeyword() []rune PopKeyword() ([]rune, []rune) StringGet() string StringSet(query string) string StringInsert(query string, idx int) string StringAdd(query string) string StringGetKeywords() []string StringGetLastKeyword() string StringPopKeyword() (string, []rune) } type Query struct { query *[]rune complete *[]rune } func NewQuery(query []rune) *Query { q := &Query{ query: &[]rune{}, complete: &[]rune{}, } _ = q.Set(query) return q } func NewQueryWithString(query string) *Query { return NewQuery([]rune(query)) } func (q *Query) Get() []rune { return *q.query } func (q *Query) GetChar(idx int) rune { var r rune = 0 qq := q.Get() if l := len(qq); l > idx && idx >= 0 { r = qq[idx] } return r } func (q *Query) Length() int { return len(q.Get()) } func (q *Query) IndexOffset(i int) int { o := 0 if l := q.Length(); i >= l { o = runewidth.StringWidth(q.StringGet()) } else if i >= 0 && i < l { o = runewidth.StringWidth(string(q.Get()[:i])) } return o } func (q *Query) Set(query []rune) []rune { if validate(query) { q.query = &query } return q.Get() } func (q *Query) Insert(query []rune, idx int) []rune { qq := q.Get() if idx == 0 { qq = append(query, qq...) } else if idx > 0 && len(qq) >= idx { _q := make([]rune, idx+len(query)-1) copy(_q, qq[:idx]) qq = append(append(_q, query...), qq[idx:]...) } return q.Set(qq) } func (q *Query) StringInsert(query string, idx int) string { return string(q.Insert([]rune(query), idx)) } func (q *Query) Add(query []rune) []rune { return q.Set(append(q.Get(), query...)) } func (q *Query) Delete(i int) []rune { var d []rune qq := q.Get() lastIdx := len(qq) if i < 0 { if lastIdx+i >= 0 { d = qq[lastIdx+i:] qq = qq[0 : lastIdx+i] } else { d = qq qq = qq[0:0] } } else if i == 0 { d = []rune{} qq = qq[1:] } else if i > 0 && i < lastIdx { d = []rune{qq[i]} qq = append(qq[:i], qq[i+1:]...) } _ = q.Set(qq) return d } func (q *Query) Clear() []rune { return q.Set([]rune("")) } func (q *Query) GetKeywords() [][]rune { qq := *q.query if qq == nil || string(qq) == "" { return [][]rune{} } splitQuery := []string{} rr := []rune{} enclosed := true ql := len(*q.query) for i := 0; i < ql; i++ { r := qq[i] if ii := i + 1; r == '\\' && ql > ii && qq[ii] == '"' { enclosed = !enclosed i++ // skip '"(double quortation)' continue } if enclosed && r == '.' { splitQuery = append(splitQuery, string(rr)) rr = []rune{} } else { rr = append(rr, r) } } if rr != nil { v := []string{string(rr)} if !enclosed { v = strings.Split(string(rr), ".") } splitQuery = append(splitQuery, v...) } lastIdx := len(splitQuery) - 1 keywords := [][]rune{} for i, keyword := range splitQuery { if keyword != "" || i == lastIdx { re := regexp.MustCompile(`\[[0-9]*\]?`) matchIndexes := re.FindAllStringIndex(keyword, -1) if len(matchIndexes) < 1 { keywords = append(keywords, []rune(keyword)) } else { if matchIndexes[0][0] > 0 { keywords = append(keywords, []rune(keyword[0:matchIndexes[0][0]])) } for _, matchIndex := range matchIndexes { k := keyword[matchIndex[0]:matchIndex[1]] keywords = append(keywords, []rune(k)) } } } } return keywords } func (q *Query) GetLastKeyword() []rune { keywords := q.GetKeywords() if l := len(keywords); l > 0 { return keywords[l-1] } return []rune("") } func (q *Query) StringGetLastKeyword() string { return string(q.GetLastKeyword()) } func (q *Query) PopKeyword() ([]rune, []rune) { keyword := q.GetLastKeyword() nq := string(keyword) qq := q.StringGet() for _, r := range keyword { if r == '.' { nq = `\"` + string(keyword) + `\"` break } } re := regexp.MustCompile(`(\.)?(\\")?` + regexp.QuoteMeta(nq) + "$") qq = re.ReplaceAllString(qq, "") query := q.Set([]rune(qq)) return keyword, query } func (q *Query) StringGet() string { return string(q.Get()) } func (q *Query) StringSet(query string) string { return string(q.Set([]rune(query))) } func (q *Query) StringAdd(query string) string { return string(q.Add([]rune(query))) } func (q *Query) StringGetKeywords() []string { var keywords []string for _, keyword := range q.GetKeywords() { keywords = append(keywords, string(keyword)) } return keywords } func (q *Query) StringPopKeyword() (string, []rune) { keyword, query := q.PopKeyword() return string(keyword), query } func validate(r []rune) bool { s := string(r) if s == "" { return true } if regexp.MustCompile(`^[^.]`).MatchString(s) { return false } if regexp.MustCompile(`\.{2,}`).MatchString(s) { return false } if regexp.MustCompile(`\[[0-9]*\][^\.\[]`).MatchString(s) { return false } if regexp.MustCompile(`\[{2,}|\]{2,}`).MatchString(s) { return false } if regexp.MustCompile(`.\.\[`).MatchString(s) { return false } return true } ================================================ FILE: query_test.go ================================================ package jid import ( "testing" "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { var assert = assert.New(t) assert.True(validate([]rune(".test.name"))) assert.True(validate([]rune(".test.name."))) assert.True(validate([]rune(".test[0].name."))) assert.True(validate([]rune(".[0].name."))) assert.True(validate([]rune(".name[9][1]"))) assert.True(validate([]rune(".[0][1].name."))) assert.False(validate([]rune("[0].name."))) assert.False(validate([]rune(".test[0]].name."))) assert.False(validate([]rune(".test..name"))) assert.False(validate([]rune(".test.name.."))) assert.False(validate([]rune(".test[[0]].name."))) assert.False(validate([]rune(".test[0]name."))) assert.False(validate([]rune(".test.[0].name."))) } func TestNewQuery(t *testing.T) { var assert = assert.New(t) v := []rune(".name") q := NewQuery(v) assert.Equal(*q.query, []rune(".name")) assert.Equal(*q.complete, []rune("")) } func TestNewQueryWithInvalidQuery(t *testing.T) { var assert = assert.New(t) v := []rune("name") q := NewQuery(v) assert.Equal(*q.query, []rune("")) assert.Equal(*q.complete, []rune("")) } func TestNewQueryWithString(t *testing.T) { var assert = assert.New(t) q := NewQueryWithString(".name") assert.Equal(*q.query, []rune(".name")) assert.Equal(*q.complete, []rune("")) } func TestNewQueryWithStringWithInvalidQuery(t *testing.T) { var assert = assert.New(t) q := NewQueryWithString("name") assert.Equal(*q.query, []rune("")) assert.Equal(*q.complete, []rune("")) } func TestQueryGet(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal(q.Get(), []rune(".test")) } func TestQueryLength(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal(5, q.Length()) v = []rune(".string.日本語.japan") q = NewQuery(v) assert.Equal(17, q.Length()) } func TestQueryIndexOffsetN(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal(4, q.IndexOffset(4)) assert.Equal(0, q.IndexOffset(0)) assert.Equal(0, q.IndexOffset(-1)) assert.Equal(5, q.IndexOffset(6)) //off-------012345679-101213|j_15,n_19 v = []rune(".string.日本語.japan") //idx-------012345678-9-10|j_12,n_16 q = NewQuery(v) assert.Equal(19, q.IndexOffset(16)) assert.Equal(10, q.IndexOffset(9)) } func TestQueryGetChar(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal('e', q.GetChar(2)) assert.Equal('t', q.GetChar(4)) assert.Equal('.', q.GetChar(0)) assert.Equal('.', q.GetChar(0)) assert.Equal(rune(0), q.GetChar(-1)) assert.Equal(rune(0), q.GetChar(6)) v = []rune(".string.日本語.japan") q = NewQuery(v) assert.Equal('n', q.GetChar(5)) assert.Equal('本', q.GetChar(9)) assert.Equal('.', q.GetChar(11)) assert.Equal(rune(0), q.GetChar(17)) } func TestQuerySet(t *testing.T) { var assert = assert.New(t) v := []rune(".hello") q := NewQuery(v) assert.Equal([]rune(".world"), q.Set([]rune(".world"))) assert.Equal("", string(q.Set([]rune("")))) } func TestQuerySetWithInvalidQuery(t *testing.T) { var assert = assert.New(t) v := []rune(".hello") q := NewQuery(v) assert.Equal(q.Set([]rune("world")), []rune(".hello")) } func TestQueryAdd(t *testing.T) { var assert = assert.New(t) v := []rune(".hello") q := NewQuery(v) assert.Equal(q.Add([]rune("world")), []rune(".helloworld")) } func TestQueryInsert(t *testing.T) { var assert = assert.New(t) v := []rune(".hello.world") q := NewQuery(v) assert.Equal([]rune(".hello.world"), q.Insert([]rune("w"), 0)) assert.Equal([]rune(".whello.world"), q.Insert([]rune("w"), 1)) assert.Equal([]rune(".wwhello.world"), q.Insert([]rune("w"), 1)) assert.Equal([]rune(".wwhello.world"), q.Insert([]rune("."), 1)) assert.Equal([]rune(".wwh.ello.world"), q.Insert([]rune("."), 4)) assert.Equal([]rune(".wwh.ello.worldg"), q.Insert([]rune("g"), 15)) assert.Equal([]rune(".wwh.ello.worldg"), q.Insert([]rune("a"), 20)) } func TestQueryStringInsert(t *testing.T) { var assert = assert.New(t) q := NewQueryWithString(".hello.world") assert.Equal(".hello.world", q.StringInsert("w", 0)) assert.Equal(".whello.world", q.StringInsert("w", 1)) assert.Equal(".wwhello.world", q.StringInsert("w", 1)) assert.Equal(".wwhello.world", q.StringInsert(".", 1)) assert.Equal(".wwh.ello.world", q.StringInsert(".", 4)) assert.Equal(".wwh.ello.worlda", q.StringInsert("a", 15)) assert.Equal(".wwh.ello.worlda", q.StringInsert("a", 20)) } func TestQueryClear(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal(q.Clear(), []rune("")) } func TestQueryDelete(t *testing.T) { var assert = assert.New(t) v := []rune(".helloworld") q := NewQuery(v) assert.Equal([]rune("d"), q.Delete(-1)) assert.Equal([]rune(".helloworl"), q.Get()) assert.Equal([]rune("l"), q.Delete(-1)) assert.Equal([]rune(".hellowor"), q.Get()) assert.Equal([]rune("or"), q.Delete(-2)) assert.Equal([]rune(".hellow"), q.Get()) assert.Equal([]rune(".hellow"), q.Delete(-8)) assert.Equal([]rune(""), q.Get()) q = NewQuery([]rune(".hello.world")) assert.Equal([]rune(""), q.Delete(0)) assert.Equal([]rune(".hello.world"), q.Get()) assert.Equal([]rune("h"), q.Delete(1)) assert.Equal([]rune(".ello.world"), q.Get()) assert.Equal([]rune("e"), q.Delete(1)) assert.Equal([]rune(".llo.world"), q.Get()) assert.Equal([]rune(""), q.Delete(0)) assert.Equal([]rune(".llo.world"), q.Get()) assert.Equal([]rune("o"), q.Delete(3)) assert.Equal([]rune(".ll.world"), q.Get()) assert.Equal([]rune("."), q.Delete(3)) assert.Equal([]rune(".llworld"), q.Get()) assert.Equal([]rune("w"), q.Delete(3)) assert.Equal([]rune(".llorld"), q.Get()) } func TestGetKeywords(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) assert.Equal([][]rune{ []rune("test"), []rune("name"), }, q.GetKeywords()) v = []rune("") q = NewQuery(v) assert.Equal([][]rune{}, q.GetKeywords()) v = []rune(".test.name.") q = NewQuery(v) assert.Equal([][]rune{ []rune("test"), []rune("name"), []rune(""), }, q.GetKeywords()) v = []rune(".hello") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), }) v = []rune(".hello.") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), []rune(""), }) v = []rune(".hello[") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), []rune("["), }) v = []rune(".hello[12") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), []rune("[12"), }) v = []rune(".hello[0]") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), []rune("[0]"), }) v = []rune(".hello[13][0]") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("hello"), []rune("[13]"), []rune("[0]"), }) v = []rune(".[3][23].hello[13][0]") q = NewQuery(v) assert.Equal(q.GetKeywords(), [][]rune{ []rune("[3]"), []rune("[23]"), []rune("hello"), []rune("[13]"), []rune("[0]"), }) } func TestGetKeywordsWithDots(t *testing.T) { var assert = assert.New(t) v := []rune(`.test.\"na.me\"`) q := NewQuery(v) assert.Equal([][]rune{ []rune("test"), []rune("na.me"), }, q.GetKeywords()) v = []rune(`.test.\"na.me\`) q = NewQuery(v) assert.Equal([][]rune{ []rune("test"), []rune("na"), []rune(`me\`), }, q.GetKeywords()) } func TestGetLastKeyword(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) assert.Equal(q.GetLastKeyword(), []rune("name")) v = []rune(".test.") q = NewQuery(v) assert.Equal(q.GetLastKeyword(), []rune("")) v = []rune(".test") q = NewQuery(v) assert.Equal(q.GetLastKeyword(), []rune("test")) v = []rune("") q = NewQuery(v) assert.Equal(q.GetLastKeyword(), []rune("")) } func TestStringGetLastKeyword(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) assert.Equal(q.StringGetLastKeyword(), "name") v = []rune(".test.") q = NewQuery(v) assert.Equal(q.StringGetLastKeyword(), "") v = []rune(".test") q = NewQuery(v) assert.Equal(q.StringGetLastKeyword(), "test") v = []rune("") q = NewQuery(v) assert.Equal(q.StringGetLastKeyword(), "") } func TestPopKeyword(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) k, query := q.PopKeyword() assert.Equal([]rune("name"), k) assert.Equal([]rune(".test"), query) assert.Equal([]rune(".test"), q.Get()) v = []rune(".a[0") q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune("[0"), k) assert.Equal([]rune(".a"), query) assert.Equal([]rune(".a"), q.Get()) k, query = q.PopKeyword() assert.Equal([]rune("a"), k) assert.Equal([]rune(""), query) assert.Equal([]rune(""), q.Get()) v = []rune(".") q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune(""), k) assert.Equal([]rune(""), query) assert.Equal([]rune(""), q.Get()) v = []rune(".test.name.") q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune(""), k) assert.Equal([]rune(".test.name"), query) assert.Equal([]rune(".test.name"), q.Get()) v = []rune(`.name.\"te.st\"`) q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune("te.st"), k) assert.Equal([]rune(".name"), query) assert.Equal([]rune(".name"), q.Get()) v = []rune(`.name.\"te.st\".hoge`) q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune("hoge"), k) assert.Equal([]rune(`.name.\"te.st\"`), query) assert.Equal([]rune(`.name.\"te.st\"`), q.Get()) v = []rune(`.name.\"te`) q = NewQuery(v) k, query = q.PopKeyword() assert.Equal([]rune(`te`), k) assert.Equal([]rune(`.name`), query) assert.Equal([]rune(`.name`), q.Get()) } func TestQueryStringGet(t *testing.T) { var assert = assert.New(t) v := []rune(".test") q := NewQuery(v) assert.Equal(q.StringGet(), ".test") } func TestQueryStringSet(t *testing.T) { var assert = assert.New(t) v := []rune(".hello") q := NewQuery(v) assert.Equal(q.StringSet(".world"), ".world") } func TestQueryStringAdd(t *testing.T) { var assert = assert.New(t) v := []rune(".hello") q := NewQuery(v) assert.Equal(q.StringAdd("world"), ".helloworld") } func TestStringGetKeywords(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) assert.Equal(q.StringGetKeywords(), []string{ "test", "name", }) v = []rune(".test.name") q = NewQuery(v) assert.Equal(q.StringGetKeywords(), []string{ "test", "name", }) v = []rune("") q = NewQuery(v) kws := q.StringGetKeywords() assert.Equal([]string(nil), kws) assert.Equal(0, len(kws)) } func TestStringPopKeyword(t *testing.T) { var assert = assert.New(t) v := []rune(".test.name") q := NewQuery(v) k, query := q.StringPopKeyword() assert.Equal(k, "name") assert.Equal(query, []rune(".test")) assert.Equal(q.Get(), []rune(".test")) v = []rune(".test.name.") q = NewQuery(v) k, query = q.StringPopKeyword() assert.Equal(k, "") assert.Equal(query, []rune(".test.name")) assert.Equal(q.Get(), []rune(".test.name")) v = []rune(".test.name[23]") q = NewQuery(v) k, query = q.StringPopKeyword() assert.Equal(k, "[23]") assert.Equal(query, []rune(".test.name")) assert.Equal(q.Get(), []rune(".test.name")) } ================================================ FILE: suggestion.go ================================================ package jid import ( "regexp" "sort" "strings" simplejson "github.com/bitly/go-simplejson" ) type SuggestionInterface interface { Get(json *simplejson.Json, keyword string) []string GetCandidateKeys(json *simplejson.Json, keyword string) []string } type SuggestionDataType int const ( UNKNOWN SuggestionDataType = iota ARRAY MAP NUMBER STRING BOOL ) type Suggestion struct { } func NewSuggestion() *Suggestion { return &Suggestion{} } func (s *Suggestion) Get(json *simplejson.Json, keyword string) []string { var completion string var suggestion string if a, err := json.Array(); err == nil { if len(a) > 1 { kw := regexp.MustCompile(`\[([0-9]+)?\]?`).FindString(keyword) if kw == "" { return []string{"[", "["} } else if kw == "[" { return []string{"", "["} } return []string{strings.Replace(kw+"]", kw, "", -1), kw + "]"} } return []string{strings.Replace(`[0]`, keyword, "", -1), `[0]`} } candidateKeys := s.GetCandidateKeys(json, keyword) if keyword == "" { if l := len(candidateKeys); l > 1 { return []string{"", ""} } else if l == 1 { return []string{candidateKeys[0], candidateKeys[0]} } } for _, key := range candidateKeys { // first if suggestion == "" && key != "" { suggestion = key } else { axis := suggestion if len(suggestion) > len(key) { axis = key } max := 0 for i, _ := range axis { if suggestion[i] != key[i] { break } max = i } if max == 0 { suggestion = "" break } suggestion = suggestion[0 : max+1] } } if reg, err := regexp.Compile("(?i)^" + keyword); err == nil { completion = reg.ReplaceAllString(suggestion, "") } return []string{completion, suggestion} } func (s *Suggestion) GetCandidateKeys(json *simplejson.Json, keyword string) []string { candidates := []string{} if _, err := json.Array(); err == nil { return []string{} } if keyword == "" { return getCurrentKeys(json) } reg, err := regexp.Compile(`(?i)^(\\")?` + keyword + `(\\")?`) if err != nil { return []string{} } for _, key := range getCurrentKeys(json) { if reg.MatchString(key) { candidates = append(candidates, key) } } return candidates } func getCurrentKeys(json *simplejson.Json) []string { kk := []string{} m, err := json.Map() if err != nil { return kk } for k := range m { kk = append(kk, k) } sort.Strings(kk) keys := []string{} for _, k := range kk { if strings.Contains(k, ".") { var sb strings.Builder sb.Grow(len(k) + 4) sb.WriteString(`\"`) sb.WriteString(k) sb.WriteString(`\"`) k = sb.String() } keys = append(keys, k) } return keys } func (s *Suggestion) GetCurrentType(json *simplejson.Json) SuggestionDataType { if _, err := json.Array(); err == nil { return ARRAY } else if _, err = json.Map(); err == nil { return MAP } else if _, err = json.String(); err == nil { return STRING } return UNKNOWN } ================================================ FILE: suggestion_test.go ================================================ package jid import ( "bytes" "io" "testing" simplejson "github.com/bitly/go-simplejson" "github.com/stretchr/testify/assert" ) func TestNewSuggestion(t *testing.T) { var assert = assert.New(t) assert.Equal(NewSuggestion(), &Suggestion{}) } func TestSuggestionGet(t *testing.T) { var assert = assert.New(t) j := createJson(`{"name":"simeji-github"}`) s := NewSuggestion() j = createJson(`{"name":"simeji-github", "naming":"simeji", "nickname":"simejisimeji"}`) assert.Equal([]string{"m", "nam"}, s.Get(j, "na")) j = createJson(`{"abcde":"simeji-github", "abcdef":"simeji", "ab":"simejisimeji"}`) assert.Equal([]string{"", ""}, s.Get(j, "")) assert.Equal([]string{"b", "ab"}, s.Get(j, "a")) assert.Equal([]string{"de", "abcde"}, s.Get(j, "abc")) assert.Equal([]string{"", "abcde"}, s.Get(j, "abcde")) j = createJson(`["zero"]`) assert.Equal([]string{"[0]", "[0]"}, s.Get(j, "")) assert.Equal([]string{"0]", "[0]"}, s.Get(j, "[")) assert.Equal([]string{"]", "[0]"}, s.Get(j, "[0")) j = createJson(`["zero", "one"]`) assert.Equal([]string{"[", "["}, s.Get(j, "")) assert.Equal([]string{"", "["}, s.Get(j, "[")) assert.Equal([]string{"]", "[0]"}, s.Get(j, "[0")) j = createJson(`{"Abcabc":"simeji-github", "Abcdef":"simeji"}`) assert.Equal([]string{"bc", "Abc"}, s.Get(j, "a")) assert.Equal([]string{"c", "Abc"}, s.Get(j, "ab")) j = createJson(`{"RootDeviceNames":"simeji-github", "RootDeviceType":"simeji"}`) assert.Equal([]string{"ootDevice", "RootDevice"}, s.Get(j, "r")) assert.Equal([]string{"ootDevice", "RootDevice"}, s.Get(j, "R")) } func TestSuggestionGetCurrentType(t *testing.T) { var assert = assert.New(t) s := NewSuggestion() j := createJson(`[1,2,3]`) assert.Equal(ARRAY, s.GetCurrentType(j)) j = createJson(`{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}`) assert.Equal(MAP, s.GetCurrentType(j)) j = createJson(`"name"`) assert.Equal(STRING, s.GetCurrentType(j)) j = createJson("1") assert.Equal(UNKNOWN, s.GetCurrentType(j)) } func TestSuggestionGetCandidateKeys(t *testing.T) { var assert = assert.New(t) j := createJson(`{"naming":"simeji", "nickname":"simejisimeji", "city":"tokyo", "name":"simeji-github" }`) s := NewSuggestion() assert.Equal([]string{"city", "name", "naming", "nickname"}, s.GetCandidateKeys(j, "")) assert.Equal([]string{"name", "naming", "nickname"}, s.GetCandidateKeys(j, "n")) assert.Equal([]string{"name", "naming"}, s.GetCandidateKeys(j, "na")) assert.Equal([]string{}, s.GetCandidateKeys(j, "nana")) j = createJson(`{"abcde":"simeji-github", "abcdef":"simeji", "ab":"simejisimeji"}`) assert.Equal([]string{"abcde", "abcdef"}, s.GetCandidateKeys(j, "abcde")) j = createJson(`{"name":"simeji-github"}`) assert.Equal([]string{"name"}, s.GetCandidateKeys(j, "")) j = createJson(`{"n":"simeji-github"}`) assert.Equal([]string{"n"}, s.GetCandidateKeys(j, "")) j = createJson(`[1,2,"aa"]`) s = NewSuggestion() assert.Equal([]string{}, s.GetCandidateKeys(j, "[")) } func TestSuggestionGetCandidateKeysWithDots(t *testing.T) { var assert = assert.New(t) j := createJson(`{"nam.ing":"simeji", "nickname":"simejisimeji", "city":"tokyo", "name":"simeji-github" }`) s := NewSuggestion() assert.Equal([]string{"city", `\"nam.ing\"`, "name", "nickname"}, s.GetCandidateKeys(j, "")) assert.Equal([]string{`\"nam.ing\"`, "name", "nickname"}, s.GetCandidateKeys(j, "n")) } func createJson(s string) *simplejson.Json { r := bytes.NewBufferString(s) buf, _ := io.ReadAll(r) j, _ := simplejson.NewJson(buf) return j } ================================================ FILE: terminal.go ================================================ package jid import ( "fmt" "io" "regexp" "strings" runewidth "github.com/mattn/go-runewidth" termbox "github.com/nsf/termbox-go" "github.com/nwidger/jsoncolor" ) type Terminal struct { defaultY int prompt string formatter *jsoncolor.Formatter monochrome bool outputArea *[][]termbox.Cell } type TerminalDrawAttributes struct { Query string Contents []string CandidateIndex int ContentsOffsetY int Complete string Candidates []string CursorOffset int } func NewTerminal(prompt string, defaultY int, monochrome bool) *Terminal { t := &Terminal{ prompt: prompt, defaultY: defaultY, monochrome: monochrome, outputArea: &[][]termbox.Cell{}, formatter: nil, } if !monochrome { t.formatter = t.initColorizeFormatter() } return t } func (t *Terminal) Draw(attr *TerminalDrawAttributes) error { query := attr.Query complete := attr.Complete rows := attr.Contents candidates := attr.Candidates candidateidx := attr.CandidateIndex contentOffsetY := attr.ContentsOffsetY termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) y := t.defaultY _, h := termbox.Size() t.drawFilterLine(query, complete) if len(candidates) > 0 { y = t.drawCandidates(0, t.defaultY, candidateidx, candidates) } cellsArr, err := t.rowsToCells(rows) if err != nil { return err } for idx, cells := range cellsArr { i := idx - contentOffsetY if i >= 0 { t.drawCells(0, i+y, cells) } if i > h { break } } termbox.SetCursor(len(t.prompt)+attr.CursorOffset, 0) termbox.Flush() return nil } func (t *Terminal) drawFilterLine(qs string, complete string) error { fs := t.prompt + qs cs := complete str := fs + cs color := termbox.ColorDefault backgroundColor := termbox.ColorDefault var cells []termbox.Cell match := []int{len(fs), len(fs + cs)} var c termbox.Attribute for i, s := range str { c = color if i >= match[0] && i < match[1] { c = termbox.ColorGreen } cells = append(cells, termbox.Cell{ Ch: s, Fg: c, Bg: backgroundColor, }) } t.drawCells(0, 0, cells) return nil } type termboxSprintfFuncer struct { fg termbox.Attribute bg termbox.Attribute outputArea *[][]termbox.Cell } func (tsf *termboxSprintfFuncer) SprintfFunc() func(format string, a ...interface{}) string { return func(format string, a ...interface{}) string { cells := tsf.outputArea idx := len(*cells) - 1 str := fmt.Sprintf(format, a...) for _, s := range str { if s == '\n' { *cells = append(*cells, []termbox.Cell{}) idx++ continue } (*cells)[idx] = append((*cells)[idx], termbox.Cell{ Ch: s, Fg: tsf.fg, Bg: tsf.bg, }) } return "dummy" } } func (t *Terminal) initColorizeFormatter() *jsoncolor.Formatter { formatter := jsoncolor.NewFormatter() regular := &termboxSprintfFuncer{ fg: termbox.ColorDefault, bg: termbox.ColorDefault, outputArea: t.outputArea, } bold := &termboxSprintfFuncer{ fg: termbox.AttrBold, bg: termbox.ColorDefault, outputArea: t.outputArea, } blueBold := &termboxSprintfFuncer{ fg: termbox.ColorBlue | termbox.AttrBold, bg: termbox.ColorDefault, outputArea: t.outputArea, } green := &termboxSprintfFuncer{ fg: termbox.ColorGreen, bg: termbox.ColorDefault, outputArea: t.outputArea, } blackBold := &termboxSprintfFuncer{ fg: termbox.ColorBlack | termbox.AttrBold, bg: termbox.ColorDefault, outputArea: t.outputArea, } formatter.SpaceColor = regular formatter.CommaColor = bold formatter.ColonColor = bold formatter.ObjectColor = bold formatter.ArrayColor = bold formatter.FieldQuoteColor = blueBold formatter.FieldColor = blueBold formatter.StringQuoteColor = green formatter.StringColor = green formatter.TrueColor = regular formatter.FalseColor = regular formatter.NumberColor = regular formatter.NullColor = blackBold return formatter } func (t *Terminal) rowsToCells(rows []string) ([][]termbox.Cell, error) { *t.outputArea = [][]termbox.Cell{[]termbox.Cell{}} var err error if t.formatter != nil { err = t.formatter.Format(io.Discard, []byte(strings.Join(rows, "\n"))) } cells := *t.outputArea if err != nil || t.monochrome { cells = [][]termbox.Cell{} for _, row := range rows { var cls []termbox.Cell for _, char := range row { cls = append(cls, termbox.Cell{ Ch: char, Fg: termbox.ColorDefault, Bg: termbox.ColorDefault, }) } cells = append(cells, cls) } } return cells, nil } func (t *Terminal) drawCells(x int, y int, cells []termbox.Cell) { i := 0 for _, c := range cells { termbox.SetCell(x+i, y, c.Ch, c.Fg, c.Bg) w := runewidth.RuneWidth(c.Ch) if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(c.Ch) { w = 1 } i += w } } func (t *Terminal) drawCandidates(x int, y int, index int, candidates []string) int { color := termbox.ColorBlack backgroundColor := termbox.ColorWhite w, _ := termbox.Size() ss := candidates[index] re := regexp.MustCompile("[[:space:]]" + regexp.QuoteMeta(ss) + "[[:space:]]") var rows []string var str string for _, word := range candidates { combine := " " if l := len(str); l+len(word)+1 >= w { rows = append(rows, str+" ") str = "" } str += combine + word } rows = append(rows, str+" ") for i, row := range rows { match := re.FindStringIndex(row) var c termbox.Attribute ii := 0 for k, s := range row { c = color backgroundColor = termbox.ColorMagenta if match != nil && k >= match[0]+1 && k < match[1]-1 { backgroundColor = termbox.ColorWhite } termbox.SetCell(x+ii, y+i, s, c, backgroundColor) w := runewidth.RuneWidth(s) if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(s) { w = 1 } ii += w } } return y + len(rows) }