Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Google Maps API For Work params #45

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 94 additions & 25 deletions google_geocoder.go
Expand Up @@ -2,6 +2,9 @@ package geo

import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand All @@ -10,10 +13,19 @@ import (
"net/url"
)

type GoogleAuthSchema int

const (
_ = iota
GoogleMapsAPIToken
GoogleMapsForWorkAuth
)

// This struct contains all the funcitonality
// of interacting with the Google Maps Geocoding Service
type GoogleGeocoder struct {
HttpClient *http.Client
AuthSchema GoogleAuthSchema
}

// This struct contains selected fields from Google's Geocoding Service response
Expand Down Expand Up @@ -44,6 +56,10 @@ var googleGeocodeURL = "https://maps.googleapis.com/maps/api/geocode/json"

var GoogleAPIKey = ""

var GoogleClientID = ""
var GooglePrivateKey = ""
var GoogleChannel = ""

// Note: In the next major revision (1.0.0), it is planned
// That Geocoders should adhere to the `geo.Geocoder`
// interface and provide versioning of APIs accordingly.
Expand All @@ -56,6 +72,18 @@ func SetGoogleAPIKey(newAPIKey string) {
GoogleAPIKey = newAPIKey
}

func SetGoogleClientID(newGoogleClientID string) {
GoogleClientID = newGoogleClientID
}

func SetGooglePrivateKey(newGooglePrivateKey string) {
GooglePrivateKey = newGooglePrivateKey
}

func SetGoogleChannel(newGoogleChannel string) {
GoogleChannel = newGoogleChannel
}

