Identity v3 Projects Create (#165)

* Identity v3 Projects Create

* Removing unused createErr function
diff --git a/acceptance/openstack/identity/v3/identity.go b/acceptance/openstack/identity/v3/identity.go
index 2969bc9..5bed8a4 100644
--- a/acceptance/openstack/identity/v3/identity.go
+++ b/acceptance/openstack/identity/v3/identity.go
@@ -3,12 +3,41 @@
 import (
 	"testing"
 
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
 	"github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints"
 	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
 	"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
 	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
 )
 
+// CreateProject will create a project with a random name.
+// It takes an optional createOpts parameter since creating a project
+// has so many options. An error will be returned if the project was
+// unable to be created.
+func CreateProject(t *testing.T, client *gophercloud.ServiceClient, c *projects.CreateOpts) (*projects.Project, error) {
+	name := tools.RandomString("ACPTTEST", 8)
+	t.Logf("Attempting to create project: %s", name)
+
+	var createOpts projects.CreateOpts
+	if c != nil {
+		createOpts = *c
+	} else {
+		createOpts = projects.CreateOpts{}
+	}
+
+	createOpts.Name = name
+
+	project, err := projects.Create(client, createOpts).Extract()
+	if err != nil {
+		return project, err
+	}
+
+	t.Logf("Successfully created project %s with ID %s", name, project.ID)
+
+	return project, nil
+}
+
 // PrintEndpoint will print an endpoint and all of its attributes.
 func PrintEndpoint(t *testing.T, endpoint *endpoints.Endpoint) {
 	t.Logf("ID: %s", endpoint.ID)
diff --git a/acceptance/openstack/identity/v3/projects_test.go b/acceptance/openstack/identity/v3/projects_test.go
index ab982b2..4b2e539 100644
--- a/acceptance/openstack/identity/v3/projects_test.go
+++ b/acceptance/openstack/identity/v3/projects_test.go
@@ -59,3 +59,72 @@
 
 	PrintProject(t, p)
 }
+
+func TestProjectsCRUD(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	project, err := CreateProject(t, client, nil)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+
+	PrintProject(t, project)
+}
+
+func TestProjectsDomain(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	var iTrue = true
+	createOpts := projects.CreateOpts{
+		IsDomain: &iTrue,
+	}
+
+	projectDomain, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+
+	PrintProject(t, projectDomain)
+
+	createOpts = projects.CreateOpts{
+		DomainID: projectDomain.ID,
+	}
+
+	project, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+
+	PrintProject(t, project)
+}
+
+func TestProjectsNested(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	projectMain, err := CreateProject(t, client, nil)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+
+	PrintProject(t, projectMain)
+
+	createOpts := projects.CreateOpts{
+		ParentID: projectMain.ID,
+	}
+
+	project, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+
+	PrintProject(t, project)
+}
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
index 73e4c4c..07b9371 100644
--- a/openstack/identity/v3/projects/requests.go
+++ b/openstack/identity/v3/projects/requests.go
@@ -83,3 +83,46 @@
 	})
 	return
 }
+
+// CreateOptsBuilder allows extensions to add additional parameters to
+// the Create request.
+type CreateOptsBuilder interface {
+	ToProjectCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts allows you to modify the details included in the Create request.
+type CreateOpts 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,required"`
+
+	// 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"`
+}
+
+// ToProjectCreateMap formats a CreateOpts into a create request.
+func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "project")
+}
+
+// Create creates a new Project.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToProjectCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), &b, &r.Body, nil)
+	return
+}
diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go
index 2ec05a8..c828ff8 100644
--- a/openstack/identity/v3/projects/results.go
+++ b/openstack/identity/v3/projects/results.go
@@ -14,6 +14,11 @@
 	projectResult
 }
 
+// CreateResult temporarily contains the reponse from the Create call.
+type CreateResult 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 cb10313..0c839b8 100644
--- a/openstack/identity/v3/projects/testing/fixtures.go
+++ b/openstack/identity/v3/projects/testing/fixtures.go
@@ -55,6 +55,16 @@
 }
 `
 
+// CreateRequest provides the input to a Create request.
+const CreateRequest = `
+{
+  "project": {
+		"description": "The team that is red",
+		"name": "Red Team"
+  }
+}
+`
+
 // RedTeam is a Project fixture.
 var RedTeam = projects.Project{
 	IsDomain:    false,
@@ -107,3 +117,16 @@
 		fmt.Fprintf(w, GetOutput)
 	})
 }
+
+// HandleCreateProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that tests project creation.
+func HandleCreateProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, CreateRequest)
+
+		w.WriteHeader(http.StatusCreated)
+		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 eeb2049..daed4eb 100644
--- a/openstack/identity/v3/projects/testing/requests_test.go
+++ b/openstack/identity/v3/projects/testing/requests_test.go
@@ -38,3 +38,18 @@
 	th.AssertNoErr(t, err)
 	th.CheckDeepEquals(t, RedTeam, *actual)
 }
+
+func TestCreateProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateProjectSuccessfully(t)
+
+	createOpts := projects.CreateOpts{
+		Name:        "Red Team",
+		Description: "The team that is red",
+	}
+
+	actual, err := projects.Create(client.ServiceClient(), createOpts).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 5effbfb..7be9dc2 100644
--- a/openstack/identity/v3/projects/urls.go
+++ b/openstack/identity/v3/projects/urls.go
@@ -9,3 +9,7 @@
 func getURL(client *gophercloud.ServiceClient, projectID string) string {
 	return client.ServiceURL("projects", projectID)
 }
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("projects")
+}