forked from paymydrink/go-yelp-fusion
Porting golang yelp wrapper to v3-fusion-api
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
# go-yelp-fusion
|
# go-yelp-fusion
|
||||||
|
|
||||||
go-yelp-fusion is a #golang wrapper for the Yelp API.
|
go-yelp-fusion is a #golang wrapper for the Yelp API.
|
||||||
|
|
||||||
|
It is based on https://github.com/JustinBeckwith/go-yelp
|
||||||
3
config.json
Normal file
3
config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"APIKey": "j9Geem7lY0Mj_qvgkd8e1tXrtLfAFIjEji_PwIefQfd7-Stip3rDsQPPxo75my47GgK06J-NLbQ_wzOdxm04NtTYOnAOXWYViQSTqTLZ0noEWnkkQMqln1RmZoy6WnYx"
|
||||||
|
}
|
||||||
71
examples/yelp-https.go
Normal file
71
examples/yelp-https.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// This example demonstrates using querystring parameters to perform
|
||||||
|
// a simple query with the yelp API.
|
||||||
|
// Example url: http://localhost:8000/?term=coffee&location=seattle
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitlab.bastian-buehrig.de/go-yelp-fusion/yelp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", res)
|
||||||
|
http.ListenAndServe(":8000", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func res(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// get the keys either from config file
|
||||||
|
options, err := getOptions(w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new yelp client with the auth keys
|
||||||
|
client := yelp.New(options, nil)
|
||||||
|
|
||||||
|
// make a simple query
|
||||||
|
term := r.URL.Query().Get("term")
|
||||||
|
location := r.URL.Query().Get("location")
|
||||||
|
results, err := client.DoSimpleSearch(term, location)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print the results
|
||||||
|
io.WriteString(w, fmt.Sprintf("<div>Found a total of %v results for \"%v\" in \"%v\".</div>", results.Total, term, location))
|
||||||
|
io.WriteString(w, "<div>-----------------------------</div>")
|
||||||
|
for i := 0; i < len(results.Businesses); i++ {
|
||||||
|
io.WriteString(w, fmt.Sprintf("<div>%v, %v</div>", results.Businesses[i].Name, results.Businesses[i].Rating))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOptions obtains the keys required to use the Yelp API from a config file
|
||||||
|
// or from environment variables.
|
||||||
|
func getOptions(w http.ResponseWriter) (options *yelp.AuthOptions, err error) {
|
||||||
|
|
||||||
|
var o *yelp.AuthOptions
|
||||||
|
|
||||||
|
// start by looking for the keys in config.json
|
||||||
|
data, err := ioutil.ReadFile("../config.json")
|
||||||
|
if err != nil {
|
||||||
|
// if the file isn't there, check environment variables
|
||||||
|
o = &yelp.AuthOptions{
|
||||||
|
APIKey: os.Getenv("API_KEY"),
|
||||||
|
}
|
||||||
|
if o.APIKey == "" {
|
||||||
|
return o, errors.New("to use the sample, keys must be provided either in a config.json file at the root of the repo, or in environment variables")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(data, &o)
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
177
yelp/business.go
Normal file
177
yelp/business.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
// A SearchResult is returned from the Search API. It includes
|
||||||
|
// the region, the total number of results, and a list of matching businesses.
|
||||||
|
// The business objects returned by this query are shallow - they will not include
|
||||||
|
// deep results such as reviews.
|
||||||
|
type SearchResult struct {
|
||||||
|
Region Region `json:"region"` // Suggested bounds in a map to display results in
|
||||||
|
Total int `json:"total"` // Total number of business results
|
||||||
|
Businesses []Business `json:"businesses"` // The list of business entries (see Business)
|
||||||
|
Error ErrorStruct `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region provides the location of a business obtained from search.
|
||||||
|
type Region struct {
|
||||||
|
Span Span // Span of suggested map bounds
|
||||||
|
Center Center // Center position of map bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Span provides the variance of the location from the region in the search result.
|
||||||
|
type Span struct {
|
||||||
|
LatitudeDelta float32 `json:"latitude_delta"` // Latitude width of map bounds
|
||||||
|
LongitudeDelta float32 `json:"longitude_delta"` // Longitude height of map bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center provides the coordinate where the business is most likely to be located.
|
||||||
|
type Center struct {
|
||||||
|
Latitude float32 // Latitude position of map bounds center
|
||||||
|
Longitude float32 // Longitude position of map bounds center
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal defines a set of special offerings from the business.
|
||||||
|
type Deal struct {
|
||||||
|
ID string // Deal identifier
|
||||||
|
Title string // Deal title
|
||||||
|
URL string // Deal URL
|
||||||
|
ImageURL string `json:"image_URL"` // Deal image URL
|
||||||
|
CurrencyCode string `json:"currency_code"` // ISO_4217 Currency Code
|
||||||
|
TimeStart float32 `json:"time_start"` // Deal start time (Unix timestamp)
|
||||||
|
TimeEnd float32 `json:"time_end"` // Deal end time (optional: this field is present only if the Deal ends)
|
||||||
|
IsPopular bool `json:"is_popular"` // Whether the Deal is popular (optional: this field is present only if true)
|
||||||
|
WhatYouGet string `json:"what_you_get"` // Additional details for the Deal, separated by newlines
|
||||||
|
ImportantRestrictions string `json:"Important_restrictions"` // Important restrictions for the Deal, separated by newlines
|
||||||
|
AdditionalRestrictions string `json:"Additional_restrictions"` // Deal additional restrictions
|
||||||
|
Options []DealOption //Deal options
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// DealOption provides options are optionally included on a deal.
|
||||||
|
type DealOption struct {
|
||||||
|
Title string // Deal option title
|
||||||
|
PurchaseURL string `json:"Purchase_URL"` // Deal option URL for purchase
|
||||||
|
Price float32 // Deal option price (in cents)
|
||||||
|
FormattedPrice string `json:"Formatted_price"` // Deal option price (formatted, e.g. "$6")
|
||||||
|
OriginalPrice float32 `json:"Original_price"` // Deal option original price (in cents)
|
||||||
|
FormattedOriginalPrice string `json:"Formatted_original_price"` // Deal option original price (formatted, e.g. "$12")
|
||||||
|
IsQuantityLimited bool `json:"Is_quantity_limited"` // Whether the deal option is limited or unlimited
|
||||||
|
RemainingCount float32 `json:"Remaining_count"` // The remaining deal options available for purchase (optional: this field is only present if the deal is limited)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiftCertificate defines optional data available on Businesses.
|
||||||
|
type GiftCertificate struct {
|
||||||
|
ID string // Gift certificate identifier
|
||||||
|
URL string // Gift certificate landing page URL
|
||||||
|
ImageURL string `json:"Image_URL"` // Gift certificate image URL
|
||||||
|
CurrencyCode string `json:"Currency_code"` // ISO_4217 Currency Code
|
||||||
|
UnusedBalances string `json:"Unused_balances"` // Whether unused balances are returned as cash or store credit
|
||||||
|
Options []GiftCertificateOptions // Gift certificate options
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiftCertificateOptions can define a set of pricing options for a gift certificate.
|
||||||
|
type GiftCertificateOptions struct {
|
||||||
|
Price float32 // Gift certificate option price (in cents)
|
||||||
|
FormattedPrice string `json:"Formatted_price"` // Gift certificate option price (formatted, e.g. "$50")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review data contains a list of user reviews for a given Business (when queried using the Business API).
|
||||||
|
type Review struct {
|
||||||
|
ID string // Review identifier
|
||||||
|
Rating float32 // Rating from 1-5
|
||||||
|
RatingImageURL string `json:"Rating_image_URL"` // URL to star rating image for this business (size = 84x17)
|
||||||
|
RatingImageSmallURL string `json:"Rating_image_small_URL"` // URL to small version of rating image for this business (size = 50x10)
|
||||||
|
RatingImageLargeURL string `json:"Rating_image_large_URL"` // URL to large version of rating image for this business (size = 166x30)
|
||||||
|
Excerpt string // Review excerpt
|
||||||
|
TimeCreated float32 `json:"Time_created"` // Time created (Unix timestamp)
|
||||||
|
User User // User who wrote the review
|
||||||
|
}
|
||||||
|
|
||||||
|
// User data is linked off of reviews.
|
||||||
|
type User struct {
|
||||||
|
ID string // User identifier
|
||||||
|
ImageURL string `json:"Image_URL"` // User profile image URL
|
||||||
|
Name string // User name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coordinates data is used with location information.
|
||||||
|
type Coordinates struct {
|
||||||
|
Latitude float32 `json:"latitude"` // Latitude of current location
|
||||||
|
Longitude float32 `json:"longitude"` // Longitude of current location
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location information defines the location of a given business.
|
||||||
|
type Location struct {
|
||||||
|
Address1 string `json:"address1"` // Address for this business. Only includes address fields.
|
||||||
|
Address2 string `json:"address2"` // Address for this business. Only includes address fields.
|
||||||
|
Address3 string `json:"address3"` // Address for this business. Only includes address fields.
|
||||||
|
City string `json:"city"` // City for this business
|
||||||
|
ZipCode string `json:"zip_code"` // Zip-code for this business
|
||||||
|
Country string `json:"country"` // Country for this business
|
||||||
|
State string `json:"state"` // State for this business
|
||||||
|
DisplayAddress []string `json:"display_address"` // Display address for the business.
|
||||||
|
CrossStreets string `json:"cross_streets"` // Cross streets for this business
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hours provides the open data for this business
|
||||||
|
type Hours struct {
|
||||||
|
Open []Open `json:"open"`
|
||||||
|
HoursType string `json:"hours_type"`
|
||||||
|
IsOpenNow bool `json:"is_open_now"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open provides the opening time for this business
|
||||||
|
type Open struct {
|
||||||
|
IsOvernight bool `json:"is_overnight"`
|
||||||
|
Start string `json:"start"`
|
||||||
|
End string `json:"end"`
|
||||||
|
Day int `json:"day"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category provides the opening time for this business
|
||||||
|
type Category struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorStruct provides errors returned by yelp
|
||||||
|
type ErrorStruct struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business information is returned in full from the business API, or shallow from the search API.
|
||||||
|
type Business struct {
|
||||||
|
ID string `json:"id"` // Yelp ID for this business
|
||||||
|
Name string `json:"name"` // Name of this business
|
||||||
|
ImageURL string `json:"image_url"` // URL of photo for this business
|
||||||
|
IsClaimed bool `json:"is_claimed"` // Whether business has been claimed by a business owner
|
||||||
|
IsClosed bool `json:"is_closed"` // Whether business has been (permanently) closed
|
||||||
|
URL string `json:"url"` // URL for business page on Yelp
|
||||||
|
Phone string `json:"phone"` // Phone number for this business with international dialing code (e.g. +442079460000)
|
||||||
|
DisplayPhone string `json:"display_phone"` // Phone number for this business formatted for display
|
||||||
|
ReviewCount int `json:"review_count"` // Number of reviews for this business
|
||||||
|
Categories []Category `json:"categories"` // Provides a list of category name, alias pairs that this business is associated with. The alias is provided so you can search with the category_filter.
|
||||||
|
Rating float32 `json:"rating"` // Rating for this business (value ranges from 1, 1.5, ... 4.5, 5)
|
||||||
|
Location Location `json:"location"` // Location data for this business
|
||||||
|
Coordinates Coordinates `json:"coordinates"` // Address for this business formatted for display. Includes all address fields, cross streets and city, state_code, etc.
|
||||||
|
Photos []string `json:"photos"` // Photos for this business
|
||||||
|
Price string `json:"price"`
|
||||||
|
Hours []Hours `json:"hours"`
|
||||||
|
Transactions []string `json:"transactions"`
|
||||||
|
Distance float32 `json:"distance"` // Distance that business is from search location in meters, if a latitude/longitude is specified.
|
||||||
|
Error ErrorStruct `json:"error"`
|
||||||
|
|
||||||
|
// MobileURL string `json:"Mobile_URL"` // URL for mobile business page on Yelp
|
||||||
|
|
||||||
|
// RatingImgURL string `json:"Rating_img_URL"` // URL to star rating image for this business (size = 84x17)
|
||||||
|
// RatingImgURLSmall string `json:"Rating_img_URL_small"` // URL to small version of rating image for this business (size = 50x10)
|
||||||
|
// RatingImgURLLarge string `json:"Rating_img_URL_large"` // URL to large version of rating image for this business (size = 166x30)
|
||||||
|
// SnippetText string `json:"Snippet_text"` // Snippet text associated with this business
|
||||||
|
// SnippetImageURL string `json:"Snippet_image_URL"` // URL of snippet image associated with this business
|
||||||
|
|
||||||
|
// MenuProvider string `json:"Menu_provider"` // Provider of the menu for this business
|
||||||
|
// MenuDateUpdated float32 `json:"Menu_date_updated"` // Last time this menu was updated on Yelp (Unix timestamp)
|
||||||
|
// Deals []Deal // Deal info for this business (optional: this field is present only if there’s a Deal)
|
||||||
|
// GiftCertificates []GiftCertificate `json:"Gift_certificates"` // Gift certificate info for this business (optional: this field is present only if there are gift certificates available)
|
||||||
|
// Reviews []Review // Contains one review associated with business
|
||||||
|
}
|
||||||
20
yelp/business_test.go
Normal file
20
yelp/business_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBusinessSearch will perform a simple search for a business by name.
|
||||||
|
func TestBusinessSearch(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
result, err := client.GetBusiness("north-india-restaurant-san-francisco")
|
||||||
|
check(t, err)
|
||||||
|
assert(t, result.Name != "", containsResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNonExistingBusinessSearch will verify searching for a non-existent business throws the right error.
|
||||||
|
func TestNonExistingBusinessSearch(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
_, err := client.GetBusiness("place-that-doesnt-exist")
|
||||||
|
assert(t, err == errBusinessNotFound, "Searching for a non-existent business should return a 404 error")
|
||||||
|
}
|
||||||
50
yelp/coordinate_options.go
Normal file
50
yelp/coordinate_options.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/guregu/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoordinateOptions are used with complex searches for locations.
|
||||||
|
// The geographic coordinate format is defined as:
|
||||||
|
// ll=latitude,longitude,accuracy,altitude,altitude_accuracy
|
||||||
|
type CoordinateOptions struct {
|
||||||
|
Latitude null.Float // Latitude of geo-point to search near (required)
|
||||||
|
Longitude null.Float // Longitude of geo-point to search near (required)
|
||||||
|
// Accuracy null.Float // Accuracy of latitude, longitude (optional)
|
||||||
|
// Altitude null.Float // Altitude (optional)
|
||||||
|
// AltitudeAccuracy null.Float // Accuracy of altitude (optional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getParameters will reflect over the values of the given
|
||||||
|
// struct, and provide a type appropriate set of querystring parameters
|
||||||
|
// that match the defined values.
|
||||||
|
func (o CoordinateOptions) getParameters() (params map[string]string, err error) {
|
||||||
|
params = make(map[string]string)
|
||||||
|
|
||||||
|
// coordinate requires at least a latitude and longitude - others are option
|
||||||
|
if !o.Latitude.Valid || !o.Longitude.Valid {
|
||||||
|
return nil, errors.New("latitude and longitude are required fields for a coordinate based search")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ll := fmt.Sprintf("%v,%v", o.Latitude.Float64, o.Longitude.Float64)
|
||||||
|
// if o.Accuracy.Valid {
|
||||||
|
// ll += fmt.Sprintf(",%v", o.Accuracy.Float64)
|
||||||
|
// }
|
||||||
|
// if o.Altitude.Valid {
|
||||||
|
// ll += fmt.Sprintf(",%v", o.Altitude.Float64)
|
||||||
|
// }
|
||||||
|
// if o.AltitudeAccuracy.Valid {
|
||||||
|
// ll += fmt.Sprintf(",%v", o.AltitudeAccuracy.Float64)
|
||||||
|
// }
|
||||||
|
|
||||||
|
if o.Latitude.Valid &&
|
||||||
|
o.Longitude.Valid {
|
||||||
|
params["latitude"] = fmt.Sprintf("%v", o.Latitude.Float64)
|
||||||
|
params["longitude"] = fmt.Sprintf("%v", o.Longitude.Float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
24
yelp/coordinate_options_test.go
Normal file
24
yelp/coordinate_options_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/guregu/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCoordinateOptions will check using location options with bounding coordinates.
|
||||||
|
func TestCoordinateOptions(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
CoordinateOptions: &CoordinateOptions{
|
||||||
|
null.FloatFrom(37.9),
|
||||||
|
null.FloatFrom(-122.5),
|
||||||
|
// null.FloatFromPtr(nil),
|
||||||
|
// null.FloatFromPtr(nil),
|
||||||
|
// null.FloatFromPtr(nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
52
yelp/general_options.go
Normal file
52
yelp/general_options.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/guregu/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GeneralOptions includes a set of standard query options for using the search API.
|
||||||
|
// They are used along with a location based option to complete a search.
|
||||||
|
type GeneralOptions struct {
|
||||||
|
Term string // Search term (e.g. "food", "restaurants"). If term isn’t included we search everything.
|
||||||
|
Limit null.Int // Number of business results to return
|
||||||
|
Offset null.Int // Offset the list of returned business results by this amount
|
||||||
|
SortBy string // Sort mode: 0=Best matched (default), 1=Distance, 2=Highest Rated. If the mode is 1 or 2 a search may retrieve an additional 20 businesses past the initial limit of the first 20 results. This is done by specifying an offset and limit of 20. Sort by distance is only supported for a location or geographic search. The rating sort is not strictly sorted by the rating value, but by an adjusted rating value that takes into account the number of ratings, similar to a bayesian average. This is so a business with 1 rating of 5 stars doesn’t immediately jump to the top.
|
||||||
|
CategoryFilter string // Category to filter search results with. See the list of supported categories. The category filter can be a list of comma delimited categories. For example, 'bars,french' will filter by Bars and French. The category identifier should be used (for example 'discgolf', not 'Disc Golf').
|
||||||
|
// RadiusFilter null.Float // Search radius in meters. If the value is too large, a AREA_TOO_LARGE error may be returned. The max value is 40000 meters (25 miles).
|
||||||
|
// DealsFilter null.Bool // Whether to exclusively search for businesses with deals
|
||||||
|
Locale string
|
||||||
|
}
|
||||||
|
|
||||||
|
// getParameters will reflect over the values of the given
|
||||||
|
// struct, and provide a type appropriate set of querystring parameters
|
||||||
|
// that match the defined values.
|
||||||
|
func (o *GeneralOptions) getParameters() (params map[string]string, err error) {
|
||||||
|
ps := make(map[string]string)
|
||||||
|
if o.Term != "" {
|
||||||
|
ps["term"] = o.Term
|
||||||
|
}
|
||||||
|
if o.Limit.Valid {
|
||||||
|
ps["limit"] = fmt.Sprintf("%v", o.Limit.Int64)
|
||||||
|
}
|
||||||
|
if o.Offset.Valid {
|
||||||
|
ps["offset"] = fmt.Sprintf("%v", o.Offset.Int64)
|
||||||
|
}
|
||||||
|
if o.SortBy != "" {
|
||||||
|
ps["sort_by"] = o.SortBy
|
||||||
|
}
|
||||||
|
if o.CategoryFilter != "" {
|
||||||
|
ps["category_filter"] = o.CategoryFilter
|
||||||
|
}
|
||||||
|
// if o.RadiusFilter.Valid {
|
||||||
|
// ps["radius_filter"] = fmt.Sprintf("%v", o.RadiusFilter.Float64)
|
||||||
|
// }
|
||||||
|
// if o.DealsFilter.Valid {
|
||||||
|
// ps["deals_filter"] = fmt.Sprintf("%v", o.DealsFilter.Bool)
|
||||||
|
// }
|
||||||
|
if o.Locale != "" {
|
||||||
|
ps["locale"] = o.Locale
|
||||||
|
}
|
||||||
|
return ps, nil
|
||||||
|
}
|
||||||
89
yelp/general_options_test.go
Normal file
89
yelp/general_options_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/guregu/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGeneralOptions will verify search with location and search term.
|
||||||
|
func TestGeneralOptions(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "coffee",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "seattle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnescapedCharactersInGeneralOptions verify URL escaped characters do not cause search to fail.
|
||||||
|
func TestUnescapedCharactersInGeneralOptions(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "Frimark Keller & Associates",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "60173",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMultipleCategories will perform a search with multiple categories on the general options filter.
|
||||||
|
func TestMultipleCategories(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
CategoryFilter: "climbing,bowling",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "Seattle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLimit verify the limit parameter works as expected.
|
||||||
|
func TestLimit(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "Coffee",
|
||||||
|
Limit: null.IntFrom(15),
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "Seattle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) == 15, "There should be 15 results.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocaleOptions will verify doing a search that includes locale options.
|
||||||
|
func TestLocaleOptions(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "coffee",
|
||||||
|
Locale: "en_US",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "seattle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
36
yelp/location_options.go
Normal file
36
yelp/location_options.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocationOptions enable specifing a Location by Neighborhood, Address, or City.
|
||||||
|
// The location format is defined: ?location=location
|
||||||
|
type LocationOptions struct {
|
||||||
|
Location string // Specifies the combination of "address, neighborhood, city, state or zip, optional country" to be used when searching for businesses. (required)
|
||||||
|
CoordinateOptions *CoordinateOptions // An optional latitude, longitude parameter can also be specified as a hint to the geocoder to disambiguate the location text. The format for this is defined as: ?cll=latitude,longitude
|
||||||
|
}
|
||||||
|
|
||||||
|
// getParameters will reflect over the values of the given
|
||||||
|
// struct, and provide a type appropriate set of querystring parameters
|
||||||
|
// that match the defined values.
|
||||||
|
func (o *LocationOptions) getParameters() (params map[string]string, err error) {
|
||||||
|
params = make(map[string]string)
|
||||||
|
|
||||||
|
// location is a required field
|
||||||
|
if o.Location == "" {
|
||||||
|
return params, errors.New("to perform a location based search, the location property must contain an area within to search. For coordinate based searches, use the CoordinateOption class")
|
||||||
|
}
|
||||||
|
params["location"] = o.Location
|
||||||
|
|
||||||
|
// if coordinates are specified add those to the parameters hash
|
||||||
|
if o.CoordinateOptions != nil &&
|
||||||
|
o.CoordinateOptions.Latitude.Valid &&
|
||||||
|
o.CoordinateOptions.Longitude.Valid {
|
||||||
|
params["latitude"] = fmt.Sprintf("%v", o.CoordinateOptions.Latitude.Float64)
|
||||||
|
params["longitude"] = fmt.Sprintf("%v", o.CoordinateOptions.Longitude.Float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
43
yelp/location_options_test.go
Normal file
43
yelp/location_options_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/guregu/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLocationOptions will check using location options by term.
|
||||||
|
func TestLocationOptions(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "coffee",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
Location: "seattle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocationWithCoordinates will check using location options with bounding coordinates.
|
||||||
|
func TestLocationWithCoordinates(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
options := SearchOptions{
|
||||||
|
GeneralOptions: &GeneralOptions{
|
||||||
|
Term: "food",
|
||||||
|
},
|
||||||
|
LocationOptions: &LocationOptions{
|
||||||
|
"bellevue",
|
||||||
|
&CoordinateOptions{
|
||||||
|
Latitude: null.FloatFrom(37.788022),
|
||||||
|
Longitude: null.FloatFrom(-122.399797),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := client.DoSearch(options)
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
64
yelp/search_options.go
Normal file
64
yelp/search_options.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptionProvider provides a base level interface which all search option interfaces implement.
|
||||||
|
// It provides information that allows for easily mapping to querystring parameters for the search query.
|
||||||
|
type OptionProvider interface {
|
||||||
|
getParameters() (params map[string]string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchOptions are the top level search parameters used for performing searches.
|
||||||
|
// You can define multiple sets of options, and use them together. One (and only one) of
|
||||||
|
// LocationOptions, CoordinateOptions, or BoundOptions can be used at the same time.
|
||||||
|
type SearchOptions struct {
|
||||||
|
GeneralOptions *GeneralOptions // standard general search options (filters, terms, etc)
|
||||||
|
LocationOptions *LocationOptions // Use a location term and potentially coordinates to define the location
|
||||||
|
CoordinateOptions *CoordinateOptions // Use coordinate options to define the location.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a map that contains the querystring parameters for
|
||||||
|
// all of the defined options.
|
||||||
|
func (o *SearchOptions) getParameters() (params map[string]string, err error) {
|
||||||
|
|
||||||
|
// ensure only one loc option provider is being used
|
||||||
|
locOptionsCnt := 0
|
||||||
|
if o.LocationOptions != nil {
|
||||||
|
locOptionsCnt++
|
||||||
|
}
|
||||||
|
if o.CoordinateOptions != nil {
|
||||||
|
locOptionsCnt++
|
||||||
|
}
|
||||||
|
// if o.BoundOptions != nil {
|
||||||
|
// locOptionsCnt++
|
||||||
|
// }
|
||||||
|
|
||||||
|
if locOptionsCnt == 0 {
|
||||||
|
return params, errors.New("a single location search options type (Location, Coordinate, Bound) must be used")
|
||||||
|
}
|
||||||
|
if locOptionsCnt > 1 {
|
||||||
|
return params, errors.New("only a single location search options type (Location, Coordinate, Bound) can be used at a time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an empty map of options
|
||||||
|
params = make(map[string]string)
|
||||||
|
|
||||||
|
// reflect over the properties in o, adding parameters to the global map
|
||||||
|
val := reflect.ValueOf(o).Elem()
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
if !val.Field(i).IsNil() {
|
||||||
|
o := val.Field(i).Interface().(OptionProvider)
|
||||||
|
fieldParams, err := o.getParameters()
|
||||||
|
if err != nil {
|
||||||
|
return params, err
|
||||||
|
}
|
||||||
|
for k, v := range fieldParams {
|
||||||
|
params[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
173
yelp/yelp.go
Normal file
173
yelp/yelp.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rootURI = "https://api.yelp.com/"
|
||||||
|
businessArea = "/v3/businesses"
|
||||||
|
searchArea = "/v3/businesses/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errUnspecifiedLocation = errors.New("location must be specified")
|
||||||
|
errBusinessNotFound = errors.New("business not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthOptions provide keys required for using the Yelp API. Find more
|
||||||
|
// information here: https://www.yelp.com/developers/documentation/v3/authentication
|
||||||
|
type AuthOptions struct {
|
||||||
|
APIKey string // API Key from the yelp API access site.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client manages all searches. All searches are performed from an instance of a client.
|
||||||
|
// It is the top level object used to perform a search or business query. C
|
||||||
|
type Client struct {
|
||||||
|
Options *AuthOptions
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoSimpleSearch performs a simple search with a term and location.
|
||||||
|
func (client *Client) DoSimpleSearch(term, location string) (result SearchResult, err error) {
|
||||||
|
|
||||||
|
// verify the term and location are not empty
|
||||||
|
if location == "" {
|
||||||
|
return SearchResult{}, errUnspecifiedLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up the query options
|
||||||
|
params := map[string]string{
|
||||||
|
"term": term,
|
||||||
|
"location": location,
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform the search request
|
||||||
|
_, err = client.makeRequest(searchArea, "", params, &result)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResult{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoSearch performs a complex search with full search options.
|
||||||
|
func (client *Client) DoSearch(options SearchOptions) (result SearchResult, err error) {
|
||||||
|
|
||||||
|
// get the options from the search provider
|
||||||
|
params, err := options.getParameters()
|
||||||
|
if err != nil {
|
||||||
|
return SearchResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform the search request
|
||||||
|
_, err = client.makeRequest(searchArea, "", params, &result)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResult{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBusiness obtains a single business by name.
|
||||||
|
func (client *Client) GetBusiness(name string) (result Business, err error) {
|
||||||
|
statusCode, err := client.makeRequest(businessArea, name, nil, &result)
|
||||||
|
if err != nil {
|
||||||
|
// At some point the Yelp API stopped reporting 404s for missing business names, and
|
||||||
|
// started reporting 400s :(
|
||||||
|
if statusCode == 400 || statusCode == 404 {
|
||||||
|
return Business{}, errBusinessNotFound
|
||||||
|
}
|
||||||
|
return Business{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRequest is an internal/private API used to make underlying requests to the Yelp API.
|
||||||
|
func (client *Client) makeRequest(area string, id string, params map[string]string, v interface{}) (statusCode int, err error) {
|
||||||
|
// get the base url
|
||||||
|
queryURI, err := url.Parse(rootURI)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the type of request we're making (search|business)
|
||||||
|
queryURI.Path = area
|
||||||
|
|
||||||
|
if id != "" {
|
||||||
|
queryURI.Path += "/" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do request
|
||||||
|
request, err := http.NewRequest("GET", queryURI.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Add("Authorization", "Bearer "+client.Options.APIKey)
|
||||||
|
q := request.URL.Query()
|
||||||
|
if params["term"] != "" {
|
||||||
|
q.Add("term", params["term"])
|
||||||
|
}
|
||||||
|
if params["location"] != "" {
|
||||||
|
q.Add("location", params["location"])
|
||||||
|
}
|
||||||
|
if params["limit"] != "" {
|
||||||
|
q.Add("limit", params["limit"])
|
||||||
|
}
|
||||||
|
if params["offset"] != "" {
|
||||||
|
q.Add("offset", params["offset"])
|
||||||
|
}
|
||||||
|
if params["sortby"] != "" {
|
||||||
|
q.Add("sort_by", params["sortby"])
|
||||||
|
}
|
||||||
|
if params["categoryfilter"] != "" {
|
||||||
|
q.Add("category_filter", params["categoryfilter"])
|
||||||
|
}
|
||||||
|
if params["locale"] != "" {
|
||||||
|
q.Add("locale", params["locale"])
|
||||||
|
}
|
||||||
|
if params["longitude"] != "" {
|
||||||
|
q.Add("longitude", params["longitude"])
|
||||||
|
}
|
||||||
|
if params["latitude"] != "" {
|
||||||
|
q.Add("latitude", params["latitude"])
|
||||||
|
}
|
||||||
|
request.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
response, err := client.Client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if response != nil {
|
||||||
|
return response.StatusCode, err
|
||||||
|
}
|
||||||
|
return 500, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the request when done
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// ensure the request returned a 200
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return response.StatusCode, errors.New(response.Status)
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(response.Body).Decode(v)
|
||||||
|
|
||||||
|
return response.StatusCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// New will create a new yelp search client. All search operations should go through this API.
|
||||||
|
func New(options *AuthOptions, httpClient *http.Client) *Client {
|
||||||
|
if httpClient == nil {
|
||||||
|
httpClient = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
Options: options,
|
||||||
|
Client: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
86
yelp/yelp_test.go
Normal file
86
yelp/yelp_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package yelp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
containsResults string = "The query returns at least one result."
|
||||||
|
shouldRequireLocation string = "The query should require a location."
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check an error result for a value. If present, fail the test with
|
||||||
|
// an error written to the console.
|
||||||
|
func check(t *testing.T, e error) {
|
||||||
|
if e != nil {
|
||||||
|
t.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert provides a simple way to verify an assertion, and fail the test
|
||||||
|
// if that assertion fails.
|
||||||
|
func assert(t *testing.T, condition bool, assertion string) {
|
||||||
|
if !condition {
|
||||||
|
t.Errorf("Assertion failed: %v", assertion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClient creates a client with keys in a json file, making it possible to run the
|
||||||
|
// tests against the public Yelp API.
|
||||||
|
func getClient(t *testing.T) *Client {
|
||||||
|
|
||||||
|
var o *AuthOptions
|
||||||
|
|
||||||
|
// start by looking for the keys in config.json
|
||||||
|
data, err := ioutil.ReadFile("../config.json")
|
||||||
|
if err != nil {
|
||||||
|
// if the file isn't there, check environment variables
|
||||||
|
o = &AuthOptions{
|
||||||
|
APIKey: os.Getenv("API_KEY"),
|
||||||
|
}
|
||||||
|
if o.APIKey == "" {
|
||||||
|
check(t, errors.New("to run tests, API key must be provided either in a config.json file at the root of the repo, or in environment variables"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(data, &o)
|
||||||
|
check(t, err)
|
||||||
|
}
|
||||||
|
client := New(o, nil)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// TESTS
|
||||||
|
//
|
||||||
|
|
||||||
|
// TestSimpleSearch verifies a simple search using a search term and location returns a set of results.
|
||||||
|
func TestSimpleSearch(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
result, err := client.DoSimpleSearch("coffee", "seattle")
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
|
||||||
|
// verify basic fields are returned
|
||||||
|
assert(t, result.Region.Center.Latitude != 0, "latitude is returned")
|
||||||
|
assert(t, result.Region.Center.Longitude != 0, "longitude is returned")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoLocation ensures validation for a missing location in the search.
|
||||||
|
func TestNoLocation(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
_, err := client.DoSimpleSearch("coffee", "")
|
||||||
|
assert(t, err == errUnspecifiedLocation, shouldRequireLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoTerm ensures you can query with no term defined and only a location.
|
||||||
|
func TestNoTerm(t *testing.T) {
|
||||||
|
client := getClient(t)
|
||||||
|
result, err := client.DoSimpleSearch("", "Seattle")
|
||||||
|
check(t, err)
|
||||||
|
assert(t, len(result.Businesses) > 0, containsResults)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user