Implementing Gravatar

Gravatar is a web service that allows users to upload a single profile picture and associate it with their e-mail address to make it available from any website. Developers, like us, can access those images for our application, just by performing a GET operation on a specific API endpoint. In this section, we will see how to implement Gravatar rather than use the picture provided by the authentication service.

Abstracting the avatar URL process

Since we have three different ways of obtaining the avatar URL in our application, we have reached the point where it would be sensible to learn how to abstract the functionality in order to cleanly implement the options. Abstraction refers to a process in which we separate the idea of something from its specific implementation. http.Handler is a great example of how a handler will be used along with its ins and outs, without being specific about what action is taken by each handler.

In Go, we start to describe our idea of getting an avatar URL by defining an interface. Let's create a new file called avatar.go and insert the following code:

package main
import (
  "errors"
)
// ErrNoAvatar is the error that is returned when the
// Avatar instance is unable to provide an avatar URL.
var ErrNoAvatarURL = errors.New("chat: Unable to get an avatar URL.")
// Avatar represents types capable of representing
// user profile pictures.
type Avatar interface {
  // GetAvatarURL gets the avatar URL for the specified client,
  // or returns an error if something goes wrong.
  // ErrNoAvatarURL is returned if the object is unable to get
  // a URL for the specified client.
  GetAvatarURL(c *client) (string, error)
}

The Avatar interface describes the GetAvatarURL method that a type must satisfy in order to be able to get avatar URLs. We took the client as an argument so that we know for which user to return the URL. The method returns two arguments: a string (which will be the URL if things go well) and an error in case something goes wrong.

One of the things that could go wrong is simply that one of the specific implementations of Avatar is unable to get the URL. In that case, GetAvatarURL will return the ErrNoAvatarURL error as the second argument. The ErrNoAvatarURL error therefore becomes a part of the interface; it's one of the possible returns from the method and something that users of our code should probably explicitly handle. We mention this in the comments part of the code for the method, which is the only way to communicate such design decisions in Go.

Tip

Because the error is initialized immediately using errors.New and stored in the ErrNoAvatarURL variable, only one of these objects will ever be created; passing the pointer of the error as a return is very inexpensive. This is unlike Java's checked exceptions—which serve a similar purpose—where expensive exception objects are created and used as part of the control flow.

The authentication service and avatar's implementation

The first implementation of Avatar we write will replace the existing functionality where we hardcoded the avatar URL obtained from the authentication service. Let's use a Test-driven Development (TDD) approach so we can be sure our code works without having to manually test it. Let's create a new file called avatar_test.go in the chat folder:

package main
import "testing"
func TestAuthAvatar(t *testing.T) {
  var authAvatar AuthAvatar
  client := new(client)
  url, err := authAvatar.GetAvatarURL(client)
  if err != ErrNoAvatarURL {
    t.Error("AuthAvatar.GetAvatarURL should return ErrNoAvatarURL when no value present")
  }
  // set a value
  testUrl := "http://url-to-gravatar/"
  client.userData = map[string]interface{}{"avatar_url": testUrl}
  url, err = authAvatar.GetAvatarURL(client)
  if err != nil {
    t.Error("AuthAvatar.GetAvatarURL should return no error when value present")
  } else {
    if url != testUrl {
      t.Error("AuthAvatar.GetAvatarURL should return correct URL")
    }
  }
}

This test file contains a test for our as-of-yet nonexistent AuthAvatar type's GetAvatarURL method. First, it uses a client with no user data and ensures that the ErrNoAvatarURL error is returned. After setting a suitable value, our test calls the method again—this time to assert that it returns the correct value. However, building this code fails because the AuthAvatar type doesn't exist, so we'll declare authAvatar next.

Before we write our implementation, it's worth noticing that we only declare the authAvatar variable as the AuthAvatar type, but never actually assign anything to it so its value remains nil. This is not a mistake; we are actually making use of Go's zero-initialization (or default initialization) capabilities. Since there is no state needed for our object (we will pass client as an argument), there is no need to waste time and memory on initializing an instance of it. In Go, it is acceptable to call a method on a nil object, provided that the method doesn't try to access a field. When we actually come to writing our implementation, we will look at a way in which we can ensure this is the case.

