Handle Unmarshaling Compute Security Group IDs (#192)

* Handling integer secgroup IDs

* Handling integer secgroup rule IDs

* Updating unit tests for integer secgroup IDs and rule IDs

* Style updates

* Style updates

* Test formatting fix
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
index 764f580..f49338a 100644
--- a/openstack/compute/v2/extensions/secgroups/results.go
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -1,6 +1,9 @@
 package secgroups
 
 import (
+	"encoding/json"
+	"strconv"
+
 	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/pagination"
 )
@@ -10,28 +13,51 @@
 	// The unique ID of the group. If Neutron is installed, this ID will be
 	// represented as a string UUID; if Neutron is not installed, it will be a
 	// numeric ID. For the sake of consistency, we always cast it to a string.
-	ID string
+	ID string `json:"-"`
 
 	// The human-readable name of the group, which needs to be unique.
-	Name string
+	Name string `json:"name"`
 
 	// The human-readable description of the group.
-	Description string
+	Description string `json:"description"`
 
 	// The rules which determine how this security group operates.
-	Rules []Rule
+	Rules []Rule `json:"rules"`
 
 	// The ID of the tenant to which this security group belongs.
 	TenantID string `json:"tenant_id"`
 }
 
+func (r *SecurityGroup) UnmarshalJSON(b []byte) error {
+	type tmp SecurityGroup
+	var s struct {
+		tmp
+		ID interface{} `json:"id"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = SecurityGroup(s.tmp)
+
+	switch t := s.ID.(type) {
+	case float64:
+		r.ID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ID = t
+	}
+
+	return err
+}
+
 // Rule represents a security group rule, a policy which determines how a
 // security group operates and what inbound traffic it allows in.
 type Rule struct {
 	// The unique ID. If Neutron is installed, this ID will be
 	// represented as a string UUID; if Neutron is not installed, it will be a
 	// numeric ID. For the sake of consistency, we always cast it to a string.
-	ID string
+	ID string `json:"-"`
 
 	// The lower bound of the port range which this security group should open up
 	FromPort int `json:"from_port"`
@@ -52,6 +78,37 @@
 	Group Group
 }
 
+func (r *Rule) UnmarshalJSON(b []byte) error {
+	type tmp Rule
+	var s struct {
+		tmp
+		ID            interface{} `json:"id"`
+		ParentGroupID interface{} `json:"parent_group_id"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Rule(s.tmp)
+
+	switch t := s.ID.(type) {
+	case float64:
+		r.ID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ID = t
+	}
+
+	switch t := s.ParentGroupID.(type) {
+	case float64:
+		r.ParentGroupID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ParentGroupID = t
+	}
+
+	return err
+}
+
 // IPRange represents the IP range whose traffic will be accepted by the
 // security group.
 type IPRange struct {
diff --git a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
index 8a83ca8..536e7f8 100644
--- a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
@@ -163,10 +163,35 @@
 		fmt.Fprintf(w, `
 {
 	"security_group": {
-		"id": "12345"
+		"id": %d
 	}
 }
-			`)
+		`, groupID)
+	})
+}
+
+func mockGetNumericIDGroupRuleResponse(t *testing.T, groupID int) {
+	url := fmt.Sprintf("%s/%d", rootPath, groupID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group": {
+    "id": %d,
+    "rules": [
+      {
+        "parent_group_id": %d,
+        "id": %d
+      }
+    ]
+  }
+}
+		`, groupID, groupID, groupID)
 	})
 }
 
diff --git a/openstack/compute/v2/extensions/secgroups/testing/requests_test.go b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go
index b7ffa20..b520764 100644
--- a/openstack/compute/v2/extensions/secgroups/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go
@@ -178,6 +178,29 @@
 	th.AssertDeepEquals(t, expected, group)
 }
 
+func TestGetNumericRuleID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	numericGroupID := 12345
+
+	mockGetNumericIDGroupRuleResponse(t, numericGroupID)
+
+	group, err := secgroups.Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{
+		ID: "12345",
+		Rules: []secgroups.Rule{
+			{
+				ParentGroupID: "12345",
+				ID:            "12345",
+			},
+		},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
 func TestDelete(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()