Generating random recommendations

In order to obtain the places from which our code will randomly build up recommendations, we need to query the Google Places API. In the meander folder, add the following query.go file:

package meander
type Place struct {
  *googleGeometry `json:"geometry"`
  Name            string         `json:"name"`
  Icon            string         `json:"icon"`
  Photos          []*googlePhoto `json:"photos"`
  Vicinity        string         `json:"vicinity"`
}
type googleResponse struct {
  Results []*Place `json:"results"`
}
type googleGeometry struct {
  *googleLocation `json:"location"`
}
type googleLocation struct {
  Lat float64 `json:"lat"`
  Lng float64 `json:"lng"`
}
type googlePhoto struct {
  PhotoRef string `json:"photo_reference"`
  URL      string `json:"url"`
}

This code defines the structures we will need to parse the JSON response from the Google Places API into usable objects.

Tip

Head over to the Google Places API documentation for an example of the response we are expecting. See http://developers.google.com/places/documentation/search.

Most of the preceding code will be obvious, but it's worth noticing that the Place type embeds the googleGeometry type, which allows us to represent the nested data as per the API, while essentially flattening it in our code. We do the same with googleLocation inside googleGeometry, which means that we will be able to access the Lat and Lng values directly on a Place object, even though they're technically nested in other structures.

Because we want to control how a Place object appears publically, let's give this type the following Public method:

func (p *Place) Public() interface{} {
  return map[string]interface{}{
    "name":     p.Name,
    "icon":     p.Icon,
    "photos":   p.Photos,
    "vicinity": p.Vicinity,
    "lat":      p.Lat,
    "lng":      p.Lng,
  }
}

Tip

Remember to run golint on this code to see which comments need to be added to the exported items.

Google Places API key

Like with most APIs, we will need an API key in order to access the remote services. Head over to the Google APIs Console, sign in with a Google account, and create a key for the Google Places API. For more detailed instructions, see the documentation on Google's developer website.

Once you have your key, let's make a variable inside the meander package that can hold it. At the top of query.go, add the following definition:

var APIKey string

Now nip back into main.go, remove the double slash // from the APIKey line, and replace the TODO value with the actual key provided by the Google APIs console.

Enumerators in Go

To handle the various cost ranges for our API, it makes sense to use an enumerator (or enum) to denote the various values and to handle conversions to and from string representations. Go doesn't explicitly provide enumerators, but there is a neat way of implementing them, which we will explore in this section.

A simple flexible checklist for writing enumerators in Go is:

  • Define a new type, based on a primitive integer type
  • Use that type whenever you need users to specify one of the appropriate values
  • Use the iota keyword to set the values in a const block, disregarding the first zero value
  • Implement a map of sensible string representations to the values of your enumerator
  • Implement a String method on the type that returns the appropriate string representation from the map
  • Implement a ParseType function that converts from a string to your type using the map

Now we will write an enumerator to represent the cost levels in our API. Create a new file called cost_level.go inside the meander folder and add the following code:

package meander
type Cost int8
const (
  _ Cost = iota
  Cost1
  Cost2
  Cost3
  Cost4
  Cost5
)

Here we define the type of our enumerator, which we have called Cost, and since we only need to represent a few values, we have based it on an int8 range. For enumerators where we need larger values, you are free to use any of the integer types that work with iota. The Cost type is now a real type in its own right, and we can use it wherever we need to represent one of the supported values—for example, we can specify a Cost type as an argument in functions, or use it as the type for a field in a struct.

We then define a list of constants of that type, and use the iota keyword to indicate that we want incrementing values for the constants. By disregarding the first iota value (which is always zero), we indicate that one of the specified constants must be explicitly used, rather than the zero value.

To provide a string representation of our enumerator, we need only add a String method to the Cost type. This is a useful exercise even if you don't need to use the strings in your code, because whenever you use the print calls from the Go standard library (such as fmt.Println), the numerical values will be used by default. Often those values are meaningless and will require you to look them up, and even count the lines to determine the numerical value for each item.

Note

For more information about the String() method in Go, see the Stringer and GoStringer interfaces in the fmt package at http://golang.org/pkg/fmt/#Stringer.

Test-driven enumerator

To be sure that our enumerator code is working correctly, we are going to write unit tests that make some assertions about expected behavior.

Alongside cost_level.go, add a new file called cost_level_test.go, and add the following unit test:

package meander_test
import (
  "testing"
  "github.com/cheekybits/is"
  "path/to/meander"
)
func TestCostValues(t *testing.T) {
  is := is.New(t)
  is.Equal(int(meander.Cost1), 1)
  is.Equal(int(meander.Cost2), 2)
  is.Equal(int(meander.Cost3), 3)
  is.Equal(int(meander.Cost4), 4)
  is.Equal(int(meander.Cost5), 5)
}

