官术网_书友最值得收藏!

Implementing Gravatar

Gravatar is a web service that allows users to upload a single profile picture and associate it with their e-mail address in order to make it available from any website. Developers, like us, can access these images for our application just by performing a GET operation on a specific API endpoint. In this section, we will look at how to implement Gravatar rather than use the picture provided by the auth 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. The http.Handler method 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 the user for which the URL to be returned. 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 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 auth service and the avatar's implementation

The first implementation of Avatar we write will replace the existing functionality where we had hardcoded the avatar URL obtained from the auth service. Let's use a Test-driven Development (TDD) approach so that 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") 
  } 
  if url != testUrl { 
    t.Error("AuthAvatar.GetAvatarURL should return correct URL") 
  } 
} 

This 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 URL, 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 in 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 at 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.

Note

The GetAvatarURL method we wrote earlier doesn't have a very nice line of sight; the happy return is buried within two if blocks. See if you can refactor it so that the last line is return urlStr, nil and the method exits early if the avatar_url field is missing. You can refactor with confidence, since this code is covered by a unit test.

For a little more on the rationale behind this kind of refactor, refer to the article at http://bit.ly/lineofsightgolang.

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 omit a variable name 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 relatively simple otherwise: we are safely looking for the value of avatar_url and ensuring that it is a string before returning it. If anything fails, 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

If all is 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 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 room struct type:

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

Update the newRoom function so that 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 saving (since we only have one room for our entire 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 shown 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() { 
  defer c.socket.Close() 
  for { 
    var msg *message 
    if err := c.socket.ReadJSON(&msg); err != nil { 
      return 
    } 
    msg.When = time.Now() 
    msg.Name = c.userData["name"].(string) 
    msg.AvatarURL, _ = c.room.avatar.GetAvatarURL(c) 
    c.room.forward <- msg 
  } 
} 

Here, we are asking the avatar instance in 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.

The Gravatar implementation

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

func TestGravatarAvatar(t *testing.T) { 
  var gravatarAvatar GravatarAvatar 
  client := new(client) 
  client.userData = map[string]interface{}{"email": 
   "MyEmailAddress@example.com"} 
  url, err := gravatarAvatar.GetAvatarURL(client) 
  if err != nil { 
    t.Error("GravatarAvatar.GetAvatarURL should not return an error") 
  } 
  if url != "http://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" { 
    t.Errorf("GravatarAvatar.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 doing.

Tip

Remember that all the source code for this book is available for download from the publishers and has also been published 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("http://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 using fmt.Sprintf.

Note

The preceding method also suffers from a bad line of sight in code. Can you live with it, or would you want to improve the readability somehow?

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 and 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 auth provider. Using your browser's inspector or debug tool will show you that the src attribute of the img tag has indeed changed:

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

主站蜘蛛池模板: 连江县| 建水县| 凌云县| 文水县| 石城县| 德保县| 罗江县| 通许县| 巴塘县| 古蔺县| 元阳县| 醴陵市| 新蔡县| 杨浦区| 河间市| 噶尔县| 韶关市| 台安县| 彭阳县| 连州市| 长宁区| 凌云县| 芷江| 延川县| 乌苏市| 长岛县| 博客| 红安县| 开远市| 日照市| 泽库县| 固镇县| 石首市| 永仁县| 咸阳市| 麟游县| 图木舒克市| 万宁市| 郴州市| 常熟市| 盘山县|