Merge pull request #199 from jrperritt/storage-v1-cleanup

Object Storage Miscellaneous
diff --git a/acceptance/openstack/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
similarity index 99%
rename from acceptance/openstack/identity_test.go
rename to acceptance/openstack/identity/v2/identity_test.go
index 8b6035d..ff4c9cd 100644
--- a/acceptance/openstack/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -1,6 +1,6 @@
 // +build acceptance
 
-package openstack
+package v2
 
 import (
 	"fmt"
diff --git a/acceptance/openstack/objectstorage/v1/accounts_test.go b/acceptance/openstack/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..6768927
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+)
+
+func TestAccounts(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client, err := newClient()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	// Update an account's metadata.
+	err = accounts.Update(client, accounts.UpdateOpts{
+		Metadata: metadata,
+	})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Defer the deletion of the metadata set above.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		err = accounts.Update(client, accounts.UpdateOpts{
+			Metadata: tempMap,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+
+	// Retrieve account metadata.
+	gr, err := accounts.Get(client, accounts.GetOpts{})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Extract the custom metadata from the 'Get' response.
+	am := accounts.ExtractMetadata(gr)
+	for k := range metadata {
+		if am[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+			return
+		}
+	}
+}
diff --git a/acceptance/openstack/objectstorage/v1/common.go b/acceptance/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..08065a4
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/common.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/utils"
+	"os"
+)
+
+var metadata = map[string]string{"gopher": "cloud"}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	ao, err := utils.AuthOptions()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
diff --git a/acceptance/openstack/objectstorage/v1/containers_test.go b/acceptance/openstack/objectstorage/v1/containers_test.go
new file mode 100644
index 0000000..b541307
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,108 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// numContainers is the number of containers to create for testing.
+var numContainers = 2
+
+func TestContainers(t *testing.T) {
+	// Create a new client to execute the HTTP requests. See common.go for newClient body.
+	client, err := newClient()
+	if err != nil {
+		t.Error(err)
+	}
+
+	// Create a slice of random container names.
+	cNames := make([]string, numContainers)
+	for i := 0; i < numContainers; i++ {
+		cNames[i] = tools.RandomString("gophercloud-test-container-", 8)
+	}
+
+	// Create numContainers containers.
+	for i := 0; i < len(cNames); i++ {
+		_, err := containers.Create(client, cNames[i], nil).ExtractHeaders()
+		if err != nil {
+			t.Error(err)
+		}
+	}
+	// Delete the numContainers containers after function completion.
+	defer func() {
+		for i := 0; i < len(cNames); i++ {
+			_, err = containers.Delete(client, cNames[i]).ExtractHeaders()
+			if err != nil {
+				t.Error(err)
+			}
+		}
+	}()
+
+	// List the numContainer names that were just created. To just list those,
+	// the 'prefix' parameter is used.
+	err = containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+		containerList, err := containers.ExtractInfo(page)
+		if err != nil {
+			t.Error(err)
+		}
+		for _, n := range containerList {
+			t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+				n.Name, n.Count, n.Bytes)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	// List the info for the numContainer containers that were created.
+	err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+		containerList, err := containers.ExtractNames(page)
+		if err != nil {
+			return false, err
+		}
+		for _, n := range containerList {
+			t.Logf("Container: Name [%s]", n)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	// Update one of the numContainer container metadata.
+	_, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+	if err != nil {
+		t.Error(err)
+	}
+	// After the tests are done, delete the metadata that was set.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		_, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+		if err != nil {
+			t.Error(err)
+		}
+	}()
+
+	// Retrieve a container's metadata.
+	cm, err := containers.Get(client, cNames[0]).ExtractMetadata()
+	if err != nil {
+		t.Error(err)
+	}
+	for k := range metadata {
+		if cm[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+		}
+	}
+}
diff --git a/acceptance/openstack/objectstorage/v1/objects_test.go b/acceptance/openstack/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..5a63a4c
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,162 @@
+// +build acceptance
+
+package v1
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// numObjects is the number of objects to create for testing.
+var numObjects = 2
+
+func TestObjects(t *testing.T) {
+	// Create a provider client for executing the HTTP request.
+	// See common.go for more information.
+	client, err := newClient()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	// Make a slice of length numObjects to hold the random object names.
+	oNames := make([]string, numObjects)
+	for i := 0; i < len(oNames); i++ {
+		oNames[i] = tools.RandomString("test-object-", 8)
+	}
+
+	// Create a container to hold the test objects.
+	cName := tools.RandomString("test-container-", 8)
+	_, err = containers.Create(client, cName, nil).ExtractHeaders()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Defer deletion of the container until after testing.
+	defer func() {
+		_, err = containers.Delete(client, cName).ExtractHeaders()
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+
+	// Create a slice of buffers to hold the test object content.
+	oContents := make([]*bytes.Buffer, numObjects)
+	for i := 0; i < numObjects; i++ {
+		oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
+		_, err = objects.Create(client, cName, oNames[i], oContents[i], nil).ExtractHeaders()
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}
+	// Delete the objects after testing.
+	defer func() {
+		for i := 0; i < numObjects; i++ {
+			_, err = objects.Delete(client, cName, oNames[i], nil).ExtractHeaders()
+		}
+	}()
+
+	ons := make([]string, 0, len(oNames))
+	err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+		names, err := objects.ExtractNames(page)
+		if err != nil {
+			return false, err
+		}
+		ons = append(ons, names...)
+
+		return true, nil
+	})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if len(ons) != len(oNames) {
+		t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
+		return
+	}
+
+	ois := make([]objects.Object, 0, len(oNames))
+	err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+		info, err := objects.ExtractInfo(page)
+		if err != nil {
+			return false, nil
+		}
+
+		ois = append(ois, info...)
+
+		return true, nil
+	})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if len(ois) != len(oNames) {
+		t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
+		return
+	}
+
+	// Copy the contents of one object to another.
+	_, err = objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}).ExtractHeaders()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	// Download one of the objects that was created above.
+	o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Download the another object that was create above.
+	o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Compare the two object's contents to test that the copy worked.
+	if string(o2Content) != string(o1Content) {
+		t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
+		return
+	}
+
+	// Update an object's metadata.
+	_, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// Delete the object's metadata after testing.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		_, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+
+	// Retrieve an object's metadata.
+	om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	for k := range metadata {
+		if om[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+			return
+		}
+	}
+}
diff --git a/acceptance/openstack/storage_test.go b/acceptance/openstack/storage_test.go
deleted file mode 100644
index e7907d1..0000000
--- a/acceptance/openstack/storage_test.go
+++ /dev/null
@@ -1,364 +0,0 @@
-// +build acceptance
-
-package openstack
-
-import (
-	"bytes"
-	"os"
-	"strings"
-	"testing"
-
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/acceptance/tools"
-	"github.com/rackspace/gophercloud/openstack"
-	"github.com/rackspace/gophercloud/openstack/storage/v1/accounts"
-	"github.com/rackspace/gophercloud/openstack/storage/v1/containers"
-	"github.com/rackspace/gophercloud/openstack/storage/v1/objects"
-	"github.com/rackspace/gophercloud/openstack/utils"
-	"github.com/rackspace/gophercloud/pagination"
-)
-
-var metadata = map[string]string{"gopher": "cloud"}
-var numContainers = 2
-var numObjects = 2
-
-func newClient() (*gophercloud.ServiceClient, error) {
-	ao, err := utils.AuthOptions()
-	if err != nil {
-		return nil, err
-	}
-
-	client, err := openstack.AuthenticatedClient(ao)
-	if err != nil {
-		return nil, err
-	}
-
-	return openstack.NewStorageV1(client, gophercloud.EndpointOpts{
-		Region: os.Getenv("OS_REGION_NAME"),
-	})
-}
-
-func TestAccount(t *testing.T) {
-	client, err := newClient()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	err = accounts.Update(client, accounts.UpdateOpts{
-		Metadata: metadata,
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	defer func() {
-		tempMap := make(map[string]string)
-		for k := range metadata {
-			tempMap[k] = ""
-		}
-		err = accounts.Update(client, accounts.UpdateOpts{
-			Metadata: tempMap,
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}()
-
-	gr, err := accounts.Get(client, accounts.GetOpts{})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	am := accounts.ExtractMetadata(gr)
-	for k := range metadata {
-		if am[k] != metadata[strings.Title(k)] {
-			t.Errorf("Expected custom metadata with key: %s", k)
-			return
-		}
-	}
-}
-
-func TestContainers(t *testing.T) {
-	client, err := newClient()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	cNames := make([]string, numContainers)
-	for i := 0; i < numContainers; i++ {
-		cNames[i] = tools.RandomString("test-container-", 8)
-	}
-
-	for i := 0; i < len(cNames); i++ {
-		_, err := containers.Create(client, containers.CreateOpts{
-			Name: cNames[i],
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}
-	defer func() {
-		for i := 0; i < len(cNames); i++ {
-			err = containers.Delete(client, containers.DeleteOpts{
-				Name: cNames[i],
-			})
-			if err != nil {
-				t.Error(err)
-				return
-			}
-		}
-	}()
-
-	cns := make([]string, 0, numContainers)
-	pager := containers.List(client, containers.ListOpts{Full: false})
-	err = pager.EachPage(func(page pagination.Page) (bool, error) {
-		names, err := containers.ExtractNames(page)
-		if err != nil {
-			return false, err
-		}
-
-		cns = append(cns, names...)
-
-		return true, nil
-	})
-	if err != nil {
-		t.Fatal(err)
-		return
-	}
-
-	if len(cns) != len(cNames) {
-		t.Errorf("Expected %d names and got %d", len(cNames), len(cns))
-		return
-	}
-
-	cis := make([]containers.Container, 0, numContainers)
-	pager = containers.List(client, containers.ListOpts{Full: true})
-	err = pager.EachPage(func(page pagination.Page) (bool, error) {
-		cisPage, err := containers.ExtractInfo(page)
-		if err != nil {
-			return false, err
-		}
-
-		cis = append(cis, cisPage...)
-
-		return true, nil
-	})
-
-	if len(cis) != len(cNames) {
-		t.Errorf("Expected %d containers and got %d", len(cNames), len(cis))
-		return
-	}
-
-	err = containers.Update(client, containers.UpdateOpts{
-		Name:     cNames[0],
-		Metadata: metadata,
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	defer func() {
-		tempMap := make(map[string]string)
-		for k := range metadata {
-			tempMap[k] = ""
-		}
-		err = containers.Update(client, containers.UpdateOpts{
-			Name:     cNames[0],
-			Metadata: tempMap,
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}()
-
-	gr, err := containers.Get(client, containers.GetOpts{})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	cm := containers.ExtractMetadata(gr)
-	for k := range metadata {
-		if cm[k] != metadata[strings.Title(k)] {
-			t.Errorf("Expected custom metadata with key: %s", k)
-			return
-		}
-	}
-}
-
-func TestObjects(t *testing.T) {
-	client, err := newClient()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	oNames := make([]string, numObjects)
-	for i := 0; i < len(oNames); i++ {
-		oNames[i] = tools.RandomString("test-object-", 8)
-	}
-
-	cName := tools.RandomString("test-container-", 8)
-	_, err = containers.Create(client, containers.CreateOpts{
-		Name: cName,
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	defer func() {
-		err = containers.Delete(client, containers.DeleteOpts{
-			Name: cName,
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}()
-
-	oContents := make([]*bytes.Buffer, numObjects)
-	for i := 0; i < numObjects; i++ {
-		oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
-		err = objects.Create(client, objects.CreateOpts{
-			Container: cName,
-			Name:      oNames[i],
-			Content:   oContents[i],
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}
-	defer func() {
-		for i := 0; i < numObjects; i++ {
-			err = objects.Delete(client, objects.DeleteOpts{
-				Container: cName,
-				Name:      oNames[i],
-			})
-		}
-	}()
-
-	pager := objects.List(client, objects.ListOpts{Full: false, Container: cName})
-	ons := make([]string, 0, len(oNames))
-	err = pager.EachPage(func(page pagination.Page) (bool, error) {
-		names, err := objects.ExtractNames(page)
-		if err != nil {
-			return false, err
-		}
-		ons = append(ons, names...)
-
-		return true, nil
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if len(ons) != len(oNames) {
-		t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
-		return
-	}
-
-	pager = objects.List(client, objects.ListOpts{Full: true, Container: cName})
-	ois := make([]objects.Object, 0, len(oNames))
-	err = pager.EachPage(func(page pagination.Page) (bool, error) {
-		info, err := objects.ExtractInfo(page)
-		if err != nil {
-			return false, nil
-		}
-
-		ois = append(ois, info...)
-
-		return true, nil
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if len(ois) != len(oNames) {
-		t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
-		return
-	}
-
-	err = objects.Copy(client, objects.CopyOpts{
-		Container:    cName,
-		Name:         oNames[0],
-		NewContainer: cName,
-		NewName:      oNames[1],
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	dr, err := objects.Download(client, objects.DownloadOpts{
-		Container: cName,
-		Name:      oNames[1],
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	o2Content, err := objects.ExtractContent(dr)
-	if err != nil {
-		t.Error(err)
-	}
-	dr, err = objects.Download(client, objects.DownloadOpts{
-		Container: cName,
-		Name:      oNames[0],
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	o1Content, err := objects.ExtractContent(dr)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if string(o2Content) != string(o1Content) {
-		t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
-		return
-	}
-
-	err = objects.Update(client, objects.UpdateOpts{
-		Container: cName,
-		Name:      oNames[0],
-		Metadata:  metadata,
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	defer func() {
-		tempMap := make(map[string]string)
-		for k := range metadata {
-			tempMap[k] = ""
-		}
-		err = objects.Update(client, objects.UpdateOpts{
-			Container: cName,
-			Name:      oNames[0],
-			Metadata:  tempMap,
-		})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-	}()
-
-	gr, err := objects.Get(client, objects.GetOpts{})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	om := objects.ExtractMetadata(gr)
-	for k := range metadata {
-		if om[k] != metadata[strings.Title(k)] {
-			t.Errorf("Expected custom metadata with key: %s", k)
-			return
-		}
-	}
-}
diff --git a/openstack/storage/v1/accounts/accounts.go b/openstack/objectstorage/v1/accounts/accounts.go
similarity index 100%
rename from openstack/storage/v1/accounts/accounts.go
rename to openstack/objectstorage/v1/accounts/accounts.go
diff --git a/openstack/objectstorage/v1/accounts/accounts_test.go b/openstack/objectstorage/v1/accounts/accounts_test.go
new file mode 100644
index 0000000..2c2a84a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/accounts_test.go
@@ -0,0 +1,19 @@
+package accounts
+
+import (
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestExtractAccountMetadata(t *testing.T) {
+	getResult := &http.Response{}
+
+	expected := map[string]string{}
+
+	actual := ExtractMetadata(getResult)
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
+	}
+}
diff --git a/openstack/objectstorage/v1/accounts/doc.go b/openstack/objectstorage/v1/accounts/doc.go
new file mode 100644
index 0000000..5c94e1a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/doc.go
@@ -0,0 +1,5 @@
+/* The accounts package defines operations performed on an object-storage account.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_account_services
+*/
+package accounts
diff --git a/openstack/storage/v1/accounts/requests.go b/openstack/objectstorage/v1/accounts/requests.go
similarity index 87%
rename from openstack/storage/v1/accounts/requests.go
rename to openstack/objectstorage/v1/accounts/requests.go
index d5b623a..c738573 100644
--- a/openstack/storage/v1/accounts/requests.go
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -22,7 +22,7 @@
 		h["X-Account-Meta-"+k] = v
 	}
 
-	_, err := perigee.Request("POST", getAccountURL(c), perigee.Options{
+	_, err := perigee.Request("POST", accountURL(c), perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{204},
 	})
@@ -38,7 +38,7 @@
 		h[k] = v
 	}
 
-	resp, err := perigee.Request("HEAD", getAccountURL(c), perigee.Options{
+	resp, err := perigee.Request("HEAD", accountURL(c), perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/objectstorage/v1/accounts/requests_test.go b/openstack/objectstorage/v1/accounts/requests_test.go
new file mode 100644
index 0000000..e1450d4
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -0,0 +1,55 @@
+package accounts
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenId = "abcabcabcabc"
+
+var metadata = map[string]string{"gophercloud-test": "accounts"}
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestUpdateAccount(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	err := Update(client, UpdateOpts{Metadata: metadata})
+	if err != nil {
+		t.Fatalf("Unable to update account: %v", err)
+	}
+}
+
+func TestGetAccount(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "HEAD")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Get(client, GetOpts{})
+	if err != nil {
+		t.Fatalf("Unable to get account metadata: %v", err)
+	}
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
new file mode 100644
index 0000000..53b1343
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -0,0 +1,8 @@
+package accounts
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI for making Account requests.
+func accountURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
diff --git a/openstack/objectstorage/v1/accounts/urls_test.go b/openstack/objectstorage/v1/accounts/urls_test.go
new file mode 100644
index 0000000..f127a5e
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls_test.go
@@ -0,0 +1,17 @@
+package accounts
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+	client := gophercloud.ServiceClient{
+		Endpoint: "http://localhost:5000/v3/",
+	}
+	url := accountURL(&client)
+	if url != "http://localhost:5000/v3/" {
+		t.Errorf("Unexpected service URL generated: [%s]", url)
+	}
+}
diff --git a/openstack/objectstorage/v1/containers/doc.go b/openstack/objectstorage/v1/containers/doc.go
new file mode 100644
index 0000000..9b6ac17
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/doc.go
@@ -0,0 +1,5 @@
+/* The containers package defines operations performed on an object-storage container.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_container_services
+*/
+package containers
diff --git a/openstack/objectstorage/v1/containers/requests.go b/openstack/objectstorage/v1/containers/requests.go
new file mode 100644
index 0000000..d772a43
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,161 @@
+package containers
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts is a structure that holds options for listing containers.
+type ListOpts struct {
+	Full      bool
+	Limit     int     `q:"limit"`
+	Marker    string  `q:"marker"`
+	EndMarker string  `q:"end_marker"`
+	Format    string  `q:"format"`
+	Prefix    string  `q:"prefix"`
+	Delimiter [1]byte `q:"delimiter"`
+}
+
+// List is a function that retrieves containers associated with the account as well as account
+// metadata. It returns a pager which can be iterated with the EachPage function.
+func List(c *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+	var headers map[string]string
+
+	url := accountURL(c)
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query.String()
+
+		if !opts.Full {
+			headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+		}
+	} else {
+		headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+	}
+
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		p := ContainerPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	pager.Headers = headers
+	return pager
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+	Metadata          map[string]string
+	ContainerRead     string `h:"X-Container-Read"`
+	ContainerSyncTo   string `h:"X-Container-Sync-To"`
+	ContainerSyncKey  string `h:"X-Container-Sync-Key"`
+	ContainerWrite    string `h:"X-Container-Write"`
+	ContentType       string `h:"Content-Type"`
+	DetectContentType bool   `h:"X-Detect-Content-Type"`
+	IfNoneMatch       string `h:"If-None-Match"`
+	VersionsLocation  string `h:"X-Versions-Location"`
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts *CreateOpts) CreateResult {
+	var res CreateResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := gophercloud.BuildHeaders(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		for k, v := range opts.Metadata {
+			h["X-Container-Meta-"+k] = v
+		}
+	}
+
+	resp, err := perigee.Request("PUT", containerURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult {
+	var res DeleteResult
+	resp, err := perigee.Request("DELETE", containerURL(c, containerName), perigee.Options{
+		MoreHeaders: c.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
+// container's metadata.
+type UpdateOpts struct {
+	Metadata               map[string]string
+	ContainerRead          string `h:"X-Container-Read"`
+	ContainerSyncTo        string `h:"X-Container-Sync-To"`
+	ContainerSyncKey       string `h:"X-Container-Sync-Key"`
+	ContainerWrite         string `h:"X-Container-Write"`
+	ContentType            string `h:"Content-Type"`
+	DetectContentType      bool   `h:"X-Detect-Content-Type"`
+	RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+	VersionsLocation       string `h:"X-Versions-Location"`
+}
+
+// Update is a function that creates, updates, or deletes a container's metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts *UpdateOpts) UpdateResult {
+	var res UpdateResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := gophercloud.BuildHeaders(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		for k, v := range opts.Metadata {
+			h["X-Container-Meta-"+k] = v
+		}
+	}
+
+	resp, err := perigee.Request("POST", containerURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
+	var res GetResult
+	resp, err := perigee.Request("HEAD", containerURL(c, containerName), perigee.Options{
+		MoreHeaders: c.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
diff --git a/openstack/objectstorage/v1/containers/requests_test.go b/openstack/objectstorage/v1/containers/requests_test.go
new file mode 100644
index 0000000..1ab09a9
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -0,0 +1,206 @@
+package containers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+	tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"gophercloud-test": "containers"}
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestListContainerInfo(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `[
+				{
+					"count": 0,
+					"bytes": 0,
+					"name": "janeausten"
+				},
+				{
+					"count": 1,
+					"bytes": 14,
+					"name": "marktwain"
+				}
+			]`)
+		case "marktwain":
+			fmt.Fprintf(w, `[]`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	count := 0
+	List(client, &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		if err != nil {
+			t.Errorf("Failed to extract container info: %v", err)
+			return false, err
+		}
+
+		expected := []Container{
+			Container{
+				Count: 0,
+				Bytes: 0,
+				Name:  "janeausten",
+			},
+			Container{
+				Count: 1,
+				Bytes: 14,
+				Name:  "marktwain",
+			},
+		}
+
+		testhelper.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestListContainerNames(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+		w.Header().Set("Content-Type", "text/plain")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, "janeausten\nmarktwain\n")
+		case "marktwain":
+			fmt.Fprintf(w, ``)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	count := 0
+	List(client, &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		expected := []string{"janeausten", "marktwain"}
+
+		testhelper.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreateContainer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "PUT")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Create(client, "testContainer", nil).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error creating container: %v", err)
+	}
+}
+
+func TestDeleteContainer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "DELETE")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Delete(client, "testContainer").ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error deleting container: %v", err)
+	}
+}
+
+func TestUpateContainer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Update(client, "testContainer", nil).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error updating container metadata: %v", err)
+	}
+}
+
+func TestGetContainer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "HEAD")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Get(client, "testContainer").ExtractMetadata()
+	if err != nil {
+		t.Fatalf("Unexpected error getting container metadata: %v", err)
+	}
+}
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
new file mode 100644
index 0000000..80425bd
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,137 @@
+package containers
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Container struct {
+	Bytes int    `json:"bytes" mapstructure:"bytes"`
+	Count int    `json:"count" mapstructure:"count"`
+	Name  string `json:"name" mapstructure:"name"`
+}
+
+// ListResult is a *http.Response that is returned from a call to the List function.
+type ContainerPage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no container names.
+func (r ContainerPage) IsEmpty() (bool, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return true, err
+	}
+	return len(names) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ContainerPage) LastMarker() (string, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return "", err
+	}
+	if len(names) == 0 {
+		return "", nil
+	}
+	return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a ListResult and returns the containers' information.
+func ExtractInfo(page pagination.Page) ([]Container, error) {
+	untyped := page.(ContainerPage).Body.([]interface{})
+	results := make([]Container, len(untyped))
+	for index, each := range untyped {
+		container := each.(map[string]interface{})
+		err := mapstructure.Decode(container, &results[index])
+		if err != nil {
+			return results, err
+		}
+	}
+	return results, nil
+}
+
+// ExtractNames is a function that takes a ListResult and returns the containers' names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	casted := page.(ContainerPage)
+	ct := casted.Header.Get("Content-Type")
+
+	switch {
+	case strings.HasPrefix(ct, "application/json"):
+		parsed, err := ExtractInfo(page)
+		if err != nil {
+			return nil, err
+		}
+
+		names := make([]string, 0, len(parsed))
+		for _, container := range parsed {
+			names = append(names, container.Name)
+		}
+		return names, nil
+	case strings.HasPrefix(ct, "text/plain"):
+		names := make([]string, 0, 50)
+
+		body := string(page.(ContainerPage).Body.([]uint8))
+		for _, name := range strings.Split(body, "\n") {
+			if len(name) > 0 {
+				names = append(names, name)
+			}
+		}
+
+		return names, nil
+	default:
+		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+	}
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	Resp *http.Response
+	Err  error
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range gr.Resp.Header {
+		if strings.HasPrefix(k, "X-Container-Meta-") {
+			key := strings.TrimPrefix(k, "X-Container-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+type commonResult struct {
+	Resp *http.Response
+	Err  error
+}
+
+func (cr commonResult) ExtractHeaders() (http.Header, error) {
+	var headers http.Header
+	if cr.Err != nil {
+		return headers, cr.Err
+	}
+
+	return cr.Resp.Header, nil
+}
+
+type CreateResult struct {
+	commonResult
+}
+
+type UpdateResult struct {
+	commonResult
+}
+
+type DeleteResult struct {
+	commonResult
+}
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
new file mode 100644
index 0000000..2a06f95
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -0,0 +1,13 @@
+package containers
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI used to list Containers.
+func accountURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
diff --git a/openstack/objectstorage/v1/containers/urls_test.go b/openstack/objectstorage/v1/containers/urls_test.go
new file mode 100644
index 0000000..da37bf6
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls_test.go
@@ -0,0 +1,29 @@
+package containers
+
+import (
+	"testing"
+	"github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+	client := gophercloud.ServiceClient{
+		Endpoint: "http://localhost:5000/v1/",
+	}
+	expected := "http://localhost:5000/v1/"
+	actual := accountURL(&client)
+	if actual != expected {
+		t.Errorf("Unexpected service URL generated: [%s]", actual)
+	}
+
+}
+
+func TestContainerURL(t *testing.T) {
+	client := gophercloud.ServiceClient{
+		Endpoint: "http://localhost:5000/v1/",
+	}
+	expected := "http://localhost:5000/v1/testContainer"
+	actual := containerURL(&client, "testContainer")
+	if actual != expected {
+		t.Errorf("Unexpected service URL generated: [%s]", actual)
+	}
+}
diff --git a/openstack/objectstorage/v1/objects/doc.go b/openstack/objectstorage/v1/objects/doc.go
new file mode 100644
index 0000000..2a7461b
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/doc.go
@@ -0,0 +1,5 @@
+/* The objects package defines operations performed on an object-storage object.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_object_services
+*/
+package objects
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
new file mode 100644
index 0000000..bc21496
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,317 @@
+package objects
+
+import (
+	"fmt"
+	"io"
+	"time"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+	Full      bool
+	Limit     int     `q:"limit"`
+	Marker    string  `q:"marker"`
+	EndMarker string  `q:"end_marker"`
+	Format    string  `q:"format"`
+	Prefix    string  `q:"prefix"`
+	Delimiter [1]byte `q:"delimiter"`
+	Path      string  `q:"path"`
+}
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *gophercloud.ServiceClient, containerName string, opts *ListOpts) pagination.Pager {
+	var headers map[string]string
+
+	url := containerURL(c, containerName)
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			fmt.Printf("Error building query string: %v", err)
+			return pagination.Pager{Err: err}
+		}
+		url += query.String()
+
+		if !opts.Full {
+			headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+		}
+	} else {
+		headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+	}
+
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		p := ObjectPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	pager.Headers = headers
+	return pager
+}
+
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+	IfMatch           string    `h:"If-Match"`
+	IfModifiedSince   time.Time `h:"If-Modified-Since"`
+	IfNoneMatch       string    `h:"If-None-Match"`
+	IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
+	Range             string    `h:"Range"`
+	Expires           string    `q:"expires"`
+	MultipartManifest string    `q:"multipart-manifest"`
+	Signature         string    `q:"signature"`
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the ExtractContent
+// function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts *DownloadOpts) DownloadResult {
+	var res DownloadResult
+
+	url := objectURL(c, containerName, objectName)
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := gophercloud.BuildHeaders(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		url += query.String()
+	}
+
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{200},
+	})
+	res.Err = err
+	res.Resp = &resp.HttpResponse
+	return res
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentLength      int    `h:"Content-Length"`
+	ContentType        string `h:"Content-Type"`
+	CopyFrom           string `h:"X-Copy-From"`
+	DeleteAfter        int    `h:"X-Delete-After"`
+	DeleteAt           int    `h:"X-Delete-At"`
+	DetectContentType  string `h:"X-Detect-Content-Type"`
+	ETag               string `h:"ETag"`
+	IfNoneMatch        string `h:"If-None-Match"`
+	ObjectManifest     string `h:"X-Object-Manifest"`
+	TransferEncoding   string `h:"Transfer-Encoding"`
+	Expires            string `q:"expires"`
+	MultipartManifest  string `q:"multiple-manifest"`
+	Signature          string `q:"signature"`
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts *CreateOpts) CreateResult {
+	var res CreateResult
+	var reqBody []byte
+
+	url := objectURL(c, containerName, objectName)
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := gophercloud.BuildHeaders(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		for k, v := range opts.Metadata {
+			h["X-Object-Meta-"+k] = v
+		}
+
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		url += query.String()
+	}
+
+	if content != nil {
+		reqBody = make([]byte, 0)
+		_, err := content.Read(reqBody)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+	}
+
+	resp, err := perigee.Request("PUT", url, perigee.Options{
+		ReqBody:     reqBody,
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to another.
+type CopyOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentType        string `h:"Content-Type"`
+	Destination        string `h:"Destination,required"`
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts *CopyOpts) CopyResult {
+	var res CopyResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts == nil {
+		res.Err = fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+		return res
+	}
+	headers, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	for k, v := range headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	url := objectURL(c, containerName, objectName)
+	resp, err := perigee.Request("COPY", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	res.Resp = &resp.HttpResponse
+	return res
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+	MultipartManifest string `q:"multipart-manifest"`
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts *DeleteOpts) DeleteResult {
+	var res DeleteResult
+	url := objectURL(c, containerName, objectName)
+
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		url += query.String()
+	}
+
+	resp, err := perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: c.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+	Expires   string `q:"expires"`
+	Signature string `q:"signature"`
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts *GetOpts) GetResult {
+	var res GetResult
+	url := objectURL(c, containerName, objectName)
+
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		url += query.String()
+	}
+
+	resp, err := perigee.Request("HEAD", url, perigee.Options{
+		MoreHeaders: c.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 204},
+	})
+	res.Err = err
+	res.Resp = &resp.HttpResponse
+	return res
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentType        string `h:"Content-Type"`
+	DeleteAfter        int    `h:"X-Delete-After"`
+	DeleteAt           int    `h:"X-Delete-At"`
+	DetectContentType  bool   `h:"X-Detect-Content-Type"`
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts *UpdateOpts) UpdateResult {
+	var res UpdateResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := gophercloud.BuildHeaders(opts)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		for k, v := range opts.Metadata {
+			h["X-Object-Meta-"+k] = v
+		}
+	}
+
+	url := objectURL(c, containerName, objectName)
+	resp, err := perigee.Request("POST", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{202},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..15956cd
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -0,0 +1,264 @@
+package objects
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+	tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"Gophercloud-Test": "objects"}
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestDownloadObject(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, "Successful download with Gophercloud")
+	})
+
+	client := serviceClient()
+	content, err := Download(client, "testContainer", "testObject", nil).ExtractContent()
+	if err != nil {
+		t.Fatalf("Unexpected error downloading object: %v", err)
+	}
+	if string(content) != "Successful download with Gophercloud" {
+		t.Errorf("Expected %s, got %s", "Successful download with Gophercloud", content)
+	}
+}
+
+func TestListObjectInfo(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `[
+				{
+					"hash": "451e372e48e0f6b1114fa0724aa79fa1",
+					"last_modified": "2009-11-10 23:00:00 +0000 UTC",
+					"bytes": 14,
+					"name": "goodbye",
+					"content_type": "application/octet-stream"
+				},
+				{
+					"hash": "451e372e48e0f6b1114fa0724aa79fa1",
+					"last_modified": "2009-11-10 23:00:00 +0000 UTC",
+					"bytes": 14,
+					"name": "hello",
+					"content_type": "application/octet-stream"
+				}
+			]`)
+		case "hello":
+			fmt.Fprintf(w, `[]`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	count := 0
+	err := List(client, "testContainer", &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		if err != nil {
+			t.Errorf("Failed to extract object info: %v", err)
+			return false, err
+		}
+
+		expected := []Object{
+			Object{
+				Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+				LastModified: "2009-11-10 23:00:00 +0000 UTC",
+				Bytes:        14,
+				Name:         "goodbye",
+				ContentType:  "application/octet-stream",
+			},
+			Object{
+				Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+				LastModified: "2009-11-10 23:00:00 +0000 UTC",
+				Bytes:        14,
+				Name:         "hello",
+				ContentType:  "application/octet-stream",
+			},
+		}
+
+		testhelper.CheckDeepEquals(t, actual, expected)
+
+		return true, nil
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestListObjectNames(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+		w.Header().Set("Content-Type", "text/plain")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, "hello\ngoodbye\n")
+		case "goodbye":
+			fmt.Fprintf(w, "")
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	count := 0
+	List(client, "testContainer", &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract object names: %v", err)
+			return false, err
+		}
+
+		expected := []string{"hello", "goodbye"}
+
+		testhelper.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreateObject(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "PUT")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	client := serviceClient()
+	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+	_, err := Create(client, "testContainer", "testObject", content, nil).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error creating object: %v", err)
+	}
+}
+
+func TestCopyObject(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "COPY")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	client := serviceClient()
+	_, err := Copy(client, "testContainer", "testObject", &CopyOpts{Destination: "/newTestContainer/newTestObject"}).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error copying object: %v", err)
+	}
+}
+
+func TestDeleteObject(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "DELETE")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	_, err := Delete(client, "testContainer", "testObject", nil).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error deleting object: %v", err)
+	}
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects")
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	client := serviceClient()
+	_, err := Update(client, "testContainer", "testObject", &UpdateOpts{Metadata: metadata}).ExtractHeaders()
+	if err != nil {
+		t.Fatalf("Unexpected error updating object metadata: %v", err)
+	}
+}
+
+func TestGetObject(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "HEAD")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	expected := metadata
+	actual, err := Get(client, "testContainer", "testObject", nil).ExtractMetadata()
+	if err != nil {
+		t.Fatalf("Unexpected error getting object metadata: %v", err)
+	}
+	testhelper.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
new file mode 100644
index 0000000..aaeb040
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,166 @@
+package objects
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object struct {
+	Bytes        int    `json:"bytes" mapstructure:"bytes"`
+	ContentType  string `json:"content_type" mapstructure:"content_type"`
+	Hash         string `json:"hash" mapstructure:"hash"`
+	LastModified string `json:"last_modified" mapstructure:"last_modified"`
+	Name         string `json:"name" mapstructure:"name"`
+}
+
+// ListResult is a single page of objects that is returned from a call to the List function.
+type ObjectPage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no object names.
+func (r ObjectPage) IsEmpty() (bool, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return true, err
+	}
+	return len(names) == 0, nil
+}
+
+// LastMarker returns the last object name in a ListResult.
+func (r ObjectPage) LastMarker() (string, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return "", err
+	}
+	if len(names) == 0 {
+		return "", nil
+	}
+	return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]Object, error) {
+	untyped := page.(ObjectPage).Body.([]interface{})
+	results := make([]Object, len(untyped))
+	for index, each := range untyped {
+		object := each.(map[string]interface{})
+		err := mapstructure.Decode(object, &results[index])
+		if err != nil {
+			return results, err
+		}
+	}
+	return results, nil
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	casted := page.(ObjectPage)
+	ct := casted.Header.Get("Content-Type")
+	switch {
+	case strings.HasPrefix(ct, "application/json"):
+		parsed, err := ExtractInfo(page)
+		if err != nil {
+			return nil, err
+		}
+
+		names := make([]string, 0, len(parsed))
+		for _, object := range parsed {
+			names = append(names, object.Name)
+		}
+
+		return names, nil
+	case strings.HasPrefix(ct, "text/plain"):
+		names := make([]string, 0, 50)
+
+		body := string(page.(ObjectPage).Body.([]uint8))
+		for _, name := range strings.Split(body, "\n") {
+			if len(name) > 0 {
+				names = append(names, name)
+			}
+		}
+
+		return names, nil
+	case strings.HasPrefix(ct, "text/html"):
+		return []string{}, nil
+	default:
+		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+	}
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+	commonResult
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+	if dr.Err != nil {
+		return nil, nil
+	}
+	var body []byte
+	defer dr.Resp.Body.Close()
+	body, err := ioutil.ReadAll(dr.Resp.Body)
+	if err != nil {
+		return body, fmt.Errorf("Error trying to read DownloadResult body: %v", err)
+	}
+	return body, nil
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+	commonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range gr.Resp.Header {
+		if strings.HasPrefix(k, "X-Object-Meta-") {
+			key := strings.TrimPrefix(k, "X-Object-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+type commonResult struct {
+	Resp *http.Response
+	Err  error
+}
+
+func (cr commonResult) ExtractHeaders() (http.Header, error) {
+	var headers http.Header
+	if cr.Err != nil {
+		return headers, cr.Err
+	}
+
+	return cr.Resp.Header, nil
+}
+
+type CreateResult struct {
+	commonResult
+}
+
+type UpdateResult struct {
+	commonResult
+}
+
+type DeleteResult struct {
+	commonResult
+}
+
+type CopyResult struct {
+	commonResult
+}
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
new file mode 100644
index 0000000..a377960
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -0,0 +1,13 @@
+package objects
+
+import "github.com/rackspace/gophercloud"
+
+// objectURL returns the URI for making Object requests.
+func objectURL(c *gophercloud.ServiceClient, container, object string) string {
+	return c.ServiceURL(container, object)
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
diff --git a/openstack/objectstorage/v1/objects/urls_test.go b/openstack/objectstorage/v1/objects/urls_test.go
new file mode 100644
index 0000000..89d1cb1
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls_test.go
@@ -0,0 +1,28 @@
+package objects
+
+import (
+	"testing"
+	"github.com/rackspace/gophercloud"
+)
+
+func TestContainerURL(t *testing.T) {
+	client := gophercloud.ServiceClient{
+		Endpoint: "http://localhost:5000/v1/",
+	}
+	expected := "http://localhost:5000/v1/testContainer"
+	actual := containerURL(&client, "testContainer")
+	if actual != expected {
+		t.Errorf("Unexpected service URL generated: %s", actual)
+	}
+}
+
+func TestObjectURL(t *testing.T) {
+	client := gophercloud.ServiceClient{
+		Endpoint: "http://localhost:5000/v1/",
+	}
+	expected := "http://localhost:5000/v1/testContainer/testObject"
+	actual := objectURL(&client, "testContainer", "testObject")
+	if actual != expected {
+		t.Errorf("Unexpected service URL generated: %s", actual)
+	}
+}
diff --git a/openstack/storage/v1/accounts/urls.go b/openstack/storage/v1/accounts/urls.go
deleted file mode 100644
index ae78ff2..0000000
--- a/openstack/storage/v1/accounts/urls.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package accounts
-
-import "github.com/rackspace/gophercloud"
-
-// getAccountURL returns the URI for making Account requests.
-func getAccountURL(c *gophercloud.ServiceClient) string {
-	return c.Endpoint
-}
diff --git a/openstack/storage/v1/containers/containers.go b/openstack/storage/v1/containers/containers.go
deleted file mode 100644
index 8fa19aa..0000000
--- a/openstack/storage/v1/containers/containers.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package containers
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/rackspace/gophercloud/pagination"
-)
-
-// Container is a structure that holds information related to a storage container.
-type Container map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing containers.
-type ListOpts struct {
-	Full   bool
-	Params map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating a container.
-type CreateOpts struct {
-	Name     string
-	Metadata map[string]string
-	Headers  map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting a container.
-type DeleteOpts struct {
-	Name   string
-	Params map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
-// container's metadata.
-type UpdateOpts struct {
-	Name     string
-	Metadata map[string]string
-	Headers  map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting a container's metadata.
-type GetOpts struct {
-	Name     string
-	Metadata map[string]string
-}
-
-// ExtractInfo is a function that takes a ListResult and returns the containers' information.
-func ExtractInfo(page pagination.Page) ([]Container, error) {
-	untyped := page.(ListResult).Body.([]interface{})
-	results := make([]Container, len(untyped))
-	for index, each := range untyped {
-		results[index] = Container(each.(map[string]interface{}))
-	}
-	return results, nil
-}
-
-// ExtractNames is a function that takes a ListResult and returns the containers' names.
-func ExtractNames(page pagination.Page) ([]string, error) {
-	casted := page.(ListResult)
-	ct := casted.Header.Get("Content-Type")
-
-	switch {
-	case strings.HasPrefix(ct, "application/json"):
-		parsed, err := ExtractInfo(page)
-		if err != nil {
-			return nil, err
-		}
-
-		names := make([]string, 0, len(parsed))
-		for _, container := range parsed {
-			names = append(names, container["name"].(string))
-		}
-		return names, nil
-	case strings.HasPrefix(ct, "text/plain"):
-		names := make([]string, 0, 50)
-
-		body := string(page.(ListResult).Body.([]uint8))
-		for _, name := range strings.Split(body, "\n") {
-			if len(name) > 0 {
-				names = append(names, name)
-			}
-		}
-
-		return names, nil
-	default:
-		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
-	}
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the container.
-func ExtractMetadata(gr GetResult) map[string]string {
-	metadata := make(map[string]string)
-	for k, v := range gr.Header {
-		if strings.HasPrefix(k, "X-Container-Meta-") {
-			key := strings.TrimPrefix(k, "X-Container-Meta-")
-			metadata[key] = v[0]
-		}
-	}
-	return metadata
-}
diff --git a/openstack/storage/v1/containers/requests.go b/openstack/storage/v1/containers/requests.go
deleted file mode 100644
index 2aa263a..0000000
--- a/openstack/storage/v1/containers/requests.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package containers
-
-import (
-	"net/http"
-
-	"github.com/racker/perigee"
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/openstack/utils"
-	"github.com/rackspace/gophercloud/pagination"
-)
-
-// ListResult is a *http.Response that is returned from a call to the List function.
-type ListResult struct {
-	pagination.MarkerPageBase
-}
-
-// IsEmpty returns true if a ListResult contains no container names.
-func (r ListResult) IsEmpty() (bool, error) {
-	names, err := ExtractNames(r)
-	if err != nil {
-		return true, err
-	}
-	return len(names) == 0, nil
-}
-
-// LastMarker returns the last container name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
-	names, err := ExtractNames(r)
-	if err != nil {
-		return "", err
-	}
-	if len(names) == 0 {
-		return "", nil
-	}
-	return names[len(names)-1], nil
-}
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// List is a function that retrieves all objects in a container. It also returns the details
-// for the account. To extract just the container information or names, pass the ListResult
-// response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	var headers map[string]string
-
-	query := utils.BuildQuery(opts.Params)
-
-	if !opts.Full {
-		headers = map[string]string{"Content-Type": "text/plain"}
-	}
-
-	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
-		p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
-		p.MarkerPageBase.Owner = p
-		return p
-	}
-
-	url := getAccountURL(c) + query
-	pager := pagination.NewPager(c, url, createPage)
-	pager.Headers = headers
-	return pager
-}
-
-// Create is a function that creates a new container.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) (Container, error) {
-	var ci Container
-
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	for k, v := range opts.Metadata {
-		h["X-Container-Meta-"+k] = v
-	}
-
-	url := getContainerURL(c, opts.Name)
-	_, err := perigee.Request("PUT", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{201, 204},
-	})
-	if err == nil {
-		ci = Container{
-			"name": opts.Name,
-		}
-	}
-	return ci, err
-}
-
-// Delete is a function that deletes a container.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	query := utils.BuildQuery(opts.Params)
-
-	url := getContainerURL(c, opts.Name) + query
-	_, err := perigee.Request("DELETE", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return err
-}
-
-// Update is a function that creates, updates, or deletes a container's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	for k, v := range opts.Metadata {
-		h["X-Container-Meta-"+k] = v
-	}
-
-	url := getContainerURL(c, opts.Name)
-	_, err := perigee.Request("POST", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return err
-}
-
-// Get is a function that retrieves the metadata of a container. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Metadata {
-		h["X-Container-Meta-"+k] = v
-	}
-
-	url := getContainerURL(c, opts.Name)
-	resp, err := perigee.Request("HEAD", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return &resp.HttpResponse, err
-}
diff --git a/openstack/storage/v1/containers/urls.go b/openstack/storage/v1/containers/urls.go
deleted file mode 100644
index 4084bcc..0000000
--- a/openstack/storage/v1/containers/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package containers
-
-import "github.com/rackspace/gophercloud"
-
-// getAccountURL returns the URI used to list Containers.
-func getAccountURL(c *gophercloud.ServiceClient) string {
-	return c.Endpoint
-}
-
-// getContainerURL returns the URI for making Container requests.
-func getContainerURL(c *gophercloud.ServiceClient, container string) string {
-	return c.ServiceURL(container)
-}
diff --git a/openstack/storage/v1/objects/objects.go b/openstack/storage/v1/objects/objects.go
deleted file mode 100644
index 4cf3d2d..0000000
--- a/openstack/storage/v1/objects/objects.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package objects
-
-import (
-	"fmt"
-	"io"
-	"io/ioutil"
-	"strings"
-
-	"github.com/rackspace/gophercloud/pagination"
-)
-
-// Object is a structure that holds information related to a storage object.
-type Object map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing objects.
-type ListOpts struct {
-	Container string
-	Full      bool
-	Params    map[string]string
-}
-
-// DownloadOpts is a structure that holds parameters for downloading an object.
-type DownloadOpts struct {
-	Container string
-	Name      string
-	Headers   map[string]string
-	Params    map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating an object.
-type CreateOpts struct {
-	Container string
-	Name      string
-	Content   io.Reader
-	Metadata  map[string]string
-	Headers   map[string]string
-	Params    map[string]string
-}
-
-// CopyOpts is a structure that holds parameters for copying one object to another.
-type CopyOpts struct {
-	Container    string
-	Name         string
-	NewContainer string
-	NewName      string
-	Metadata     map[string]string
-	Headers      map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting an object.
-type DeleteOpts struct {
-	Container string
-	Name      string
-	Params    map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting an object's metadata.
-type GetOpts struct {
-	Container string
-	Name      string
-	Headers   map[string]string
-	Params    map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
-// object's metadata.
-type UpdateOpts struct {
-	Container string
-	Name      string
-	Metadata  map[string]string
-	Headers   map[string]string
-}
-
-// ExtractInfo is a function that takes a page of objects and returns their full information.
-func ExtractInfo(page pagination.Page) ([]Object, error) {
-	untyped := page.(ListResult).Body.([]interface{})
-	results := make([]Object, len(untyped))
-	for index, each := range untyped {
-		results[index] = Object(each.(map[string]interface{}))
-	}
-	return results, nil
-}
-
-// ExtractNames is a function that takes a page of objects and returns only their names.
-func ExtractNames(page pagination.Page) ([]string, error) {
-	casted := page.(ListResult)
-	ct := casted.Header.Get("Content-Type")
-
-	switch {
-	case strings.HasPrefix(ct, "application/json"):
-		parsed, err := ExtractInfo(page)
-		if err != nil {
-			return nil, err
-		}
-
-		names := make([]string, 0, len(parsed))
-		for _, object := range parsed {
-			names = append(names, object["name"].(string))
-		}
-		return names, nil
-	case strings.HasPrefix(ct, "text/plain"):
-		names := make([]string, 0, 50)
-
-		body := string(page.(ListResult).Body.([]uint8))
-		for _, name := range strings.Split(body, "\n") {
-			if len(name) > 0 {
-				names = append(names, name)
-			}
-		}
-
-		return names, nil
-	default:
-		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
-	}
-}
-
-// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
-// and returns the object's content.
-func ExtractContent(dr DownloadResult) ([]byte, error) {
-	var body []byte
-	defer dr.Body.Close()
-	body, err := ioutil.ReadAll(dr.Body)
-	return body, err
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the object.
-func ExtractMetadata(gr GetResult) map[string]string {
-	metadata := make(map[string]string)
-	for k, v := range gr.Header {
-		if strings.HasPrefix(k, "X-Object-Meta-") {
-			key := strings.TrimPrefix(k, "X-Object-Meta-")
-			metadata[key] = v[0]
-		}
-	}
-	return metadata
-}
diff --git a/openstack/storage/v1/objects/requests.go b/openstack/storage/v1/objects/requests.go
deleted file mode 100644
index 67aba34..0000000
--- a/openstack/storage/v1/objects/requests.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package objects
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/racker/perigee"
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/openstack/utils"
-	"github.com/rackspace/gophercloud/pagination"
-)
-
-// ListResult is a single page of objects that is returned from a call to the List function.
-type ListResult struct {
-	pagination.MarkerPageBase
-}
-
-// IsEmpty returns true if a ListResult contains no object names.
-func (r ListResult) IsEmpty() (bool, error) {
-	names, err := ExtractNames(r)
-	if err != nil {
-		return true, err
-	}
-	return len(names) == 0, nil
-}
-
-// LastMarker returns the last object name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
-	names, err := ExtractNames(r)
-	if err != nil {
-		return "", err
-	}
-	if len(names) == 0 {
-		return "", nil
-	}
-	return names[len(names)-1], nil
-}
-
-// DownloadResult is a *http.Response that is returned from a call to the Download function.
-type DownloadResult *http.Response
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// List is a function that retrieves all objects in a container. It also returns the details
-// for the container. To extract only the object information or names, pass the ListResult
-// response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	var headers map[string]string
-
-	query := utils.BuildQuery(opts.Params)
-
-	if !opts.Full {
-		headers = map[string]string{"Content-Type": "text/plain"}
-	}
-
-	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
-		p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
-		p.MarkerPageBase.Owner = p
-		return p
-	}
-
-	url := getContainerURL(c, opts.Container) + query
-	pager := pagination.NewPager(c, url, createPage)
-	pager.Headers = headers
-	return pager
-}
-
-// Download is a function that retrieves the content and metadata for an object.
-// To extract just the content, pass the DownloadResult response to the ExtractContent
-// function.
-func Download(c *gophercloud.ServiceClient, opts DownloadOpts) (DownloadResult, error) {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	query := utils.BuildQuery(opts.Params)
-
-	url := getObjectURL(c, opts.Container, opts.Name) + query
-	resp, err := perigee.Request("GET", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{200},
-	})
-	return &resp.HttpResponse, err
-}
-
-// Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) error {
-	var reqBody []byte
-
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	for k, v := range opts.Metadata {
-		h["X-Object-Meta-"+k] = v
-	}
-
-	query := utils.BuildQuery(opts.Params)
-
-	content := opts.Content
-	if content != nil {
-		reqBody = make([]byte, 0)
-		_, err := content.Read(reqBody)
-		if err != nil {
-			return err
-		}
-	}
-
-	url := getObjectURL(c, opts.Container, opts.Name) + query
-	_, err := perigee.Request("PUT", url, perigee.Options{
-		ReqBody:     reqBody,
-		MoreHeaders: h,
-		OkCodes:     []int{201},
-	})
-	return err
-}
-
-// Copy is a function that copies one object to another.
-func Copy(c *gophercloud.ServiceClient, opts CopyOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Metadata {
-		h["X-Object-Meta-"+k] = v
-	}
-
-	h["Destination"] = fmt.Sprintf("/%s/%s", opts.NewContainer, opts.NewName)
-
-	url := getObjectURL(c, opts.Container, opts.Name)
-	_, err := perigee.Request("COPY", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{201},
-	})
-	return err
-}
-
-// Delete is a function that deletes an object.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	query := utils.BuildQuery(opts.Params)
-
-	url := getObjectURL(c, opts.Container, opts.Name) + query
-	_, err := perigee.Request("DELETE", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return err
-}
-
-// Get is a function that retrieves the metadata of an object. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	url := getObjectURL(c, opts.Container, opts.Name)
-	resp, err := perigee.Request("HEAD", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return &resp.HttpResponse, err
-}
-
-// Update is a function that creates, updates, or deletes an object's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	for k, v := range opts.Metadata {
-		h["X-Object-Meta-"+k] = v
-	}
-
-	url := getObjectURL(c, opts.Container, opts.Name)
-	_, err := perigee.Request("POST", url, perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{202},
-	})
-	return err
-}
diff --git a/openstack/storage/v1/objects/urls.go b/openstack/storage/v1/objects/urls.go
deleted file mode 100644
index 5a52aed..0000000
--- a/openstack/storage/v1/objects/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package objects
-
-import "github.com/rackspace/gophercloud"
-
-// getObjectURL returns the URI for making Object requests.
-func getObjectURL(c *gophercloud.ServiceClient, container, object string) string {
-	return c.ServiceURL(container, object)
-}
-
-// getContainerURL returns the URI for making Container requests.
-func getContainerURL(c *gophercloud.ServiceClient, container string) string {
-	return c.ServiceURL(container)
-}
diff --git a/pagination/pager.go b/pagination/pager.go
index 806d98a..22d6d84 100644
--- a/pagination/pager.go
+++ b/pagination/pager.go
@@ -35,6 +35,8 @@
 
 	createPage func(r LastHTTPResponse) Page
 
+	Err error
+
 	// Headers supplies additional HTTP headers to populate on each paged request.
 	Headers map[string]string
 }
@@ -66,6 +68,9 @@
 // EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
 // Return "false" from the handler to prematurely stop iterating.
 func (p Pager) EachPage(handler func(Page) (bool, error)) error {
+	if p.Err != nil {
+		return p.Err
+	}
 	currentURL := p.initialURL
 	for {
 		currentPage, err := p.fetchNextPage(currentURL)
diff --git a/params.go b/params.go
index f11a29d..c492ec0 100644
--- a/params.go
+++ b/params.go
@@ -1,5 +1,14 @@
 package gophercloud
 
+import (
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
 // MaybeString takes a string that might be a zero-value, and either returns a
 // pointer to its address or a nil value (i.e. empty pointer). This is useful
 // for converting zero values in options structs when the end-user hasn't
@@ -11,3 +20,142 @@
 	}
 	return nil
 }
+
+var t time.Time
+
+func isZero(v reflect.Value) bool {
+	switch v.Kind() {
+	case reflect.Func, reflect.Map, reflect.Slice:
+		return v.IsNil()
+	case reflect.Array:
+		z := true
+		for i := 0; i < v.Len(); i++ {
+			z = z && isZero(v.Index(i))
+		}
+		return z
+	case reflect.Struct:
+		if v.Type() == reflect.TypeOf(t) {
+			if v.Interface().(time.Time).IsZero() {
+				return true
+			}
+			return false
+		}
+		z := true
+		for i := 0; i < v.NumField(); i++ {
+			z = z && isZero(v.Field(i))
+		}
+		return z
+	}
+	// Compare other types directly:
+	z := reflect.Zero(v.Type())
+	return v.Interface() == z.Interface()
+}
+
+func BuildQueryString(opts interface{}) (*url.URL, error) {
+	optsValue := reflect.ValueOf(opts)
+	if optsValue.Kind() == reflect.Ptr {
+		optsValue = optsValue.Elem()
+	}
+
+	optsType := reflect.TypeOf(opts)
+	if optsType.Kind() == reflect.Ptr {
+		optsType = optsType.Elem()
+	}
+
+	optsSlice := make([]string, 0)
+	if optsValue.Kind() == reflect.Struct {
+		for i := 0; i < optsValue.NumField(); i++ {
+			v := optsValue.Field(i)
+			f := optsType.Field(i)
+			qTag := f.Tag.Get("q")
+
+			// if the field has a 'q' tag, it goes in the query string
+			if qTag != "" {
+				tags := strings.Split(qTag, ",")
+
+				// if the field is set, add it to the slice of query pieces
+				if !isZero(v) {
+					switch v.Kind() {
+					case reflect.String:
+						optsSlice = append(optsSlice, tags[0]+"="+v.String())
+					case reflect.Int:
+						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+					case reflect.Bool:
+						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+					}
+				} else {
+					// Otherwise, the field is not set.
+					if len(tags) == 2 && tags[1] == "required" {
+						// And the field is required. Return an error.
+						return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
+					}
+				}
+			}
+
+		}
+		// URL encode the string for safety.
+		s := strings.Join(optsSlice, "&")
+		if s != "" {
+			s = "?" + s
+		}
+		u, err := url.Parse(s)
+		if err != nil {
+			return nil, err
+		}
+		return u, nil
+	}
+	// Return an error if the underlying type of 'opts' isn't a struct.
+	return nil, fmt.Errorf("Options type is not a struct.")
+}
+
+func BuildRequestBody(opts interface{}) (map[string]interface{}, error) {
+	return nil, nil
+}
+
+func BuildHeaders(opts interface{}) (map[string]string, error) {
+	optsValue := reflect.ValueOf(opts)
+	if optsValue.Kind() == reflect.Ptr {
+		optsValue = optsValue.Elem()
+	}
+
+	optsType := reflect.TypeOf(opts)
+	if optsType.Kind() == reflect.Ptr {
+		optsType = optsType.Elem()
+	}
+
+	optsMap := make(map[string]string)
+	if optsValue.Kind() == reflect.Struct {
+		for i := 0; i < optsValue.NumField(); i++ {
+			v := optsValue.Field(i)
+			f := optsType.Field(i)
+			hTag := f.Tag.Get("h")
+
+			// if the field has a 'h' tag, it goes in the header
+			if hTag != "" {
+				tags := strings.Split(hTag, ",")
+
+				// if the field is set, add it to the slice of query pieces
+				if !isZero(v) {
+					switch v.Kind() {
+					case reflect.String:
+						optsMap[tags[0]] = v.String()
+					case reflect.Int:
+						optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
+					case reflect.Bool:
+						optsMap[tags[0]] = strconv.FormatBool(v.Bool())
+					}
+				} else {
+					// Otherwise, the field is not set.
+					if len(tags) == 2 && tags[1] == "required" {
+						// And the field is required. Return an error.
+						return optsMap, fmt.Errorf("Required header not set.")
+					}
+				}
+			}
+
+		}
+		return optsMap, nil
+	}
+	// Return an error if the underlying type of 'opts' isn't a struct.
+	return optsMap, fmt.Errorf("Options type is not a struct.")
+}
diff --git a/params_test.go b/params_test.go
new file mode 100644
index 0000000..03eaefc
--- /dev/null
+++ b/params_test.go
@@ -0,0 +1,59 @@
+package gophercloud
+
+import (
+	"net/url"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMaybeStringWithNonEmptyString(t *testing.T) {
+	testString := "carol"
+	expected := &testString
+	actual := MaybeString("carol")
+	th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestMaybeStringWithEmptyString(t *testing.T) {
+	var expected *string
+	actual := MaybeString("")
+	th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestBuildQueryStringWithPointerToStruct(t *testing.T) {
+	expected := &url.URL{
+		RawQuery: "j=2&r=red",
+	}
+
+	type Opts struct {
+		J int    `q:"j"`
+		R string `q:"r"`
+		C bool
+	}
+
+	opts := Opts{J: 2, R: "red"}
+
+	actual, err := BuildQueryString(&opts)
+	if err != nil {
+		t.Errorf("Error building query string: %v", err)
+	}
+
+	th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestBuildQueryStringWithoutRequiredFieldSet(t *testing.T) {
+	type Opts struct {
+		J int    `q:"j"`
+		R string `q:"r,required"`
+		C bool
+	}
+
+	opts := Opts{J: 2, C: true}
+
+	_, err := BuildQueryString(&opts)
+	if err == nil {
+		t.Error("Unexpected result: There should be an error thrown when a required field isn't set.")
+	}
+
+	t.Log(err)
+}