Skip to content

Commit 45b200d

Browse files
committed
feat(cli): add exists and unset commands
1 parent 729e025 commit 45b200d

4 files changed

Lines changed: 161 additions & 7 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ It provides direct terminal access to the chunk protocol for operational checks,
1616
- `info`
1717
- `auth`
1818
- `get`
19+
- `exists`
1920
- `set`
21+
- `unset`
2022
- `chunk`
2123
- `chunkbin`
2224
- `shell`
@@ -50,11 +52,19 @@ Default URI is `chunk://127.0.0.1:4242/`.
5052
chunk-cli ping
5153
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ info
5254
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ get 0 0
55+
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ exists 0 0
5356
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ set 0 0 10110011
57+
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ unset 0 0
5458
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunk 0 0
5559
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkbin 0 0
5660
```
5761

62+
Block-state note:
63+
64+
- `get <x> <y>` prints zero bits for an unset block
65+
- `exists <x> <y>` prints `1` when the block is explicitly present, `0` when it is unset
66+
- `set <x> <y> 000...0` is distinct from `unset <x> <y>`
67+
5868
## Interactive Shell
5969

6070
Start the interactive shell:
@@ -69,7 +79,9 @@ The shell prompt is `chunk>`. Supported shell commands:
6979
- `info`
7080
- `auth [token]`
7181
- `get <x> <y>`
82+
- `exists <x> <y>`
7283
- `set <x> <y> <bits>`
84+
- `unset <x> <y>`
7385
- `chunk <cx> <cy>`
7486
- `chunkbin [--out <file>] <cx> <cy>`
7587
- `quit`
@@ -80,10 +92,18 @@ Example session:
8092
```text
8193
chunk> ping
8294
PONG
95+
chunk> exists 0 0
96+
0
8397
chunk> set 0 0 1111000011110000
8498
OK
99+
chunk> exists 0 0
100+
1
85101
chunk> get 0 0
86102
1111000011110000
103+
chunk> unset 0 0
104+
OK
105+
chunk> get 0 0
106+
0000000000000000
87107
chunk> info
88108
chunkdb_version=1
89109
...
@@ -127,8 +147,12 @@ Auth behavior:
127147
- sends `AUTH <token>`, prints simple response
128148
- `get <x> <y>`
129149
- sends `GET`, prints block bit payload
150+
- `exists <x> <y>`
151+
- sends `EXISTS`, prints `1` when present and `0` when unset
130152
- `set <x> <y> <bits>`
131153
- sends `SET`; validates `bits` as binary (`0`/`1`) before request
154+
- `unset <x> <y>`
155+
- sends `UNSET`, clears explicit block presence, prints simple response
132156
- `chunk <cx> <cy>`
133157
- sends `CHUNK`, prints text chunk payload
134158
- `chunkbin [--out <file>] <cx> <cy>`

