tenantattr extension for cinder (#186)

* tenantattr extension for cinder; ExtractInto method for handling custom Volume objects

* make sure interface{} parameter is *struct

* ExtractInto*Ptr methods for Result

* use gophercloud.ExtractInto*Ptr for ExtractInto and ^CtractVolumesInto

* use type instead of struct literal in unit test

* comments for tenantattr pkg

* call volumes.ExtractInto from volumes.Extract

* clean up extractIntoPtr and add comments for new exported methods

* add comment about ExtractInto*Ptr being for internal use only

* check for http response error in ExtractInto*Ptr methods

rename tenantattr pkg to volumetenants

* rename tenantattr to volumetenants; remove commented code
diff --git a/openstack/blockstorage/extensions/volumetenants/results.go b/openstack/blockstorage/extensions/volumetenants/results.go
new file mode 100644
index 0000000..0d4c670
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumetenants/results.go
@@ -0,0 +1,7 @@
+package volumetenants
+
+// An extension to the base Volume object
+type VolumeExt struct {
+	// TenantID is the id of the project that owns the volume.
+	TenantID string `json:"os-vol-tenant-attr:tenant_id"`
+}
diff --git a/openstack/blockstorage/v2/volumes/results.go b/openstack/blockstorage/v2/volumes/results.go
index 2ad94cd..41fbf5c 100644
--- a/openstack/blockstorage/v2/volumes/results.go
+++ b/openstack/blockstorage/v2/volumes/results.go
@@ -57,16 +57,6 @@
 	Multiattach bool `json:"multiattach"`
 }
 
-/*
-THESE BELONG IN EXTENSIONS:
-// ReplicationDriverData contains data about the replication driver.
-ReplicationDriverData string `json:"os-volume-replication:driver_data"`
-// ReplicationExtendedStatus contains extended status about replication.
-ReplicationExtendedStatus string `json:"os-volume-replication:extended_status"`
-// TenantID is the id of the project that owns the volume.
-TenantID string `json:"os-vol-tenant-attr:tenant_id"`
-*/
-
 // VolumePage is a pagination.pager that is returned from a call to the List function.
 type VolumePage struct {
 	pagination.SinglePageBase
@@ -80,11 +70,9 @@
 
 // ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
 func ExtractVolumes(r pagination.Page) ([]Volume, error) {
-	var s struct {
-		Volumes []Volume `json:"volumes"`
-	}
-	err := (r.(VolumePage)).ExtractInto(&s)
-	return s.Volumes, err
+	var s []Volume
+	err := ExtractVolumesInto(r, &s)
+	return s, err
 }
 
 type commonResult struct {
@@ -93,11 +81,17 @@
 
 // Extract will get the Volume object out of the commonResult object.
 func (r commonResult) Extract() (*Volume, error) {
-	var s struct {
-		Volume *Volume `json:"volume"`
-	}
+	var s Volume
 	err := r.ExtractInto(&s)
-	return s.Volume, err
+	return &s, err
+}
+
+func (r commonResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "volume")
+}
+
+func ExtractVolumesInto(r pagination.Page, v interface{}) error {
+	return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes")
 }
 
 // CreateResult contains the response body and error from a Create request.
diff --git a/openstack/blockstorage/v2/volumes/testing/requests_test.go b/openstack/blockstorage/v2/volumes/testing/requests_test.go
index 789072a..fd3dbcb 100644
--- a/openstack/blockstorage/v2/volumes/testing/requests_test.go
+++ b/openstack/blockstorage/v2/volumes/testing/requests_test.go
@@ -5,13 +5,14 @@
 	"time"
 
 	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants"
 	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
 	"github.com/gophercloud/gophercloud/pagination"
 	th "github.com/gophercloud/gophercloud/testhelper"
 	"github.com/gophercloud/gophercloud/testhelper/client"
 )
 
-func TestList(t *testing.T) {
+func TestListWithExtensions(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
@@ -94,6 +95,27 @@
 	}
 }
 
+func TestListAllWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	type VolumeWithExt struct {
+		volumes.Volume
+		volumetenants.VolumeExt
+	}
+
+	allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+
+	var actual []VolumeWithExt
+	err = volumes.ExtractVolumesInto(allPages, &actual)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 2, len(actual))
+	th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", actual[0].TenantID)
+}
+
 func TestListAll(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
@@ -214,3 +236,23 @@
 	th.AssertNoErr(t, err)
 	th.CheckEquals(t, "vol-002", v.Name)
 }
+
+func TestGetWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	var s struct {
+		volumes.Volume
+		volumetenants.VolumeExt
+	}
+	err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(&s)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", s.TenantID)
+
+	err = volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(s)
+	if err == nil {
+		t.Errorf("Expected error when providing non-pointer struct")
+	}
+}
diff --git a/results.go b/results.go
index 8cca421..76c16ef 100644
--- a/results.go
+++ b/results.go
@@ -3,8 +3,10 @@
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
+	"reflect"
 	"strconv"
 	"time"
 )
@@ -60,6 +62,78 @@
 	return err
 }
 
+func (r Result) extractIntoPtr(to interface{}, label string) error {
+	if label == "" {
+		return r.ExtractInto(&to)
+	}
+
+	var m map[string]interface{}
+	err := r.ExtractInto(&m)
+	if err != nil {
+		return err
+	}
+
+	b, err := json.Marshal(m[label])
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &to)
+	return err
+}
+
+// ExtractIntoStructPtr will unmarshal the Result (r) into the provided
+// interface{} (to).
+//
+// NOTE: For internal use only
+//
+// `to` must be a pointer to an underlying struct type
+//
+// If provided, `label` will be filtered out of the response
+// body prior to `r` being unmarshalled into `to`.
+func (r Result) ExtractIntoStructPtr(to interface{}, label string) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	t := reflect.TypeOf(to)
+	if k := t.Kind(); k != reflect.Ptr {
+		return fmt.Errorf("Expected pointer, got %v", k)
+	}
+	switch t.Elem().Kind() {
+	case reflect.Struct:
+		return r.extractIntoPtr(to, label)
+	default:
+		return fmt.Errorf("Expected pointer to struct, got: %v", t)
+	}
+}
+
+// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided
+// interface{} (to).
+//
+// NOTE: For internal use only
+//
+// `to` must be a pointer to an underlying slice type
+//
+// If provided, `label` will be filtered out of the response
+// body prior to `r` being unmarshalled into `to`.
+func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	t := reflect.TypeOf(to)
+	if k := t.Kind(); k != reflect.Ptr {
+		return fmt.Errorf("Expected pointer, got %v", k)
+	}
+	switch t.Elem().Kind() {
+	case reflect.Slice:
+		return r.extractIntoPtr(to, label)
+	default:
+		return fmt.Errorf("Expected pointer to slice, got: %v", t)
+	}
+}
+
 // PrettyPrintJSON creates a string containing the full response body as
 // pretty-printed JSON. It's useful for capturing test fixtures and for
 // debugging extraction bugs. If you include its output in an issue related to