Let's head back over to avatar.go and make our test pass. Add the following code to the bottom of the file:

type AuthAvatar struct{}
var UseAuthAvatar AuthAvatar
func (_ AuthAvatar) GetAvatarURL(c *client) (string, error) {
  if url, ok := c.userData["avatar_url"]; ok {
    if urlStr, ok := url.(string); ok {
      return urlStr, nil
    }
  }
  return "", ErrNoAvatarURL
}

Here, we define our AuthAvatar type as an empty struct and define the implementation of the GetAvatarURL method. We also create a handy variable called UseAuthAvatar that has the AuthAvatar type but which remains of nil value. We can later assign the UseAuthAvatar variable to any field looking for an Avatar interface type.

Normally, the receiver of a method (the type defined in parentheses before the name) will be assigned to a variable so that it can be accessed in the body of the method. Since, in our case, we assume the object can have nil value, we can use an underscore to tell Go to throw away the reference. This serves as an added reminder to ourselves that we should avoid using it.

The body of our implementation is otherwise relatively simple: we are safely looking for the value of avatar_url and ensuring it is a string before returning it. If anything fails along the way, we return the ErrNoAvatarURL error as defined in the interface.

Let's run the tests by opening a terminal and then navigating to the chat folder and typing the following:

go test

All being well, our tests will pass and we will have successfully created our first Avatar implementation.

Using an implementation

When we use an implementation, we could refer to either the helper variables directly or create our own instance of the interface whenever we need the functionality. However, this would defeat the very object of the abstraction. Instead, we use the Avatar interface type to indicate where we need the capability.

For our chat application, we will have a single way to obtain an avatar URL per chat room. So let's update the room type so it can hold an Avatar object. In room.go, add the following field definition to the type room struct:

// avatar is how avatar information will be obtained.
avatar Avatar

Update the newRoom function so we can pass in an Avatar implementation for use; we will just assign this implementation to the new field when we create our room instance:

// newRoom makes a new room that is ready to go.
func newRoom(avatar Avatar) *room {
  return &room{
    forward: make(chan *message),
    join:    make(chan *client),
    leave:   make(chan *client),
    clients: make(map[*client]bool),
    tracer:  trace.Off(),
    avatar:  avatar,
  }
}

Building the project now will highlight the fact that the call to newRoom in main.go is broken because we have not provided an Avatar argument; let's update it by passing in our handy UseAuthAvatar variable as follows:

r := newRoom(UseAuthAvatar)

We didn't have to create an instance of AuthAvatar, so no memory was allocated. In our case, this doesn't result in great savings (since we only have one room for our whole application), but imagine the size of the potential savings if our application has thousands of rooms. The way we named the UseAuthAvatar variable means that the preceding code is very easy to read and it also makes our intention obvious.

Tip

Thinking about code readability is important when designing interfaces. Consider a method that takes a Boolean input—just passing in true or false hides the real meaning if you don't know the argument names. Consider defining a couple of helper constants as in the following short example:

func move(animated bool) { /* ... */ }
const Animate = true
const DontAnimate = false

Think about which of the following calls to move are easier to understand:

move(true)
move(false)
move(Animate)
move(DontAnimate)

All that is left now is to change client to use our new Avatar interface. In client.go, update the read method as follows:

func (c *client) read() {
  for {
    var msg *message
    if err := c.socket.ReadJSON(&msg); err == nil {
      msg.When = time.Now()
      msg.Name = c.userData["name"].(string)
      msg.AvatarURL, _ = c.room.avatar.GetAvatarURL(c)
      c.room.forward <- msg
    } else {
      break
    }
  }
  c.socket.Close()
}

Here, we are asking the avatar instance on room to get the avatar URL for us instead of extracting it from userData ourselves.

When you build and run the application, you will notice that (although we have refactored things a little) the behavior and user experience hasn't changed at all. This is because we told our room to use the AuthAvatar implementation.

