parent
7145bf2b1c
commit
522731953d
@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.1 - Unreleased
|
||||
## 0.4.0 - 2026-05-04
|
||||
|
||||
- Add `business_status` to search, nearby, details, JSON output, and human CLI output. (#8) - thanks @doomsday-rgb
|
||||
- Add drive-only `--avoid-tolls`, `--avoid-highways`, and `--avoid-ferries` direction flags backed by Routes API `routeModifiers`. (#7) - thanks @gabob23
|
||||
|
||||
## 0.3.0 - 2026-02-14
|
||||
|
||||
|
||||
14
README.md
14
README.md
@ -9,7 +9,7 @@ Modern Go client + CLI for the Google Places API (New). Fast for humans, tidy fo
|
||||
- Nearby search around a location restriction.
|
||||
- Place photos in details + photo media URLs.
|
||||
- Route search along a driving path (Routes API).
|
||||
- Directions between two points with distance, duration, and steps (Routes API).
|
||||
- Directions between two points with distance, duration, steps, and optional drive route modifiers (Routes API).
|
||||
- Location bias (lat/lng/radius) and pagination tokens.
|
||||
- Place details: hours, phone, website, rating, price, types.
|
||||
- Optional reviews in details (`--reviews` / `IncludeReviews`).
|
||||
@ -20,7 +20,7 @@ Modern Go client + CLI for the Google Places API (New). Fast for humans, tidy fo
|
||||
|
||||
## Install / Run
|
||||
|
||||
Latest release: v0.3.0 (2026-02-14).
|
||||
Latest release: v0.4.0 (2026-05-04).
|
||||
|
||||
- Homebrew: `brew install steipete/tap/goplaces`
|
||||
- Go: `go install github.com/steipete/goplaces/cmd/goplaces@latest`
|
||||
@ -129,6 +129,13 @@ goplaces directions --from "Pike Place Market" --to "Space Needle"
|
||||
goplaces directions --from-place-id <fromId> --to-place-id <toId> --compare drive --steps
|
||||
```
|
||||
|
||||
Driving route modifiers:
|
||||
|
||||
```bash
|
||||
goplaces directions --from "Paris" --to "Brest" --mode drive --avoid-tolls
|
||||
goplaces directions --from "Paris" --to "Brest" --mode drive --avoid-highways --avoid-ferries
|
||||
```
|
||||
|
||||
Units (default metric):
|
||||
|
||||
```bash
|
||||
@ -230,6 +237,8 @@ route, err := client.Route(ctx, goplaces.RouteRequest{
|
||||
- Reviews are returned only when `IncludeReviews`/`--reviews` is set.
|
||||
- Photos are returned only when `IncludePhotos`/`--photos` is set.
|
||||
- Route search requires the Google Routes API to be enabled.
|
||||
- `business_status` is returned for search, nearby, and details when Google includes it.
|
||||
- Direction route modifiers (`--avoid-tolls`, `--avoid-highways`, `--avoid-ferries`) require `--mode drive`.
|
||||
- Field masks are defined alongside each request (e.g. `search.go`, `details.go`, `autocomplete.go`).
|
||||
- The Places API is billed and quota-limited; keep an eye on your Cloud Console quotas.
|
||||
|
||||
@ -252,3 +261,4 @@ Optional env overrides:
|
||||
- Override the search text used in E2E: `GOOGLE_PLACES_E2E_QUERY`
|
||||
- Override language code for E2E: `GOOGLE_PLACES_E2E_LANGUAGE`
|
||||
- Override region code for E2E: `GOOGLE_PLACES_E2E_REGION`
|
||||
- Override directions endpoints/locations: `GOOGLE_DIRECTIONS_E2E_BASE_URL`, `GOOGLE_PLACES_E2E_DIRECTIONS_FROM`, `GOOGLE_PLACES_E2E_DIRECTIONS_TO`
|
||||
|
||||
@ -47,7 +47,8 @@ func TestSearchSuccess(t *testing.T) {
|
||||
"userRatingCount": 532,
|
||||
"priceLevel": "PRICE_LEVEL_MODERATE",
|
||||
"types": ["cafe"],
|
||||
"currentOpeningHours": {"openNow": true}
|
||||
"currentOpeningHours": {"openNow": true},
|
||||
"businessStatus": "OPERATIONAL"
|
||||
}
|
||||
],
|
||||
"nextPageToken": "next"
|
||||
@ -102,6 +103,9 @@ func TestSearchSuccess(t *testing.T) {
|
||||
if result.OpenNow == nil || *result.OpenNow != true {
|
||||
t.Fatalf("unexpected openNow: %#v", result.OpenNow)
|
||||
}
|
||||
if result.BusinessStatus != "OPERATIONAL" {
|
||||
t.Fatalf("unexpected business status: %s", result.BusinessStatus)
|
||||
}
|
||||
if response.NextPageToken != "next" {
|
||||
t.Fatalf("unexpected token: %s", response.NextPageToken)
|
||||
}
|
||||
@ -396,6 +400,7 @@ func TestDetailsSuccess(t *testing.T) {
|
||||
"types": ["park"],
|
||||
"regularOpeningHours": {"weekdayDescriptions": ["Mon: 9-5"]},
|
||||
"currentOpeningHours": {"openNow": false},
|
||||
"businessStatus": "CLOSED_TEMPORARILY",
|
||||
"nationalPhoneNumber": "+1 555",
|
||||
"websiteUri": "https://example.com"
|
||||
}`))
|
||||
@ -420,6 +425,9 @@ func TestDetailsSuccess(t *testing.T) {
|
||||
if place.OpenNow == nil || *place.OpenNow != false {
|
||||
t.Fatalf("unexpected openNow")
|
||||
}
|
||||
if place.BusinessStatus != "CLOSED_TEMPORARILY" {
|
||||
t.Fatalf("unexpected business status: %s", place.BusinessStatus)
|
||||
}
|
||||
if len(place.Hours) != 1 {
|
||||
t.Fatalf("unexpected hours")
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
detailsFieldMaskBase = "id,displayName,formattedAddress,location,rating,userRatingCount,priceLevel,types,regularOpeningHours,currentOpeningHours,nationalPhoneNumber,websiteUri"
|
||||
detailsFieldMaskBase = "id,displayName,formattedAddress,location,rating,userRatingCount,priceLevel,types,regularOpeningHours,currentOpeningHours,businessStatus,nationalPhoneNumber,websiteUri"
|
||||
detailsFieldMaskReview = "reviews"
|
||||
detailsFieldMaskPhotos = "photos"
|
||||
)
|
||||
@ -73,6 +73,7 @@ func mapPlaceDetails(place placeItem) PlaceDetails {
|
||||
Website: place.WebsiteURI,
|
||||
Hours: weekdayDescriptions(place.RegularOpeningHours),
|
||||
OpenNow: openNow(place.CurrentOpeningHours),
|
||||
BusinessStatus: strings.TrimSpace(place.BusinessStatus),
|
||||
Reviews: mapReviews(place.Reviews),
|
||||
Photos: mapPhotos(place.Photos),
|
||||
}
|
||||
|
||||
@ -40,16 +40,19 @@ var directionsUnits = map[string]struct{}{
|
||||
|
||||
// DirectionsRequest describes a directions query between two locations.
|
||||
type DirectionsRequest struct {
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
FromPlaceID string `json:"from_place_id,omitempty"`
|
||||
ToPlaceID string `json:"to_place_id,omitempty"`
|
||||
FromLocation *LatLng `json:"from_location,omitempty"`
|
||||
ToLocation *LatLng `json:"to_location,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
Units string `json:"units,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
FromPlaceID string `json:"from_place_id,omitempty"`
|
||||
ToPlaceID string `json:"to_place_id,omitempty"`
|
||||
FromLocation *LatLng `json:"from_location,omitempty"`
|
||||
ToLocation *LatLng `json:"to_location,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
Units string `json:"units,omitempty"`
|
||||
AvoidTolls bool `json:"avoid_tolls,omitempty"`
|
||||
AvoidHighways bool `json:"avoid_highways,omitempty"`
|
||||
AvoidFerries bool `json:"avoid_ferries,omitempty"`
|
||||
}
|
||||
|
||||
// DirectionsResponse contains a single route summary and steps.
|
||||
@ -164,6 +167,9 @@ func validateDirectionsRequest(req DirectionsRequest) error {
|
||||
return ValidationError{Field: "units", Message: "must be metric or imperial"}
|
||||
}
|
||||
}
|
||||
if (req.AvoidTolls || req.AvoidHighways || req.AvoidFerries) && req.Mode != directionsModeDrive {
|
||||
return ValidationError{Field: "route_modifiers", Message: "avoid tolls/highways/ferries require drive mode"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -290,6 +296,13 @@ func buildDirectionsBody(req DirectionsRequest) map[string]any {
|
||||
if strings.TrimSpace(req.Region) != "" {
|
||||
body["regionCode"] = strings.TrimSpace(req.Region)
|
||||
}
|
||||
if req.AvoidTolls || req.AvoidHighways || req.AvoidFerries {
|
||||
body["routeModifiers"] = map[string]any{
|
||||
"avoidTolls": req.AvoidTolls,
|
||||
"avoidHighways": req.AvoidHighways,
|
||||
"avoidFerries": req.AvoidFerries,
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
|
||||
@ -124,6 +124,14 @@ func TestDirectionsUnitsValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionsRouteModifiersRequireDrive(t *testing.T) {
|
||||
req := DirectionsRequest{From: "A", To: "B", Mode: "walk", AvoidTolls: true}
|
||||
err := validateDirectionsRequest(applyDirectionsDefaults(req))
|
||||
if err == nil || !strings.Contains(err.Error(), "route_modifiers") {
|
||||
t.Fatalf("expected route modifier validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionsLocationValidation(t *testing.T) {
|
||||
req := DirectionsRequest{FromPlaceID: "a", From: "b", To: "c"}
|
||||
if err := validateDirectionsRequest(applyDirectionsDefaults(req)); err == nil {
|
||||
@ -283,6 +291,49 @@ func TestDirectionsRequestLocaleAndImperialUnits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionsRequestRouteModifiers(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != routesPath {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
modifiers, ok := payload["routeModifiers"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing routeModifiers: %#v", payload)
|
||||
}
|
||||
if modifiers["avoidTolls"] != true || modifiers["avoidHighways"] != true || modifiers["avoidFerries"] != true {
|
||||
t.Fatalf("unexpected routeModifiers: %#v", modifiers)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{
|
||||
"routes":[{
|
||||
"legs":[{
|
||||
"distanceMeters":1609,
|
||||
"duration":"300s",
|
||||
"localizedValues":{"distance":{"text":"1 mi"},"duration":{"text":"5 mins"}},
|
||||
"steps":[]
|
||||
}]
|
||||
}]
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(Options{APIKey: "test-key", DirectionsBaseURL: server.URL})
|
||||
_, err := client.Directions(context.Background(), DirectionsRequest{
|
||||
From: "Seattle",
|
||||
To: "Portland",
|
||||
Mode: "drive",
|
||||
AvoidTolls: true,
|
||||
AvoidHighways: true,
|
||||
AvoidFerries: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Directions error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionsLocationBoundsValidation(t *testing.T) {
|
||||
req := DirectionsRequest{
|
||||
FromLocation: &LatLng{Lat: 91, Lng: 0},
|
||||
|
||||
@ -33,9 +33,17 @@ Imperial units:
|
||||
goplaces directions --from-place-id <fromId> --to-place-id <toId> --units imperial
|
||||
```
|
||||
|
||||
Driving route modifiers:
|
||||
|
||||
```bash
|
||||
goplaces directions --from "Paris" --to "Brest" --mode drive --avoid-tolls
|
||||
goplaces directions --from "Paris" --to "Brest" --mode drive --avoid-highways --avoid-ferries
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Default mode is walking.
|
||||
- Default units are metric (use `--units imperial` for miles/feet).
|
||||
- Use `--steps` for turn-by-turn instructions.
|
||||
- Use `--compare drive` to add a driving ETA.
|
||||
- Use `--avoid-tolls`, `--avoid-highways`, and `--avoid-ferries` with `--mode drive` to request drive routes that avoid those features when reasonable.
|
||||
|
||||
@ -34,3 +34,4 @@ response, err := client.NearbySearch(ctx, goplaces.NearbySearchRequest{
|
||||
|
||||
- Location restriction (lat/lng/radius) is required.
|
||||
- Use `IncludedTypes`/`--type` to filter result types.
|
||||
- Results include `business_status` when Google returns it.
|
||||
|
||||
40
e2e_test.go
40
e2e_test.go
@ -148,6 +148,46 @@ func TestE2ENearbySearch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2EDirectionsAvoidTolls(t *testing.T) {
|
||||
apiKey := os.Getenv("GOOGLE_PLACES_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("GOOGLE_PLACES_API_KEY not set")
|
||||
}
|
||||
|
||||
from := os.Getenv("GOOGLE_PLACES_E2E_DIRECTIONS_FROM")
|
||||
if from == "" {
|
||||
from = "Paris, France"
|
||||
}
|
||||
to := os.Getenv("GOOGLE_PLACES_E2E_DIRECTIONS_TO")
|
||||
if to == "" {
|
||||
to = "Brest, France"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := NewClient(Options{
|
||||
APIKey: apiKey,
|
||||
DirectionsBaseURL: os.Getenv("GOOGLE_DIRECTIONS_E2E_BASE_URL"),
|
||||
Timeout: 15 * time.Second,
|
||||
})
|
||||
|
||||
response, err := client.Directions(ctx, DirectionsRequest{
|
||||
From: from,
|
||||
To: to,
|
||||
Mode: "drive",
|
||||
AvoidTolls: true,
|
||||
Language: "en",
|
||||
Region: "FR",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("directions error: %v", err)
|
||||
}
|
||||
if response.DistanceMeters == 0 || response.DurationSeconds == 0 {
|
||||
t.Fatalf("expected distance and duration, got %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2EPhotoMedia(t *testing.T) {
|
||||
apiKey := os.Getenv("GOOGLE_PLACES_API_KEY")
|
||||
if apiKey == "" {
|
||||
|
||||
@ -162,3 +162,55 @@ func TestRunDirectionsWithEqualsFlags(t *testing.T) {
|
||||
t.Fatalf("unexpected stdout: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDirectionsWithAvoidFlags(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != directionsPath {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode payload: %v", err)
|
||||
}
|
||||
if payload["travelMode"] != directionsModeDriveAPI {
|
||||
t.Fatalf("unexpected mode: %#v", payload["travelMode"])
|
||||
}
|
||||
modifiers, ok := payload["routeModifiers"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing routeModifiers: %#v", payload)
|
||||
}
|
||||
if modifiers["avoidTolls"] != true || modifiers["avoidHighways"] != true || modifiers["avoidFerries"] != true {
|
||||
t.Fatalf("unexpected routeModifiers: %#v", modifiers)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{
|
||||
"routes":[{"legs":[{"distanceMeters":1000,"duration":"600s","localizedValues":{"distance":{"text":"1 km"},"duration":{"text":"10 mins"}},"steps":[]}]}]
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
exitCode := Run([]string{
|
||||
"directions",
|
||||
"--from=A",
|
||||
"--to=B",
|
||||
"--api-key=test-key",
|
||||
"--directions-base-url=" + server.URL,
|
||||
"--mode=drive",
|
||||
"--avoid-tolls",
|
||||
"--avoid-highways",
|
||||
"--avoid-ferries",
|
||||
"--json",
|
||||
}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("unexpected stderr: %s", stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "\"mode\": \"DRIVING\"") {
|
||||
t.Fatalf("unexpected stdout: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,20 +9,23 @@ import (
|
||||
|
||||
// DirectionsCmd fetches directions between two points.
|
||||
type DirectionsCmd struct {
|
||||
From string `help:"Origin address or place name."`
|
||||
To string `help:"Destination address or place name."`
|
||||
FromPlaceID string `help:"Origin place ID." name:"from-place-id"`
|
||||
ToPlaceID string `help:"Destination place ID." name:"to-place-id"`
|
||||
FromLat *float64 `help:"Origin latitude." name:"from-lat"`
|
||||
FromLng *float64 `help:"Origin longitude." name:"from-lng"`
|
||||
ToLat *float64 `help:"Destination latitude." name:"to-lat"`
|
||||
ToLng *float64 `help:"Destination longitude." name:"to-lng"`
|
||||
Mode string `help:"Travel mode: walk, drive, bicycle, transit." default:"walk"`
|
||||
Compare string `help:"Compare with another mode: walk, drive, bicycle, transit."`
|
||||
Steps bool `help:"Include step-by-step instructions."`
|
||||
Units string `help:"Units: metric or imperial." default:"metric"`
|
||||
Language string `help:"BCP-47 language code (e.g. en, en-US)."`
|
||||
Region string `help:"CLDR region code (e.g. US, DE)."`
|
||||
From string `help:"Origin address or place name."`
|
||||
To string `help:"Destination address or place name."`
|
||||
FromPlaceID string `help:"Origin place ID." name:"from-place-id"`
|
||||
ToPlaceID string `help:"Destination place ID." name:"to-place-id"`
|
||||
FromLat *float64 `help:"Origin latitude." name:"from-lat"`
|
||||
FromLng *float64 `help:"Origin longitude." name:"from-lng"`
|
||||
ToLat *float64 `help:"Destination latitude." name:"to-lat"`
|
||||
ToLng *float64 `help:"Destination longitude." name:"to-lng"`
|
||||
Mode string `help:"Travel mode: walk, drive, bicycle, transit." default:"walk"`
|
||||
Compare string `help:"Compare with another mode: walk, drive, bicycle, transit."`
|
||||
Steps bool `help:"Include step-by-step instructions."`
|
||||
Units string `help:"Units: metric or imperial." default:"metric"`
|
||||
AvoidTolls bool `help:"Avoid toll roads when driving."`
|
||||
AvoidHighways bool `help:"Avoid highways when driving."`
|
||||
AvoidFerries bool `help:"Avoid ferries when driving."`
|
||||
Language string `help:"BCP-47 language code (e.g. en, en-US)."`
|
||||
Region string `help:"CLDR region code (e.g. US, DE)."`
|
||||
}
|
||||
|
||||
// Run executes the directions command.
|
||||
@ -43,14 +46,17 @@ func (c *DirectionsCmd) Run(app *App) error {
|
||||
}
|
||||
|
||||
request := goplaces.DirectionsRequest{
|
||||
From: c.From,
|
||||
To: c.To,
|
||||
FromPlaceID: c.FromPlaceID,
|
||||
ToPlaceID: c.ToPlaceID,
|
||||
Mode: primaryMode,
|
||||
Units: c.Units,
|
||||
Language: c.Language,
|
||||
Region: c.Region,
|
||||
From: c.From,
|
||||
To: c.To,
|
||||
FromPlaceID: c.FromPlaceID,
|
||||
ToPlaceID: c.ToPlaceID,
|
||||
Mode: primaryMode,
|
||||
Units: c.Units,
|
||||
AvoidTolls: c.AvoidTolls,
|
||||
AvoidHighways: c.AvoidHighways,
|
||||
AvoidFerries: c.AvoidFerries,
|
||||
Language: c.Language,
|
||||
Region: c.Region,
|
||||
}
|
||||
if c.FromLat != nil || c.FromLng != nil {
|
||||
if c.FromLat == nil || c.FromLng == nil {
|
||||
@ -74,6 +80,9 @@ func (c *DirectionsCmd) Run(app *App) error {
|
||||
if compareMode != "" {
|
||||
compareRequest := request
|
||||
compareRequest.Mode = compareMode
|
||||
compareRequest.AvoidTolls = false
|
||||
compareRequest.AvoidHighways = false
|
||||
compareRequest.AvoidFerries = false
|
||||
second, err := app.client.Directions(context.Background(), compareRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -237,6 +237,7 @@ func writePlaceSummary(out *bytes.Buffer, color Color, place goplaces.PlaceSumma
|
||||
writeRating(out, color, place.Rating, place.UserRatingCount, place.PriceLevel)
|
||||
writeTypes(out, color, place.Types)
|
||||
writeOpenNow(out, color, place.OpenNow)
|
||||
writeLine(out, color, "Status", place.BusinessStatus)
|
||||
}
|
||||
|
||||
func writeAutocompleteSuggestion(out *bytes.Buffer, color Color, suggestion goplaces.AutocompleteSuggestion) {
|
||||
@ -255,6 +256,7 @@ func writePlaceDetails(out *bytes.Buffer, color Color, place goplaces.PlaceDetai
|
||||
writeRating(out, color, place.Rating, place.UserRatingCount, place.PriceLevel)
|
||||
writeTypes(out, color, place.Types)
|
||||
writeOpenNow(out, color, place.OpenNow)
|
||||
writeLine(out, color, "Status", place.BusinessStatus)
|
||||
writeLine(out, color, "Phone", place.Phone)
|
||||
writeLine(out, color, "Website", place.Website)
|
||||
writePhotos(out, color, place.Photos)
|
||||
|
||||
@ -25,6 +25,7 @@ func TestRenderSearch(t *testing.T) {
|
||||
PriceLevel: &level,
|
||||
Types: []string{"cafe", "coffee_shop"},
|
||||
OpenNow: &open,
|
||||
BusinessStatus: "OPERATIONAL",
|
||||
},
|
||||
},
|
||||
NextPageToken: "next",
|
||||
@ -43,6 +44,9 @@ func TestRenderSearch(t *testing.T) {
|
||||
if !strings.Contains(output, "Open now") {
|
||||
t.Fatalf("missing open now")
|
||||
}
|
||||
if !strings.Contains(output, "Status: OPERATIONAL") {
|
||||
t.Fatalf("missing status")
|
||||
}
|
||||
if !strings.Contains(output, "next") {
|
||||
t.Fatalf("missing next page token")
|
||||
}
|
||||
@ -226,16 +230,17 @@ func TestRenderDetailsAndResolve(t *testing.T) {
|
||||
open := false
|
||||
level := 0
|
||||
details := goplaces.PlaceDetails{
|
||||
PlaceID: "place-1",
|
||||
Name: "Park",
|
||||
Address: "Central",
|
||||
Rating: floatPtr(4.2),
|
||||
PriceLevel: &level,
|
||||
Types: []string{"park"},
|
||||
Phone: "+1 555",
|
||||
Website: "https://example.com",
|
||||
Hours: []string{"Mon: 9-5"},
|
||||
OpenNow: &open,
|
||||
PlaceID: "place-1",
|
||||
Name: "Park",
|
||||
Address: "Central",
|
||||
Rating: floatPtr(4.2),
|
||||
PriceLevel: &level,
|
||||
Types: []string{"park"},
|
||||
Phone: "+1 555",
|
||||
Website: "https://example.com",
|
||||
Hours: []string{"Mon: 9-5"},
|
||||
OpenNow: &open,
|
||||
BusinessStatus: "CLOSED_TEMPORARILY",
|
||||
Photos: []goplaces.Photo{
|
||||
{Name: "places/place-1/photos/photo-1", WidthPx: 1200, HeightPx: 800},
|
||||
},
|
||||
@ -258,6 +263,9 @@ func TestRenderDetailsAndResolve(t *testing.T) {
|
||||
if !strings.Contains(output, "Reviews:") || !strings.Contains(output, "Alice") {
|
||||
t.Fatalf("missing reviews output: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Status: CLOSED_TEMPORARILY") {
|
||||
t.Fatalf("missing status output: %s", output)
|
||||
}
|
||||
|
||||
resolve := goplaces.LocationResolveResponse{
|
||||
Results: []goplaces.ResolvedLocation{{PlaceID: "loc-1", Name: "Downtown"}},
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const nearbyFieldMask = "places.id,places.displayName,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.priceLevel,places.types,places.currentOpeningHours"
|
||||
const nearbyFieldMask = "places.id,places.displayName,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.priceLevel,places.types,places.currentOpeningHours,places.businessStatus"
|
||||
|
||||
// NearbySearch performs a nearby search around a location restriction.
|
||||
func (c *Client) NearbySearch(ctx context.Context, req NearbySearchRequest) (NearbySearchResponse, error) {
|
||||
|
||||
@ -16,6 +16,7 @@ type placeItem struct {
|
||||
Types []string `json:"types,omitempty"`
|
||||
CurrentOpeningHours *openingHours `json:"currentOpeningHours,omitempty"`
|
||||
RegularOpeningHours *openingHours `json:"regularOpeningHours,omitempty"`
|
||||
BusinessStatus string `json:"businessStatus,omitempty"`
|
||||
NationalPhoneNumber string `json:"nationalPhoneNumber,omitempty"`
|
||||
WebsiteURI string `json:"websiteUri,omitempty"`
|
||||
Reviews []reviewPayload `json:"reviews,omitempty"`
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const searchFieldMask = "places.id,places.displayName,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.priceLevel,places.types,places.currentOpeningHours,nextPageToken"
|
||||
const searchFieldMask = "places.id,places.displayName,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.priceLevel,places.types,places.currentOpeningHours,places.businessStatus,nextPageToken"
|
||||
|
||||
// Search performs a text search with optional filters.
|
||||
func (c *Client) Search(ctx context.Context, req SearchRequest) (SearchResponse, error) {
|
||||
@ -109,6 +109,7 @@ func mapPlaceSummary(place placeItem) PlaceSummary {
|
||||
PriceLevel: mapPriceLevel(place.PriceLevel),
|
||||
Types: place.Types,
|
||||
OpenNow: openNow(place.CurrentOpeningHours),
|
||||
BusinessStatus: strings.TrimSpace(place.BusinessStatus),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
types.go
2
types.go
@ -93,6 +93,7 @@ type PlaceSummary struct {
|
||||
PriceLevel *int `json:"price_level,omitempty"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
OpenNow *bool `json:"open_now,omitempty"`
|
||||
BusinessStatus string `json:"business_status,omitempty"`
|
||||
}
|
||||
|
||||
// PlaceDetails is a detailed view of a place.
|
||||
@ -109,6 +110,7 @@ type PlaceDetails struct {
|
||||
Website string `json:"website,omitempty"`
|
||||
Hours []string `json:"hours,omitempty"`
|
||||
OpenNow *bool `json:"open_now,omitempty"`
|
||||
BusinessStatus string `json:"business_status,omitempty"`
|
||||
Reviews []Review `json:"reviews,omitempty"`
|
||||
Photos []Photo `json:"photos,omitempty"`
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user