You will need to run go get to get the CheekyBits' is package (from github.com/cheekybits/is).

Tip

The is package is an alternative testing helper package, but this one is ultra-simple and deliberately bare-bones. You get to pick your favorite when you write your own projects.

Normally, we wouldn't worry about the actual integer value of constants in our enumerator, but since the Google Places API uses numerical values to represent the same thing, we need to care about the values.

Note

You might have noticed something strange about this test file that breaks from convention. Although it is inside the meander folder, it is not a part of the meander package; rather it's in meander_test.

In Go, this is an error in every case except for tests. Because we are putting our test code into its own package, it means that we no longer have access to the internals of the meander package—notice how we have to use the package prefix. This may seem like a disadvantage, but in fact it allows us to be sure that we are testing the package as though we were a real user of it. We may only call exported methods and only have visibility into exported types; just like our users.

Run the tests by running go test in a terminal, and notice that it passes.

Let's add another test to make assertions about the string representations for each Cost constant. In cost_level_test.go, add the following unit test:

func TestCostString(t *testing.T) {
  is := is.New(t)
  is.Equal(meander.Cost1.String(), "$")
  is.Equal(meander.Cost2.String(), "$$")
  is.Equal(meander.Cost3.String(), "$$$")
  is.Equal(meander.Cost4.String(), "$$$$")
  is.Equal(meander.Cost5.String(), "$$$$$")
}

This test asserts that calling the String method for each constant yields the expected value. Running these tests will of course fail, because we haven't yet implemented the String method.

Underneath the Cost constants, add the following map and the String method:

var costStrings = map[string]Cost{
  "$":     Cost1,
  "$$":    Cost2,
  "$$$":   Cost3,
  "$$$$":  Cost4,
  "$$$$$": Cost5,
}
func (l Cost) String() string {
  for s, v := range costStrings {
    if l == v {
      return s
    }
  }
  return "invalid"
}

The map[string]Cost variable maps the cost values to the string representation, and the String method iterates over the map to return the appropriate value.

Tip

In our case, a simple return strings.Repeat("$", int(l)) would work just as well (and wins because it's simpler code), but it often won't, therefore this section explores the general approach.

Now if we were to print out the Cost3 value, we would actually see $$$, which is much more useful than numerical vales. However, since we do want to use these strings in our API, we are also going to add a ParseCost method.

In cost_value_test.go, add the following unit test:

func TestParseCost(t *testing.T) {
  is := is.New(t)
  is.Equal(meander.Cost1, meander.ParseCost("$"))
  is.Equal(meander.Cost2, meander.ParseCost("$$"))
  is.Equal(meander.Cost3, meander.ParseCost("$$$"))
  is.Equal(meander.Cost4, meander.ParseCost("$$$$"))
  is.Equal(meander.Cost5, meander.ParseCost("$$$$$"))
}

Here we assert that calling ParseCost will in fact yield the appropriate value depending on the input string.

In cost_value.go, add the following implementation code:

func ParseCost(s string) Cost {
  return costStrings[s]
}

Parsing a Cost string is very simple since this is how our map is laid out.

As we need to represent a range of cost values, let's imagine a CostRange type, and write the tests out for how we intend to use it. Add the following tests to cost_value_test.go:

func TestParseCostRange(t *testing.T) {
  is := is.New(t)
  var l *meander.CostRange
  l = meander.ParseCostRange("$$...$$$")
  is.Equal(l.From, meander.Cost2)
  is.Equal(l.To, meander.Cost3)
  l = meander.ParseCostRange("$...$$$$$")
  is.Equal(l.From, meander.Cost1)
  is.Equal(l.To, meander.Cost5)
}
func TestCostRangeString(t *testing.T) {
  is := is.New(t)
  is.Equal("$$...$$$$", (&meander.CostRange{
    From: meander.Cost2,
    To:   meander.Cost4,
  }).String())
}

We specify that passing in a string with two dollar characters first, followed by three dots and then three dollar characters should create a new meander.CostRange type that has From set to meander.Cost2, and To set to meander.Cost3. The second test does the reverse by testing that the CostRange.String method returns the appropriate value.

To make our tests pass, add the following CostRange type and associated String and ParseString functions:

type CostRange struct {
  From Cost
  To   Cost
}
func (r CostRange) String() string {
  return r.From.String() + "..." + r.To.String()
}
func ParseCostRange(s string) *CostRange {
  segs := strings.Split(s, "...")
  return &CostRange{
    From: ParseCost(segs[0]),
    To:   ParseCost(segs[1]),
  }
}

This allows us to convert a string such as $...$$$$$ to a structure that contains two Cost values; a From and To set and vice versa.

Querying the Google Places API

Now that we are capable of representing the results of the API, we need a way to represent and initiate the actual query. Add the following structure to query.go:

type Query struct {
  Lat          float64
  Lng          float64
  Journey      []string
  Radius       int
  CostRangeStr string
}

This structure contains all the information we will need to build up the query, all of which will actually come from the URL parameters in the requests from the client. Next, add the following find method, which will be responsible for making the actual request to Google's servers:

func (q *Query) find(types string) (*googleResponse, error) {
  u := "https://maps.googleapis.com/maps/api/place/nearbysearch/json"
  vals := make(url.Values)
  vals.Set("location", fmt.Sprintf("%g,%g", q.Lat, q.Lng))
  vals.Set("radius", fmt.Sprintf("%d", q.Radius))
  vals.Set("types", types)
  vals.Set("key", APIKey)
  if len(q.CostRangeStr) > 0 {
    r := ParseCostRange(q.CostRangeStr)
    vals.Set("minprice", fmt.Sprintf("%d", int(r.From)-1))
    vals.Set("maxprice", fmt.Sprintf("%d", int(r.To)-1))
  }
  res, err := http.Get(u + "?" + vals.Encode())
  if err != nil {
    return nil, err
  }
  defer res.Body.Close()
  var response googleResponse
  if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
    return nil, err
  }
  return &response, nil
}

