├── .circleci └── config.yml ├── .gitignore ├── .goreleaser.yml ├── ChangeLog ├── LICENSE ├── README.md ├── cmd └── jid │ ├── jid.go │ └── jid_test.go ├── 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 /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/golang:latest 6 | steps: 7 | - checkout 8 | - run: go test -v ./ 9 | - run: go test -v ./cmd/jid 10 | release: 11 | docker: 12 | - image: circleci/golang:latest 13 | steps: 14 | - checkout 15 | - run: 16 | name: build jid using goreleaser 17 | command: curl -sL https://git.io/goreleaser | bash 18 | workflows: 19 | version: 2.1 20 | test_and_release: 21 | jobs: 22 | - test: 23 | filters: 24 | tags: 25 | only: /v[0-9]+(\.[0-9]+)*(-.*)*/ 26 | - release: 27 | requires: 28 | - test 29 | filters: 30 | branches: 31 | ignore: /.*/ 32 | tags: 33 | only: /v[0-9]+(\.[0-9]+)*(-.*)*/ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.swp 26 | *.test 27 | test.* 28 | 29 | # jid package 30 | jid 31 | *.out 32 | *.log 33 | dist 34 | 35 | .idea 36 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # you may remove this if you don't use vgo 6 | - go mod download 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | main: ./cmd/jid/jid.go 13 | ldflags: 14 | - -s -w 15 | goos: 16 | - windows 17 | - openbsd 18 | - netbsd 19 | - linux 20 | - freebsd 21 | - darwin 22 | goarch: 23 | - arm64 24 | - amd64 25 | - 386 26 | archive: 27 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 28 | format: zip 29 | files: 30 | - none* 31 | checksum: 32 | name_template: 'checksums.txt' 33 | snapshot: 34 | name_template: "{{ .Tag }}-next" 35 | changelog: 36 | sort: asc 37 | filters: 38 | exclude: 39 | - '^docs:' 40 | - '^test:' 41 | # for Validation 42 | #release: 43 | # disable: true 44 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ======= 3 | 4 | ### 0.7.2 - Jan 23 2017 5 | 6 | BugFix: 7 | * fix version number 8 | 9 | ### 0.7.1 - Jan 5 2017 10 | 11 | Features: 12 | * scroll to bottom and top 13 | 14 | ### 0.7.0 - Jan 4 2017 15 | 16 | Features: 17 | * output with color 18 | * monochrome mode option 19 | 20 | BugFix: 21 | * multibyte Query & Json key/value 22 | 23 | Modified README 24 | 25 | ### 0.6.3 - 15 Dec 2016 26 | 27 | Features: 28 | * Add `-help` and `-h` command for show a help 29 | * Add Ctrl-U command and a query behavior 30 | 31 | Change Behaviors: 32 | * Force insert `.` 33 | 34 | ### 0.6.2 - 9 Dec 2016 35 | 36 | Features: 37 | * Add --version flag for homebrew test 38 | 39 | ### 0.6.1 - 5 Dec 2016 40 | 41 | Features: 42 | * Get first argument of `jid` for initial query 43 | 44 | Change Behaviors: 45 | * Auto input . at first character of query 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 simeji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jid 2 | 3 | [![Circle CI](https://circleci.com/gh/simeji/jid/tree/master.svg?style=shield)](https://circleci.com/gh/simeji/jid/tree/master) 4 | 5 | Json Incremental Digger 6 | 7 | It's a very simple tool. 8 | You can drill down JSON interactively by using filtering queries like [jq](https://stedolan.github.io/jq/). 9 | 10 | **Suggestion** and **Auto completion** of this tool will provide you a very comfortable JSON drill down. 11 | 12 | ## Demo 13 | 14 | ![demo-jid-main](https://github.com/simeji/jid/wiki/images/demo-jid-main-640-colorize.gif) 15 | 16 | ## Installation 17 | 18 | * [With HomeBrew (for macOS)](#with-homebrew-for-macos) 19 | * [With MacPorts (for macOS)](#with-macports-for-macos) 20 | * [With pkg (for FreeBSD)](#with-pkg-for-freebsd) 21 | * [With scoop (for Windows)](#with-scoop-for-windows) 22 | * [Other package management system](#other-package-management-systems) 23 | * [Simply use "jid" command](#simply-use-jid-command) 24 | * [Build](#build) 25 | 26 | ### With HomeBrew (for macOS) 27 | 28 | ``` 29 | brew install jid 30 | ``` 31 | 32 | ### With MacPorts (for macOS) 33 | 34 | ``` 35 | sudo port install jid 36 | ``` 37 | 38 | ### With pkg (for FreeBSD) 39 | 40 | ``` 41 | pkg install jid 42 | ``` 43 | 44 | ### With scoop (for Windows) 45 | 46 | ``` 47 | scoop install jid 48 | ``` 49 | 50 | ### Other package management systems 51 | 52 | Jid can install by package management systems of below OS. 53 | 54 | [![Packaging status](https://repology.org/badge/vertical-allrepos/jid.svg)](https://repology.org/metapackage/jid/versions) 55 | 56 | 57 | ### Simply use "jid" command 58 | 59 | If you simply want to use `jid` command, please download binary from below. 60 | 61 | https://github.com/simeji/jid/releases 62 | 63 | ## Build 64 | 65 | ``` 66 | go install github.com/simeji/jid/cmd/jid@latest 67 | ``` 68 | 69 | ## Usage 70 | 71 | ### Quick start 72 | 73 | * [simple json example](#simple-json-example) 74 | * [simple json example2](#simple-json-example2) 75 | * [with initial query](#with-initial-query) 76 | * [with curl](#with-curl) 77 | 78 | #### simple json example 79 | 80 | Please execute the below command. 81 | 82 | ``` 83 | echo '{"aa":"2AA2","bb":{"aaa":[123,"cccc",[1,2]],"c":321}}'| jid 84 | ``` 85 | 86 | then, jid will be running. 87 | 88 | You can dig JSON data incrementally. 89 | 90 | When you enter `.bb.aaa[2]`, you will see the following. 91 | 92 | ``` 93 | [Filter]> .bb.aaa[2] 94 | [ 95 | 1, 96 | 2 97 | ] 98 | ``` 99 | 100 | Then, you press Enter key and output `[1,2]` and exit. 101 | 102 | #### simple json example2 103 | 104 | This json is used by [demo section](https://github.com/simeji/jid#demo). 105 | ``` 106 | 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 107 | ``` 108 | 109 | #### With a initial query 110 | 111 | First argument of `jid` is initial query. 112 | (Use JSON same as [Demo](#demo)) 113 | 114 | ![demo-jid-with-query](https://github.com/simeji/jid/wiki/images/demo-jid-with-query-640.gif) 115 | 116 | #### with curl 117 | 118 | Sample for using [RDAP](https://datatracker.ietf.org/wg/weirds/documents/) data. 119 | 120 | ``` 121 | curl -s http://rdg.afilias.info/rdap/domain/example.info | jid 122 | ``` 123 | 124 | #### Load JSON from a file 125 | 126 | ``` 127 | jid < file.json 128 | ``` 129 | 130 | ## Keymaps 131 | 132 | |key|description| 133 | |:-----------|:----------| 134 | |`TAB` / `CTRL` + `I` |Show available items and choice them| 135 | |`CTRL` + `W` |Delete from the cursor to the start of the word| 136 | |`CTRL` + `U` |Delete whole query| 137 | |`CTRL` + `F` / Right Arrow (:arrow_right:)|Move cursor a character to the right| 138 | |`CTRL` + `B` / Left Arrow (:arrow_left:)|Move cursor a character to the left| 139 | |`CTRL` + `A`|To the first character of the 'Filter'| 140 | |`CTRL` + `E`|To the end of the 'Filter'| 141 | |`CTRL` + `J`|Scroll json buffer 1 line downwards| 142 | |`CTRL` + `K`|Scroll json buffer 1 line upwards| 143 | |`CTRL` + `G`|Scroll json buffer to bottom| 144 | |`CTRL` + `T`|Scroll json buffer to top| 145 | |`CTRL` + `N`|Scroll json buffer 'Page Down'| 146 | |`CTRL` + `P`|Scroll json buffer 'Page Up'| 147 | |`CTRL` + `L`|Change view mode whole json or keys (only object)| 148 | |`ESC`|Hide a candidate box| 149 | 150 | ### Option 151 | 152 | |option|description| 153 | |:-----------|:----------| 154 | |First argument ($1) | Initial query| 155 | |-h | print a help| 156 | |-help | print a help| 157 | |-version | print the version and exit| 158 | |-q | Output query mode (for jq)| 159 | |-M | monochrome output mode| 160 | -------------------------------------------------------------------------------- /cmd/jid/jid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/simeji/jid" 9 | ) 10 | 11 | const VERSION = "0.7.6" 12 | 13 | func main() { 14 | content := os.Stdin 15 | 16 | var qm bool 17 | var help bool 18 | var version bool 19 | var mono bool 20 | var pretty bool 21 | qs := "." 22 | 23 | flag.BoolVar(&qm, "q", false, "Output query mode") 24 | flag.BoolVar(&help, "h", false, "print a help") 25 | flag.BoolVar(&help, "help", false, "print a help") 26 | flag.BoolVar(&version, "version", false, "print the version and exit") 27 | flag.BoolVar(&mono, "M", false, "monochrome output mode") 28 | flag.BoolVar(&pretty, "p", false, "pretty print json result") 29 | flag.Parse() 30 | 31 | if help { 32 | flag.Usage() 33 | fmt.Println(getHelpString()) 34 | os.Exit(0) 35 | } 36 | 37 | if version { 38 | fmt.Println(fmt.Sprintf("jid version v%s", VERSION)) 39 | os.Exit(0) 40 | } 41 | args := flag.Args() 42 | if len(args) > 0 { 43 | qs = args[0] 44 | } 45 | 46 | ea := &jid.EngineAttribute{ 47 | DefaultQuery: qs, 48 | Monochrome: mono, 49 | PrettyResult: pretty, 50 | } 51 | 52 | e, err := jid.NewEngine(content, ea) 53 | 54 | if err != nil { 55 | fmt.Println(err) 56 | os.Exit(1) 57 | } 58 | os.Exit(run(e, qm)) 59 | } 60 | 61 | func run(e jid.EngineInterface, qm bool) int { 62 | 63 | result := e.Run() 64 | if result.GetError() != nil { 65 | return 2 66 | } 67 | if qm { 68 | fmt.Printf("%s", result.GetQueryString()) 69 | } else { 70 | fmt.Printf("%s", result.GetContent()) 71 | } 72 | return 0 73 | } 74 | 75 | func getHelpString() string { 76 | return ` 77 | 78 | ============ Load JSON from a file ============== 79 | 80 | $ jid < file.json 81 | 82 | ============ With a JSON filter mode ============= 83 | 84 | TAB / CTRL-I 85 | Show available items and choice them 86 | 87 | CTRL-W 88 | Delete from the cursor to the start of the word 89 | 90 | CTRL-U 91 | Delete whole query 92 | 93 | CTRL-F / Right Arrow 94 | Move cursor a character to the right 95 | 96 | CTRL-B / Left Arrow 97 | Move cursor a character to the left 98 | 99 | CTRL-A 100 | To the first character of the 'Filter' 101 | 102 | CTRL-E 103 | To the end of the 'Filter' 104 | 105 | CTRL-J 106 | Scroll json buffer 1 line downwards 107 | 108 | CTRL-K 109 | Scroll json buffer 1 line upwards 110 | 111 | CTRL-G 112 | Scroll json buffer to bottom 113 | 114 | CTRL-T 115 | Scroll json buffer to top 116 | 117 | CTRL-N 118 | Scroll json buffer 'Page Down' 119 | 120 | CTRL-P 121 | Scroll json buffer 'Page Up' 122 | 123 | CTRL-L 124 | Change view mode whole json or keys (only object) 125 | 126 | ESC 127 | Hide a candidate box 128 | 129 | ` 130 | } 131 | -------------------------------------------------------------------------------- /cmd/jid/jid_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/simeji/jid" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var called int = 0 13 | 14 | func TestMain(m *testing.M) { 15 | called = 0 16 | code := m.Run() 17 | defer os.Exit(code) 18 | } 19 | 20 | func TestJidRun(t *testing.T) { 21 | var assert = assert.New(t) 22 | 23 | e := &EngineMock{err: nil} 24 | result := run(e, false) 25 | assert.Zero(result) 26 | assert.Equal(2, called) 27 | 28 | result = run(e, true) 29 | assert.Equal(1, called) 30 | 31 | result = run(e, false) 32 | assert.Zero(result) 33 | } 34 | 35 | func TestJidRunWithError(t *testing.T) { 36 | called = 0 37 | var assert = assert.New(t) 38 | e := &EngineMock{err: fmt.Errorf("")} 39 | result := run(e, false) 40 | assert.Equal(2, result) 41 | assert.Equal(0, called) 42 | } 43 | 44 | type EngineMock struct{ err error } 45 | 46 | func (e *EngineMock) Run() jid.EngineResultInterface { 47 | return &EngineResultMock{err: e.err} 48 | } 49 | func (e *EngineMock) GetQuery() jid.QueryInterface { 50 | return jid.NewQuery([]rune("")) 51 | } 52 | 53 | type EngineResultMock struct{ err error } 54 | 55 | func (e *EngineResultMock) GetQueryString() string { 56 | called = 1 57 | return ".querystring" 58 | } 59 | func (e *EngineResultMock) GetContent() string { 60 | called = 2 61 | return `{"test":"result"}` 62 | } 63 | func (e *EngineResultMock) GetError() error { 64 | return e.err 65 | } 66 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | termbox "github.com/nsf/termbox-go" 8 | ) 9 | 10 | const ( 11 | DefaultY int = 1 12 | FilterPrompt string = "[Filter]> " 13 | ) 14 | 15 | type EngineInterface interface { 16 | Run() EngineResultInterface 17 | GetQuery() QueryInterface 18 | } 19 | 20 | type EngineResultInterface interface { 21 | GetQueryString() string 22 | GetContent() string 23 | GetError() error 24 | } 25 | 26 | type Engine struct { 27 | manager *JsonManager 28 | query QueryInterface 29 | queryCursorIdx int 30 | term *Terminal 31 | complete []string 32 | keymode bool 33 | candidates []string 34 | candidatemode bool 35 | candidateidx int 36 | contentOffset int 37 | queryConfirm bool 38 | prettyResult bool 39 | } 40 | 41 | type EngineAttribute struct { 42 | DefaultQuery string 43 | Monochrome bool 44 | PrettyResult bool 45 | } 46 | 47 | func NewEngine(s io.Reader, ea *EngineAttribute) (EngineInterface, error) { 48 | j, err := NewJsonManager(s) 49 | if err != nil { 50 | return nil, err 51 | } 52 | e := &Engine{ 53 | manager: j, 54 | term: NewTerminal(FilterPrompt, DefaultY, ea.Monochrome), 55 | query: NewQuery([]rune(ea.DefaultQuery)), 56 | complete: []string{"", ""}, 57 | keymode: false, 58 | candidates: []string{}, 59 | candidatemode: false, 60 | candidateidx: 0, 61 | contentOffset: 0, 62 | queryConfirm: false, 63 | prettyResult: ea.PrettyResult, 64 | } 65 | e.queryCursorIdx = e.query.Length() 66 | return e, nil 67 | } 68 | 69 | type EngineResult struct { 70 | content string 71 | qs string 72 | err error 73 | } 74 | 75 | func (er *EngineResult) GetQueryString() string { 76 | return er.qs 77 | } 78 | 79 | func (er *EngineResult) GetContent() string { 80 | return er.content 81 | } 82 | func (er *EngineResult) GetError() error { 83 | return er.err 84 | } 85 | 86 | func (e *Engine) GetQuery() QueryInterface { 87 | return e.query 88 | } 89 | 90 | func (e *Engine) Run() EngineResultInterface { 91 | 92 | err := termbox.Init() 93 | if err != nil { 94 | panic(err) 95 | } 96 | defer termbox.Close() 97 | 98 | var contents []string 99 | 100 | for { 101 | 102 | if e.query.StringGet() == "" { 103 | e.query.StringSet(".") 104 | e.queryCursorIdx = e.query.Length() 105 | } 106 | 107 | bl := len(contents) 108 | contents = e.getContents() 109 | e.setCandidateData() 110 | e.queryConfirm = false 111 | if bl != len(contents) { 112 | e.contentOffset = 0 113 | } 114 | 115 | ta := &TerminalDrawAttributes{ 116 | Query: e.query.StringGet(), 117 | Contents: contents, 118 | CandidateIndex: e.candidateidx, 119 | ContentsOffsetY: e.contentOffset, 120 | Complete: e.complete[0], 121 | Candidates: e.candidates, 122 | CursorOffset: e.query.IndexOffset(e.queryCursorIdx), 123 | } 124 | err = e.term.Draw(ta) 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | switch ev := termbox.PollEvent(); ev.Type { 130 | case termbox.EventKey: 131 | switch ev.Key { 132 | case 0: 133 | e.inputChar(ev.Ch) 134 | case termbox.KeyBackspace, termbox.KeyBackspace2: 135 | e.deleteChar() 136 | case termbox.KeyTab: 137 | e.tabAction() 138 | case termbox.KeyArrowLeft, termbox.KeyCtrlB: 139 | e.moveCursorBackward() 140 | case termbox.KeyArrowRight, termbox.KeyCtrlF: 141 | e.moveCursorForward() 142 | case termbox.KeyHome, termbox.KeyCtrlA: 143 | e.moveCursorToTop() 144 | case termbox.KeyEnd, termbox.KeyCtrlE: 145 | e.moveCursorToEnd() 146 | case termbox.KeyCtrlK: 147 | e.scrollToAbove() 148 | case termbox.KeyCtrlJ: 149 | e.scrollToBelow() 150 | case termbox.KeyCtrlG: 151 | e.scrollToBottom(len(contents)) 152 | case termbox.KeyCtrlT: 153 | e.scrollToTop() 154 | case termbox.KeyCtrlN: 155 | _, h := termbox.Size() 156 | e.scrollPageDown(len(contents), h) 157 | case termbox.KeyCtrlP: 158 | _, h := termbox.Size() 159 | e.scrollPageUp(h) 160 | case termbox.KeyCtrlL: 161 | e.toggleKeymode() 162 | case termbox.KeyCtrlU: 163 | e.deleteLineQuery() 164 | case termbox.KeyCtrlW: 165 | e.deleteWordBackward() 166 | case termbox.KeyEsc: 167 | e.escapeCandidateMode() 168 | case termbox.KeyEnter: 169 | if !e.candidatemode { 170 | var cc string 171 | var err error 172 | if e.prettyResult { 173 | cc, _, _, err = e.manager.GetPretty(e.query, true) 174 | } else { 175 | cc, _, _, err = e.manager.Get(e.query, true) 176 | } 177 | 178 | return &EngineResult{ 179 | content: cc, 180 | qs: e.query.StringGet(), 181 | err: err, 182 | } 183 | } 184 | e.confirmCandidate() 185 | case termbox.KeyCtrlC: 186 | return &EngineResult{} 187 | default: 188 | } 189 | case termbox.EventError: 190 | panic(ev.Err) 191 | break 192 | default: 193 | } 194 | } 195 | } 196 | 197 | func (e *Engine) getContents() []string { 198 | var c string 199 | var contents []string 200 | c, e.complete, e.candidates, _ = e.manager.GetPretty(e.query, e.queryConfirm) 201 | if e.keymode { 202 | contents = e.candidates 203 | } else { 204 | contents = strings.Split(c, "\n") 205 | } 206 | return contents 207 | } 208 | 209 | func (e *Engine) setCandidateData() { 210 | if l := len(e.candidates); e.complete[0] == "" && l > 1 { 211 | if e.candidateidx >= l { 212 | e.candidateidx = 0 213 | } 214 | } else { 215 | e.candidatemode = false 216 | } 217 | if !e.candidatemode { 218 | e.candidateidx = 0 219 | e.candidates = []string{} 220 | } 221 | } 222 | 223 | func (e *Engine) confirmCandidate() { 224 | _, _ = e.query.PopKeyword() 225 | _ = e.query.StringAdd(".") 226 | _ = e.query.StringAdd(e.candidates[e.candidateidx]) 227 | e.queryCursorIdx = e.query.Length() 228 | e.queryConfirm = true 229 | } 230 | 231 | func (e *Engine) deleteChar() { 232 | if i := e.queryCursorIdx - 1; i > 0 { 233 | _ = e.query.Delete(i) 234 | e.queryCursorIdx-- 235 | } 236 | 237 | } 238 | 239 | func (e *Engine) deleteLineQuery() { 240 | _ = e.query.StringSet("") 241 | e.queryCursorIdx = 0 242 | } 243 | 244 | func (e *Engine) scrollToBelow() { 245 | e.contentOffset++ 246 | } 247 | 248 | func (e *Engine) scrollToAbove() { 249 | if o := e.contentOffset - 1; o >= 0 { 250 | e.contentOffset = o 251 | } 252 | } 253 | 254 | func (e *Engine) scrollToBottom(rownum int) { 255 | e.contentOffset = rownum - 1 256 | } 257 | 258 | func (e *Engine) scrollToTop() { 259 | e.contentOffset = 0 260 | } 261 | 262 | func (e *Engine) scrollPageDown(rownum int, height int) { 263 | co := rownum - 1 264 | if o := rownum - e.contentOffset; o > height { 265 | co = e.contentOffset + (height - DefaultY) 266 | } 267 | e.contentOffset = co 268 | } 269 | 270 | func (e *Engine) scrollPageUp(height int) { 271 | co := 0 272 | if o := e.contentOffset - (height - DefaultY); o > 0 { 273 | co = o 274 | } 275 | e.contentOffset = co 276 | } 277 | 278 | func (e *Engine) toggleKeymode() { 279 | e.keymode = !e.keymode 280 | } 281 | func (e *Engine) deleteWordBackward() { 282 | if k, _ := e.query.StringPopKeyword(); k != "" && !strings.Contains(k, "[") { 283 | _ = e.query.StringAdd(".") 284 | } 285 | e.queryCursorIdx = e.query.Length() 286 | } 287 | func (e *Engine) tabAction() { 288 | if !e.candidatemode { 289 | e.candidatemode = true 290 | if e.complete[0] != e.complete[1] && e.complete[0] != "" { 291 | if k, _ := e.query.StringPopKeyword(); !strings.Contains(k, "[") { 292 | _ = e.query.StringAdd(".") 293 | } 294 | _ = e.query.StringAdd(e.complete[1]) 295 | } else { 296 | _ = e.query.StringAdd(e.complete[0]) 297 | } 298 | } else { 299 | e.candidateidx = e.candidateidx + 1 300 | } 301 | e.queryCursorIdx = e.query.Length() 302 | } 303 | func (e *Engine) escapeCandidateMode() { 304 | e.candidatemode = false 305 | } 306 | func (e *Engine) inputChar(ch rune) { 307 | _ = e.query.Insert([]rune{ch}, e.queryCursorIdx) 308 | e.queryCursorIdx++ 309 | } 310 | 311 | func (e *Engine) moveCursorBackward() { 312 | if i := e.queryCursorIdx - 1; i >= 0 { 313 | e.queryCursorIdx-- 314 | } 315 | } 316 | 317 | func (e *Engine) moveCursorForward() { 318 | if e.query.Length() > e.queryCursorIdx { 319 | e.queryCursorIdx++ 320 | } 321 | } 322 | 323 | func (e *Engine) moveCursorWordBackwark() { 324 | } 325 | func (e *Engine) moveCursorWordForward() { 326 | } 327 | func (e *Engine) moveCursorToTop() { 328 | e.queryCursorIdx = 0 329 | } 330 | func (e *Engine) moveCursorToEnd() { 331 | e.queryCursorIdx = e.query.Length() 332 | } 333 | -------------------------------------------------------------------------------- /engine_test.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewEngine(t *testing.T) { 12 | var assert = assert.New(t) 13 | 14 | f, _ := os.Create("/dev/null") 15 | e, err := NewEngine(f, &EngineAttribute{ 16 | DefaultQuery: "", 17 | Monochrome: false, 18 | }) 19 | assert.Nil(e) 20 | assert.NotNil(err) 21 | 22 | ee := getEngine(`{"name":"go"}`, "") 23 | assert.NotNil(ee) 24 | assert.Equal("", ee.query.StringGet()) 25 | assert.Equal(0, ee.queryCursorIdx) 26 | } 27 | 28 | func TestNewEngineWithQuery(t *testing.T) { 29 | var assert = assert.New(t) 30 | e := getEngine(`{"name":"go"}`, ".nam") 31 | assert.Equal(".nam", e.query.StringGet()) 32 | assert.Equal(4, e.queryCursorIdx) 33 | 34 | e = getEngine(`{"name":"go"}`, "nam") 35 | assert.Equal("", e.query.StringGet()) 36 | assert.Equal(0, e.queryCursorIdx) 37 | 38 | e = getEngine(`{"name":"go"}`, ".nam..") 39 | assert.Equal("", e.query.StringGet()) 40 | assert.Equal(0, e.queryCursorIdx) 41 | } 42 | 43 | func TestDeleteChar(t *testing.T) { 44 | var assert = assert.New(t) 45 | e := getEngine(`{"name":"go"}`, "") 46 | e.query.StringSet(".name") 47 | e.queryCursorIdx = e.query.Length() 48 | 49 | e.deleteChar() 50 | assert.Equal(".nam", e.query.StringGet()) 51 | assert.Equal(4, e.queryCursorIdx) 52 | } 53 | 54 | func TestDeleteWordBackward(t *testing.T) { 55 | var assert = assert.New(t) 56 | e := getEngine(`{"name":"go"}`, "") 57 | e.query.StringSet(".name") 58 | 59 | e.deleteWordBackward() 60 | assert.Equal(".", e.query.StringGet()) 61 | assert.Equal(1, e.queryCursorIdx) 62 | 63 | e.query.StringSet(".name[1]") 64 | e.deleteWordBackward() 65 | assert.Equal(".name", e.query.StringGet()) 66 | assert.Equal(5, e.queryCursorIdx) 67 | 68 | e.query.StringSet(".name[") 69 | e.deleteWordBackward() 70 | assert.Equal(".name", e.query.StringGet()) 71 | assert.Equal(5, e.queryCursorIdx) 72 | } 73 | 74 | func TestDeleteLineQuery(t *testing.T) { 75 | var assert = assert.New(t) 76 | e := getEngine(`{"name":"go"}`, "") 77 | 78 | e.query.StringSet(".name") 79 | e.deleteLineQuery() 80 | assert.Equal("", e.query.StringGet()) 81 | assert.Equal(0, e.queryCursorIdx) 82 | } 83 | 84 | func TestScrollToAbove(t *testing.T) { 85 | var assert = assert.New(t) 86 | e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") 87 | assert.Equal(0, e.contentOffset) 88 | e.scrollToAbove() 89 | assert.Equal(0, e.contentOffset) 90 | e.contentOffset = 5 91 | e.scrollToAbove() 92 | assert.Equal(4, e.contentOffset) 93 | e.scrollToAbove() 94 | assert.Equal(3, e.contentOffset) 95 | } 96 | 97 | func TestScrollToBelow(t *testing.T) { 98 | var assert = assert.New(t) 99 | e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") 100 | e.scrollToBelow() 101 | assert.Equal(1, e.contentOffset) 102 | e.scrollToBelow() 103 | e.scrollToBelow() 104 | assert.Equal(3, e.contentOffset) 105 | } 106 | func TestScrollToBottomAndTop(t *testing.T) { 107 | var assert = assert.New(t) 108 | e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") 109 | 110 | e.scrollToBottom(5) 111 | assert.Equal(4, e.contentOffset) 112 | 113 | e.scrollToTop() 114 | assert.Equal(0, e.contentOffset) 115 | } 116 | 117 | func TestScrollPageUpDown(t *testing.T) { 118 | var assert = assert.New(t) 119 | e := getEngine(`{"named":"go","NameTest":[1,2,3]}`, "") 120 | 121 | cl := len(e.getContents()) 122 | // Then DefaultY = 1 123 | e.scrollPageDown(cl, 3) 124 | assert.Equal(2, e.contentOffset) 125 | e.scrollPageDown(cl, 3) 126 | assert.Equal(4, e.contentOffset) 127 | 128 | e.scrollPageUp(3) 129 | assert.Equal(2, e.contentOffset) 130 | 131 | // term height changed 132 | e.scrollPageDown(cl, 5) 133 | assert.Equal(6, e.contentOffset) 134 | 135 | e.scrollPageDown(cl, 5) 136 | assert.Equal(7, e.contentOffset) 137 | 138 | e.scrollPageDown(cl, 5) 139 | assert.Equal(7, e.contentOffset) 140 | 141 | e.scrollPageUp(5) 142 | assert.Equal(3, e.contentOffset) 143 | 144 | e.scrollPageUp(5) 145 | assert.Equal(0, e.contentOffset) 146 | 147 | e.scrollPageUp(5) 148 | assert.Equal(0, e.contentOffset) 149 | 150 | // term height > content size + default Y (a filter query line) 151 | e.scrollPageDown(cl, 10) 152 | assert.Equal(7, e.contentOffset) 153 | } 154 | func TestGetContents(t *testing.T) { 155 | var assert = assert.New(t) 156 | 157 | e := getEngine(`{"name":"go"}`, "") 158 | c := e.getContents() 159 | assert.Equal([]string{`{`, ` "name": "go"`, "}"}, c) 160 | assert.Equal([]string{}, e.candidates) 161 | assert.Equal([]string{"", ""}, e.complete) 162 | 163 | e = getEngine(`{"name":"go", "naming":"simeji", "foo":"bar"}`, "") 164 | e.query.StringSet(".n") 165 | c = e.getContents() 166 | assert.Equal([]string{`{`, ` "foo": "bar",`, ` "name": "go",`, ` "naming": "simeji"`, "}"}, c) 167 | assert.Equal([]string{"name", "naming"}, e.candidates) 168 | assert.Equal([]string{"am", "nam"}, e.complete) 169 | 170 | e.keymode = true 171 | c = e.getContents() 172 | assert.Equal([]string{"name", "naming"}, c) 173 | assert.Equal([]string{"name", "naming"}, e.candidates) 174 | assert.Equal([]string{"am", "nam"}, e.complete) 175 | } 176 | 177 | func TestSetCandidateData(t *testing.T) { 178 | var assert = assert.New(t) 179 | e := getEngine(`{"name":"go"}`, "") 180 | 181 | // case 1 182 | e.candidates = []string{"test", "testing"} 183 | e.complete = []string{"est", "test"} 184 | e.candidatemode = true 185 | e.candidateidx = 1 186 | 187 | e.setCandidateData() 188 | assert.False(e.candidatemode) 189 | assert.Zero(e.candidateidx) 190 | assert.Equal([]string{}, e.candidates) 191 | 192 | // case 2 193 | e.candidates = []string{"test"} 194 | e.complete = []string{"", "test"} 195 | e.candidatemode = true 196 | e.candidateidx = 1 197 | 198 | e.setCandidateData() 199 | assert.False(e.candidatemode) 200 | assert.Zero(e.candidateidx) 201 | assert.Equal([]string{}, e.candidates) 202 | 203 | // case 3 204 | e.candidates = []string{"test", "testing"} 205 | e.complete = []string{"", "test"} 206 | e.candidatemode = true 207 | e.candidateidx = 2 208 | 209 | e.setCandidateData() 210 | assert.True(e.candidatemode) 211 | assert.Zero(e.candidateidx) 212 | assert.Equal([]string{"test", "testing"}, e.candidates) 213 | 214 | // case 4 215 | e.candidates = []string{"test", "testing"} 216 | e.complete = []string{"", "test"} 217 | e.candidatemode = true 218 | e.candidateidx = 1 219 | 220 | e.setCandidateData() 221 | assert.True(e.candidatemode) 222 | assert.Equal(1, e.candidateidx) 223 | assert.Equal([]string{"test", "testing"}, e.candidates) 224 | 225 | // case 4 226 | e.candidates = []string{"test", "testing"} 227 | e.complete = []string{"", "test"} 228 | e.candidatemode = false 229 | e.candidateidx = 1 230 | 231 | e.setCandidateData() 232 | assert.False(e.candidatemode) 233 | assert.Equal(0, e.candidateidx) 234 | assert.Equal([]string{}, e.candidates) 235 | 236 | } 237 | 238 | func TestConfirmCandidate(t *testing.T) { 239 | var assert = assert.New(t) 240 | e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") 241 | e.query.StringSet(".") 242 | e.queryConfirm = false 243 | e.candidates = []string{"test", "testing", "foo"} 244 | 245 | e.candidateidx = 0 246 | e.confirmCandidate() 247 | assert.Equal(".test", e.query.StringGet()) 248 | assert.True(e.queryConfirm) 249 | assert.Equal(5, e.queryCursorIdx) 250 | 251 | e.candidateidx = 2 252 | e.confirmCandidate() 253 | assert.Equal(".foo", e.query.StringGet()) 254 | 255 | assert.True(e.queryConfirm) 256 | assert.Equal(4, e.queryCursorIdx) 257 | 258 | e = getEngine(`{"name":"go"}`, "") 259 | e.query.StringSet(".name.hoge") 260 | e.candidates = []string{"aaa", "bbb", "ccc"} 261 | e.candidateidx = 1 262 | e.confirmCandidate() 263 | 264 | assert.True(e.queryConfirm) 265 | assert.Equal(9, e.queryCursorIdx) 266 | assert.Equal(".name.bbb", e.query.StringGet()) 267 | } 268 | 269 | func TestCtrllAction(t *testing.T) { 270 | var assert = assert.New(t) 271 | e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") 272 | assert.False(e.keymode) 273 | e.toggleKeymode() 274 | assert.True(e.keymode) 275 | e.toggleKeymode() 276 | assert.False(e.keymode) 277 | } 278 | 279 | func TestTabAction(t *testing.T) { 280 | var assert = assert.New(t) 281 | e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") 282 | e.query.StringSet(".namet") 283 | e.complete = []string{"est", "NameTest"} 284 | 285 | e.candidatemode = false 286 | e.tabAction() 287 | assert.Equal(".NameTest", e.query.StringGet()) 288 | 289 | _, e.complete, _, _ = e.manager.GetPretty(e.query, true) 290 | e.candidatemode = false 291 | e.tabAction() 292 | assert.Equal(".NameTest[", e.query.StringGet()) 293 | 294 | _, e.complete, _, _ = e.manager.GetPretty(e.query, true) 295 | e.candidatemode = false 296 | e.tabAction() 297 | assert.Equal(".NameTest[", e.query.StringGet()) 298 | } 299 | 300 | func TestEscAction(t *testing.T) { 301 | var assert = assert.New(t) 302 | e := getEngine(`{"name":"go","NameTest":[1,2,3]}`, "") 303 | assert.False(e.candidatemode) 304 | e.escapeCandidateMode() 305 | assert.False(e.candidatemode) 306 | e.candidatemode = true 307 | e.escapeCandidateMode() 308 | assert.False(e.candidatemode) 309 | } 310 | 311 | func TestInputChar(t *testing.T) { 312 | var assert = assert.New(t) 313 | e := getEngine(`{"name":"go"}`, "") 314 | e.query.StringSet(".name") 315 | e.queryCursorIdx = e.query.Length() 316 | assert.Equal(5, e.queryCursorIdx) 317 | 318 | e.inputChar('n') 319 | assert.Equal(".namen", e.query.StringGet()) 320 | assert.Equal(6, e.queryCursorIdx) 321 | 322 | e.inputChar('.') 323 | assert.Equal(".namen.", e.query.StringGet()) 324 | assert.Equal(7, e.queryCursorIdx) 325 | } 326 | 327 | func TestMoveCursorForwardAndBackward(t *testing.T) { 328 | var assert = assert.New(t) 329 | e := getEngine(`{"name":"simeji"}`, "") 330 | e.query.StringSet(".ne") 331 | 332 | e.moveCursorForward() 333 | assert.Equal(1, e.queryCursorIdx) 334 | e.moveCursorForward() 335 | assert.Equal(2, e.queryCursorIdx) 336 | e.moveCursorForward() 337 | assert.Equal(3, e.queryCursorIdx) 338 | e.moveCursorForward() 339 | assert.Equal(3, e.queryCursorIdx) 340 | 341 | e.moveCursorBackward() 342 | assert.Equal(2, e.queryCursorIdx) 343 | e.moveCursorBackward() 344 | assert.Equal(1, e.queryCursorIdx) 345 | e.moveCursorBackward() 346 | assert.Equal(0, e.queryCursorIdx) 347 | e.moveCursorBackward() 348 | assert.Equal(0, e.queryCursorIdx) 349 | } 350 | 351 | func TestMoveCursorToTopAndEnd(t *testing.T) { 352 | var assert = assert.New(t) 353 | e := getEngine(`{"name":"simeji"}`, "") 354 | e.query.StringSet(".ne") 355 | 356 | e.moveCursorToTop() 357 | assert.Zero(e.queryCursorIdx) 358 | 359 | e.moveCursorToEnd() 360 | assert.Equal(3, e.queryCursorIdx) 361 | } 362 | 363 | func getEngine(j string, qs string) *Engine { 364 | r := bytes.NewBufferString(j) 365 | e, _ := NewEngine(r, &EngineAttribute{ 366 | DefaultQuery: qs, 367 | Monochrome: false, 368 | }) 369 | ee := e.(*Engine) 370 | return ee 371 | } 372 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simeji/jid 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bitly/go-simplejson v0.5.0 7 | github.com/mattn/go-runewidth v0.0.4 8 | github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed 9 | github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5 10 | github.com/pkg/errors v0.8.0 11 | github.com/stretchr/testify v1.8.1 12 | ) 13 | 14 | require ( 15 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/fatih/color v1.7.0 // indirect 18 | github.com/kr/pretty v0.3.1 // indirect 19 | github.com/mattn/go-colorable v0.0.9 // indirect 20 | github.com/mattn/go-isatty v0.0.4 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/sys v0.10.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 2 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 3 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 4 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 10 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 16 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 17 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 18 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 19 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 20 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 21 | github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed h1:bAVGG6B+R5qpSylrrA+BAMrzYkdAoiTaKPVxRB+4cyM= 22 | github.com/nsf/termbox-go v0.0.0-20181027232701-60ab7e3d12ed/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 23 | github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5 h1:d+C3xJdxZT7wNlxqEwbXn3R355CwAhYBL9raVNfSnK0= 24 | github.com/nwidger/jsoncolor v0.0.0-20170215171346-75a6de4340e5/go.mod h1:GYFm0zZgTNeoK1QxuIofRDasy2ibmaJZhZLzwsMXUF4= 25 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 26 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 27 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 31 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 34 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 35 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 37 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 38 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 39 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 40 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /json_manager.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "github.com/bitly/go-simplejson" 5 | "github.com/pkg/errors" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | //"strings" 10 | ) 11 | 12 | type JsonManager struct { 13 | current *simplejson.Json 14 | origin *simplejson.Json 15 | suggestion *Suggestion 16 | } 17 | 18 | func NewJsonManager(reader io.Reader) (*JsonManager, error) { 19 | buf, err := io.ReadAll(reader) 20 | 21 | if err != nil { 22 | return nil, errors.Wrap(err, "invalid data") 23 | } 24 | 25 | j, err2 := simplejson.NewJson(buf) 26 | 27 | if err2 != nil { 28 | return nil, errors.Wrap(err2, "invalid json format") 29 | } 30 | 31 | json := &JsonManager{ 32 | origin: j, 33 | current: j, 34 | suggestion: NewSuggestion(), 35 | } 36 | 37 | return json, nil 38 | } 39 | 40 | func (jm *JsonManager) Get(q QueryInterface, confirm bool) (string, []string, []string, error) { 41 | json, suggestion, candidates, _ := jm.GetFilteredData(q, confirm) 42 | 43 | data, enc_err := json.Encode() 44 | 45 | if enc_err != nil { 46 | return "", []string{"", ""}, []string{"", ""}, errors.Wrap(enc_err, "failure json encode") 47 | } 48 | 49 | return string(data), suggestion, candidates, nil 50 | } 51 | 52 | func (jm *JsonManager) GetPretty(q QueryInterface, confirm bool) (string, []string, []string, error) { 53 | json, suggestion, candidates, _ := jm.GetFilteredData(q, confirm) 54 | s, err := json.EncodePretty() 55 | if err != nil { 56 | return "", []string{"", ""}, []string{"", ""}, errors.Wrap(err, "failure json encode") 57 | } 58 | return string(s), suggestion, candidates, nil 59 | } 60 | 61 | func (jm *JsonManager) GetFilteredData(q QueryInterface, confirm bool) (*simplejson.Json, []string, []string, error) { 62 | json := jm.origin 63 | 64 | lastKeyword := q.StringGetLastKeyword() 65 | keywords := q.StringGetKeywords() 66 | 67 | idx := 0 68 | if l := len(keywords); l == 0 { 69 | return json, []string{"", ""}, []string{}, nil 70 | } else if l > 0 { 71 | idx = l - 1 72 | } 73 | for _, keyword := range keywords[0:idx] { 74 | json, _ = getItem(json, keyword) 75 | } 76 | reg := regexp.MustCompile(`\[[0-9]*$`) 77 | 78 | suggest := jm.suggestion.Get(json, lastKeyword) 79 | candidateKeys := jm.suggestion.GetCandidateKeys(json, lastKeyword) 80 | // hash 81 | if len(reg.FindString(lastKeyword)) < 1 { 82 | candidateNum := len(candidateKeys) 83 | if j, exist := getItem(json, lastKeyword); exist && (confirm || candidateNum == 1) { 84 | json = j 85 | candidateKeys = []string{} 86 | if _, err := json.Array(); err == nil { 87 | suggest = jm.suggestion.Get(json, "") 88 | } else { 89 | suggest = []string{"", ""} 90 | } 91 | } else if candidateNum < 1 { 92 | json = j 93 | suggest = jm.suggestion.Get(json, "") 94 | } 95 | } 96 | return json, suggest, candidateKeys, nil 97 | } 98 | 99 | func (jm *JsonManager) GetCandidateKeys(q QueryInterface) []string { 100 | return jm.suggestion.GetCandidateKeys(jm.current, q.StringGetLastKeyword()) 101 | } 102 | 103 | func getItem(json *simplejson.Json, s string) (*simplejson.Json, bool) { 104 | var result *simplejson.Json 105 | var exist bool 106 | 107 | re := regexp.MustCompile(`\[([0-9]+)\]`) 108 | matches := re.FindStringSubmatch(s) 109 | 110 | if s == "" { 111 | return json, false 112 | } 113 | 114 | // Query include [ 115 | if len(matches) > 0 { 116 | index, _ := strconv.Atoi(matches[1]) 117 | if a, err := json.Array(); err != nil { 118 | exist = false 119 | } else if len(a) < index { 120 | exist = false 121 | } 122 | result = json.GetIndex(index) 123 | } else { 124 | result, exist = json.CheckGet(s) 125 | if result == nil { 126 | result = &simplejson.Json{} 127 | } 128 | } 129 | return result, exist 130 | } 131 | 132 | func isEmptyJson(j *simplejson.Json) bool { 133 | switch j.Interface().(type) { 134 | case nil: 135 | return true 136 | default: 137 | return false 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /json_manager_test.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | simplejson "github.com/bitly/go-simplejson" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewJson(t *testing.T) { 13 | var assert = assert.New(t) 14 | 15 | r := bytes.NewBufferString("{\"name\":\"go\"}") 16 | jm, e := NewJsonManager(r) 17 | 18 | rr := bytes.NewBufferString("{\"name\":\"go\"}") 19 | buf, _ := io.ReadAll(rr) 20 | sj, _ := simplejson.NewJson(buf) 21 | 22 | assert.Equal(jm, &JsonManager{ 23 | current: sj, 24 | origin: sj, 25 | suggestion: NewSuggestion(), 26 | }) 27 | assert.Nil(e) 28 | 29 | assert.Equal("go", jm.current.Get("name").MustString()) 30 | } 31 | 32 | func TestNewJsonWithError(t *testing.T) { 33 | var assert = assert.New(t) 34 | 35 | r := bytes.NewBufferString("{\"name\":\"go\"") 36 | jm, e := NewJsonManager(r) 37 | 38 | assert.Nil(jm) 39 | assert.Regexp("invalid json format", e.Error()) 40 | } 41 | 42 | func TestGet(t *testing.T) { 43 | var assert = assert.New(t) 44 | 45 | r := bytes.NewBufferString("{\"name\":\"go\"}") 46 | jm, _ := NewJsonManager(r) 47 | q := NewQueryWithString(".name") 48 | result, suggest, candidateKeys, err := jm.Get(q, false) 49 | 50 | assert.Nil(err) 51 | assert.Equal(`"go"`, result) 52 | assert.Equal([]string{``, ``}, suggest) 53 | assert.Equal([]string{}, candidateKeys) 54 | 55 | // data 56 | data := `{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}}` 57 | r = bytes.NewBufferString(data) 58 | jm, _ = NewJsonManager(r) 59 | 60 | // case 2 61 | q = NewQueryWithString(".abcde") 62 | result, suggest, candidateKeys, err = jm.Get(q, false) 63 | assert.Nil(err) 64 | //assert.Equal(`"2AA2"`, result) 65 | assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}}`, result) 66 | assert.Equal([]string{``, "abcde"}, suggest) 67 | 68 | // case 3 69 | q = NewQueryWithString(".abcde_fgh") 70 | result, suggest, candidateKeys, err = jm.Get(q, false) 71 | assert.Nil(err) 72 | assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, result) 73 | assert.Equal([]string{``, ``}, suggest) 74 | 75 | // case 4 76 | q = NewQueryWithString(".abcde_fgh.aaa[2]") 77 | result, suggest, candidateKeys, err = jm.Get(q, false) 78 | assert.Equal(`[1,2]`, result) 79 | 80 | // case 5 81 | q = NewQueryWithString(".abcde_fgh.aaa[3]") 82 | result, suggest, candidateKeys, err = jm.Get(q, false) 83 | assert.Nil(err) 84 | assert.Equal(`null`, result) 85 | 86 | // case 6 87 | q = NewQueryWithString(".abcde_fgh.aa") 88 | result, suggest, candidateKeys, err = jm.Get(q, false) 89 | assert.Nil(err) 90 | assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, result) 91 | assert.Equal([]string{`a`, `aaa`}, suggest) 92 | 93 | // case 7 94 | q = NewQueryWithString(".abcde_fgh.ac") 95 | result, suggest, candidateKeys, err = jm.Get(q, false) 96 | assert.Nil(err) 97 | assert.Equal(`null`, result) 98 | assert.Equal([]string{``, ``}, suggest) 99 | 100 | // data 101 | data = `{"abc":"2AA2","def":{"aaa":"bbb"}}` 102 | r = bytes.NewBufferString(data) 103 | jm, _ = NewJsonManager(r) 104 | 105 | // case 2 106 | q = NewQueryWithString(".def") 107 | result, suggest, candidateKeys, err = jm.Get(q, false) 108 | assert.Nil(err) 109 | assert.Equal(`{"aaa":"bbb"}`, result) 110 | assert.Equal([]string{``, ``}, suggest) 111 | } 112 | 113 | func TestGetPretty(t *testing.T) { 114 | var assert = assert.New(t) 115 | 116 | r := bytes.NewBufferString("{\"name\":\"go\"}") 117 | jm, _ := NewJsonManager(r) 118 | q := NewQueryWithString(".name") 119 | result, _, _, err := jm.GetPretty(q, true) 120 | 121 | assert.Nil(err) 122 | assert.Equal(`"go"`, result) 123 | } 124 | 125 | func TestGetItem(t *testing.T) { 126 | var assert = assert.New(t) 127 | 128 | rr := bytes.NewBufferString(`{"name":"go"}`) 129 | buf, _ := io.ReadAll(rr) 130 | sj, _ := simplejson.NewJson(buf) 131 | 132 | d, _ := getItem(sj, "") 133 | result, _ := d.Encode() 134 | assert.Equal(`{"name":"go"}`, string(result)) 135 | 136 | d, _ = getItem(sj, "name") 137 | result, _ = d.Encode() 138 | assert.Equal(`"go"`, string(result)) 139 | 140 | // case 2 141 | rr = bytes.NewBufferString(`{"name":"go","age":20}`) 142 | buf, _ = io.ReadAll(rr) 143 | sj, _ = simplejson.NewJson(buf) 144 | 145 | d, _ = getItem(sj, "age") 146 | result, _ = d.Encode() 147 | assert.Equal("20", string(result)) 148 | 149 | // case 3 150 | rr = bytes.NewBufferString(`{"data":{"name":"go","age":20}}`) 151 | buf, _ = io.ReadAll(rr) 152 | sj, _ = simplejson.NewJson(buf) 153 | 154 | d, _ = getItem(sj, "data") 155 | d2, _ := getItem(d, "name") 156 | d3, _ := getItem(d, "age") 157 | result2, _ := d2.Encode() 158 | result3, _ := d3.Encode() 159 | 160 | assert.Equal(`"go"`, string(result2)) 161 | assert.Equal(`20`, string(result3)) 162 | 163 | // case 4 164 | rr = bytes.NewBufferString(`{"data":[{"name":"test","age":30},{"name":"go","age":20}]}`) 165 | buf, _ = io.ReadAll(rr) 166 | sj, _ = simplejson.NewJson(buf) 167 | 168 | d, _ = getItem(sj, "data") 169 | d2, _ = getItem(d, "[1]") 170 | d3, _ = getItem(d2, "name") 171 | result, _ = d3.Encode() 172 | 173 | assert.Equal(`"go"`, string(result)) 174 | 175 | // case 5 176 | rr = bytes.NewBufferString(`[{"name":"go","age":20}]`) 177 | buf, _ = io.ReadAll(rr) 178 | sj, _ = simplejson.NewJson(buf) 179 | 180 | d, _ = getItem(sj, "") 181 | result, _ = d.Encode() 182 | assert.Equal(`[{"age":20,"name":"go"}]`, string(result)) 183 | 184 | // case 6 185 | d, _ = getItem(sj, "[0]") 186 | result, _ = d.Encode() 187 | assert.Equal(`{"age":20,"name":"go"}`, string(result)) 188 | 189 | // case 7 key contains '.' 190 | rr = bytes.NewBufferString(`{"na.me":"go","age":20}`) 191 | buf, _ = io.ReadAll(rr) 192 | sj, _ = simplejson.NewJson(buf) 193 | 194 | d, _ = getItem(sj, "na.me") 195 | result, _ = d.Encode() 196 | assert.Equal(`"go"`, string(result)) 197 | 198 | } 199 | 200 | func TestGetFilteredData(t *testing.T) { 201 | var assert = assert.New(t) 202 | 203 | // data 204 | data := `{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}` 205 | r := bytes.NewBufferString(data) 206 | jm, _ := NewJsonManager(r) 207 | 208 | // case 1 209 | q := NewQueryWithString(".abcde") 210 | result, s, c, err := jm.GetFilteredData(q, false) 211 | assert.Nil(err) 212 | d, _ := result.Encode() 213 | assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}`, string(d)) 214 | //assert.Equal(`"2AA2"`, string(d)) 215 | assert.Equal([]string{``, `abcde`}, s) 216 | assert.Equal([]string{"abcde", "abcde_fgh"}, c) 217 | 218 | // case 2 219 | q = NewQueryWithString(".abcde_fgh") 220 | result, s, c, err = jm.GetFilteredData(q, false) 221 | assert.Nil(err) 222 | d, _ = result.Encode() 223 | assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, string(d)) 224 | assert.Equal([]string{``, ``}, s) 225 | assert.Equal([]string{}, c) 226 | 227 | // case 3 228 | q = NewQueryWithString(".abcde_fgh.aaa[2]") 229 | result, s, c, err = jm.GetFilteredData(q, false) 230 | assert.Nil(err) 231 | d, _ = result.Encode() 232 | assert.Equal(`[1,2]`, string(d)) 233 | assert.Equal([]string{`[`, `[`}, s) 234 | 235 | // case 4 236 | q = NewQueryWithString(".abcde_fgh.aaa[3]") 237 | result, s, c, err = jm.GetFilteredData(q, false) 238 | assert.Nil(err) 239 | d, _ = result.Encode() 240 | assert.Equal(`null`, string(d)) 241 | assert.Equal([]string{``, ``}, s) 242 | 243 | // case 5 244 | q = NewQueryWithString(".abcde_fgh.aaa") 245 | result, s, c, err = jm.GetFilteredData(q, false) 246 | assert.Nil(err) 247 | d, _ = result.Encode() 248 | assert.Equal(`[123,"cccc",[1,2]]`, string(d)) 249 | assert.Equal([]string{`[`, `[`}, s) 250 | 251 | // case 6 252 | q = NewQueryWithString(".abcde_fgh.aa") 253 | result, s, c, err = jm.GetFilteredData(q, false) 254 | assert.Nil(err) 255 | d, _ = result.Encode() 256 | assert.Equal(`{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"}`, string(d)) 257 | assert.Equal([]string{`a`, `aaa`}, s) 258 | 259 | // case 7 260 | q = NewQueryWithString(".abcde_fgh.aaa[") 261 | result, s, c, err = jm.GetFilteredData(q, false) 262 | assert.Nil(err) 263 | d, _ = result.Encode() 264 | assert.Equal(`[123,"cccc",[1,2]]`, string(d)) 265 | assert.Equal([]string{``, `[`}, s) 266 | 267 | // case 8 268 | q = NewQueryWithString(".") 269 | result, s, c, err = jm.GetFilteredData(q, false) 270 | assert.Nil(err) 271 | d, _ = result.Encode() 272 | assert.Equal(`{"abcde":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}`, string(d)) 273 | assert.Equal([]string{``, ``}, s) 274 | 275 | // case 9 276 | q = NewQueryWithString(".cc.") 277 | result, s, c, err = jm.GetFilteredData(q, false) 278 | assert.Nil(err) 279 | d, _ = result.Encode() 280 | assert.Equal(`{"a":[3,4]}`, string(d)) 281 | assert.Equal([]string{`a`, `a`}, s) 282 | assert.Equal([]string{"a"}, c) 283 | 284 | // case 2-1 285 | data = `{"arraytest":[{"aaa":123,"aab":234},[1,2]]}` 286 | r = bytes.NewBufferString(data) 287 | jm, _ = NewJsonManager(r) 288 | 289 | q = NewQueryWithString(".arraytest[0]") 290 | result, s, c, err = jm.GetFilteredData(q, false) 291 | assert.Nil(err) 292 | d, _ = result.Encode() 293 | assert.Equal(`{"aaa":123,"aab":234}`, string(d)) 294 | assert.Equal([]string{``, ``}, s) 295 | assert.Equal([]string{}, c) 296 | 297 | // case 3-1 298 | data = `{"aa":"abcde","bb":{"foo":"bar"}}` 299 | r = bytes.NewBufferString(data) 300 | jm, _ = NewJsonManager(r) 301 | 302 | q = NewQueryWithString(".bb") 303 | result, s, c, err = jm.GetFilteredData(q, false) 304 | assert.Nil(err) 305 | d, _ = result.Encode() 306 | assert.Equal(`{"foo":"bar"}`, string(d)) 307 | assert.Equal([]string{``, ``}, s) 308 | assert.Equal([]string{}, c) 309 | 310 | // case 4-1 311 | data = `[{"name": "simeji"},{"name": "simeji2"}]` 312 | r = bytes.NewBufferString(data) 313 | jm, _ = NewJsonManager(r) 314 | 315 | q = NewQueryWithString("") 316 | result, s, c, err = jm.GetFilteredData(q, false) 317 | assert.Nil(err) 318 | d, _ = result.Encode() 319 | assert.Equal(`[{"name":"simeji"},{"name":"simeji2"}]`, string(d)) 320 | assert.Equal([]string{``, ``}, s) 321 | assert.Equal([]string{}, c) 322 | 323 | // case 5-1 324 | data = `{"PrivateName":"simei", "PrivateAlias": "simeji2"}` 325 | r = bytes.NewBufferString(data) 326 | jm, _ = NewJsonManager(r) 327 | 328 | q = NewQueryWithString(".Private") 329 | result, s, c, err = jm.GetFilteredData(q, false) 330 | d, _ = result.Encode() 331 | assert.Equal([]string{``, `Private`}, s) 332 | assert.Equal([]string{"PrivateAlias", "PrivateName"}, c) 333 | 334 | } 335 | 336 | func TestGetFilteredDataWithMatchQuery(t *testing.T) { 337 | var assert = assert.New(t) 338 | 339 | data := `{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}` 340 | r := bytes.NewBufferString(data) 341 | jm, _ := NewJsonManager(r) 342 | 343 | q := NewQueryWithString(`.name`) 344 | result, s, c, err := jm.GetFilteredData(q, false) 345 | assert.Nil(err) 346 | d, _ := result.Encode() 347 | assert.Equal(`[1,2,3]`, string(d)) 348 | assert.Equal([]string{"[", "["}, s) 349 | assert.Equal([]string{}, c) 350 | 351 | q = NewQueryWithString(`.naming`) 352 | result, s, c, err = jm.GetFilteredData(q, false) 353 | assert.Nil(err) 354 | d, _ = result.Encode() 355 | assert.Equal(`{"account":"simeji"}`, string(d)) 356 | assert.Equal([]string{"", ""}, s) 357 | assert.Equal([]string{}, c) 358 | 359 | q = NewQueryWithString(`.naming.`) 360 | result, s, c, err = jm.GetFilteredData(q, false) 361 | assert.Nil(err) 362 | d, _ = result.Encode() 363 | assert.Equal(`{"account":"simeji"}`, string(d)) 364 | assert.Equal([]string{"account", "account"}, s) 365 | assert.Equal([]string{"account"}, c) 366 | 367 | q = NewQueryWithString(`.test`) 368 | result, s, c, err = jm.GetFilteredData(q, false) 369 | assert.Nil(err) 370 | d, _ = result.Encode() 371 | assert.Equal(`{"name":[1,2,3],"naming":{"account":"simeji"},"test":"simeji","testing":"ok"}`, string(d)) 372 | assert.Equal([]string{"", "test"}, s) 373 | assert.Equal([]string{"test", "testing"}, c) 374 | } 375 | 376 | func TestGetFilteredDataWithContainDots(t *testing.T) { 377 | var assert = assert.New(t) 378 | 379 | // data 380 | data := `{"abc.de":"2AA2","abcde_fgh":{"aaa":[123,"cccc",[1,2]],"c":"JJJJ"},"cc":{"a":[3,4]}}` 381 | r := bytes.NewBufferString(data) 382 | jm, _ := NewJsonManager(r) 383 | 384 | // case 1 385 | q := NewQueryWithString(`.\"abc.de\"`) 386 | result, s, c, err := jm.GetFilteredData(q, false) 387 | assert.Nil(err) 388 | d, _ := result.Encode() 389 | assert.Equal(`"2AA2"`, string(d)) 390 | assert.Equal([]string{``, ``}, s) 391 | assert.Equal([]string{}, c) 392 | 393 | // case 2 394 | q = NewQueryWithString(`."abc.de"`) 395 | result, s, c, err = jm.GetFilteredData(q, false) 396 | assert.Nil(err) 397 | d, _ = result.Encode() 398 | assert.Equal(`null`, string(d)) 399 | assert.Equal([]string{``, ``}, s) 400 | assert.Equal([]string{}, c) 401 | 402 | // case 3 403 | q = NewQueryWithString(`.abc.de`) 404 | result, s, c, err = jm.GetFilteredData(q, false) 405 | assert.Nil(err) 406 | d, _ = result.Encode() 407 | assert.Equal(`null`, string(d)) 408 | assert.Equal([]string{"", ""}, s) 409 | assert.Equal([]string{}, c) 410 | } 411 | 412 | func TestGetCandidateKeys(t *testing.T) { 413 | var assert = assert.New(t) 414 | data := `{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}` 415 | r := bytes.NewBufferString(data) 416 | jm, _ := NewJsonManager(r) 417 | 418 | q := NewQueryWithString(`.n`) 419 | 420 | keys := jm.GetCandidateKeys(q) 421 | assert.Equal([]string{"name", "naming"}, keys) 422 | 423 | q = NewQueryWithString(`.`) 424 | keys = jm.GetCandidateKeys(q) 425 | assert.Equal([]string{"name", "naming", "test", "testing"}, keys) 426 | 427 | q = NewQueryWithString(`.test`) 428 | keys = jm.GetCandidateKeys(q) 429 | assert.Equal([]string{"test", "testing"}, keys) 430 | 431 | q = NewQueryWithString(`.testi`) 432 | keys = jm.GetCandidateKeys(q) 433 | assert.Equal([]string{"testing"}, keys) 434 | 435 | q = NewQueryWithString(`.testia`) 436 | keys = jm.GetCandidateKeys(q) 437 | assert.Equal([]string{}, keys) 438 | } 439 | 440 | func TestGetCurrentKeys(t *testing.T) { 441 | var assert = assert.New(t) 442 | r := bytes.NewBufferString(`{"name":"go","age":20,"weight":60}`) 443 | buf, _ := io.ReadAll(r) 444 | sj, _ := simplejson.NewJson(buf) 445 | 446 | keys := getCurrentKeys(sj) 447 | assert.Equal([]string{"age", "name", "weight"}, keys) 448 | 449 | r = bytes.NewBufferString(`[2,3,"aa"]`) 450 | buf, _ = io.ReadAll(r) 451 | sj, _ = simplejson.NewJson(buf) 452 | 453 | keys = getCurrentKeys(sj) 454 | assert.Equal([]string{}, keys) 455 | } 456 | 457 | func TestIsEmptyJson(t *testing.T) { 458 | var assert = assert.New(t) 459 | r := bytes.NewBufferString(`{"name":"go"}`) 460 | buf, _ := io.ReadAll(r) 461 | sj, _ := simplejson.NewJson(buf) 462 | 463 | assert.Equal(false, isEmptyJson(sj)) 464 | assert.Equal(true, isEmptyJson(&simplejson.Json{})) 465 | } 466 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | runewidth "github.com/mattn/go-runewidth" 8 | ) 9 | 10 | type QueryInterface interface { 11 | Get() []rune 12 | Set(query []rune) []rune 13 | Insert(query []rune, idx int) []rune 14 | Add(query []rune) []rune 15 | Delete(i int) []rune 16 | Clear() []rune 17 | Length() int 18 | IndexOffset(int) int 19 | GetChar(int) rune 20 | GetKeywords() [][]rune 21 | GetLastKeyword() []rune 22 | PopKeyword() ([]rune, []rune) 23 | StringGet() string 24 | StringSet(query string) string 25 | StringInsert(query string, idx int) string 26 | StringAdd(query string) string 27 | StringGetKeywords() []string 28 | StringGetLastKeyword() string 29 | StringPopKeyword() (string, []rune) 30 | } 31 | 32 | type Query struct { 33 | query *[]rune 34 | complete *[]rune 35 | } 36 | 37 | func NewQuery(query []rune) *Query { 38 | q := &Query{ 39 | query: &[]rune{}, 40 | complete: &[]rune{}, 41 | } 42 | _ = q.Set(query) 43 | return q 44 | } 45 | func NewQueryWithString(query string) *Query { 46 | return NewQuery([]rune(query)) 47 | } 48 | 49 | func (q *Query) Get() []rune { 50 | return *q.query 51 | } 52 | 53 | func (q *Query) GetChar(idx int) rune { 54 | var r rune = 0 55 | qq := q.Get() 56 | if l := len(qq); l > idx && idx >= 0 { 57 | r = qq[idx] 58 | } 59 | return r 60 | } 61 | 62 | func (q *Query) Length() int { 63 | return len(q.Get()) 64 | } 65 | 66 | func (q *Query) IndexOffset(i int) int { 67 | o := 0 68 | if l := q.Length(); i >= l { 69 | o = runewidth.StringWidth(q.StringGet()) 70 | } else if i >= 0 && i < l { 71 | o = runewidth.StringWidth(string(q.Get()[:i])) 72 | } 73 | return o 74 | } 75 | 76 | func (q *Query) Set(query []rune) []rune { 77 | if validate(query) { 78 | q.query = &query 79 | } 80 | return q.Get() 81 | } 82 | 83 | func (q *Query) Insert(query []rune, idx int) []rune { 84 | qq := q.Get() 85 | if idx == 0 { 86 | qq = append(query, qq...) 87 | } else if idx > 0 && len(qq) >= idx { 88 | _q := make([]rune, idx+len(query)-1) 89 | copy(_q, qq[:idx]) 90 | qq = append(append(_q, query...), qq[idx:]...) 91 | } 92 | return q.Set(qq) 93 | } 94 | 95 | func (q *Query) StringInsert(query string, idx int) string { 96 | return string(q.Insert([]rune(query), idx)) 97 | } 98 | 99 | func (q *Query) Add(query []rune) []rune { 100 | return q.Set(append(q.Get(), query...)) 101 | } 102 | 103 | func (q *Query) Delete(i int) []rune { 104 | var d []rune 105 | qq := q.Get() 106 | lastIdx := len(qq) 107 | if i < 0 { 108 | if lastIdx+i >= 0 { 109 | d = qq[lastIdx+i:] 110 | qq = qq[0 : lastIdx+i] 111 | } else { 112 | d = qq 113 | qq = qq[0:0] 114 | } 115 | } else if i == 0 { 116 | d = []rune{} 117 | qq = qq[1:] 118 | } else if i > 0 && i < lastIdx { 119 | d = []rune{qq[i]} 120 | qq = append(qq[:i], qq[i+1:]...) 121 | } 122 | _ = q.Set(qq) 123 | return d 124 | } 125 | 126 | func (q *Query) Clear() []rune { 127 | return q.Set([]rune("")) 128 | } 129 | 130 | func (q *Query) GetKeywords() [][]rune { 131 | qq := *q.query 132 | 133 | if qq == nil || string(qq) == "" { 134 | return [][]rune{} 135 | } 136 | 137 | splitQuery := []string{} 138 | rr := []rune{} 139 | enclosed := true 140 | ql := len(*q.query) 141 | for i := 0; i < ql; i++ { 142 | r := qq[i] 143 | if ii := i + 1; r == '\\' && ql > ii && qq[ii] == '"' { 144 | enclosed = !enclosed 145 | i++ // skip '"(double quortation)' 146 | continue 147 | } 148 | if enclosed && r == '.' { 149 | splitQuery = append(splitQuery, string(rr)) 150 | rr = []rune{} 151 | } else { 152 | rr = append(rr, r) 153 | } 154 | } 155 | if rr != nil { 156 | v := []string{string(rr)} 157 | if !enclosed { 158 | v = strings.Split(string(rr), ".") 159 | } 160 | splitQuery = append(splitQuery, v...) 161 | } 162 | lastIdx := len(splitQuery) - 1 163 | 164 | keywords := [][]rune{} 165 | for i, keyword := range splitQuery { 166 | if keyword != "" || i == lastIdx { 167 | re := regexp.MustCompile(`\[[0-9]*\]?`) 168 | matchIndexes := re.FindAllStringIndex(keyword, -1) 169 | if len(matchIndexes) < 1 { 170 | keywords = append(keywords, []rune(keyword)) 171 | } else { 172 | if matchIndexes[0][0] > 0 { 173 | keywords = append(keywords, []rune(keyword[0:matchIndexes[0][0]])) 174 | } 175 | for _, matchIndex := range matchIndexes { 176 | k := keyword[matchIndex[0]:matchIndex[1]] 177 | keywords = append(keywords, []rune(k)) 178 | } 179 | } 180 | } 181 | } 182 | return keywords 183 | } 184 | 185 | func (q *Query) GetLastKeyword() []rune { 186 | keywords := q.GetKeywords() 187 | if l := len(keywords); l > 0 { 188 | return keywords[l-1] 189 | } 190 | return []rune("") 191 | } 192 | 193 | func (q *Query) StringGetLastKeyword() string { 194 | return string(q.GetLastKeyword()) 195 | } 196 | 197 | func (q *Query) PopKeyword() ([]rune, []rune) { 198 | keyword := q.GetLastKeyword() 199 | nq := string(keyword) 200 | qq := q.StringGet() 201 | 202 | for _, r := range keyword { 203 | if r == '.' { 204 | nq = `\"` + string(keyword) + `\"` 205 | break 206 | } 207 | } 208 | re := regexp.MustCompile(`(\.)?(\\")?` + regexp.QuoteMeta(nq) + "$") 209 | 210 | qq = re.ReplaceAllString(qq, "") 211 | 212 | query := q.Set([]rune(qq)) 213 | return keyword, query 214 | } 215 | 216 | func (q *Query) StringGet() string { 217 | return string(q.Get()) 218 | } 219 | 220 | func (q *Query) StringSet(query string) string { 221 | return string(q.Set([]rune(query))) 222 | } 223 | 224 | func (q *Query) StringAdd(query string) string { 225 | return string(q.Add([]rune(query))) 226 | } 227 | 228 | func (q *Query) StringGetKeywords() []string { 229 | var keywords []string 230 | for _, keyword := range q.GetKeywords() { 231 | keywords = append(keywords, string(keyword)) 232 | } 233 | return keywords 234 | } 235 | 236 | func (q *Query) StringPopKeyword() (string, []rune) { 237 | keyword, query := q.PopKeyword() 238 | return string(keyword), query 239 | } 240 | 241 | func validate(r []rune) bool { 242 | s := string(r) 243 | if s == "" { 244 | return true 245 | } 246 | if regexp.MustCompile(`^[^.]`).MatchString(s) { 247 | return false 248 | } 249 | if regexp.MustCompile(`\.{2,}`).MatchString(s) { 250 | return false 251 | } 252 | if regexp.MustCompile(`\[[0-9]*\][^\.\[]`).MatchString(s) { 253 | return false 254 | } 255 | if regexp.MustCompile(`\[{2,}|\]{2,}`).MatchString(s) { 256 | return false 257 | } 258 | if regexp.MustCompile(`.\.\[`).MatchString(s) { 259 | return false 260 | } 261 | return true 262 | } 263 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidate(t *testing.T) { 10 | var assert = assert.New(t) 11 | 12 | assert.True(validate([]rune(".test.name"))) 13 | assert.True(validate([]rune(".test.name."))) 14 | assert.True(validate([]rune(".test[0].name."))) 15 | assert.True(validate([]rune(".[0].name."))) 16 | assert.True(validate([]rune(".name[9][1]"))) 17 | assert.True(validate([]rune(".[0][1].name."))) 18 | 19 | assert.False(validate([]rune("[0].name."))) 20 | assert.False(validate([]rune(".test[0]].name."))) 21 | assert.False(validate([]rune(".test..name"))) 22 | assert.False(validate([]rune(".test.name.."))) 23 | assert.False(validate([]rune(".test[[0]].name."))) 24 | assert.False(validate([]rune(".test[0]name."))) 25 | assert.False(validate([]rune(".test.[0].name."))) 26 | } 27 | 28 | func TestNewQuery(t *testing.T) { 29 | var assert = assert.New(t) 30 | 31 | v := []rune(".name") 32 | q := NewQuery(v) 33 | 34 | assert.Equal(*q.query, []rune(".name")) 35 | assert.Equal(*q.complete, []rune("")) 36 | } 37 | 38 | func TestNewQueryWithInvalidQuery(t *testing.T) { 39 | var assert = assert.New(t) 40 | 41 | v := []rune("name") 42 | q := NewQuery(v) 43 | 44 | assert.Equal(*q.query, []rune("")) 45 | assert.Equal(*q.complete, []rune("")) 46 | } 47 | 48 | func TestNewQueryWithString(t *testing.T) { 49 | var assert = assert.New(t) 50 | 51 | q := NewQueryWithString(".name") 52 | 53 | assert.Equal(*q.query, []rune(".name")) 54 | assert.Equal(*q.complete, []rune("")) 55 | } 56 | 57 | func TestNewQueryWithStringWithInvalidQuery(t *testing.T) { 58 | var assert = assert.New(t) 59 | 60 | q := NewQueryWithString("name") 61 | 62 | assert.Equal(*q.query, []rune("")) 63 | assert.Equal(*q.complete, []rune("")) 64 | } 65 | 66 | func TestQueryGet(t *testing.T) { 67 | var assert = assert.New(t) 68 | 69 | v := []rune(".test") 70 | q := NewQuery(v) 71 | 72 | assert.Equal(q.Get(), []rune(".test")) 73 | } 74 | 75 | func TestQueryLength(t *testing.T) { 76 | var assert = assert.New(t) 77 | 78 | v := []rune(".test") 79 | q := NewQuery(v) 80 | 81 | assert.Equal(5, q.Length()) 82 | 83 | v = []rune(".string.日本語.japan") 84 | q = NewQuery(v) 85 | 86 | assert.Equal(17, q.Length()) 87 | } 88 | 89 | func TestQueryIndexOffsetN(t *testing.T) { 90 | var assert = assert.New(t) 91 | 92 | v := []rune(".test") 93 | q := NewQuery(v) 94 | 95 | assert.Equal(4, q.IndexOffset(4)) 96 | assert.Equal(0, q.IndexOffset(0)) 97 | assert.Equal(0, q.IndexOffset(-1)) 98 | assert.Equal(5, q.IndexOffset(6)) 99 | 100 | //off-------012345679-101213|j_15,n_19 101 | v = []rune(".string.日本語.japan") 102 | //idx-------012345678-9-10|j_12,n_16 103 | q = NewQuery(v) 104 | 105 | assert.Equal(19, q.IndexOffset(16)) 106 | assert.Equal(10, q.IndexOffset(9)) 107 | } 108 | 109 | func TestQueryGetChar(t *testing.T) { 110 | var assert = assert.New(t) 111 | 112 | v := []rune(".test") 113 | q := NewQuery(v) 114 | 115 | assert.Equal('e', q.GetChar(2)) 116 | assert.Equal('t', q.GetChar(4)) 117 | assert.Equal('.', q.GetChar(0)) 118 | assert.Equal('.', q.GetChar(0)) 119 | assert.Equal(rune(0), q.GetChar(-1)) 120 | assert.Equal(rune(0), q.GetChar(6)) 121 | 122 | v = []rune(".string.日本語.japan") 123 | q = NewQuery(v) 124 | 125 | assert.Equal('n', q.GetChar(5)) 126 | assert.Equal('本', q.GetChar(9)) 127 | assert.Equal('.', q.GetChar(11)) 128 | assert.Equal(rune(0), q.GetChar(17)) 129 | } 130 | 131 | func TestQuerySet(t *testing.T) { 132 | var assert = assert.New(t) 133 | 134 | v := []rune(".hello") 135 | q := NewQuery(v) 136 | 137 | assert.Equal([]rune(".world"), q.Set([]rune(".world"))) 138 | assert.Equal("", string(q.Set([]rune("")))) 139 | } 140 | 141 | func TestQuerySetWithInvalidQuery(t *testing.T) { 142 | var assert = assert.New(t) 143 | 144 | v := []rune(".hello") 145 | q := NewQuery(v) 146 | 147 | assert.Equal(q.Set([]rune("world")), []rune(".hello")) 148 | } 149 | 150 | func TestQueryAdd(t *testing.T) { 151 | var assert = assert.New(t) 152 | 153 | v := []rune(".hello") 154 | q := NewQuery(v) 155 | 156 | assert.Equal(q.Add([]rune("world")), []rune(".helloworld")) 157 | } 158 | func TestQueryInsert(t *testing.T) { 159 | var assert = assert.New(t) 160 | v := []rune(".hello.world") 161 | q := NewQuery(v) 162 | 163 | assert.Equal([]rune(".hello.world"), q.Insert([]rune("w"), 0)) 164 | assert.Equal([]rune(".whello.world"), q.Insert([]rune("w"), 1)) 165 | assert.Equal([]rune(".wwhello.world"), q.Insert([]rune("w"), 1)) 166 | assert.Equal([]rune(".wwhello.world"), q.Insert([]rune("."), 1)) 167 | assert.Equal([]rune(".wwh.ello.world"), q.Insert([]rune("."), 4)) 168 | assert.Equal([]rune(".wwh.ello.worldg"), q.Insert([]rune("g"), 15)) 169 | assert.Equal([]rune(".wwh.ello.worldg"), q.Insert([]rune("a"), 20)) 170 | } 171 | func TestQueryStringInsert(t *testing.T) { 172 | var assert = assert.New(t) 173 | q := NewQueryWithString(".hello.world") 174 | 175 | assert.Equal(".hello.world", q.StringInsert("w", 0)) 176 | assert.Equal(".whello.world", q.StringInsert("w", 1)) 177 | assert.Equal(".wwhello.world", q.StringInsert("w", 1)) 178 | assert.Equal(".wwhello.world", q.StringInsert(".", 1)) 179 | assert.Equal(".wwh.ello.world", q.StringInsert(".", 4)) 180 | assert.Equal(".wwh.ello.worlda", q.StringInsert("a", 15)) 181 | assert.Equal(".wwh.ello.worlda", q.StringInsert("a", 20)) 182 | } 183 | 184 | func TestQueryClear(t *testing.T) { 185 | var assert = assert.New(t) 186 | 187 | v := []rune(".test") 188 | q := NewQuery(v) 189 | 190 | assert.Equal(q.Clear(), []rune("")) 191 | } 192 | 193 | func TestQueryDelete(t *testing.T) { 194 | var assert = assert.New(t) 195 | 196 | v := []rune(".helloworld") 197 | q := NewQuery(v) 198 | 199 | assert.Equal([]rune("d"), q.Delete(-1)) 200 | assert.Equal([]rune(".helloworl"), q.Get()) 201 | assert.Equal([]rune("l"), q.Delete(-1)) 202 | assert.Equal([]rune(".hellowor"), q.Get()) 203 | assert.Equal([]rune("or"), q.Delete(-2)) 204 | assert.Equal([]rune(".hellow"), q.Get()) 205 | assert.Equal([]rune(".hellow"), q.Delete(-8)) 206 | assert.Equal([]rune(""), q.Get()) 207 | 208 | q = NewQuery([]rune(".hello.world")) 209 | assert.Equal([]rune(""), q.Delete(0)) 210 | assert.Equal([]rune(".hello.world"), q.Get()) 211 | assert.Equal([]rune("h"), q.Delete(1)) 212 | assert.Equal([]rune(".ello.world"), q.Get()) 213 | assert.Equal([]rune("e"), q.Delete(1)) 214 | assert.Equal([]rune(".llo.world"), q.Get()) 215 | assert.Equal([]rune(""), q.Delete(0)) 216 | assert.Equal([]rune(".llo.world"), q.Get()) 217 | assert.Equal([]rune("o"), q.Delete(3)) 218 | assert.Equal([]rune(".ll.world"), q.Get()) 219 | assert.Equal([]rune("."), q.Delete(3)) 220 | assert.Equal([]rune(".llworld"), q.Get()) 221 | assert.Equal([]rune("w"), q.Delete(3)) 222 | assert.Equal([]rune(".llorld"), q.Get()) 223 | } 224 | 225 | func TestGetKeywords(t *testing.T) { 226 | var assert = assert.New(t) 227 | 228 | v := []rune(".test.name") 229 | q := NewQuery(v) 230 | assert.Equal([][]rune{ 231 | []rune("test"), 232 | []rune("name"), 233 | }, q.GetKeywords()) 234 | 235 | v = []rune("") 236 | q = NewQuery(v) 237 | assert.Equal([][]rune{}, q.GetKeywords()) 238 | 239 | v = []rune(".test.name.") 240 | q = NewQuery(v) 241 | assert.Equal([][]rune{ 242 | []rune("test"), 243 | []rune("name"), 244 | []rune(""), 245 | }, q.GetKeywords()) 246 | 247 | v = []rune(".hello") 248 | q = NewQuery(v) 249 | assert.Equal(q.GetKeywords(), [][]rune{ 250 | []rune("hello"), 251 | }) 252 | 253 | v = []rune(".hello.") 254 | q = NewQuery(v) 255 | assert.Equal(q.GetKeywords(), [][]rune{ 256 | []rune("hello"), 257 | []rune(""), 258 | }) 259 | 260 | v = []rune(".hello[") 261 | q = NewQuery(v) 262 | assert.Equal(q.GetKeywords(), [][]rune{ 263 | []rune("hello"), 264 | []rune("["), 265 | }) 266 | 267 | v = []rune(".hello[12") 268 | q = NewQuery(v) 269 | assert.Equal(q.GetKeywords(), [][]rune{ 270 | []rune("hello"), 271 | []rune("[12"), 272 | }) 273 | 274 | v = []rune(".hello[0]") 275 | q = NewQuery(v) 276 | assert.Equal(q.GetKeywords(), [][]rune{ 277 | []rune("hello"), 278 | []rune("[0]"), 279 | }) 280 | 281 | v = []rune(".hello[13][0]") 282 | q = NewQuery(v) 283 | assert.Equal(q.GetKeywords(), [][]rune{ 284 | []rune("hello"), 285 | []rune("[13]"), 286 | []rune("[0]"), 287 | }) 288 | 289 | v = []rune(".[3][23].hello[13][0]") 290 | q = NewQuery(v) 291 | assert.Equal(q.GetKeywords(), [][]rune{ 292 | []rune("[3]"), 293 | []rune("[23]"), 294 | []rune("hello"), 295 | []rune("[13]"), 296 | []rune("[0]"), 297 | }) 298 | 299 | } 300 | func TestGetKeywordsWithDots(t *testing.T) { 301 | var assert = assert.New(t) 302 | 303 | v := []rune(`.test.\"na.me\"`) 304 | q := NewQuery(v) 305 | assert.Equal([][]rune{ 306 | []rune("test"), 307 | []rune("na.me"), 308 | }, q.GetKeywords()) 309 | 310 | v = []rune(`.test.\"na.me\`) 311 | q = NewQuery(v) 312 | assert.Equal([][]rune{ 313 | []rune("test"), 314 | []rune("na"), 315 | []rune(`me\`), 316 | }, q.GetKeywords()) 317 | 318 | } 319 | 320 | func TestGetLastKeyword(t *testing.T) { 321 | var assert = assert.New(t) 322 | 323 | v := []rune(".test.name") 324 | q := NewQuery(v) 325 | assert.Equal(q.GetLastKeyword(), []rune("name")) 326 | 327 | v = []rune(".test.") 328 | q = NewQuery(v) 329 | assert.Equal(q.GetLastKeyword(), []rune("")) 330 | 331 | v = []rune(".test") 332 | q = NewQuery(v) 333 | assert.Equal(q.GetLastKeyword(), []rune("test")) 334 | 335 | v = []rune("") 336 | q = NewQuery(v) 337 | assert.Equal(q.GetLastKeyword(), []rune("")) 338 | } 339 | 340 | func TestStringGetLastKeyword(t *testing.T) { 341 | var assert = assert.New(t) 342 | 343 | v := []rune(".test.name") 344 | q := NewQuery(v) 345 | assert.Equal(q.StringGetLastKeyword(), "name") 346 | 347 | v = []rune(".test.") 348 | q = NewQuery(v) 349 | assert.Equal(q.StringGetLastKeyword(), "") 350 | 351 | v = []rune(".test") 352 | q = NewQuery(v) 353 | assert.Equal(q.StringGetLastKeyword(), "test") 354 | 355 | v = []rune("") 356 | q = NewQuery(v) 357 | assert.Equal(q.StringGetLastKeyword(), "") 358 | } 359 | 360 | func TestPopKeyword(t *testing.T) { 361 | var assert = assert.New(t) 362 | 363 | v := []rune(".test.name") 364 | q := NewQuery(v) 365 | k, query := q.PopKeyword() 366 | assert.Equal([]rune("name"), k) 367 | assert.Equal([]rune(".test"), query) 368 | assert.Equal([]rune(".test"), q.Get()) 369 | 370 | v = []rune(".a[0") 371 | q = NewQuery(v) 372 | k, query = q.PopKeyword() 373 | assert.Equal([]rune("[0"), k) 374 | assert.Equal([]rune(".a"), query) 375 | assert.Equal([]rune(".a"), q.Get()) 376 | 377 | k, query = q.PopKeyword() 378 | assert.Equal([]rune("a"), k) 379 | assert.Equal([]rune(""), query) 380 | assert.Equal([]rune(""), q.Get()) 381 | 382 | v = []rune(".") 383 | q = NewQuery(v) 384 | k, query = q.PopKeyword() 385 | assert.Equal([]rune(""), k) 386 | assert.Equal([]rune(""), query) 387 | assert.Equal([]rune(""), q.Get()) 388 | 389 | v = []rune(".test.name.") 390 | q = NewQuery(v) 391 | k, query = q.PopKeyword() 392 | assert.Equal([]rune(""), k) 393 | assert.Equal([]rune(".test.name"), query) 394 | assert.Equal([]rune(".test.name"), q.Get()) 395 | 396 | v = []rune(`.name.\"te.st\"`) 397 | q = NewQuery(v) 398 | k, query = q.PopKeyword() 399 | assert.Equal([]rune("te.st"), k) 400 | assert.Equal([]rune(".name"), query) 401 | assert.Equal([]rune(".name"), q.Get()) 402 | 403 | v = []rune(`.name.\"te.st\".hoge`) 404 | q = NewQuery(v) 405 | k, query = q.PopKeyword() 406 | assert.Equal([]rune("hoge"), k) 407 | assert.Equal([]rune(`.name.\"te.st\"`), query) 408 | assert.Equal([]rune(`.name.\"te.st\"`), q.Get()) 409 | 410 | v = []rune(`.name.\"te`) 411 | q = NewQuery(v) 412 | k, query = q.PopKeyword() 413 | assert.Equal([]rune(`te`), k) 414 | assert.Equal([]rune(`.name`), query) 415 | assert.Equal([]rune(`.name`), q.Get()) 416 | } 417 | 418 | func TestQueryStringGet(t *testing.T) { 419 | var assert = assert.New(t) 420 | 421 | v := []rune(".test") 422 | q := NewQuery(v) 423 | 424 | assert.Equal(q.StringGet(), ".test") 425 | } 426 | 427 | func TestQueryStringSet(t *testing.T) { 428 | var assert = assert.New(t) 429 | 430 | v := []rune(".hello") 431 | q := NewQuery(v) 432 | 433 | assert.Equal(q.StringSet(".world"), ".world") 434 | } 435 | 436 | func TestQueryStringAdd(t *testing.T) { 437 | var assert = assert.New(t) 438 | 439 | v := []rune(".hello") 440 | q := NewQuery(v) 441 | 442 | assert.Equal(q.StringAdd("world"), ".helloworld") 443 | } 444 | 445 | func TestStringGetKeywords(t *testing.T) { 446 | var assert = assert.New(t) 447 | 448 | v := []rune(".test.name") 449 | q := NewQuery(v) 450 | assert.Equal(q.StringGetKeywords(), []string{ 451 | "test", 452 | "name", 453 | }) 454 | 455 | v = []rune(".test.name") 456 | q = NewQuery(v) 457 | assert.Equal(q.StringGetKeywords(), []string{ 458 | "test", 459 | "name", 460 | }) 461 | 462 | v = []rune("") 463 | q = NewQuery(v) 464 | kws := q.StringGetKeywords() 465 | assert.Equal([]string(nil), kws) 466 | assert.Equal(0, len(kws)) 467 | } 468 | 469 | func TestStringPopKeyword(t *testing.T) { 470 | var assert = assert.New(t) 471 | 472 | v := []rune(".test.name") 473 | q := NewQuery(v) 474 | k, query := q.StringPopKeyword() 475 | assert.Equal(k, "name") 476 | assert.Equal(query, []rune(".test")) 477 | assert.Equal(q.Get(), []rune(".test")) 478 | 479 | v = []rune(".test.name.") 480 | q = NewQuery(v) 481 | k, query = q.StringPopKeyword() 482 | assert.Equal(k, "") 483 | assert.Equal(query, []rune(".test.name")) 484 | assert.Equal(q.Get(), []rune(".test.name")) 485 | 486 | v = []rune(".test.name[23]") 487 | q = NewQuery(v) 488 | k, query = q.StringPopKeyword() 489 | assert.Equal(k, "[23]") 490 | assert.Equal(query, []rune(".test.name")) 491 | assert.Equal(q.Get(), []rune(".test.name")) 492 | } 493 | -------------------------------------------------------------------------------- /suggestion.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strings" 7 | 8 | simplejson "github.com/bitly/go-simplejson" 9 | ) 10 | 11 | type SuggestionInterface interface { 12 | Get(json *simplejson.Json, keyword string) []string 13 | GetCandidateKeys(json *simplejson.Json, keyword string) []string 14 | } 15 | 16 | type SuggestionDataType int 17 | 18 | const ( 19 | UNKNOWN SuggestionDataType = iota 20 | ARRAY 21 | MAP 22 | NUMBER 23 | STRING 24 | BOOL 25 | ) 26 | 27 | type Suggestion struct { 28 | } 29 | 30 | func NewSuggestion() *Suggestion { 31 | return &Suggestion{} 32 | } 33 | 34 | func (s *Suggestion) Get(json *simplejson.Json, keyword string) []string { 35 | var completion string 36 | var suggestion string 37 | 38 | if a, err := json.Array(); err == nil { 39 | if len(a) > 1 { 40 | kw := regexp.MustCompile(`\[([0-9]+)?\]?`).FindString(keyword) 41 | if kw == "" { 42 | return []string{"[", "["} 43 | } else if kw == "[" { 44 | return []string{"", "["} 45 | } 46 | return []string{strings.Replace(kw+"]", kw, "", -1), kw + "]"} 47 | } 48 | return []string{strings.Replace(`[0]`, keyword, "", -1), `[0]`} 49 | } 50 | 51 | candidateKeys := s.GetCandidateKeys(json, keyword) 52 | 53 | if keyword == "" { 54 | if l := len(candidateKeys); l > 1 { 55 | return []string{"", ""} 56 | } else if l == 1 { 57 | return []string{candidateKeys[0], candidateKeys[0]} 58 | } 59 | } 60 | 61 | for _, key := range candidateKeys { 62 | // first 63 | if suggestion == "" && key != "" { 64 | suggestion = key 65 | } else { 66 | axis := suggestion 67 | if len(suggestion) > len(key) { 68 | axis = key 69 | } 70 | max := 0 71 | for i, _ := range axis { 72 | if suggestion[i] != key[i] { 73 | break 74 | } 75 | max = i 76 | } 77 | if max == 0 { 78 | suggestion = "" 79 | break 80 | } 81 | suggestion = suggestion[0 : max+1] 82 | } 83 | } 84 | if reg, err := regexp.Compile("(?i)^" + keyword); err == nil { 85 | completion = reg.ReplaceAllString(suggestion, "") 86 | } 87 | return []string{completion, suggestion} 88 | } 89 | 90 | func (s *Suggestion) GetCandidateKeys(json *simplejson.Json, keyword string) []string { 91 | candidates := []string{} 92 | 93 | if _, err := json.Array(); err == nil { 94 | return []string{} 95 | } 96 | 97 | if keyword == "" { 98 | return getCurrentKeys(json) 99 | } 100 | 101 | reg, err := regexp.Compile(`(?i)^(\\")?` + keyword + `(\\")?`) 102 | if err != nil { 103 | return []string{} 104 | } 105 | for _, key := range getCurrentKeys(json) { 106 | if reg.MatchString(key) { 107 | candidates = append(candidates, key) 108 | } 109 | } 110 | return candidates 111 | } 112 | 113 | func getCurrentKeys(json *simplejson.Json) []string { 114 | 115 | kk := []string{} 116 | m, err := json.Map() 117 | 118 | if err != nil { 119 | return kk 120 | } 121 | for k := range m { 122 | kk = append(kk, k) 123 | } 124 | sort.Strings(kk) 125 | 126 | keys := []string{} 127 | for _, k := range kk { 128 | if strings.Contains(k, ".") { 129 | var sb strings.Builder 130 | sb.Grow(len(k) + 4) 131 | sb.WriteString(`\"`) 132 | sb.WriteString(k) 133 | sb.WriteString(`\"`) 134 | k = sb.String() 135 | } 136 | keys = append(keys, k) 137 | } 138 | return keys 139 | } 140 | 141 | func (s *Suggestion) GetCurrentType(json *simplejson.Json) SuggestionDataType { 142 | if _, err := json.Array(); err == nil { 143 | return ARRAY 144 | } else if _, err = json.Map(); err == nil { 145 | return MAP 146 | } else if _, err = json.String(); err == nil { 147 | return STRING 148 | } 149 | return UNKNOWN 150 | } 151 | -------------------------------------------------------------------------------- /suggestion_test.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | simplejson "github.com/bitly/go-simplejson" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewSuggestion(t *testing.T) { 13 | var assert = assert.New(t) 14 | assert.Equal(NewSuggestion(), &Suggestion{}) 15 | } 16 | 17 | func TestSuggestionGet(t *testing.T) { 18 | var assert = assert.New(t) 19 | j := createJson(`{"name":"simeji-github"}`) 20 | s := NewSuggestion() 21 | 22 | j = createJson(`{"name":"simeji-github", "naming":"simeji", "nickname":"simejisimeji"}`) 23 | assert.Equal([]string{"m", "nam"}, s.Get(j, "na")) 24 | 25 | j = createJson(`{"abcde":"simeji-github", "abcdef":"simeji", "ab":"simejisimeji"}`) 26 | assert.Equal([]string{"", ""}, s.Get(j, "")) 27 | assert.Equal([]string{"b", "ab"}, s.Get(j, "a")) 28 | assert.Equal([]string{"de", "abcde"}, s.Get(j, "abc")) 29 | assert.Equal([]string{"", "abcde"}, s.Get(j, "abcde")) 30 | 31 | j = createJson(`["zero"]`) 32 | assert.Equal([]string{"[0]", "[0]"}, s.Get(j, "")) 33 | assert.Equal([]string{"0]", "[0]"}, s.Get(j, "[")) 34 | assert.Equal([]string{"]", "[0]"}, s.Get(j, "[0")) 35 | 36 | j = createJson(`["zero", "one"]`) 37 | assert.Equal([]string{"[", "["}, s.Get(j, "")) 38 | assert.Equal([]string{"", "["}, s.Get(j, "[")) 39 | assert.Equal([]string{"]", "[0]"}, s.Get(j, "[0")) 40 | 41 | j = createJson(`{"Abcabc":"simeji-github", "Abcdef":"simeji"}`) 42 | assert.Equal([]string{"bc", "Abc"}, s.Get(j, "a")) 43 | assert.Equal([]string{"c", "Abc"}, s.Get(j, "ab")) 44 | 45 | j = createJson(`{"RootDeviceNames":"simeji-github", "RootDeviceType":"simeji"}`) 46 | assert.Equal([]string{"ootDevice", "RootDevice"}, s.Get(j, "r")) 47 | assert.Equal([]string{"ootDevice", "RootDevice"}, s.Get(j, "R")) 48 | } 49 | 50 | func TestSuggestionGetCurrentType(t *testing.T) { 51 | var assert = assert.New(t) 52 | s := NewSuggestion() 53 | 54 | j := createJson(`[1,2,3]`) 55 | assert.Equal(ARRAY, s.GetCurrentType(j)) 56 | j = createJson(`{"name":[1,2,3], "naming":{"account":"simeji"}, "test":"simeji", "testing":"ok"}`) 57 | assert.Equal(MAP, s.GetCurrentType(j)) 58 | j = createJson(`"name"`) 59 | assert.Equal(STRING, s.GetCurrentType(j)) 60 | j = createJson("1") 61 | assert.Equal(UNKNOWN, s.GetCurrentType(j)) 62 | } 63 | 64 | func TestSuggestionGetCandidateKeys(t *testing.T) { 65 | var assert = assert.New(t) 66 | j := createJson(`{"naming":"simeji", "nickname":"simejisimeji", "city":"tokyo", "name":"simeji-github" }`) 67 | s := NewSuggestion() 68 | 69 | assert.Equal([]string{"city", "name", "naming", "nickname"}, s.GetCandidateKeys(j, "")) 70 | assert.Equal([]string{"name", "naming", "nickname"}, s.GetCandidateKeys(j, "n")) 71 | assert.Equal([]string{"name", "naming"}, s.GetCandidateKeys(j, "na")) 72 | assert.Equal([]string{}, s.GetCandidateKeys(j, "nana")) 73 | 74 | j = createJson(`{"abcde":"simeji-github", "abcdef":"simeji", "ab":"simejisimeji"}`) 75 | assert.Equal([]string{"abcde", "abcdef"}, s.GetCandidateKeys(j, "abcde")) 76 | 77 | j = createJson(`{"name":"simeji-github"}`) 78 | assert.Equal([]string{"name"}, s.GetCandidateKeys(j, "")) 79 | 80 | j = createJson(`{"n":"simeji-github"}`) 81 | assert.Equal([]string{"n"}, s.GetCandidateKeys(j, "")) 82 | 83 | j = createJson(`[1,2,"aa"]`) 84 | s = NewSuggestion() 85 | assert.Equal([]string{}, s.GetCandidateKeys(j, "[")) 86 | } 87 | func TestSuggestionGetCandidateKeysWithDots(t *testing.T) { 88 | var assert = assert.New(t) 89 | j := createJson(`{"nam.ing":"simeji", "nickname":"simejisimeji", "city":"tokyo", "name":"simeji-github" }`) 90 | s := NewSuggestion() 91 | 92 | assert.Equal([]string{"city", `\"nam.ing\"`, "name", "nickname"}, s.GetCandidateKeys(j, "")) 93 | assert.Equal([]string{`\"nam.ing\"`, "name", "nickname"}, s.GetCandidateKeys(j, "n")) 94 | } 95 | 96 | func createJson(s string) *simplejson.Json { 97 | r := bytes.NewBufferString(s) 98 | buf, _ := io.ReadAll(r) 99 | j, _ := simplejson.NewJson(buf) 100 | return j 101 | } 102 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | package jid 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "regexp" 7 | "strings" 8 | 9 | runewidth "github.com/mattn/go-runewidth" 10 | termbox "github.com/nsf/termbox-go" 11 | "github.com/nwidger/jsoncolor" 12 | ) 13 | 14 | type Terminal struct { 15 | defaultY int 16 | prompt string 17 | formatter *jsoncolor.Formatter 18 | monochrome bool 19 | outputArea *[][]termbox.Cell 20 | } 21 | 22 | type TerminalDrawAttributes struct { 23 | Query string 24 | Contents []string 25 | CandidateIndex int 26 | ContentsOffsetY int 27 | Complete string 28 | Candidates []string 29 | CursorOffset int 30 | } 31 | 32 | func NewTerminal(prompt string, defaultY int, monochrome bool) *Terminal { 33 | t := &Terminal{ 34 | prompt: prompt, 35 | defaultY: defaultY, 36 | monochrome: monochrome, 37 | outputArea: &[][]termbox.Cell{}, 38 | formatter: nil, 39 | } 40 | if !monochrome { 41 | t.formatter = t.initColorizeFormatter() 42 | } 43 | return t 44 | } 45 | 46 | func (t *Terminal) Draw(attr *TerminalDrawAttributes) error { 47 | 48 | query := attr.Query 49 | complete := attr.Complete 50 | rows := attr.Contents 51 | candidates := attr.Candidates 52 | candidateidx := attr.CandidateIndex 53 | contentOffsetY := attr.ContentsOffsetY 54 | 55 | termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) 56 | 57 | y := t.defaultY 58 | _, h := termbox.Size() 59 | 60 | t.drawFilterLine(query, complete) 61 | 62 | if len(candidates) > 0 { 63 | y = t.drawCandidates(0, t.defaultY, candidateidx, candidates) 64 | } 65 | 66 | cellsArr, err := t.rowsToCells(rows) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | for idx, cells := range cellsArr { 72 | i := idx - contentOffsetY 73 | if i >= 0 { 74 | t.drawCells(0, i+y, cells) 75 | } 76 | if i > h { 77 | break 78 | } 79 | } 80 | 81 | termbox.SetCursor(len(t.prompt)+attr.CursorOffset, 0) 82 | 83 | termbox.Flush() 84 | return nil 85 | } 86 | 87 | func (t *Terminal) drawFilterLine(qs string, complete string) error { 88 | fs := t.prompt + qs 89 | cs := complete 90 | str := fs + cs 91 | 92 | color := termbox.ColorDefault 93 | backgroundColor := termbox.ColorDefault 94 | 95 | var cells []termbox.Cell 96 | match := []int{len(fs), len(fs + cs)} 97 | 98 | var c termbox.Attribute 99 | for i, s := range str { 100 | c = color 101 | if i >= match[0] && i < match[1] { 102 | c = termbox.ColorGreen 103 | } 104 | cells = append(cells, termbox.Cell{ 105 | Ch: s, 106 | Fg: c, 107 | Bg: backgroundColor, 108 | }) 109 | } 110 | t.drawCells(0, 0, cells) 111 | return nil 112 | } 113 | 114 | type termboxSprintfFuncer struct { 115 | fg termbox.Attribute 116 | bg termbox.Attribute 117 | outputArea *[][]termbox.Cell 118 | } 119 | 120 | func (tsf *termboxSprintfFuncer) SprintfFunc() func(format string, a ...interface{}) string { 121 | return func(format string, a ...interface{}) string { 122 | cells := tsf.outputArea 123 | idx := len(*cells) - 1 124 | str := fmt.Sprintf(format, a...) 125 | for _, s := range str { 126 | if s == '\n' { 127 | *cells = append(*cells, []termbox.Cell{}) 128 | idx++ 129 | continue 130 | } 131 | (*cells)[idx] = append((*cells)[idx], termbox.Cell{ 132 | Ch: s, 133 | Fg: tsf.fg, 134 | Bg: tsf.bg, 135 | }) 136 | } 137 | return "dummy" 138 | } 139 | } 140 | 141 | func (t *Terminal) initColorizeFormatter() *jsoncolor.Formatter { 142 | formatter := jsoncolor.NewFormatter() 143 | 144 | regular := &termboxSprintfFuncer{ 145 | fg: termbox.ColorDefault, 146 | bg: termbox.ColorDefault, 147 | outputArea: t.outputArea, 148 | } 149 | 150 | bold := &termboxSprintfFuncer{ 151 | fg: termbox.AttrBold, 152 | bg: termbox.ColorDefault, 153 | outputArea: t.outputArea, 154 | } 155 | 156 | blueBold := &termboxSprintfFuncer{ 157 | fg: termbox.ColorBlue | termbox.AttrBold, 158 | bg: termbox.ColorDefault, 159 | outputArea: t.outputArea, 160 | } 161 | 162 | green := &termboxSprintfFuncer{ 163 | fg: termbox.ColorGreen, 164 | bg: termbox.ColorDefault, 165 | outputArea: t.outputArea, 166 | } 167 | 168 | blackBold := &termboxSprintfFuncer{ 169 | fg: termbox.ColorBlack | termbox.AttrBold, 170 | bg: termbox.ColorDefault, 171 | outputArea: t.outputArea, 172 | } 173 | 174 | formatter.SpaceColor = regular 175 | formatter.CommaColor = bold 176 | formatter.ColonColor = bold 177 | formatter.ObjectColor = bold 178 | formatter.ArrayColor = bold 179 | formatter.FieldQuoteColor = blueBold 180 | formatter.FieldColor = blueBold 181 | formatter.StringQuoteColor = green 182 | formatter.StringColor = green 183 | formatter.TrueColor = regular 184 | formatter.FalseColor = regular 185 | formatter.NumberColor = regular 186 | formatter.NullColor = blackBold 187 | 188 | return formatter 189 | } 190 | 191 | func (t *Terminal) rowsToCells(rows []string) ([][]termbox.Cell, error) { 192 | *t.outputArea = [][]termbox.Cell{[]termbox.Cell{}} 193 | 194 | var err error 195 | 196 | if t.formatter != nil { 197 | err = t.formatter.Format(io.Discard, []byte(strings.Join(rows, "\n"))) 198 | } 199 | 200 | cells := *t.outputArea 201 | 202 | if err != nil || t.monochrome { 203 | cells = [][]termbox.Cell{} 204 | for _, row := range rows { 205 | var cls []termbox.Cell 206 | for _, char := range row { 207 | cls = append(cls, termbox.Cell{ 208 | Ch: char, 209 | Fg: termbox.ColorDefault, 210 | Bg: termbox.ColorDefault, 211 | }) 212 | } 213 | cells = append(cells, cls) 214 | } 215 | } 216 | 217 | return cells, nil 218 | } 219 | 220 | func (t *Terminal) drawCells(x int, y int, cells []termbox.Cell) { 221 | i := 0 222 | for _, c := range cells { 223 | termbox.SetCell(x+i, y, c.Ch, c.Fg, c.Bg) 224 | 225 | w := runewidth.RuneWidth(c.Ch) 226 | if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(c.Ch) { 227 | w = 1 228 | } 229 | 230 | i += w 231 | } 232 | } 233 | 234 | func (t *Terminal) drawCandidates(x int, y int, index int, candidates []string) int { 235 | color := termbox.ColorBlack 236 | backgroundColor := termbox.ColorWhite 237 | 238 | w, _ := termbox.Size() 239 | 240 | ss := candidates[index] 241 | re := regexp.MustCompile("[[:space:]]" + regexp.QuoteMeta(ss) + "[[:space:]]") 242 | 243 | var rows []string 244 | var str string 245 | for _, word := range candidates { 246 | combine := " " 247 | if l := len(str); l+len(word)+1 >= w { 248 | rows = append(rows, str+" ") 249 | str = "" 250 | } 251 | str += combine + word 252 | } 253 | rows = append(rows, str+" ") 254 | 255 | for i, row := range rows { 256 | match := re.FindStringIndex(row) 257 | var c termbox.Attribute 258 | ii := 0 259 | for k, s := range row { 260 | c = color 261 | backgroundColor = termbox.ColorMagenta 262 | if match != nil && k >= match[0]+1 && k < match[1]-1 { 263 | backgroundColor = termbox.ColorWhite 264 | } 265 | termbox.SetCell(x+ii, y+i, s, c, backgroundColor) 266 | w := runewidth.RuneWidth(s) 267 | if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(s) { 268 | w = 1 269 | } 270 | ii += w 271 | } 272 | } 273 | return y + len(rows) 274 | } 275 | --------------------------------------------------------------------------------