cmd/chunk-cli/main.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func main() {
5151
case "help", "-h", "--help":
5252
printUsage()
5353
return
54-
case "ping", "info", "auth", "get", "set", "chunk", "chunkbin", "shell":
54+
case "ping", "info", "auth", "get", "exists", "set", "unset", "chunk", "chunkbin", "shell":
5555
// network command
5656
default:
5757
fatal(fmt.Errorf("unknown command %q", cmd))
@@ -125,12 +125,24 @@ func main() {
125125
fatal(err)
126126
}
127127
printTextPayload(os.Stdout, payload)
128+
case "exists":
129+
text, err := runSimple(client, fmt.Sprintf("EXISTS %s %s", cmdArgs[0], cmdArgs[1]))
130+
if err != nil {
131+
fatal(err)
132+
}
133+
fmt.Println(text)
128134
case "set":
129135
text, err := runSimple(client, fmt.Sprintf("SET %s %s %s", cmdArgs[0], cmdArgs[1], cmdArgs[2]))
130136
if err != nil {
131137
fatal(err)
132138
}
133139
fmt.Println(text)
140+
case "unset":
141+
text, err := runSimple(client, fmt.Sprintf("UNSET %s %s", cmdArgs[0], cmdArgs[1]))
142+
if err != nil {
143+
fatal(err)
144+
}
145+
fmt.Println(text)
134146
case "chunk":
135147
cx, cy := cmdArgs[0], cmdArgs[1]
136148
payload, err := runBulk(client, fmt.Sprintf("CHUNK %s %s", cx, cy))
@@ -169,6 +181,16 @@ func validateCommandArgs(cmd string, cmdArgs []string) error {
169181
if err := validateIntArg(cmdArgs[1], "y"); err != nil {
170182
return err
171183
}
184+
case "exists":
185+
if len(cmdArgs) != 2 {
186+
return fmt.Errorf("usage: exists <x> <y>")
187+
}
188+
if err := validateIntArg(cmdArgs[0], "x"); err != nil {
189+
return err
190+
}
191+
if err := validateIntArg(cmdArgs[1], "y"); err != nil {
192+
return err
193+
}
172194
case "set":
173195
if len(cmdArgs) != 3 {
174196
return fmt.Errorf("usage: set <x> <y> <bits>")
@@ -185,6 +207,16 @@ func validateCommandArgs(cmd string, cmdArgs []string) error {
185207
if err := validateBits(cmdArgs[2]); err != nil {
186208
return err
187209
}
210+
case "unset":
211+
if len(cmdArgs) != 2 {
212+
return fmt.Errorf("usage: unset <x> <y>")
213+
}
214+
if err := validateIntArg(cmdArgs[0], "x"); err != nil {
215+
return err
216+
}
217+
if err := validateIntArg(cmdArgs[1], "y"); err != nil {
218+
return err
219+
}
188220
case "chunk":
189221
if len(cmdArgs) != 2 {
190222
return fmt.Errorf("usage: chunk <cx> <cy>")
@@ -355,6 +387,17 @@ func runShell(
355387
continue
356388
}
357389
printTextPayload(stdout, payload)
390+
case "exists":
391+
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
392+
fmt.Fprintf(stderr, "error: %v\n", err)
393+
continue
394+
}
395+
text, err := runSimple(client, fmt.Sprintf("EXISTS %s %s", cmdArgs[0], cmdArgs[1]))
396+
if err != nil {
397+
fmt.Fprintf(stderr, "error: %v\n", err)
398+
continue
399+
}
400+
fmt.Fprintln(stdout, text)
358401
case "set":
359402
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
360403
fmt.Fprintf(stderr, "error: %v\n", err)
@@ -366,6 +409,17 @@ func runShell(
366409
continue
367410
}
368411
fmt.Fprintln(stdout, text)
412+
case "unset":
413+
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
414+
fmt.Fprintf(stderr, "error: %v\n", err)
415+
continue
416+
}
417+
text, err := runSimple(client, fmt.Sprintf("UNSET %s %s", cmdArgs[0], cmdArgs[1]))
418+
if err != nil {
419+
fmt.Fprintf(stderr, "error: %v\n", err)
420+
continue
421+
}
422+
fmt.Fprintln(stdout, text)
369423
case "chunk":
370424
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
371425
fmt.Fprintf(stderr, "error: %v\n", err)
@@ -460,7 +514,9 @@ Commands:
460514
info
461515
auth <token>
462516
get <x> <y>
517+
exists <x> <y>
463518
set <x> <y> <bits>
519+
unset <x> <y>
464520
chunk <cx> <cy>
465521
chunkbin [--out <file>] <cx> <cy>
466522
shell
@@ -476,6 +532,7 @@ Global options:
476532
Examples:
477533
chunk-cli --uri chunk://token@127.0.0.1:4242/ ping
478534
chunk-cli --uri chunk://token@127.0.0.1:4242/ get 0 0
535+
chunk-cli --uri chunk://token@127.0.0.1:4242/ exists 0 0
479536
chunk-cli --uri chunk://token@127.0.0.1:4242/ shell
480537
chunk-cli --uri chunks://token@127.0.0.1:4242/ --tls-insecure info
481538
`)

cmd/chunk-cli/main_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ func TestValidateCommandArgs(t *testing.T) {
2626
{name: "ping extra", cmd: "ping", args: []string{"x"}, wantErr: true},
2727
{name: "get ok", cmd: "get", args: []string{"1", "2"}, wantErr: false},
2828
{name: "get bad int", cmd: "get", args: []string{"a", "2"}, wantErr: true},
29+
{name: "exists ok", cmd: "exists", args: []string{"1", "2"}, wantErr: false},
30+
{name: "exists bad int", cmd: "exists", args: []string{"a", "2"}, wantErr: true},
2931
{name: "set ok", cmd: "set", args: []string{"1", "2", "0101"}, wantErr: false},
3032
{name: "set bad bits", cmd: "set", args: []string{"1", "2", "01x1"}, wantErr: true},
33+
{name: "unset ok", cmd: "unset", args: []string{"1", "2"}, wantErr: false},
34+
{name: "unset bad int", cmd: "unset", args: []string{"1", "b"}, wantErr: true},
3135
{name: "chunk ok", cmd: "chunk", args: []string{"0", "0"}, wantErr: false},
3236
{name: "auth too many", cmd: "auth", args: []string{"a", "b"}, wantErr: true},
3337
{name: "chunkbin passthrough", cmd: "chunkbin", args: []string{"--out", "x", "1", "2"}, wantErr: false},

cmd/chunk-cli/shell_test.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type shellServerState struct {
2020
sawAuth bool
2121
commands []string
2222
blocks map[string]string
23+
present map[string]bool
2324
}
2425

2526
func (s *shellServerState) record(cmd string) {
@@ -37,7 +38,17 @@ func (s *shellServerState) setAuth() {
3738
func (s *shellServerState) setBlock(x string, y string, bits string) {
3839
s.mu.Lock()
3940
defer s.mu.Unlock()
40-
s.blocks[x+":"+y] = bits
41+
key := x + ":" + y
42+
s.blocks[key] = bits
43+
s.present[key] = true
44+
}
45+
46+
func (s *shellServerState) unsetBlock(x string, y string) {
47+
s.mu.Lock()
48+
defer s.mu.Unlock()
49+
key := x + ":" + y
50+
delete(s.present, key)
51+
s.blocks[key] = "0000"
4152
}
4253

4354
func (s *shellServerState) getBlock(x string, y string) string {
@@ -49,6 +60,12 @@ func (s *shellServerState) getBlock(x string, y string) string {
4960
return "0000"
5061
}
5162

63+
func (s *shellServerState) blockExists(x string, y string) bool {
64+
s.mu.Lock()
65+
defer s.mu.Unlock()
66+
return s.present[x+":"+y]
67+
}
68+
5269
func startShellTestServer(t *testing.T, token string) (string, *shellServerState, func()) {
5370
t.Helper()
5471

@@ -57,7 +74,10 @@ func startShellTestServer(t *testing.T, token string) (string, *shellServerState
5774
t.Fatalf("listen: %v", err)
5875
}
5976

60-
state := &shellServerState{blocks: make(map[string]string)}
77+
state := &shellServerState{
78+
blocks: make(map[string]string),
79+
present: make(map[string]bool),
80+
}
6181
done := make(chan struct{})
6282

6383
go func() {
@@ -138,6 +158,23 @@ func startShellTestServer(t *testing.T, token string) (string, *shellServerState
138158
if err := writeSimple(writer, "OK"); err != nil {
139159
return
140160
}
161+
case "UNSET":
162+
if !authed {
163+
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
164+
return
165+
}
166+
continue
167+
}
168+
if len(fields) != 3 {
169+
if err := writeError(writer, "INVALID_ARGUMENT UNSET requires 2 args"); err != nil {
170+
return
171+
}
172+
continue
173+
}
174+
state.unsetBlock(fields[1], fields[2])
175+
if err := writeSimple(writer, "OK"); err != nil {
176+
return
177+
}
141178
case "GET":
142179
if !authed {
143180
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
@@ -154,6 +191,28 @@ func startShellTestServer(t *testing.T, token string) (string, *shellServerState
154191
if err := writeBulk(writer, []byte(state.getBlock(fields[1], fields[2]))); err != nil {
155192
return
156193
}
194+
case "EXISTS":
195+
if !authed {
196+
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
197+
return
198+
}
199+
continue
200+
}
201+
if len(fields) != 3 {
202+
if err := writeError(writer, "INVALID_ARGUMENT EXISTS requires 2 args"); err != nil {
203+
return
204+
}
205+
continue
206+
}
207+
if state.blockExists(fields[1], fields[2]) {
208+
if err := writeSimple(writer, "1"); err != nil {
209+
return
210+
}
211+
} else {
212+
if err := writeSimple(writer, "0"); err != nil {
213+
return
214+
}
215+
}
157216
case "INFO":
158217
if !authed {
159218
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
@@ -241,7 +300,7 @@ func isBits(bits string) bool {
241300
return bits != ""
242301
}
243302

244-
func TestRunShellConnectAuthPingGetSetQuit(t *testing.T) {
303+
func TestRunShellConnectAuthPingExistsGetSetUnsetQuit(t *testing.T) {
245304
uri, state, stop := startShellTestServer(t, "dev-token")
246305
defer stop()
247306

@@ -256,7 +315,7 @@ func TestRunShellConnectAuthPingGetSetQuit(t *testing.T) {
256315
}
257316
defer client.Close()
258317

259-
input := strings.NewReader("ping\nset 1 2 1010\nget 1 2\nquit\n")
318+
input := strings.NewReader("ping\nexists 1 2\nset 1 2 1010\nexists 1 2\nget 1 2\nunset 1 2\nexists 1 2\nget 1 2\nquit\n")
260319
var out bytes.Buffer
261320
var errOut bytes.Buffer
262321

@@ -270,16 +329,26 @@ func TestRunShellConnectAuthPingGetSetQuit(t *testing.T) {
270329

271330
state.mu.Lock()
272331
sawAuth := state.sawAuth
332+
commands := append([]string(nil), state.commands...)
273333
state.mu.Unlock()
274334
if !sawAuth {
275335
t.Fatalf("expected shell auto-auth to run AUTH")
276336
}
337+
expectedCommands := []string{"AUTH", "PING", "EXISTS", "SET", "EXISTS", "GET", "UNSET", "EXISTS", "GET", "QUIT"}
338+
if len(commands) != len(expectedCommands) {
339+
t.Fatalf("unexpected command count: got %v want %v", commands, expectedCommands)
340+
}
341+
for i := range expectedCommands {
342+
if commands[i] != expectedCommands[i] {
343+
t.Fatalf("unexpected command sequence: got %v want %v", commands, expectedCommands)
344+
}
345+
}
277346

278347
output := out.String()
279-
if strings.Count(output, "chunk> ") < 4 {
348+
if strings.Count(output, "chunk> ") < 8 {
280349
t.Fatalf("expected repeated prompt, got %q", output)
281350
}
282-
for _, expected := range []string{"PONG", "OK", "1010", "BYE"} {
351+
for _, expected := range []string{"PONG", "chunk> 0\n", "chunk> 1\n", "1010", "0000", "BYE"} {
283352
if !strings.Contains(output, expected) {
284353
t.Fatalf("expected %q in output %q", expected, output)
285354
}

0 commit comments

Comments
 (0)