First we build the request URL as per the Google Places API specification, by appending the url.Values encoded string of the data for lat, lng, radius, and of course the APIKey values.

Note

The url.Values type is actually a map[string][]string type, which is why we use make rather than new.

The types value we specify as an argument represents the kind of business to look for. If there is a CostRangeStr, we parse it and set the minprice and maxprice values, before finally calling http.Get to actually make the request. If the request is successful, we defer the closing of the response body and use a json.Decoder method to decode the JSON that comes back from the API into our googleResponse type.

Building recommendations

Next we need to write a method that will allow us to make many calls to find, for the different steps in a journey. Underneath the find method, add the following Run method to the Query struct:

// Run runs the query concurrently, and returns the results.
func (q *Query) Run() []interface{} {
  rand.Seed(time.Now().UnixNano())
  var w sync.WaitGroup
  var l sync.Mutex
  places := make([]interface{}, len(q.Journey))
  for i, r := range q.Journey {
    w.Add(1)
    go func(types string, i int) {
      defer w.Done()
      response, err := q.find(types)
      if err != nil {
        log.Println("Failed to find places:", err)
        return
      }
      if len(response.Results) == 0 {
        log.Println("No places found for", types)
        return
      }
      for _, result := range response.Results {
        for _, photo := range result.Photos {
          photo.URL = "https://maps.googleapis.com/maps/api/place/photo?" +
            "maxwidth=1000&photoreference=" + photo.PhotoRef + "&key=" + APIKey
        }
      }
      randI := rand.Intn(len(response.Results))
      l.Lock()
      places[i] = response.Results[randI]
      l.Unlock()
    }(r, i)
  }
  w.Wait() // wait for everything to finish
  return places
}

The first thing we do is set the random seed to the current time in nanoseconds past since January 1, 1970 UTC. This ensures that every time we call the Run method and use the rand package, the results will be different. If we didn't do this, our code would suggest the same recommendations every time, which defeats the object.

Since we need to make many requests to Google—and since we want to make sure this is as quick as possible—we are going to run all the queries at the same time by making concurrent calls to our Query.find method. So we next create a sync.WaitGroup method, and a map to hold the selected places along with a sync.Mutex method to allow many go routines to access the map concurrently.

We then iterate over each item in the Journey slice, which might be bar, cafe, movie_theater. For each item, we add 1 to the WaitGroup object, and call a goroutine. Inside the routine, we first defer the w.Done call informing the WaitGroup object that this request has completed, before calling our find method to make the actual request. Assuming no errors occurred, and it was indeed able to find some places, we iterate over the results and build up a usable URL for any photos that might be present. According to the Google Places API, we are given a photoreference key, which we can use in another API call to get the actual image. To save our clients from having to have knowledge of the Google Places API at all, we build the complete URL for them.

We then lock the map locker and with a call to rand.Intn, pick one of the options at random and insert it into the right position in the places slice, before unlocking the sync.Mutex method.

Finally, we wait for all goroutines to complete with a call to w.Wait, before returning the places.

Handlers that use query parameters

Now we need to wire up our /recommendations call, so head back to main.go in the cmd folder, and add the following code inside the main function:

http.HandleFunc("/recommendations", func(w http.ResponseWriter, r *http.Request) {
  q := &meander.Query{
    Journey: strings.Split(r.URL.Query().Get("journey"), "|"),
  }
  q.Lat, _ = strconv.ParseFloat(r.URL.Query().Get("lat"), 64)
  q.Lng, _ = strconv.ParseFloat(r.URL.Query().Get("lng"), 64)
  q.Radius, _ = strconv.Atoi(r.URL.Query().Get("radius"))
  q.CostRangeStr = r.URL.Query().Get("cost")
  places := q.Run()
  respond(w, r, places)
})

This handler is responsible for preparing the meander.Query object and calling its Run method, before responding with the results. The http.Request type's URL value exposes the Query data that provides a Get method that, in turn, looks up a value for a given key.

The journey string is translated from the bar|cafe|movie_theater format to a slice of strings, by splitting on the pipe character. Then a few calls to functions in the strconv package turn the string latitude, longitude, and radius values into numerical types.

CORS

The final piece of the first version of our API will be to implement CORS as we did in the previous chapter. See if you can solve this problem yourself before reading on to the solution in the next section.

Tip

If you are going to tackle this yourself, remember that your aim is to set the Access-Control-Allow-Origin response header to *. Also consider the http.HandlerFunc wrapping we did in the previous chapter. The best place for this code is probably in the cmd program, since that is what exposes the functionality through an HTTP endpoint.

In main.go, add the following cors function:

func cors(f http.HandlerFunc) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "*")
    f(w, r)
  }
}

This familiar pattern takes in an http.HandlerFunc type and returns a new one that sets the appropriate header before calling the passed-in function. Now we can modify our code to make sure the cors function gets called for both of our endpoints. Update the appropriate lines in the main function:

func main() {
  runtime.GOMAXPROCS(runtime.NumCPU())
  meander.APIKey = "YOUR_API_KEY"
  http.HandleFunc("/journeys", cors(func(w http.ResponseWriter, r *http.Request) {
    respond(w, r, meander.Journeys)
  }))
  http.HandleFunc("/recommendations", cors(func(w http.ResponseWriter, r *http.Request) {
    q := &meander.Query{
      Journey: strings.Split(r.URL.Query().Get("journey"), "|"),
    }
    q.Lat, _ = strconv.ParseFloat(r.URL.Query().Get("lat"), 64)
    q.Lng, _ = strconv.ParseFloat(r.URL.Query().Get("lng"), 64)
    q.Radius, _ = strconv.Atoi(r.URL.Query().Get("radius"))
    q.CostRangeStr = r.URL.Query().Get("cost")
    places := q.Run()
    respond(w, r, places)
  }))
  http.ListenAndServe(":8080", http.DefaultServeMux)
}

Now calls to our API will be allowed from any domain without a cross-origin error occurring.

Testing our API

Now that we are ready to test our API, head to a console and navigate to the cmd folder. Because our program imports the meander package, building the program will automatically build our meander package too.

Build and run the program:

go build –o meanderapi
./meanderapi

To see meaningful results from our API, let's take a minute to find your actual latitude and longitude. Head over to http://mygeoposition.com/ and use the web tools to get the x,y values for a location you are familiar with.

Or pick from these popular cities:

  • London, England: 51.520707 x 0.153809
  • New York, USA: 40.7127840 x -74.0059410
  • Tokyo, Japan: 35.6894870 x 139.6917060
  • San Francisco, USA: 37.7749290 x -122.4194160

Now open a web browser and access the /recommendations endpoint with some appropriate values for the fields:

http://localhost:8080/recommendations?
  lat=51.520707&lng=-0.153809&radius=5000&
  journey=cafe|bar|casino|restaurant&
  cost=$...$$$

The following screenshot shows what a sample recommendation around London might look like:

Testing our API

Feel free to play around with the values in the URL to see how powerful the simple API is by trying various journey strings, tweaking the locations, and trying different cost range value strings.

Web application

We are going to download a complete web application built to the same API specifications, and point it at our implementation to see it come to life before our eyes. Head over to https://github.com/matryer/goblueprints/tree/master/chapter7/meanderweb and download the meanderweb project into your GOPATH.

In a terminal, navigate to the meanderweb folder, and build and run it:

go build –o meanderweb
./meanderweb

This will start a website running on localhost:8081, which is hardcoded to look for the API running at localhost:8080. Because we added the CORS support, this won't be a problem despite them running on different domains.

Open a browser to http://localhost:8081/ and interact with the application, while somebody else built the UI it would be pretty useless without the API that we built powering it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.38.92