Identity v3 Projects Update (#167)

diff --git a/acceptance/openstack/identity/v3/projects_test.go b/acceptance/openstack/identity/v3/projects_test.go
index 8c1c28a..ebb5543 100644
--- a/acceptance/openstack/identity/v3/projects_test.go
+++ b/acceptance/openstack/identity/v3/projects_test.go
@@ -73,6 +73,18 @@
 	defer DeleteProject(t, client, project.ID)
 
 	PrintProject(t, project)
+
+	var iFalse bool = false
+	updateOpts := projects.UpdateOpts{
+		Enabled: &iFalse,
+	}
+
+	updatedProject, err := projects.Update(client, project.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update project: %v", err)
+	}
+
+	PrintProject(t, updatedProject)
 }
 
 func TestProjectsDomain(t *testing.T) {
@@ -90,6 +102,7 @@
 	if err != nil {
 		t.Fatalf("Unable to create project: %v", err)
 	}
+	defer DeleteProject(t, client, projectDomain.ID)
 
 	PrintProject(t, projectDomain)
 
@@ -101,8 +114,19 @@
 	if err != nil {
 		t.Fatalf("Unable to create project: %v", err)
 	}
+	defer DeleteProject(t, client, project.ID)
 
 	PrintProject(t, project)
+
+	var iFalse = false
+	updateOpts := projects.UpdateOpts{
+		Enabled: &iFalse,
+	}
+
+	_, err = projects.Update(client, projectDomain.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to disable domain: %v")
+	}
 }
 
 func TestProjectsNested(t *testing.T) {
@@ -115,6 +139,7 @@
 	if err != nil {
 		t.Fatalf("Unable to create project: %v", err)
 	}
+	defer DeleteProject(t, client, projectMain.ID)
 
 	PrintProject(t, projectMain)
 
@@ -126,6 +151,7 @@
 	if err != nil {
 		t.Fatalf("Unable to create project: %v", err)
 	}
+	defer DeleteProject(t, client, project.ID)
 
 	PrintProject(t, project)
 }
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
index 5f64211..74a9b15 100644
--- a/openstack/identity/v3/projects/requests.go
+++ b/openstack/identity/v3/projects/requests.go
@@ -132,3 +132,48 @@
 	_, r.Err = client.Delete(deleteURL(client, projectID), nil)
 	return
 }
+
+// UpdateOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateOptsBuilder interface {
+	ToProjectUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts allows you to modify the details included in the Update request.
+type UpdateOpts struct {
+	// DomainID is the ID this project will belong under.
+	DomainID string `json:"domain_id,omitempty"`
+
+	// Enabled sets the project status to enabled or disabled.
+	Enabled *bool `json:"enabled,omitempty"`
+
+	// IsDomain indicates if this project is a domain.
+	IsDomain *bool `json:"is_domain,omitempty"`
+
+	// Name is the name of the project.
+	Name string `json:"name,omitempty"`
+
+	// ParentID specifies the parent project of this new project.
+	ParentID string `json:"parent_id,omitempty"`
+
+	// Description is the description of the project.
+	Description string `json:"description,omitempty"`
+}
+
+// ToUpdateCreateMap formats a UpdateOpts into an update request.
+func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "project")
+}
+
+// Update modifies the attributes of a project.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToProjectUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Patch(updateURL(client, id), b, &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 5a29657..a441e7f 100644
--- a/openstack/identity/v3/projects/results.go
+++ b/openstack/identity/v3/projects/results.go
@@ -24,6 +24,11 @@
 	gophercloud.ErrResult
 }
 
+// UpdateResult temporarily contains the response from the Update call.
+type UpdateResult struct {
+	projectResult
+}
+
 // Project is a base unit of ownership.
 type Project struct {
 	// IsDomain indicates whether the project is a domain.
diff --git a/openstack/identity/v3/projects/testing/fixtures.go b/openstack/identity/v3/projects/testing/fixtures.go
index 96a4e41..caa5567 100644
--- a/openstack/identity/v3/projects/testing/fixtures.go
+++ b/openstack/identity/v3/projects/testing/fixtures.go
@@ -65,6 +65,31 @@
 }
 `
 
+// UpdateRequest provides the input to an Update request.
+const UpdateRequest = `
+{
+  "project": {
+		"description": "The team that is bright red",
+		"name": "Bright Red Team"
+  }
+}
+`
+
+// UpdateOutput provides an Update response.
+const UpdateOutput = `
+{
+  "project": {
+		"is_domain": false,
+		"description": "The team that is bright red",
+		"domain_id": "default",
+		"enabled": true,
+		"id": "1234",
+		"name": "Bright Red Team",
+		"parent_id": null
+  }
+}
+`
+
 // RedTeam is a Project fixture.
 var RedTeam = projects.Project{
 	IsDomain:    false,
@@ -87,6 +112,17 @@
 	ParentID:    "",
 }
 
+// UpdatedRedTeam is a Project Fixture.
+var UpdatedRedTeam = projects.Project{
+	IsDomain:    false,
+	Description: "The team that is bright red",
+	DomainID:    "default",
+	Enabled:     true,
+	ID:          "1234",
+	Name:        "Bright Red Team",
+	ParentID:    "",
+}
+
 // ExpectedProjectSlice is the slice of projects expected to be returned from ListOutput.
 var ExpectedProjectSlice = []projects.Project{RedTeam, BlueTeam}
 
@@ -141,3 +177,16 @@
 		w.WriteHeader(http.StatusNoContent)
 	})
 }
+
+// HandleUpdateProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that tests project updates.
+func HandleUpdateProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, UpdateRequest)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
diff --git a/openstack/identity/v3/projects/testing/requests_test.go b/openstack/identity/v3/projects/testing/requests_test.go
index 2b2e27c..5782480 100644
--- a/openstack/identity/v3/projects/testing/requests_test.go
+++ b/openstack/identity/v3/projects/testing/requests_test.go
@@ -62,3 +62,18 @@
 	res := projects.Delete(client.ServiceClient(), "1234")
 	th.AssertNoErr(t, res.Err)
 }
+
+func TestUpdateProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateProjectSuccessfully(t)
+
+	updateOpts := projects.UpdateOpts{
+		Name:        "Bright Red Team",
+		Description: "The team that is bright red",
+	}
+
+	actual, err := projects.Update(client.ServiceClient(), "1234", updateOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, UpdatedRedTeam, *actual)
+}
diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go
index c9f0d58..e26cf36 100644
--- a/openstack/identity/v3/projects/urls.go
+++ b/openstack/identity/v3/projects/urls.go
@@ -17,3 +17,7 @@
 func deleteURL(client *gophercloud.ServiceClient, projectID string) string {
 	return client.ServiceURL("projects", projectID)
 }
+
+func updateURL(client *gophercloud.ServiceClient, projectID string) string {
+	return client.ServiceURL("projects", projectID)
+}