Merge remote-tracking branch 'upstream/v0.2.0' into update-identity-v2

Conflicts:
	openstack/common/extensions/requests.go
	openstack/identity/v3/tokens/results.go
	openstack/networking/v2/extensions/delegate_test.go
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index ab4ae05..c8587b6 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -20,7 +20,7 @@
 }
 
 // Create authenticates and either generates a new token, or changes the Scope of an existing token.
-func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) (gophercloud.AuthResults, error) {
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
 	type domainReq struct {
 		ID   *string `json:"id,omitempty"`
 		Name *string `json:"name,omitempty"`
@@ -73,13 +73,13 @@
 
 	// Test first for unrecognized arguments.
 	if options.APIKey != "" {
-		return nil, ErrAPIKeyProvided
+		return createErr(ErrAPIKeyProvided)
 	}
 	if options.TenantID != "" {
-		return nil, ErrTenantIDProvided
+		return createErr(ErrTenantIDProvided)
 	}
 	if options.TenantName != "" {
-		return nil, ErrTenantNameProvided
+		return createErr(ErrTenantNameProvided)
 	}
 
 	if options.Password == "" {
@@ -87,16 +87,16 @@
 			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
 			// parameters.
 			if options.Username != "" {
-				return nil, ErrUsernameWithToken
+				return createErr(ErrUsernameWithToken)
 			}
 			if options.UserID != "" {
-				return nil, ErrUserIDWithToken
+				return createErr(ErrUserIDWithToken)
 			}
 			if options.DomainID != "" {
-				return nil, ErrDomainIDWithToken
+				return createErr(ErrDomainIDWithToken)
 			}
 			if options.DomainName != "" {
-				return nil, ErrDomainNameWithToken
+				return createErr(ErrDomainNameWithToken)
 			}
 
 			// Configure the request for Token authentication.
@@ -106,7 +106,7 @@
 			}
 		} else {
 			// If no password or token ID are available, authentication can't continue.
-			return nil, ErrMissingPassword
+			return createErr(ErrMissingPassword)
 		}
 	} else {
 		// Password authentication.
@@ -114,23 +114,23 @@
 
 		// At least one of Username and UserID must be specified.
 		if options.Username == "" && options.UserID == "" {
-			return nil, ErrUsernameOrUserID
+			return createErr(ErrUsernameOrUserID)
 		}
 
 		if options.Username != "" {
 			// If Username is provided, UserID may not be provided.
 			if options.UserID != "" {
-				return nil, ErrUsernameOrUserID
+				return createErr(ErrUsernameOrUserID)
 			}
 
 			// Either DomainID or DomainName must also be specified.
 			if options.DomainID == "" && options.DomainName == "" {
-				return nil, ErrDomainIDOrDomainName
+				return createErr(ErrDomainIDOrDomainName)
 			}
 
 			if options.DomainID != "" {
 				if options.DomainName != "" {
-					return nil, ErrDomainIDOrDomainName
+					return createErr(ErrDomainIDOrDomainName)
 				}
 
 				// Configure the request for Username and Password authentication with a DomainID.
@@ -158,10 +158,10 @@
 		if options.UserID != "" {
 			// If UserID is specified, neither DomainID nor DomainName may be.
 			if options.DomainID != "" {
-				return nil, ErrDomainIDWithUserID
+				return createErr(ErrDomainIDWithUserID)
 			}
 			if options.DomainName != "" {
-				return nil, ErrDomainNameWithUserID
+				return createErr(ErrDomainNameWithUserID)
 			}
 
 			// Configure the request for UserID and Password authentication.
@@ -177,10 +177,10 @@
 			// ProjectName provided: either DomainID or DomainName must also be supplied.
 			// ProjectID may not be supplied.
 			if scope.DomainID == "" && scope.DomainName == "" {
-				return nil, ErrScopeDomainIDOrDomainName
+				return createErr(ErrScopeDomainIDOrDomainName)
 			}
 			if scope.ProjectID != "" {
-				return nil, ErrScopeProjectIDOrProjectName
+				return createErr(ErrScopeProjectIDOrProjectName)
 			}
 
 			if scope.DomainID != "" {
@@ -205,10 +205,10 @@
 		} else if scope.ProjectID != "" {
 			// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
 			if scope.DomainID != "" {
-				return nil, ErrScopeProjectIDAlone
+				return createErr(ErrScopeProjectIDAlone)
 			}
 			if scope.DomainName != "" {
-				return nil, ErrScopeProjectIDAlone
+				return createErr(ErrScopeProjectIDAlone)
 			}
 
 			// ProjectID
@@ -218,7 +218,7 @@
 		} else if scope.DomainID != "" {
 			// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
 			if scope.DomainName != "" {
-				return nil, ErrScopeDomainIDOrDomainName
+				return createErr(ErrScopeDomainIDOrDomainName)
 			}
 
 			// DomainID
@@ -226,51 +226,45 @@
 				Domain: &domainReq{ID: &scope.DomainID},
 			}
 		} else if scope.DomainName != "" {
-			return nil, ErrScopeDomainName
+			return createErr(ErrScopeDomainName)
 		} else {
-			return nil, ErrScopeEmpty
+			return createErr(ErrScopeEmpty)
 		}
 	}
 
-	var result TokenCreateResult
-	response, err := perigee.Request("POST", getTokenURL(c), perigee.Options{
+	var result CreateResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
 		ReqBody: &req,
-		Results: &result.response,
+		Results: &result.Resp,
 		OkCodes: []int{201},
 	})
-	if err != nil {
-		return nil, err
+	if result.Err != nil {
+		return result
 	}
-
-	// Extract the token ID from the response, if present.
-	result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
-	return &result, nil
+	result.header = response.HttpResponse.Header
+	return result
 }
 
 // Get validates and retrieves information about another token.
-func Get(c *gophercloud.ServiceClient, token string) (*TokenCreateResult, error) {
-	var result TokenCreateResult
-
-	response, err := perigee.Request("GET", getTokenURL(c), perigee.Options{
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+	var result GetResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
-		Results:     &result.response,
+		Results:     &result.Resp,
 		OkCodes:     []int{200, 203},
 	})
-
-	if err != nil {
-		return nil, err
+	if result.Err != nil {
+		return result
 	}
-
-	// Extract the token ID from the response, if present.
-	result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
-	return &result, nil
+	result.header = response.HttpResponse.Header
+	return result
 }
 
 // Validate determines if a specified token is valid or not.
 func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
-	response, err := perigee.Request("HEAD", getTokenURL(c), perigee.Options{
+	response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
 		OkCodes:     []int{204, 404},
 	})
@@ -283,7 +277,7 @@
 
 // Revoke immediately makes specified token invalid.
 func Revoke(c *gophercloud.ServiceClient, token string) error {
-	_, err := perigee.Request("DELETE", getTokenURL(c), perigee.Options{
+	_, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
index d0813ac..367c73c 100644
--- a/openstack/identity/v3/tokens/requests_test.go
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -29,10 +29,14 @@
 		testhelper.TestJSONRequest(t, r, requestJSON)
 
 		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, `{}`)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
 	})
 
-	_, err := Create(&client, options, scope)
+	_, err := Create(&client, options, scope).Extract()
 	if err != nil {
 		t.Errorf("Create returned an error: %v", err)
 	}
@@ -50,7 +54,7 @@
 		client.Provider.TokenID = "abcdef123456"
 	}
 
-	_, err := Create(&client, options, scope)
+	_, err := Create(&client, options, scope).Extract()
 	if err == nil {
 		t.Errorf("Create did NOT return an error")
 	}
@@ -250,18 +254,21 @@
 		w.Header().Add("X-Subject-Token", "aaa111")
 
 		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, `{}`)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
 	})
 
 	options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
