Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
fb660d880f fix: document json arrays + regressions (#2) (thanks @salmonumbrella)
Some checks failed
ci / test (push) Has been cancelled
2026-01-10 18:36:12 +01:00
salmonumbrella
04d3133126 fix(cli): output JSON arrays directly for jq compatibility
Change --json output to emit just the results array instead of
a wrapper object, enabling standard jq piping patterns like
`goplaces search "coffee" --json | jq '.[0]'`.

- search, nearby, resolve: output .Results array, write
  next_page_token to stderr if present
- autocomplete: output .Suggestions array
- details, photo, route: unchanged (single objects)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:35:35 +01:00
4 changed files with 148 additions and 16 deletions

View File

@ -1,5 +1,9 @@
# Changelog
## 0.2.1 - Unreleased
- CLI: JSON output for search/nearby/autocomplete/resolve now emits arrays; pagination token goes to stderr. (#2) — thanks @salmonumbrella
## 0.2.0 - 2026-01-02
- Autocomplete suggestions for places and queries (client + CLI).

View File

@ -139,10 +139,16 @@ Resolve:
goplaces resolve "Riverside Park, New York" --limit 5
```
JSON output:
JSON output (arrays for search/nearby/autocomplete/resolve):
```bash
goplaces search "sushi" --json
goplaces search "sushi" --json | jq '.[0]'
```
Search/nearby pagination tokens are printed to stderr to keep stdout clean:
```bash
goplaces search "sushi" --json 2>token.txt | jq '.[0]'
```
## Library

View File

@ -40,8 +40,12 @@ func TestRunSearchJSON(t *testing.T) {
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
if !strings.Contains(stdout.String(), "\"results\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if results[0]["place_id"] != "abc" {
t.Fatalf("unexpected result payload: %#v", results[0])
}
}
@ -115,8 +119,12 @@ func TestRunSearchWithFilters(t *testing.T) {
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
if !strings.Contains(stdout.String(), "\"results\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if results[0]["place_id"] != "abc" {
t.Fatalf("unexpected result payload: %#v", results[0])
}
}
@ -143,8 +151,15 @@ func TestRunAutocompleteJSON(t *testing.T) {
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), "\"suggestions\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
suggestions := decodeJSONArray(t, stdout.String())
if len(suggestions) != 1 {
t.Fatalf("expected 1 suggestion, got %d (stdout=%s)", len(suggestions), stdout.String())
}
if suggestions[0]["place_id"] != "abc" {
t.Fatalf("unexpected suggestion payload: %#v", suggestions[0])
}
}
@ -200,8 +215,15 @@ func TestRunNearbyJSON(t *testing.T) {
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"results\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if results[0]["place_id"] != "abc" {
t.Fatalf("unexpected result payload: %#v", results[0])
}
}
@ -555,8 +577,15 @@ func TestRunResolveJSON(t *testing.T) {
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"results\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if results[0]["place_id"] != "loc-2" {
t.Fatalf("unexpected result payload: %#v", results[0])
}
}
@ -633,6 +662,78 @@ func TestVersionFlagIsBool(t *testing.T) {
}
}
func TestRunSearchJSONWithNextPageToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}], "nextPageToken": "token123"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"search",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if strings.Contains(stdout.String(), "next_page_token") {
t.Fatalf("unexpected next_page_token in stdout: %s", stdout.String())
}
if !strings.Contains(stderr.String(), "next_page_token: token123") {
t.Fatalf("expected next_page_token in stderr, got: %s", stderr.String())
}
}
func TestRunNearbyJSONWithNextPageToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places:searchNearby" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}], "nextPageToken": "nearby-token"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"nearby",
"--lat", "1",
"--lng", "2",
"--radius-m", "3",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
results := decodeJSONArray(t, stdout.String())
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d (stdout=%s)", len(results), stdout.String())
}
if strings.Contains(stdout.String(), "next_page_token") {
t.Fatalf("unexpected next_page_token in stdout: %s", stdout.String())
}
if !strings.Contains(stderr.String(), "next_page_token: nearby-token") {
t.Fatalf("expected next_page_token in stderr, got: %s", stderr.String())
}
}
func TestWriteJSONError(t *testing.T) {
err := writeJSON(&bytes.Buffer{}, map[string]any{"bad": func() {}})
if err == nil {
@ -650,6 +751,15 @@ func TestWriteJSON(t *testing.T) {
}
}
func decodeJSONArray(t *testing.T, payload string) []map[string]any {
t.Helper()
var items []map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(payload)), &items); err != nil {
t.Fatalf("expected JSON array output, got error: %v (payload=%s)", err, payload)
}
return items
}
func TestHandleError(t *testing.T) {
if code := handleError(&bytes.Buffer{}, nil); code != 0 {
t.Fatalf("expected 0")

View File

@ -168,7 +168,13 @@ func (c *SearchCmd) Run(app *App) error {
}
if app.json {
return writeJSON(app.out, response)
if err := writeJSON(app.out, response.Results); err != nil {
return err
}
if response.NextPageToken != "" {
_, _ = fmt.Fprintln(app.err, "next_page_token:", response.NextPageToken)
}
return nil
}
_, err = fmt.Fprintln(app.out, renderSearch(app.color, response))
@ -202,7 +208,7 @@ func (c *AutocompleteCmd) Run(app *App) error {
}
if app.json {
return writeJSON(app.out, response)
return writeJSON(app.out, response.Suggestions)
}
_, err = fmt.Fprintln(app.out, renderAutocomplete(app.color, response))
@ -234,7 +240,13 @@ func (c *NearbyCmd) Run(app *App) error {
}
if app.json {
return writeJSON(app.out, response)
if err := writeJSON(app.out, response.Results); err != nil {
return err
}
if response.NextPageToken != "" {
_, _ = fmt.Fprintln(app.err, "next_page_token:", response.NextPageToken)
}
return nil
}
_, err = fmt.Fprintln(app.out, renderNearby(app.color, response))
@ -296,7 +308,7 @@ func (c *ResolveCmd) Run(app *App) error {
}
if app.json {
return writeJSON(app.out, response)
return writeJSON(app.out, response.Results)
}
_, err = fmt.Fprintln(app.out, renderResolve(app.color, response))