// Issues a request to the google geocoding service and forwards the passed in params string
// as a URL-encoded entity. Returns an array of byes as a result, or an error if one occurs during the process.
// Note: Since this is an arbitrary request, you are responsible for passing in your API key if you want one.
Expand All @@ -66,7 +94,7 @@ func (g *GoogleGeocoder) Request(params string) ([]byte, error) {

client := g.HttpClient

fullUrl := fmt.Sprintf("%s?sensor=false&%s", googleGeocodeURL, params)
fullUrl := fmt.Sprintf("%s?%s", googleGeocodeURL, params)

// TODO Potentially refactor out from MapQuestGeocoder as well
req, _ := http.NewRequest("GET", fullUrl, nil)
Expand All @@ -88,7 +116,9 @@ func (g *GoogleGeocoder) Request(params string) ([]byte, error) {
// Geocodes the passed in query string and returns a pointer to a new Point struct.
// Returns an error if the underlying request cannot complete.
func (g *GoogleGeocoder) Geocode(address string) (*Point, error) {
queryStr, err := googleGeocodeQueryStr(address)
params := googleGeocodeQueryStr(address)

queryStr, err := g.googleFormattedRequestStr(params)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -116,29 +146,81 @@ func (g *GoogleGeocoder) Geocode(address string) (*Point, error) {
return point, nil
}

func googleGeocodeQueryStr(address string) (string, error) {
url_safe_query := url.QueryEscape(address)
func (g *GoogleGeocoder) googleFormattedRequestStr(params string) (string, error) {
query := fmt.Sprintf("%s&%s", "sensor=false", params)

var queryStr = bytes.NewBufferString("")
_, err := queryStr.WriteString(fmt.Sprintf("address=%s", url_safe_query))
switch g.AuthSchema {
case GoogleMapsAPIToken:
return buildGoogleMapsClientSideQuery(query)
case GoogleMapsForWorkAuth:
return buildGoogleMapsForWorkQuery(query)
default:
return buildDefaultGoogleMapsQuery(query)
}
}

func buildGoogleMapsClientSideQuery(query string) (string, error) {
queryBuffer := bytes.NewBufferString(query)

_, err := queryBuffer.WriteString(fmt.Sprintf("&key=%s", GoogleAPIKey))
if err != nil {
return "", err
}

if GoogleAPIKey != "" {
_, err := queryStr.WriteString(fmt.Sprintf("&key=%s", GoogleAPIKey))
return queryBuffer.String(), nil
}

func buildGoogleMapsForWorkQuery(query string) (string, error) {
queryBuffer := bytes.NewBufferString(query)

if GoogleChannel != "" {
_, err := queryBuffer.WriteString(fmt.Sprintf("&channel=%s", GoogleChannel))
if err != nil {
return "", err
}
}

return queryStr.String(), err
_, err := queryBuffer.WriteString(fmt.Sprintf("&client=%s", GoogleClientID))
if err != nil {
return "", err
}

u, err := url.Parse(fmt.Sprintf("%s?%s", googleGeocodeURL, queryBuffer.String()))
if err != nil {
return "", err
}

requestUri := u.RequestURI()

decodedKey, err := base64.URLEncoding.DecodeString(GooglePrivateKey)
if err != nil {
return "", err
}

mac := hmac.New(sha1.New, decodedKey)
_, err = mac.Write([]byte(requestUri))

encodedSignature := base64.URLEncoding.EncodeToString(mac.Sum(nil))

return fmt.Sprintf("%s&signature=%s", queryBuffer.String(), encodedSignature), nil
}

func buildDefaultGoogleMapsQuery(query string) (string, error) {
return query, nil
}

func googleGeocodeQueryStr(address string) string {
url_safe_query := url.QueryEscape(address)

return fmt.Sprintf("address=%s", url_safe_query)
}

// Reverse geocodes the pointer to a Point struct and returns the first address that matches
// or returns an error if the underlying request cannot complete.
func (g *GoogleGeocoder) ReverseGeocode(p *Point) (string, error) {
queryStr, err := googleReverseGeocodeQueryStr(p)
params := googleReverseGeocodeQueryStr(p)

queryStr, err := g.googleFormattedRequestStr(params)
if err != nil {
return "", err
}
Expand All @@ -161,19 +243,6 @@ func (g *GoogleGeocoder) ReverseGeocode(p *Point) (string, error) {
return res.Results[0].FormattedAddress, err
}

func googleReverseGeocodeQueryStr(p *Point) (string, error) {
var queryStr = bytes.NewBufferString("")
_, err := queryStr.WriteString(fmt.Sprintf("latlng=%f,%f", p.lat, p.lng))
if err != nil {
return "", err
}

if GoogleAPIKey != "" {
_, err := queryStr.WriteString(fmt.Sprintf("&key=%s", GoogleAPIKey))
if err != nil {
return "", err
}
}

return queryStr.String(), err
func googleReverseGeocodeQueryStr(p *Point) string {
return fmt.Sprintf("latlng=%f,%f", p.lat, p.lng)
}
84 changes: 67 additions & 17 deletions google_geocoder_test.go
Expand Up @@ -22,55 +22,105 @@ func TestSetGoogleGeocodeURL(t *testing.T) {
}
}

func TestGoogleGeocoderQueryStr(t *testing.T) {
// Empty API Key
SetGoogleAPIKey("")
func TestSetGoogleClientID(t *testing.T) {
SetGoogleClientID("foo")
if GoogleClientID != "foo" {
t.Errorf("Mismatched value for GoogleClientID. Expected: 'foo', Actual: %s", GoogleAPIKey)
}
}

func TestSetGooglePrivateKey(t *testing.T) {
SetGooglePrivateKey("foo")
if GooglePrivateKey != "foo" {
t.Errorf("Mismatched value for GooglePrivateKey. Expected: 'foo', Actual: %s", GoogleAPIKey)
}
}

func TestSetGoogleChannel(t *testing.T) {
SetGoogleChannel("foo")
if GoogleChannel != "foo" {
t.Errorf("Mismatched value for GoogleChannel. Expected: 'foo', Actual: %s", GoogleAPIKey)
}
}

func TestGoogleGeocodeQueryStr(t *testing.T) {
address := "123 fake st"
res, err := googleGeocodeQueryStr(address)

res := googleGeocodeQueryStr(address)

expected := "address=123+fake+st"
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}
}

func TestGoogleReverseGeocodeQueryStr(t *testing.T) {
p := &Point{lat: 123.45, lng: 56.78}
res := googleReverseGeocodeQueryStr(p)

expected := "latlng=123.450000,56.780000"
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}
}

func TestGoogleFormattedRequestStr(t *testing.T) {
// Empty API Key and Client ID
SetGoogleAPIKey("")
SetGoogleClientID("")
params := "latlng=123.450000,56.780000"

g := &GoogleGeocoder{}
res, err := g.googleFormattedRequestStr(params)
if err != nil {
t.Errorf("Error creating query string: %v", err)
}

expected := "address=123+fake+st"
expected := "sensor=false&latlng=123.450000,56.780000"
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}

// Set api key to some value
SetGoogleAPIKey("foo")
res, err = googleGeocodeQueryStr(address)

g.AuthSchema = GoogleMapsAPIToken
res, err = g.googleFormattedRequestStr(params)
if err != nil {
t.Errorf("Error creating query string: %v", err)
}

expected = "address=123+fake+st&key=foo"
expected = "sensor=false&latlng=123.450000,56.780000&key=foo"
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}
}

func TestGoogleReverseGeocoderQueryStr(t *testing.T) {
// Empty API Key
// Set Client ID and Private Key to some value
SetGoogleAPIKey("")
p := &Point{lat: 123.45, lng: 56.78}
res, err := googleReverseGeocodeQueryStr(p)
SetGoogleClientID("clientID")
SetGooglePrivateKey("vNIXE0xscrmjlyV-12Nj_BvUPaw=")
SetGoogleChannel("")
params = "address=New+York"

g.AuthSchema = GoogleMapsForWorkAuth
res, err = g.googleFormattedRequestStr(params)
if err != nil {
t.Errorf("Error creating query string: %v", err)
}

expected := "latlng=123.450000,56.780000"
expected = "sensor=false&address=New+York&client=clientID&signature=N5nLIw-ytshbH2swgE9pzmZaIjU="
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}

// Set api key to some value
SetGoogleAPIKey("foo")
res, err = googleReverseGeocodeQueryStr(p)
// Set Channel
SetGoogleChannel("chan")
res, err = g.googleFormattedRequestStr(params)
if err != nil {
t.Errorf("Error creating query string: %v", err)
}

expected = "latlng=123.450000,56.780000&key=foo"
expected = "sensor=false&address=New+York&channel=chan&client=clientID&signature=UKgre7hzowedRuWgv8NfwnOoTCc="
if res != expected {
t.Errorf(fmt.Sprintf("Mismatched query string. Expected: %s. Actual: %s", expected, res))
}
Expand Down