Identity v3 Projects Get (#164)

* Identity v3 Projects Get

* Renaming ToGetQuery to ToProjectGetQuery

* Fixing acceptance test
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
index 213db9c..73e4c4c 100644
--- a/openstack/identity/v3/projects/requests.go
+++ b/openstack/identity/v3/projects/requests.go
@@ -50,3 +50,36 @@
 		return ProjectPage{pagination.LinkedPageBase{PageResult: r}}
 	})
 }
+
+// GetOptsBuilder allows extensions to add additional parameters to
+// the Get request.
+type GetOptsBuilder interface {
+	ToProjectGetQuery() (string, error)
+}
+
+// GetOpts allows you to modify the details included in the Get request.
+type GetOpts struct{}
+
+// ToProjectGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToProjectGetQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// Get retrieves details on a single project, by ID.
+func Get(client *gophercloud.ServiceClient, id string, opts GetOptsBuilder) (r GetResult) {
+	url := getURL(client, id)
+	if opts != nil {
+		query, err := opts.ToProjectGetQuery()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += query
+	}
+
+	_, r.Err = client.Get(url, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go
index 33c70e1..2ec05a8 100644
--- a/openstack/identity/v3/projects/results.go
+++ b/openstack/identity/v3/projects/results.go
@@ -1,9 +1,19 @@
 package projects
 
 import (
+	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/pagination"
 )
 
+type projectResult struct {
+	gophercloud.Result
+}
+
+// GetResult temporarily contains the response from the Get call.
+type GetResult struct {
+	projectResult
+}
+
 // Project is a base unit of ownership.
 type Project struct {
 	// IsDomain indicates whether the project is a domain.
@@ -62,3 +72,12 @@
 	err := (r.(ProjectPage)).ExtractInto(&s)
 	return s.Projects, err
 }
+
+// Extract interprets any projectResults as a Project.
+func (r projectResult) Extract() (*Project, error) {
+	var s struct {
+		Project *Project `json:"project"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Project, err
+}
diff --git a/openstack/identity/v3/projects/testing/fixtures.go b/openstack/identity/v3/projects/testing/fixtures.go
index c4d9c03..cb10313 100644
--- a/openstack/identity/v3/projects/testing/fixtures.go
+++ b/openstack/identity/v3/projects/testing/fixtures.go
@@ -40,6 +40,21 @@
 }
 `
 
+// GetOutput provides a Get result.
+const GetOutput = `
+{
+  "project": {
+		"is_domain": false,
+		"description": "The team that is red",
+		"domain_id": "default",
+		"enabled": true,
+		"id": "1234",
+		"name": "Red Team",
+		"parent_id": null
+  }
+}
+`
+
 // RedTeam is a Project fixture.
 var RedTeam = projects.Project{
 	IsDomain:    false,
@@ -65,7 +80,7 @@
 // ExpectedProjectSlice is the slice of projects expected to be returned from ListOutput.
 var ExpectedProjectSlice = []projects.Project{RedTeam, BlueTeam}
 
-// HandleListProjectSuccessfully creates an HTTP handler at `/projects` on the
+// HandleListProjectsSuccessfully creates an HTTP handler at `/projects` on the
 // test handler mux that responds with a list of two tenants.
 func HandleListProjectsSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) {
@@ -78,3 +93,17 @@
 		fmt.Fprintf(w, ListOutput)
 	})
 }
+
+// HandleGetProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that responds with a single project.
+func HandleGetProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/identity/v3/projects/testing/requests_test.go b/openstack/identity/v3/projects/testing/requests_test.go
index 31730f9..eeb2049 100644
--- a/openstack/identity/v3/projects/testing/requests_test.go
+++ b/openstack/identity/v3/projects/testing/requests_test.go
@@ -28,3 +28,13 @@
 	th.AssertNoErr(t, err)
 	th.CheckEquals(t, count, 1)
 }
+
+func TestGetProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetProjectSuccessfully(t)
+
+	actual, err := projects.Get(client.ServiceClient(), "1234", nil).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, RedTeam, *actual)
+}
diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go
index e80a395..5effbfb 100644
--- a/openstack/identity/v3/projects/urls.go
+++ b/openstack/identity/v3/projects/urls.go
@@ -5,3 +5,7 @@
 func listURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("projects")
 }
+
+func getURL(client *gophercloud.ServiceClient, projectID string) string {
+	return client.ServiceURL("projects", projectID)
+}