Fix list response keys derived from the API name (#162)
CloudStack keys a list response's items by the response object name, not by the
API name the generator derived the tag from, so 16 list APIs decoded as a
permanently empty slice while count parsed from the same body and no error was
returned. The keys are now data in a documented listResponseKeys map rather than
24 hand-written switch arms, each one observed on a real server and pinned by a
test, leaving only genuine shape differences in the switch; listCaCertificate and
listUsageServerMetrics also become pointers, since they return a single object
with no count. Also moves the getRawValue empty-array guard from #154 into the
generator, where it had been edited into the generated file only and so was
reverted by every `make code`.
diff --git a/cloudstack/ASNumberRangeService.go b/cloudstack/ASNumberRangeService.go
index e781764..95f73c6 100644
--- a/cloudstack/ASNumberRangeService.go
+++ b/cloudstack/ASNumberRangeService.go
@@ -385,7 +385,7 @@
type ListASNRangesResponse struct {
Count int `json:"count"`
- ASNRanges []*ASNRange `json:"asnrange"`
+ ASNRanges []*ASNRange `json:"asnumberrange"`
}
type ASNRange struct {
diff --git a/cloudstack/BackupService.go b/cloudstack/BackupService.go
index 35f1f48..0abbe71 100644
--- a/cloudstack/BackupService.go
+++ b/cloudstack/BackupService.go
@@ -3544,7 +3544,7 @@
type ListBackupProvidersResponse struct {
Count int `json:"count"`
- BackupProviders []*BackupProvider `json:"backupprovider"`
+ BackupProviders []*BackupProvider `json:"providers"`
}
type BackupProvider struct {
diff --git a/cloudstack/CertificateService.go b/cloudstack/CertificateService.go
index 26c5843..8939c35 100644
--- a/cloudstack/CertificateService.go
+++ b/cloudstack/CertificateService.go
@@ -366,8 +366,7 @@
}
type ListCaCertificateResponse struct {
- Count int `json:"count"`
- CaCertificate []*CaCertificate `json:"cacertificate"`
+ CaCertificate *CaCertificate `json:"cacertificates"`
}
type CaCertificate struct {
diff --git a/cloudstack/ClusterService.go b/cloudstack/ClusterService.go
index 1e10470..e16583d 100644
--- a/cloudstack/ClusterService.go
+++ b/cloudstack/ClusterService.go
@@ -2686,7 +2686,7 @@
type ListClustersMetricsResponse struct {
Count int `json:"count"`
- ClustersMetrics []*ClustersMetric `json:"clustersmetric"`
+ ClustersMetrics []*ClustersMetric `json:"cluster"`
}
type ClustersMetric struct {
diff --git a/cloudstack/ExtensionService.go b/cloudstack/ExtensionService.go
index fe3b281..818b4c6 100644
--- a/cloudstack/ExtensionService.go
+++ b/cloudstack/ExtensionService.go
@@ -1197,7 +1197,7 @@
type ListCustomActionsResponse struct {
Count int `json:"count"`
- CustomActions []*CustomAction `json:"customaction"`
+ CustomActions []*CustomAction `json:"extensioncustomaction"`
}
type CustomAction struct {
diff --git a/cloudstack/FirewallService.go b/cloudstack/FirewallService.go
index e6b78c0..d00d50b 100644
--- a/cloudstack/FirewallService.go
+++ b/cloudstack/FirewallService.go
@@ -4094,7 +4094,7 @@
type ListRoutingFirewallRulesResponse struct {
Count int `json:"count"`
- RoutingFirewallRules []*RoutingFirewallRule `json:"routingfirewallrule"`
+ RoutingFirewallRules []*RoutingFirewallRule `json:"firewallrule"`
}
type RoutingFirewallRule struct {
diff --git a/cloudstack/HostService.go b/cloudstack/HostService.go
index 792b2bc..7706952 100644
--- a/cloudstack/HostService.go
+++ b/cloudstack/HostService.go
@@ -4193,7 +4193,7 @@
type ListHostsMetricsResponse struct {
Count int `json:"count"`
- HostsMetrics []*HostsMetric `json:"hostsmetric"`
+ HostsMetrics []*HostsMetric `json:"host"`
}
type HostsMetric struct {
diff --git a/cloudstack/NetworkService.go b/cloudstack/NetworkService.go
index 4bf894f..fe99e20 100644
--- a/cloudstack/NetworkService.go
+++ b/cloudstack/NetworkService.go
@@ -3775,7 +3775,7 @@
type ListNetworkIsolationMethodsResponse struct {
Count int `json:"count"`
- NetworkIsolationMethods []*NetworkIsolationMethod `json:"networkisolationmethod"`
+ NetworkIsolationMethods []*NetworkIsolationMethod `json:"isolationmethod"`
}
type NetworkIsolationMethod struct {
@@ -6424,7 +6424,7 @@
type ListSupportedNetworkServicesResponse struct {
Count int `json:"count"`
- SupportedNetworkServices []*SupportedNetworkService `json:"supportednetworkservice"`
+ SupportedNetworkServices []*SupportedNetworkService `json:"networkservice"`
}
type SupportedNetworkService struct {
diff --git a/cloudstack/SystemVMService.go b/cloudstack/SystemVMService.go
index c9e573b..2fe912f 100644
--- a/cloudstack/SystemVMService.go
+++ b/cloudstack/SystemVMService.go
@@ -1108,7 +1108,7 @@
type ListSystemVmsUsageHistoryResponse struct {
Count int `json:"count"`
- SystemVmsUsageHistory []*SystemVmsUsageHistory `json:"systemvmsusagehistory"`
+ SystemVmsUsageHistory []*SystemVmsUsageHistory `json:"virtualmachine"`
}
type SystemVmsUsageHistory struct {
diff --git a/cloudstack/UsageService.go b/cloudstack/UsageService.go
index ef89ced..d7310a7 100644
--- a/cloudstack/UsageService.go
+++ b/cloudstack/UsageService.go
@@ -1086,7 +1086,7 @@
type ListTrafficTypeImplementorsResponse struct {
Count int `json:"count"`
- TrafficTypeImplementors []*TrafficTypeImplementor `json:"traffictypeimplementor"`
+ TrafficTypeImplementors []*TrafficTypeImplementor `json:"traffictypeimplementorresponse"`
}
type TrafficTypeImplementor struct {
@@ -2128,8 +2128,7 @@
}
type ListUsageServerMetricsResponse struct {
- Count int `json:"count"`
- UsageServerMetrics []*UsageServerMetric `json:"usageservermetric"`
+ UsageServerMetrics *UsageServerMetric `json:"usageMetrics"`
}
type UsageServerMetric struct {
diff --git a/cloudstack/UserService.go b/cloudstack/UserService.go
index 10fc217..7c09679 100644
--- a/cloudstack/UserService.go
+++ b/cloudstack/UserService.go
@@ -955,7 +955,7 @@
type ListUserTwoFactorAuthenticatorProvidersResponse struct {
Count int `json:"count"`
- UserTwoFactorAuthenticatorProviders []*UserTwoFactorAuthenticatorProvider `json:"usertwofactorauthenticatorprovider"`
+ UserTwoFactorAuthenticatorProviders []*UserTwoFactorAuthenticatorProvider `json:"providers"`
}
type UserTwoFactorAuthenticatorProvider struct {
diff --git a/cloudstack/VolumeService.go b/cloudstack/VolumeService.go
index 386c72e..0b7f985 100644
--- a/cloudstack/VolumeService.go
+++ b/cloudstack/VolumeService.go
@@ -4490,7 +4490,7 @@
type ListVolumesMetricsResponse struct {
Count int `json:"count"`
- VolumesMetrics []*VolumesMetric `json:"volumesmetric"`
+ VolumesMetrics []*VolumesMetric `json:"volume"`
}
type VolumesMetric struct {
diff --git a/cloudstack/ZoneService.go b/cloudstack/ZoneService.go
index a9f0329..40440f7 100644
--- a/cloudstack/ZoneService.go
+++ b/cloudstack/ZoneService.go
@@ -2113,7 +2113,7 @@
type ListIpv4SubnetsForZoneResponse struct {
Count int `json:"count"`
- Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"ipv4subnetsforzone"`
+ Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"zoneipv4subnet"`
}
type Ipv4SubnetsForZone struct {
@@ -3076,7 +3076,7 @@
type ListZonesMetricsResponse struct {
Count int `json:"count"`
- ZonesMetrics []*ZonesMetric `json:"zonesmetric"`
+ ZonesMetrics []*ZonesMetric `json:"zone"`
}
type ZonesMetric struct {
diff --git a/generate/generate.go b/generate/generate.go
index d1d0802..a0b24e4 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -179,6 +179,59 @@
"findHostsForMigration": "HostForMigration",
}
+// listResponseKeys records the JSON key CloudStack uses for the items of a list
+// response, for every API where that key differs from the one derived from the
+// API name.
+var listResponseKeys = map[string]string{
+ "listAsyncJobs": "asyncjobs",
+ "listDomainChildren": "domain",
+ "listEgressFirewallRules": "firewallrule",
+ "listGuestNetworkIpv6Prefixes": "guestnetworkipv6prefix",
+ "listHostHAProviders": "haprovider",
+ "listHostHAResources": "hostha",
+ "listHypervisorCapabilities": "hypervisorCapabilities",
+ "listImageStoreObjects": "datastoreobject",
+ "listLBHealthCheckPolicies": "healthcheckpolicies",
+ "listLBStickinessPolicies": "stickinesspolicies",
+ "listManagementServersMetrics": "managementserver",
+ "listObjectStoragePools": "objectstore",
+ "listSecondaryStorageSelectors": "heuristics",
+ "listStoragePoolObjects": "datastoreobject",
+ "listStoragePoolsMetrics": "storagepool",
+ "listVirtualMachinesMetrics": "virtualmachine",
+ "listVirtualMachinesUsageHistory": "virtualmachine",
+ "listVmwareDcVms": "unmanagedinstance",
+ "listVolumesUsageHistory": "volume",
+ "quotaSummary": "summary",
+ "quotaTariffList": "quotatariff",
+ "registerTemplate": "template",
+ "listVnfAppliances": "virtualmachine",
+ "listVnfTemplates": "template",
+ "listBackupProviders": "providers",
+ "listClustersMetrics": "cluster",
+ "listCustomActions": "extensioncustomaction",
+ "listHostsMetrics": "host",
+ "listNetworkIsolationMethods": "isolationmethod",
+ "listRoutingFirewallRules": "firewallrule",
+ "listSupportedNetworkServices": "networkservice",
+ "listSystemVmsUsageHistory": "virtualmachine",
+ "listTrafficTypeImplementors": "traffictypeimplementorresponse",
+ "listUserTwoFactorAuthenticatorProviders": "providers",
+ "listVolumesMetrics": "volume",
+ "listZonesMetrics": "zone",
+ "listASNRanges": "asnumberrange",
+ "listIpv4SubnetsForZone": "zoneipv4subnet",
+}
+
+// listResponseKey returns the JSON key for an API's list items, preferring an
+// observed key over the one derived from the API name.
+func listResponseKey(apiName, listName string) string {
+ if key, ok := listResponseKeys[apiName]; ok {
+ return key
+ }
+ return strings.ToLower(parseSingular(listName))
+}
+
// We prefill this one value to make sure it is not
// created twice, as this is also a top level type.
var typeNames = map[string]bool{"Nic": true}
@@ -726,6 +779,9 @@
pn(" if err := json.Unmarshal(v, &resp); err != nil {")
pn(" return nil, err")
pn(" }")
+ pn(" if len(resp) == 0 {")
+ pn(" return nil, fmt.Errorf(\"Unable to extract raw value: empty array for key %%q in:\\n\\n%%s\\n\\n\", k, string(b))")
+ pn(" }")
pn(" return resp[0], nil")
pn(" }")
pn(" }")
@@ -2078,40 +2134,28 @@
a.Name == "registerCniConfiguration" || a.Name == "quotaBalance" || a.Name == "quotaSummary" || a.Name == "quotaTariffList" {
pn("type %s struct {", tn)
- // This nasty check is for some specific response that do not behave consistent
+ // Responses whose *shape* differs: a single object instead of an array,
+ // no count, more than one collection, or a hand-written field list.
+ // Responses that differ only in the item key are handled by the default
+ // arm through listResponseKeys.
switch a.Name {
- case "listAsyncJobs":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "asyncjobs")
case "listCapabilities":
pn(" %s *%s `json:\"%s\"`", ln, parseSingular(ln), "capability")
- case "listEgressFirewallRules":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "firewallrule")
+ case "listCaCertificate":
+ // Returns a single object under "cacertificates", with no count.
+ pn(" %s *%s `json:\"%s\"`", ln, parseSingular(ln), "cacertificates")
+ case "listUsageServerMetrics":
+ // Returns a single object under "usageMetrics", with no count.
+ pn(" %s *%s `json:\"%s\"`", ln, parseSingular(ln), "usageMetrics")
case "listLoadBalancerRuleInstances":
pn(" Count int `json:\"count\"`")
pn(" LBRuleVMIDIPs []*%s `json:\"%s\"`", parseSingular(ln), "lbrulevmidip")
pn(" LoadBalancerRuleInstances []*VirtualMachine `json:\"%s\"`", strings.ToLower(parseSingular(ln)))
- case "listVirtualMachinesMetrics":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "virtualmachine")
- case "listManagementServersMetrics":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "managementserver")
case "listDbMetrics":
pn(" %s %s `json:\"%s\"`", ln, parseSingular(ln), "dbMetrics")
- case "registerTemplate":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "template")
- case "listDomainChildren":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "domain")
case "findHostsForMigration":
pn(" Count int `json:\"count\"`")
pn(" Host []*%s `json:\"%s\"`", customResponseStructTypes[a.Name], "host")
- case "listVmwareDcVms":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "unmanagedinstance")
case "registerUserData":
pn(" Account string `json:\"account\"`")
pn(" Accountid string `json:\"accountid\"`")
@@ -2126,69 +2170,14 @@
pn(" Userdata string `json:\"userdata\"`")
case "registerCniConfiguration":
pn(" CniConfiguration *UserData `json:\"cniconfig\"`")
- case "listObjectStoragePools":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "objectstore")
- case "listStoragePoolObjects":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "datastoreobject")
- case "listImageStoreObjects":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "datastoreobject")
- case "listVolumesUsageHistory":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "volume")
- case "listHostHAProviders":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "haprovider")
- case "listSecondaryStorageSelectors":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "heuristics")
- case "listVirtualMachinesUsageHistory":
- pn(" Count int `json:\"count\"`")
- pn(" VirtualMachinesUsageHistory []*VirtualMachinesUsageHistory `json:\"virtualmachine\"`")
- case "listHostHAResources":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "hostha")
case "listInfrastructure":
pn(" Count int `json:\"count\"`")
pn(" %s *%s `json:\"%s\"`", ln, parseSingular(ln), "infrastructure")
- case "listStoragePoolsMetrics":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "storagepool")
- case "quotaTariffList":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "quotatariff")
case "quotaBalance":
pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "balance")
- case "quotaSummary":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "summary")
- case "listHypervisorCapabilities":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "hypervisorCapabilities")
- case "listGuestNetworkIpv6Prefixes":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "guestnetworkipv6prefix")
- case "listLBHealthCheckPolicies":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "healthcheckpolicies")
- case "listLBStickinessPolicies":
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "stickinesspolicies")
- case "listVnfTemplates":
- // ListVnfTemplatesCmd is an empty subclass of ListTemplatesCmd, so the
- // server returns the items under "template", not "vnftemplate".
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "template")
- case "listVnfAppliances":
- // ListVnfAppliancesCmd inherits execute() from ListVMsCmd, so the server
- // returns the items under "virtualmachine", not "vnfappliance".
- pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "virtualmachine")
default:
pn(" Count int `json:\"count\"`")
- pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), strings.ToLower(parseSingular(ln)))
+ pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), listResponseKey(a.Name, ln))
}
pn("}")
pn("")
diff --git a/test/ListResponseJSONTagsRegression_test.go b/test/ListResponseJSONTagsRegression_test.go
index 1552328..575e6fc 100644
--- a/test/ListResponseJSONTagsRegression_test.go
+++ b/test/ListResponseJSONTagsRegression_test.go
@@ -71,3 +71,193 @@
}
})
}
+
+// These assertions exist because the key cannot be derived from the API name, so
+// a regeneration can otherwise quietly reintroduce any of them.
+func TestListResponseKeysObservedFromServer(t *testing.T) {
+ sliceCases := []struct {
+ name string
+ key string
+ body string
+ decode func([]byte) (int, error)
+ }{
+ {
+ name: "ASNRanges", key: "asnumberrange",
+ body: `{"count":1,"asnumberrange":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListASNRangesResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.ASNRanges), err
+ },
+ },
+ {
+ name: "Ipv4SubnetsForZone", key: "zoneipv4subnet",
+ body: `{"count":1,"zoneipv4subnet":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListIpv4SubnetsForZoneResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.Ipv4SubnetsForZone), err
+ },
+ },
+ {
+ name: "BackupProviders", key: "providers",
+ body: `{"count":1,"providers":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListBackupProvidersResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.BackupProviders), err
+ },
+ },
+ {
+ name: "ClustersMetrics", key: "cluster",
+ body: `{"count":1,"cluster":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListClustersMetricsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.ClustersMetrics), err
+ },
+ },
+ {
+ name: "CustomActions", key: "extensioncustomaction",
+ body: `{"count":1,"extensioncustomaction":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListCustomActionsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.CustomActions), err
+ },
+ },
+ {
+ name: "HostsMetrics", key: "host",
+ body: `{"count":1,"host":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListHostsMetricsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.HostsMetrics), err
+ },
+ },
+ {
+ name: "NetworkIsolationMethods", key: "isolationmethod",
+ body: `{"count":1,"isolationmethod":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListNetworkIsolationMethodsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.NetworkIsolationMethods), err
+ },
+ },
+ {
+ name: "RoutingFirewallRules", key: "firewallrule",
+ body: `{"count":1,"firewallrule":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListRoutingFirewallRulesResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.RoutingFirewallRules), err
+ },
+ },
+ {
+ name: "SupportedNetworkServices", key: "networkservice",
+ body: `{"count":1,"networkservice":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListSupportedNetworkServicesResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.SupportedNetworkServices), err
+ },
+ },
+ {
+ name: "SystemVmsUsageHistory", key: "virtualmachine",
+ body: `{"count":1,"virtualmachine":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListSystemVmsUsageHistoryResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.SystemVmsUsageHistory), err
+ },
+ },
+ {
+ name: "TrafficTypeImplementors", key: "traffictypeimplementorresponse",
+ body: `{"count":1,"traffictypeimplementorresponse":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListTrafficTypeImplementorsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.TrafficTypeImplementors), err
+ },
+ },
+ {
+ name: "UserTwoFactorAuthenticatorProviders", key: "providers",
+ body: `{"count":1,"providers":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListUserTwoFactorAuthenticatorProvidersResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.UserTwoFactorAuthenticatorProviders), err
+ },
+ },
+ {
+ name: "VolumesMetrics", key: "volume",
+ body: `{"count":1,"volume":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListVolumesMetricsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.VolumesMetrics), err
+ },
+ },
+ {
+ name: "ZonesMetrics", key: "zone",
+ body: `{"count":1,"zone":[{}]}`,
+ decode: func(b []byte) (int, error) {
+ var r cloudstack.ListZonesMetricsResponse
+ err := json.Unmarshal(b, &r)
+ return len(r.ZonesMetrics), err
+ },
+ },
+ }
+
+ for _, tc := range sliceCases {
+ t.Run(tc.name, func(t *testing.T) {
+ n, err := tc.decode([]byte(tc.body))
+ if err != nil {
+ t.Fatalf("decoding key %q: %v", tc.key, err)
+ }
+ if n != 1 {
+ t.Fatalf("expected 1 item under key %q, got %d (nil slice means the json tag does not match the key the server sends)", tc.key, n)
+ }
+ })
+ }
+
+ // These two return a single object and no count, so the field is a pointer
+ // rather than a slice.
+ objectCases := []struct {
+ name string
+ key string
+ body string
+ decode func([]byte) (bool, error)
+ }{
+ {
+ name: "CaCertificate", key: "cacertificates",
+ body: `{"cacertificates":{}}`,
+ decode: func(b []byte) (bool, error) {
+ var r cloudstack.ListCaCertificateResponse
+ err := json.Unmarshal(b, &r)
+ return r.CaCertificate != nil, err
+ },
+ },
+ {
+ name: "UsageServerMetrics", key: "usageMetrics",
+ body: `{"usageMetrics":{}}`,
+ decode: func(b []byte) (bool, error) {
+ var r cloudstack.ListUsageServerMetricsResponse
+ err := json.Unmarshal(b, &r)
+ return r.UsageServerMetrics != nil, err
+ },
+ },
+ }
+
+ for _, tc := range objectCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ok, err := tc.decode([]byte(tc.body))
+ if err != nil {
+ t.Fatalf("decoding key %q: %v", tc.key, err)
+ }
+ if !ok {
+ t.Fatalf("expected an object under key %q, got nil", tc.key)
+ }
+ })
+ }
+}