chore: fix lint config and docs
This commit is contained in:
parent
70d4a6aecb
commit
56400a603f
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
12
client.go
12
client.go
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
2
internal/cli/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package cli implements the goplaces command-line interface.
|
||||
package cli
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user