-	result, err := Create(&client, options, nil)
+	token, err := Create(&client, options, nil).Extract()
 	if err != nil {
-		t.Errorf("Create returned an error: %v", err)
+		t.Fatalf("Create returned an error: %v", err)
 	}
 
-	token, _ := result.TokenID()
-	if token != "aaa111" {
-		t.Errorf("Expected token to be aaa111, but was %s", token)
+	if token.ID != "aaa111" {
+		t.Errorf("Expected token to be aaa111, but was %s", token.ID)
 	}
 }
 
@@ -413,19 +420,14 @@
 		`)
 	})
 
-	result, err := Get(&client, "abcdef12345")
+	token, err := Get(&client, "abcdef12345").Extract()
 	if err != nil {
 		t.Errorf("Info returned an error: %v", err)
 	}
 
-	expires, err := result.ExpiresAt()
-	if err != nil {
-		t.Errorf("Error extracting token expiration time: %v", err)
-	}
-
 	expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
-	if expires != expected {
-		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), expires.Format(time.UnixDate))
+	if token.ExpiresAt != expected {
+		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
 	}
 }
 
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index 145a43d..e96da51 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -1,44 +1,78 @@
 package tokens
 
 import (
+	"net/http"
 	"time"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 )
 
-// TokenCreateResult contains the document structure returned from a Create call.
-type TokenCreateResult struct {
-	response map[string]interface{}
-	tokenID  string
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+	gophercloud.CommonResult
+
+	// header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+	header http.Header
 }
 
-// TokenID retrieves a token generated by a Create call from an token creation response.
-func (r *TokenCreateResult) TokenID() (string, error) {
-	return r.tokenID, nil
-}
-
-// ExpiresAt retrieves the token expiration time.
-func (r *TokenCreateResult) ExpiresAt() (time.Time, error) {
-	type tokenResp struct {
-		ExpiresAt string `mapstructure:"expires_at"`
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+	if r.Err != nil {
+		return nil, r.Err
 	}
 
-	type response struct {
-		Token tokenResp `mapstructure:"token"`
+	var response struct {
+		Token struct {
+			ExpiresAt string `mapstructure:"expires_at"`
+		} `mapstructure:"token"`
 	}
 
-	var resp response
-	err := mapstructure.Decode(r.response, &resp)
+	var token Token
+
+	// Parse the token itself from the stored headers.
+	token.ID = r.header.Get("X-Subject-Token")
+
+	err := mapstructure.Decode(r.Resp, &response)
 	if err != nil {
-		return time.Time{}, err
+		return nil, err
 	}
 
 	// Attempt to parse the timestamp.
-	ts, err := time.Parse(gophercloud.RFC3339Milli, resp.Token.ExpiresAt)
+	token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
 	if err != nil {
-		return time.Time{}, err
+		return nil, err
 	}
 
-	return ts, nil
+	return &token, nil
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+	return CreateResult{
+		commonResult: commonResult{
+			CommonResult: gophercloud.CommonResult{Err: err},
+			header:       nil,
+		},
+	}
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+	// ID is the issued token.
+	ID string
+
+	// ExpiresAt is the timestamp at which this token will no longer be accepted.
+	ExpiresAt time.Time
 }
diff --git a/openstack/identity/v3/tokens/results_test.go b/openstack/identity/v3/tokens/results_test.go
deleted file mode 100644
index 669db61..0000000
--- a/openstack/identity/v3/tokens/results_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package tokens
-
-import (
-	"testing"
-	"time"
-)
-
-func TestTokenID(t *testing.T) {
-	result := TokenCreateResult{tokenID: "1234"}
-
-	token, _ := result.TokenID()
-	if token != "1234" {
-		t.Errorf("Expected tokenID of 1234, got %s", token)
-	}
-}
-
-func TestExpiresAt(t *testing.T) {
-	resp := map[string]interface{}{
-		"token": map[string]string{
-			"expires_at": "2013-02-02T18:30:59.000000Z",
-		},
-	}
-
-	result := TokenCreateResult{
-		tokenID:  "1234",
-		response: resp,
-	}
-
-	expected, _ := time.Parse(time.UnixDate, "Sat Feb 2 18:30:59 UTC 2013")
-	actual, err := result.ExpiresAt()
-	if err != nil {
-		t.Errorf("Error extraction expiration time: %v", err)
-	}
-	if actual != expected {
-		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), actual.Format(time.UnixDate))
-	}
-}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
index 5b47c02..360b60a 100644
--- a/openstack/identity/v3/tokens/urls.go
+++ b/openstack/identity/v3/tokens/urls.go
@@ -2,6 +2,6 @@
 
 import "github.com/rackspace/gophercloud"
 
-func getTokenURL(c *gophercloud.ServiceClient) string {
+func tokenURL(c *gophercloud.ServiceClient) string {
 	return c.ServiceURL("auth", "tokens")
 }
diff --git a/openstack/identity/v3/tokens/urls_test.go b/openstack/identity/v3/tokens/urls_test.go
index 5ff8bc6..549c398 100644
--- a/openstack/identity/v3/tokens/urls_test.go
+++ b/openstack/identity/v3/tokens/urls_test.go
@@ -14,7 +14,7 @@
 	client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
 
 	expected := testhelper.Endpoint() + "auth/tokens"
-	actual := getTokenURL(&client)
+	actual := tokenURL(&client)
 	if actual != expected {
 		t.Errorf("Expected URL %s, but was %s", expected, actual)
 	}