This is an automated email from the ASF dual-hosted git repository. abh1sar pushed a commit to branch fix-list-response-keys in repository https://gitbox.apache.org/repos/asf/cloudstack-go.git
commit e847e5d262614df75197dec465761556a53565f9 Author: Abhisar Sinha <[email protected]> AuthorDate: Tue Aug 11 17:23:01 2026 +0530 Fix list response keys derived from the API name 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`. --- cloudstack/ASNumberRangeService.go | 2 +- cloudstack/BackupService.go | 2 +- cloudstack/CertificateService.go | 3 +- cloudstack/ClusterService.go | 2 +- cloudstack/ExtensionService.go | 2 +- cloudstack/FirewallService.go | 2 +- cloudstack/HostService.go | 2 +- cloudstack/NetworkService.go | 4 +- cloudstack/SystemVMService.go | 2 +- cloudstack/UsageService.go | 5 +- cloudstack/UserService.go | 2 +- cloudstack/VolumeService.go | 2 +- cloudstack/ZoneService.go | 4 +- generate/generate.go | 174 +++++++++++++----------- test/ListResponseJSONTagsRegression_test.go | 196 ++++++++++++++++++++++++++++ 15 files changed, 308 insertions(+), 96 deletions(-) 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 @@ func (s *ASNumberRangeService) ListASNRanges(p *ListASNRangesParams) (*ListASNRa 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 @@ func (s *BackupService) ListBackupProviders(p *ListBackupProvidersParams) (*List 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 @@ func (s *CertificateService) ListCaCertificate(p *ListCaCertificateParams) (*Lis } 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 @@ func (s *ClusterService) ListClustersMetrics(p *ListClustersMetricsParams) (*Lis 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 @@ func (s *ExtensionService) ListCustomActions(p *ListCustomActionsParams) (*ListC 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 @@ func (s *FirewallService) ListRoutingFirewallRules(p *ListRoutingFirewallRulesPa 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 @@ func (s *HostService) ListHostsMetrics(p *ListHostsMetricsParams) (*ListHostsMet 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 @@ func (s *NetworkService) ListNetworkIsolationMethods(p *ListNetworkIsolationMeth type ListNetworkIsolationMethodsResponse struct { Count int `json:"count"` - NetworkIsolationMethods []*NetworkIsolationMethod `json:"networkisolationmethod"` + NetworkIsolationMethods []*NetworkIsolationMethod `json:"isolationmethod"` } type NetworkIsolationMethod struct { @@ -6424,7 +6424,7 @@ func (s *NetworkService) ListSupportedNetworkServices(p *ListSupportedNetworkSer 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 @@ func (s *SystemVMService) ListSystemVmsUsageHistory(p *ListSystemVmsUsageHistory 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 @@ func (s *UsageService) ListTrafficTypeImplementors(p *ListTrafficTypeImplementor type ListTrafficTypeImplementorsResponse struct { Count int `json:"count"` - TrafficTypeImplementors []*TrafficTypeImplementor `json:"traffictypeimplementor"` + TrafficTypeImplementors []*TrafficTypeImplementor `json:"traffictypeimplementorresponse"` } type TrafficTypeImplementor struct { @@ -2128,8 +2128,7 @@ func (s *UsageService) ListUsageServerMetrics(p *ListUsageServerMetricsParams) ( } 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 @@ func (s *UserService) ListUserTwoFactorAuthenticatorProviders(p *ListUserTwoFact 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 @@ func (s *VolumeService) ListVolumesMetrics(p *ListVolumesMetricsParams) (*ListVo 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 @@ func (s *ZoneService) ListIpv4SubnetsForZone(p *ListIpv4SubnetsForZoneParams) (* type ListIpv4SubnetsForZoneResponse struct { Count int `json:"count"` - Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"ipv4subnetsforzone"` + Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"zoneipv4subnet"` } type Ipv4SubnetsForZone struct { @@ -3076,7 +3076,7 @@ func (s *ZoneService) ListZonesMetrics(p *ListZonesMetricsParams) (*ListZonesMet 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..cc73c7c 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -179,6 +179,85 @@ var customResponseStructTypes = map[string]string{ "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. +// +// The key cannot be computed. CloudStack keys the item array by the *response +// object* name (UserVmResponse serialises as "virtualmachine", TemplateResponse +// as "template"), while the fallback below derives it from the API name. When an +// API's response object does not share its name, the two disagree and there is +// nothing in listApis to reconcile them from: it reports each response's fields +// but never the object name, and `related` is empty for exactly these APIs. +// +// Getting this wrong fails silently and is therefore expensive: `count` parses +// from the same body, so a caller sees a correct count beside an empty slice and +// no error. Do not add a guessed entry; an entry invented from the API name is +// as harmful as a missing one, because it looks deliberate. Every key here was +// observed in the response of a real management server, and +// test/ListResponseJSONTagsRegression_test.go pins them so a regeneration +// cannot quietly undo one. +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", + + // ListVnfTemplatesCmd is an empty subclass of ListTemplatesCmd and + // ListVnfAppliancesCmd inherits execute() from ListVMsCmd, so both return + // the response object of the command they extend. + "listVnfAppliances": "virtualmachine", + "listVnfTemplates": "template", + + // Found by sweeping every list API against two 4.22.1.0 servers and reading + // the raw response keys. Each had been decoding as a permanently empty slice. + "listBackupProviders": "providers", + "listClustersMetrics": "cluster", + "listCustomActions": "extensioncustomaction", + "listHostsMetrics": "host", + "listNetworkIsolationMethods": "isolationmethod", + "listRoutingFirewallRules": "firewallrule", + "listSupportedNetworkServices": "networkservice", + "listSystemVmsUsageHistory": "virtualmachine", + "listTrafficTypeImplementors": "traffictypeimplementorresponse", + "listUserTwoFactorAuthenticatorProviders": "providers", + "listVolumesMetrics": "volume", + "listZonesMetrics": "zone", + + // Found by populating a simulator so these two returned data at all. Both + // are corroborated by setObjectName in the CloudStack source and by the keys + // the CloudStack UI itself reads. + "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 +805,12 @@ func (as *allServices) GeneralCode() ([]byte, error) { pn(" if err := json.Unmarshal(v, &resp); err != nil {") pn(" return nil, err") pn(" }") + // Guard added in #154. It has to live here rather than in the generated + // file, otherwise `make code` reverts it, which is exactly what happened + // between #154 and now. + 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 +2163,28 @@ func (s *service) generateResponseType(a *API) { 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 +2199,14 @@ func (s *service) generateResponseType(a *API) { 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..9bea38a 100644 --- a/test/ListResponseJSONTagsRegression_test.go +++ b/test/ListResponseJSONTagsRegression_test.go @@ -71,3 +71,199 @@ func TestListResponseJSONTagsPopulateSlices(t *testing.T) { } }) } + +// Regression test for the response keys found by sweeping every list API against +// live 4.22.1.0 servers and reading the raw response keys. Each of these decoded +// as a permanently empty slice before the keys were corrected in +// generate/generate.go, and each failed silently: count parses from the same +// body, so callers saw a correct count beside no results and no error. +// +// 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) + } + }) + } +}
