Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions v1/.openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

# Custom unmarshaler for ComputePool.Status (and its tests) that handles the
# flat API response shape.
model_compute_pool_custom.go
model_compute_pool_custom_test.go
37 changes: 37 additions & 0 deletions v1/model_compute_pool_custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package v1

import (
"bytes"
"encoding/json"
"fmt"
)

// UnmarshalJSON wraps every value in ComputePool.Status as {"value": v} so the
// CMF API's flat status object (e.g. {"phase":"RUNNING"}) fits the generated
// type *map[string]map[string]interface{}. Non-object status shapes surface a
// decode error rather than being silently dropped.
func (o *ComputePool) UnmarshalJSON(data []byte) error {
// Alias strips methods so the inner decode doesn't re-enter UnmarshalJSON.
type Alias ComputePool
aux := struct {
Status json.RawMessage `json:"status"`
*Alias
}{Alias: (*Alias)(o)}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
o.Status = nil // reset for receiver reuse
if len(aux.Status) == 0 || bytes.Equal(aux.Status, []byte("null")) {
return nil
Comment thread
paras-negi-flink marked this conversation as resolved.
}
var flat map[string]interface{}
if err := json.Unmarshal(aux.Status, &flat); err != nil {
return fmt.Errorf("unmarshal ComputePool.Status: %w", err)
}
wrapped := make(map[string]map[string]interface{}, len(flat))
for k, v := range flat {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapped[k] = {"value": v} wraps object values too, which prevents the “pass-through unchanged for object values” behavior described in the PR and makes nested server shapes harder to consume. To support mixed shapes, detect when v is a map[string]interface{} and assign it directly to wrapped[k]; only wrap scalar/null values as { "value": v }.

Suggested change
for k, v := range flat {
for k, v := range flat {
if obj, ok := v.(map[string]interface{}); ok {
wrapped[k] = obj
continue
}

Copilot uses AI. Check for mistakes.
wrapped[k] = map[string]interface{}{"value": v}
}
o.Status = &wrapped
return nil
}
262 changes: 262 additions & 0 deletions v1/model_compute_pool_custom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package v1

import (
"encoding/json"
"strings"
"testing"
)

// makeComputePoolJSON builds a ComputePool payload. statusLiteral is raw JSON
// (e.g. `"RUNNING"`, `null`, `{"phase":"X"}`), or "" to omit the status field.
func makeComputePoolJSON(name, statusLiteral string) []byte {
if statusLiteral == "" {
return []byte(`{
"apiVersion": "cmf.confluent.io/v1",
"kind": "ComputePool",
"metadata": {"name": "` + name + `"},
"spec": {"type": "DEDICATED", "clusterSpec": {}}
}`)
}
return []byte(`{
"apiVersion": "cmf.confluent.io/v1",
"kind": "ComputePool",
"metadata": {"name": "` + name + `"},
"spec": {"type": "DEDICATED", "clusterSpec": {}},
"status": ` + statusLiteral + `
}`)
}

func wrappedValue(pool ComputePool, key string) (interface{}, bool) {
if pool.Status == nil {
return nil, false
}
inner, ok := (*pool.Status)[key]
if !ok {
return nil, false
}
v, ok := inner["value"]
return v, ok
}

func TestComputePoolUnmarshal_FlatStatus(t *testing.T) {
data := makeComputePoolJSON("pool-a", `{"phase":"RUNNING","message":null,"reason":"x","ready":true}`)

var pool ComputePool
if err := json.Unmarshal(data, &pool); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pool.Metadata.Name != "pool-a" {
t.Errorf("metadata.name = %q, want %q", pool.Metadata.Name, "pool-a")
}
if pool.Spec.Type != "DEDICATED" {
t.Errorf("spec.type = %q, want %q", pool.Spec.Type, "DEDICATED")
}
if pool.Status == nil {
t.Fatal("status is nil")
}
wantValues := map[string]interface{}{
"phase": "RUNNING",
"message": nil,
"reason": "x",
"ready": true,
}
status := *pool.Status
if len(status) != len(wantValues) {
t.Errorf("status has %d keys, want %d", len(status), len(wantValues))
}
for k, want := range wantValues {
inner, ok := status[k]
if !ok {
t.Errorf("status[%q] missing", k)
continue
}
// Literal "value" key catches format drift.
got, hasValue := inner["value"]
if !hasValue {
t.Errorf("status[%q] missing %q key; got %v", k, "value", inner)
continue
}
if got != want {
t.Errorf("status[%q][\"value\"] = %v, want %v", k, got, want)
}
}
}

func TestComputePoolUnmarshal_NullStatus(t *testing.T) {
data := makeComputePoolJSON("pool-d", `null`)

var pool ComputePool
if err := json.Unmarshal(data, &pool); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pool.Status != nil {
t.Errorf("status = %v, want nil", pool.Status)
}
}

func TestComputePoolUnmarshal_MissingStatus(t *testing.T) {
data := makeComputePoolJSON("pool-e", "")

var pool ComputePool
if err := json.Unmarshal(data, &pool); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pool.Status != nil {
t.Errorf("status = %v, want nil", pool.Status)
}
if pool.Metadata.Name != "pool-e" {
t.Errorf("metadata.name = %q, want %q", pool.Metadata.Name, "pool-e")
}
}

func TestComputePoolUnmarshal_EmptyObjectStatus(t *testing.T) {
data := makeComputePoolJSON("pool-f", `{}`)

var pool ComputePool
if err := json.Unmarshal(data, &pool); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pool.Status == nil {
t.Fatal("status is nil, want non-nil empty map")
}
if len(*pool.Status) != 0 {
t.Errorf("status has %d keys, want 0", len(*pool.Status))
}
}

func TestComputePoolUnmarshal_AllNullValues(t *testing.T) {
data := makeComputePoolJSON("pool-g", `{"phase":null,"message":null}`)

var pool ComputePool
if err := json.Unmarshal(data, &pool); err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, k := range []string{"phase", "message"} {
v, ok := wrappedValue(pool, k)
if !ok {
t.Errorf("%s.value missing", k)
continue
}
if v != nil {
t.Errorf("%s.value = %v, want nil", k, v)
}
}
}

// Non-object status shapes indicate API regressions; surface the decode error.
func TestComputePoolUnmarshal_NonObjectStatusFails(t *testing.T) {
cases := []struct {
name string
status string
}{
{"array", `[1,2,3]`},
{"string", `"RUNNING"`},
{"number", `42`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data := makeComputePoolJSON("pool-"+tc.name, tc.status)
var pool ComputePool
err := json.Unmarshal(data, &pool)
if err == nil {
t.Fatalf("expected error for %s status, got nil", tc.name)
}
if !strings.Contains(err.Error(), "ComputePool.Status") {
t.Errorf("err = %q, want it to mention ComputePool.Status", err)
}
})
}
Comment thread
paras-negi-flink marked this conversation as resolved.
}

// A null payload on a reused receiver must clear Status, not retain the prior value.
func TestComputePoolUnmarshal_ReceiverReuse(t *testing.T) {
var pool ComputePool
first := makeComputePoolJSON("first", `{"phase":"RUNNING"}`)
if err := json.Unmarshal(first, &pool); err != nil {
t.Fatalf("first unmarshal: %v", err)
}
if pool.Status == nil {
t.Fatal("status nil after first unmarshal")
}
second := makeComputePoolJSON("second", `null`)
if err := json.Unmarshal(second, &pool); err != nil {
t.Fatalf("second unmarshal: %v", err)
}
if pool.Status != nil {
t.Errorf("status = %v after null payload, want nil", pool.Status)
}
}

// ComputePoolsPage.Items must invoke our UnmarshalJSON per element.
func TestComputePoolUnmarshal_PageResponse(t *testing.T) {
data := []byte(`{
"items": [
{
"apiVersion": "cmf.confluent.io/v1",
"kind": "ComputePool",
"metadata": {"name": "pool-1"},
"spec": {"type": "DEDICATED", "clusterSpec": {}},
"status": {"phase": "RUNNING"}
},
{
"apiVersion": "cmf.confluent.io/v1",
"kind": "ComputePool",
"metadata": {"name": "pool-2"},
"spec": {"type": "DEDICATED", "clusterSpec": {}},
"status": {"phase": "PENDING"}
}
]
}`)

var page ComputePoolsPage
if err := json.Unmarshal(data, &page); err != nil {
t.Fatalf("unexpected error: %v", err)
}
items := page.GetItems()
if len(items) != 2 {
t.Fatalf("got %d items, want 2", len(items))
}
wantPhases := []string{"RUNNING", "PENDING"}
for i, item := range items {
v, ok := wrappedValue(item, "phase")
if !ok {
t.Errorf("item %d: phase.value missing", i)
continue
}
if v != wantPhases[i] {
t.Errorf("item %d phase.value = %v, want %q", i, v, wantPhases[i])
}
}
}

func TestComputePoolUnmarshal_EmptyPageItems(t *testing.T) {
for _, tc := range []struct {
name string
body string
}{
{"empty", `{"items":[]}`},
{"missing", `{}`},
{"null", `{"items":null}`},
} {
t.Run(tc.name, func(t *testing.T) {
var page ComputePoolsPage
if err := json.Unmarshal([]byte(tc.body), &page); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n := len(page.GetItems()); n != 0 {
t.Errorf("got %d items, want 0", n)
}
})
}
}

func TestComputePoolUnmarshal_InvalidJSON(t *testing.T) {
var pool ComputePool
err := json.Unmarshal([]byte(`{not valid json`), &pool)
if err == nil {
t.Fatal("expected error, got nil")
}
if _, isSyntax := err.(*json.SyntaxError); !isSyntax {
t.Errorf("err type = %T, want *json.SyntaxError", err)
}
}