Now let's add another implementation to the room.

Gravatar implementation

The Gravatar implementation in Avitar will do the same job as the AuthAvatar implementation, except it will generate a URL for a profile picture hosted on Gravatar.com. Let's start by adding a test to our avatar_test.go file:

func TestGravatarAvatar(t *testing.T) {
  var gravatarAvitar GravatarAvatar
  client := new(client)
  client.userData = map[string]interface{}{"email": "[email protected]"}
  url, err := gravatarAvitar.GetAvatarURL(client)
  if err != nil {
    t.Error("GravatarAvitar.GetAvatarURL should not return an error")
  }
  if url != "//www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" {
    t.Errorf("GravatarAvitar.GetAvatarURL wrongly returned %s", url)
  }
}

Gravatar uses a hash of the e-mail address to generate a unique ID for each profile picture, so we set up a client and ensure userData contains an e-mail address. Next, we call the same GetAvatarURL method, but this time on an object that has the GravatarAvatar type. We then assert that a correct URL was returned. We already know this is the appropriate URL for the specified e-mail address because it is listed as an example in the Gravatar documentation—a great strategy to ensure our code is doing what it should be.

Tip

Recall that all the source code for this book is available on GitHub. You can save time on building the preceding core by copying and pasting bits and pieces from https://github.com/matryer/goblueprints. Hardcoding things such as the base URL is not usually a good idea; we have hardcoded throughout the book to make the code snippets easier to read and more obvious, but you are welcome to extract them as you go along if you like.

Running these tests (with go test) obviously causes errors because we haven't defined our types yet. Let's head back to avatar.go and add the following code while being sure to import the io package:

type GravatarAvatar struct{}
var UseGravatar GravatarAvatar
func (_ GravatarAvatar) GetAvatarURL(c *client) (string, error) {
  if email, ok := c.userData["email"]; ok {
    if emailStr, ok := email.(string); ok {
      m := md5.New()
      io.WriteString(m, strings.ToLower(emailStr))
      return fmt.Sprintf("//www.gravatar.com/avatar/%x", m.Sum(nil)), nil
    }
  }
  return "", ErrNoAvatarURL
}

We used the same pattern as we did for AuthAvatar: we have an empty struct, a helpful UseGravatar variable, and the GetAvatarURL method implementation itself. In this method, we follow Gravatar's guidelines to generate an MD5 hash from the e-mail address (after we ensured it was lowercase) and append it to the hardcoded base URL.

It is very easy to achieve hashing in Go, thanks to the hard work put in by the writers of the Go standard library. The crypto package has an impressive array of cryptography and hashing capabilities—all very easy to use. In our case, we create a new md5 hasher; because the hasher implements the io.Writer interface, we can use io.WriteString to write a string of bytes to it. Calling Sum returns the current hash for the bytes written.

Tip

You might have noticed that we end up hashing the e-mail address every time we need the avatar URL. This is pretty inefficient, especially at scale, but we should prioritize getting stuff done over optimization. If we need to, we can always come back later and change the way this works.

Running the tests now shows us that our code is working, but we haven't yet included an e-mail address in the auth cookie. We do this by locating the code where we assign to the authCookieValue object in auth.go and updating it to grab the Email value from Gomniauth:

authCookieValue := objx.New(map[string]interface{}{
  "name":       user.Name(),
  "avatar_url": user.AvatarURL(),
  "email":      user.Email(),
}).MustBase64()

The final thing we must do is tell our room to use the Gravatar implementation instead of the AuthAvatar implementation. We do this by calling newRoom in main.go and making the following change:

r := newRoom(UseGravatar)

Build and run the chat program once again and head to the browser. Remember, since we have changed the information stored in the cookie, we must sign out and sign back in again in order to see our changes take effect.

Assuming you have a different image for your Gravatar account, you will notice that the system is now pulling the image from Gravatar instead of the authentication provider. Using your browser's inspector or debug tool will show you that the src attribute of the img tag has indeed changed.

Gravatar implementation

If you don't have a Gravatar account, you'll likely see a default placeholder image in place of your profile picture.

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

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