This is an automated email from the ASF dual-hosted git repository.

abh1sar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 340f0b1  Fix list-response JSON tags and guard getRawValue against 
empty arrays (#154)
340f0b1 is described below

commit 340f0b17b9e212ca40a0f11ac2e17a1d7b948597
Author: James Peru Mmbono <[email protected]>
AuthorDate: Wed Aug 5 19:08:00 2026 +0300

    Fix list-response JSON tags and guard getRawValue against empty arrays 
(#154)
    
    Several List<X>Response structs use a json tag that does not match the key
    CloudStack actually returns (the generator derives it from parseSingular()),
    so Count parses but the slice stays nil — silent data loss. Verified against
    the CloudStack source object names (setObjectName):
    
    - listHypervisorCapabilities: hypervisorcapability -> hypervisorCapabilities
    - listGuestNetworkIpv6Prefixes: guestnetworkipv6prefixe -> 
guestnetworkipv6prefix
    - listLBHealthCheckPolicies: lbhealthcheckpolicy -> healthcheckpolicies
    - listLBStickinessPolicies: lbstickinesspolicy -> stickinesspolicies
    
    Fixed via explicit cases in the generator's override switch (alongside the
    existing metrics cases) and the regenerated tags.
    
    Also guards getRawValue() against a count-wrapped response carrying an empty
    data array, which previously panicked on resp[0] (index out of range); it 
now
    returns a descriptive error.
    
    Adds regression tests for both.
    
    Signed-off-by: James Peru <[email protected]>
    Co-authored-by: James Peru <[email protected]>
---
 cloudstack/GetRawValueGuard_test.go         | 36 ++++++++++++++
 cloudstack/HypervisorService.go             |  2 +-
 cloudstack/LoadBalancerService.go           |  2 +-
 cloudstack/NetworkService.go                |  2 +-
 cloudstack/cloudstack.go                    |  3 ++
 generate/generate.go                        |  9 ++++
 test/ListResponseJSONTagsRegression_test.go | 73 +++++++++++++++++++++++++++++
 7 files changed, 124 insertions(+), 3 deletions(-)

diff --git a/cloudstack/GetRawValueGuard_test.go 
b/cloudstack/GetRawValueGuard_test.go
new file mode 100644
index 0000000..8fa4bae
--- /dev/null
+++ b/cloudstack/GetRawValueGuard_test.go
@@ -0,0 +1,36 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package cloudstack
+
+import "testing"
+
+// getRawValue must not panic when a count-wrapped response carries an empty
+// data array ({"count":0,"<entity>":[]}); it should return a descriptive 
error.
+func TestGetRawValueEmptyArrayReturnsErrorNotPanic(t *testing.T) {
+       defer func() {
+               if r := recover(); r != nil {
+                       t.Fatalf("getRawValue panicked on a count-wrapped empty 
array: %v", r)
+               }
+       }()
+       _, err := getRawValue([]byte(`{"count":0,"entity":[]}`))
+       if err == nil {
+               t.Fatal("expected an error for a count-wrapped empty array, got 
nil")
+       }
+}
diff --git a/cloudstack/HypervisorService.go b/cloudstack/HypervisorService.go
index 605070e..688b203 100644
--- a/cloudstack/HypervisorService.go
+++ b/cloudstack/HypervisorService.go
@@ -229,7 +229,7 @@ func (s *HypervisorService) ListHypervisorCapabilities(p 
*ListHypervisorCapabili
 
 type ListHypervisorCapabilitiesResponse struct {
        Count                  int                     `json:"count"`
-       HypervisorCapabilities []*HypervisorCapability 
`json:"hypervisorcapability"`
+       HypervisorCapabilities []*HypervisorCapability 
`json:"hypervisorCapabilities"`
 }
 
 type HypervisorCapability struct {
diff --git a/cloudstack/LoadBalancerService.go 
b/cloudstack/LoadBalancerService.go
index a6afb81..18997dc 100644
--- a/cloudstack/LoadBalancerService.go
+++ b/cloudstack/LoadBalancerService.go
@@ -3539,7 +3539,7 @@ func (s *LoadBalancerService) ListLBHealthCheckPolicies(p 
*ListLBHealthCheckPoli
 
 type ListLBHealthCheckPoliciesResponse struct {
        Count                 int                    `json:"count"`
-       LBHealthCheckPolicies []*LBHealthCheckPolicy 
`json:"lbhealthcheckpolicy"`
+       LBHealthCheckPolicies []*LBHealthCheckPolicy 
`json:"healthcheckpolicies"`
 }
 
 type LBHealthCheckPolicy struct {
diff --git a/cloudstack/NetworkService.go b/cloudstack/NetworkService.go
index 96f1a2e..cccb28f 100644
--- a/cloudstack/NetworkService.go
+++ b/cloudstack/NetworkService.go
@@ -8475,7 +8475,7 @@ func (s *NetworkService) ListGuestNetworkIpv6Prefixes(p 
*ListGuestNetworkIpv6Pre
 
 type ListGuestNetworkIpv6PrefixesResponse struct {
        Count                    int                        `json:"count"`
-       GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe 
`json:"guestnetworkipv6prefixe"`
+       GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe 
`json:"guestnetworkipv6prefix"`
 }
 
 type GuestNetworkIpv6Prefixe struct {
diff --git a/cloudstack/cloudstack.go b/cloudstack/cloudstack.go
index e16a934..240174e 100644
--- a/cloudstack/cloudstack.go
+++ b/cloudstack/cloudstack.go
@@ -653,6 +653,9 @@ func getRawValue(b json.RawMessage) (json.RawMessage, 
error) {
                                if err := json.Unmarshal(v, &resp); err != nil {
                                        return nil, err
                                }
+                               if len(resp) == 0 {
+                                       return nil, fmt.Errorf("Unable to 
extract raw value: empty array for key %q in:\n\n%s\n\n", k, string(b))
+                               }
                                return resp[0], nil
                        }
                }
diff --git a/generate/generate.go b/generate/generate.go
index 52577e0..a11213d 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -2111,6 +2111,15 @@ func (s *service) generateResponseType(a *API) {
                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")
diff --git a/test/ListResponseJSONTagsRegression_test.go 
b/test/ListResponseJSONTagsRegression_test.go
new file mode 100644
index 0000000..1552328
--- /dev/null
+++ b/test/ListResponseJSONTagsRegression_test.go
@@ -0,0 +1,73 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package test
+
+import (
+       "encoding/json"
+       "testing"
+
+       "github.com/apache/cloudstack-go/v2/cloudstack"
+)
+
+// Regression test for list-response JSON-tag mismatches: the response struct
+// tag must match the object key CloudStack actually returns, otherwise Count
+// parses but the slice stays nil (silent data loss). Keys below are the
+// authoritative server object names (setObjectName in the CloudStack source).
+func TestListResponseJSONTagsPopulateSlices(t *testing.T) {
+       t.Run("HypervisorCapabilities", func(t *testing.T) {
+               var r cloudstack.ListHypervisorCapabilitiesResponse
+               if err := 
json.Unmarshal([]byte(`{"count":1,"hypervisorCapabilities":[{"id":"h1"}]}`), 
&r); err != nil {
+                       t.Fatal(err)
+               }
+               if len(r.HypervisorCapabilities) != 1 {
+                       t.Fatalf("expected 1 item under key 
'hypervisorCapabilities', got %d (nil slice = wrong json tag)", 
len(r.HypervisorCapabilities))
+               }
+       })
+
+       t.Run("GuestNetworkIpv6Prefixes", func(t *testing.T) {
+               var r cloudstack.ListGuestNetworkIpv6PrefixesResponse
+               if err := 
json.Unmarshal([]byte(`{"count":1,"guestnetworkipv6prefix":[{"id":"p1"}]}`), 
&r); err != nil {
+                       t.Fatal(err)
+               }
+               if len(r.GuestNetworkIpv6Prefixes) != 1 {
+                       t.Fatalf("expected 1 item under key 
'guestnetworkipv6prefix', got %d", len(r.GuestNetworkIpv6Prefixes))
+               }
+       })
+
+       t.Run("LBHealthCheckPolicies", func(t *testing.T) {
+               var r cloudstack.ListLBHealthCheckPoliciesResponse
+               if err := 
json.Unmarshal([]byte(`{"count":1,"healthcheckpolicies":[{"lbruleid":"r1"}]}`), 
&r); err != nil {
+                       t.Fatal(err)
+               }
+               if len(r.LBHealthCheckPolicies) != 1 {
+                       t.Fatalf("expected 1 item under key 
'healthcheckpolicies', got %d", len(r.LBHealthCheckPolicies))
+               }
+       })
+
+       t.Run("LBStickinessPolicies", func(t *testing.T) {
+               var r cloudstack.ListLBStickinessPoliciesResponse
+               if err := 
json.Unmarshal([]byte(`{"count":1,"stickinesspolicies":[{"lbruleid":"r1"}]}`), 
&r); err != nil {
+                       t.Fatal(err)
+               }
+               if len(r.LBStickinessPolicies) != 1 {
+                       t.Fatalf("expected 1 item under key 
'stickinesspolicies', got %d", len(r.LBStickinessPolicies))
+               }
+       })
+}

Reply via email to