chore: fix lint config and docs

This commit is contained in:
Peter Steinberger 2026-01-02 17:34:55 +01:00
parent 70d4a6aecb
commit 56400a603f
10 changed files with 63 additions and 20 deletions

3
.gitignore vendored
View File

@ -7,6 +7,8 @@
*.dll
*.so
*.dylib
*.a
*.o
# Test binary, built with `go test -c`
*.test
@ -41,6 +43,7 @@ go.work.sum
.LSOverride
# Local build output
goplaces
bin/
dist/
tmp/

View File

@ -1,3 +1,5 @@
version: "2"
run:
timeout: 5m
tests: true
@ -5,27 +7,30 @@ run:
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- gocritic
- goconst
- gofmt
- gofumpt
- goimports
- misspell
- prealloc
- revive
- unparam
linters-settings:
formatters:
enable:
- gofmt
- gofumpt
- goimports
formatters-settings:
gofumpt:
extra-rules: true
goimports:
local-prefixes: github.com/steipete/goplaces
linters-settings:
revive:
rules:
- name: early-return

View File

@ -1,3 +1,4 @@
// Package goplaces provides a Go client for the Google Places API (New).
package goplaces
import (
@ -12,6 +13,7 @@ import (
"time"
)
// DefaultBaseURL is the default endpoint for the Places API (New).
const DefaultBaseURL = "https://places.googleapis.com/v1"
const (
@ -51,12 +53,14 @@ var enumToPriceLevel = map[string]int{
priceLevelVeryExp: 4,
}
// Client wraps access to the Google Places API.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
// Options configures the Places client.
type Options struct {
APIKey string
BaseURL string
@ -64,6 +68,7 @@ type Options struct {
Timeout time.Duration
}
// NewClient builds a client with sane defaults.
func NewClient(opts Options) *Client {
baseURL := strings.TrimRight(opts.BaseURL, "/")
if baseURL == "" {
@ -86,6 +91,7 @@ func NewClient(opts Options) *Client {
}
}
// Search performs a text search with optional filters.
func (c *Client) Search(ctx context.Context, req SearchRequest) (SearchResponse, error) {
req = applySearchDefaults(req)
if err := validateSearchRequest(req); err != nil {
@ -114,6 +120,7 @@ func (c *Client) Search(ctx context.Context, req SearchRequest) (SearchResponse,
}, nil
}
// Details fetches details for a specific place ID.
func (c *Client) Details(ctx context.Context, placeID string) (PlaceDetails, error) {
placeID = strings.TrimSpace(placeID)
if placeID == "" {
@ -134,6 +141,7 @@ func (c *Client) Details(ctx context.Context, placeID string) (PlaceDetails, err
return mapPlaceDetails(place), nil
}
// Resolve converts a free-form location string into candidate places.
func (c *Client) Resolve(ctx context.Context, req LocationResolveRequest) (LocationResolveResponse, error) {
req = applyResolveDefaults(req)
if err := validateResolveRequest(req); err != nil {
@ -197,7 +205,9 @@ func (c *Client) doRequest(
if err != nil {
return nil, fmt.Errorf("goplaces: request failed: %w", err)
}
defer response.Body.Close()
defer func() {
_ = response.Body.Close()
}()
payload, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {

View File

@ -111,7 +111,7 @@ func TestSearchSuccess(t *testing.T) {
}
func TestSearchHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("bad"))
}))
@ -129,7 +129,7 @@ func TestSearchHTTPError(t *testing.T) {
}
func TestSearchInvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("not-json"))
}))
defer server.Close()

View File

@ -43,7 +43,7 @@ func TestRunSearchJSON(t *testing.T) {
}
func TestRunSearchHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"places": [{"id": "abc", "displayName": {"text": "Cafe"}}]}`))
}))
defer server.Close()
@ -95,7 +95,7 @@ func TestRunDetailsJSON(t *testing.T) {
}
func TestRunDetailsHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"id": "place-2", "displayName": {"text": "Park"}}`))
}))
defer server.Close()
@ -149,7 +149,7 @@ func TestRunResolveHuman(t *testing.T) {
}
func TestRunResolveJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"places": [{"id": "loc-2"}]}`))
}))
defer server.Close()

View File

@ -5,30 +5,37 @@ import (
"strings"
)
// Color renders ANSI color sequences.
type Color struct {
enabled bool
}
// NewColor returns a color helper, optionally disabled.
func NewColor(enabled bool) Color {
return Color{enabled: enabled}
}
// Bold wraps a string in bold ANSI codes.
func (c Color) Bold(value string) string {
return c.wrap("1", value)
}
// Cyan wraps a string in cyan ANSI codes.
func (c Color) Cyan(value string) string {
return c.wrap("36", value)
}
// Green wraps a string in green ANSI codes.
func (c Color) Green(value string) string {
return c.wrap("32", value)
}
// Yellow wraps a string in yellow ANSI codes.
func (c Color) Yellow(value string) string {
return c.wrap("33", value)
}
// Dim wraps a string in dim ANSI codes.
func (c Color) Dim(value string) string {
return c.wrap("2", value)
}

2
internal/cli/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package cli implements the goplaces command-line interface.
package cli

View File

@ -4,6 +4,7 @@ import (
"time"
)
// Root defines the CLI command tree.
type Root struct {
Global GlobalOptions `embed:""`
Search SearchCmd `cmd:"" help:"Search places by text query."`
@ -11,6 +12,7 @@ type Root struct {
Resolve ResolveCmd `cmd:"" help:"Resolve a location string to candidate places."`
}
// GlobalOptions are flags shared by all commands.
type GlobalOptions struct {
APIKey string `help:"Google Places API key." env:"GOOGLE_PLACES_API_KEY"`
BaseURL string `help:"Places API base URL." env:"GOOGLE_PLACES_BASE_URL" default:"https://places.googleapis.com/v1"`
@ -21,6 +23,7 @@ type GlobalOptions struct {
Version VersionFlag `name:"version" help:"Print version and exit."`
}
// SearchCmd runs text search queries.
type SearchCmd struct {
Query string `arg:"" name:"query" help:"Search text."`
Limit int `help:"Max results (1-20)." default:"10"`
@ -35,10 +38,12 @@ type SearchCmd struct {
RadiusM *float64 `help:"Radius in meters for location bias."`
}
// DetailsCmd fetches place details.
type DetailsCmd struct {
PlaceID string `arg:"" name:"place_id" help:"Place ID."`
}
// ResolveCmd resolves a location string into candidates.
type ResolveCmd struct {
LocationText string `arg:"" name:"location" help:"Location text to resolve."`
Limit int `help:"Max results (1-10)." default:"5"`

View File

@ -12,6 +12,7 @@ import (
"github.com/steipete/goplaces"
)
// App wires CLI output and API access.
type App struct {
client *goplaces.Client
out io.Writer
@ -20,6 +21,7 @@ type App struct {
color Color
}
// Run executes the CLI with the provided arguments.
func Run(args []string, stdout io.Writer, stderr io.Writer) int {
if stdout == nil {
stdout = os.Stdout
@ -44,7 +46,7 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
kong.Vars{"version": Version},
)
if err != nil {
fmt.Fprintln(stderr, err)
_, _ = fmt.Fprintln(stderr, err)
return 1
}
@ -55,10 +57,10 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
if err != nil {
if parseErr, ok := err.(*kong.ParseError); ok {
_ = parseErr.Context.PrintUsage(true)
fmt.Fprintln(stderr, parseErr.Error())
_, _ = fmt.Fprintln(stderr, parseErr.Error())
return parseErr.ExitCode()
}
fmt.Fprintln(stderr, err)
_, _ = fmt.Fprintln(stderr, err)
return 2
}
if root.Global.JSON {
@ -110,6 +112,7 @@ func parseWithExit(parser *kong.Kong, args []string, exitCode *int) (ctx *kong.C
return ctx, exited, err
}
// Run executes the search command.
func (c *SearchCmd) Run(app *App) error {
request := goplaces.SearchRequest{
Query: c.Query,
@ -167,6 +170,7 @@ func (c *SearchCmd) Run(app *App) error {
return err
}
// Run executes the details command.
func (c *DetailsCmd) Run(app *App) error {
response, err := app.client.Details(context.Background(), c.PlaceID)
if err != nil {
@ -181,6 +185,7 @@ func (c *DetailsCmd) Run(app *App) error {
return err
}
// Run executes the resolve command.
func (c *ResolveCmd) Run(app *App) error {
request := goplaces.LocationResolveRequest{
LocationText: c.LocationText,
@ -215,13 +220,13 @@ func handleError(writer io.Writer, err error) int {
}
var validation goplaces.ValidationError
if errors.As(err, &validation) {
fmt.Fprintln(writer, validation.Error())
_, _ = fmt.Fprintln(writer, validation.Error())
return 2
}
if errors.Is(err, goplaces.ErrMissingAPIKey) {
fmt.Fprintln(writer, err.Error())
_, _ = fmt.Fprintln(writer, err.Error())
return 2
}
fmt.Fprintln(writer, err.Error())
_, _ = fmt.Fprintln(writer, err.Error())
return 1
}

View File

@ -6,15 +6,21 @@ import (
"github.com/alecthomas/kong"
)
// Version is the CLI version string.
const Version = "0.1.0"
// VersionFlag prints the version and exits.
type VersionFlag string
// Decode is a no-op for the boolean version flag.
func (v VersionFlag) Decode(_ *kong.DecodeContext) error { return nil }
func (v VersionFlag) IsBool() bool { return true }
// IsBool marks the version flag as boolean.
func (v VersionFlag) IsBool() bool { return true }
// BeforeApply prints the version and exits.
func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
fmt.Fprintln(app.Stdout, vars["version"])
_, _ = fmt.Fprintln(app.Stdout, vars["version"])
app.Exit(0)
return nil
}