diff --git a/v1/.openapi-generator-ignore b/v1/.openapi-generator-ignore index 7484ee5..04d93d3 100644 --- a/v1/.openapi-generator-ignore +++ b/v1/.openapi-generator-ignore @@ -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 diff --git a/v1/model_compute_pool_custom.go b/v1/model_compute_pool_custom.go new file mode 100644 index 0000000..2247ac3 --- /dev/null +++ b/v1/model_compute_pool_custom.go @@ -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 + } + 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 { + wrapped[k] = map[string]interface{}{"value": v} + } + o.Status = &wrapped + return nil +} diff --git a/v1/model_compute_pool_custom_test.go b/v1/model_compute_pool_custom_test.go new file mode 100644 index 0000000..d633fa7 --- /dev/null +++ b/v1/model_compute_pool_custom_test.go @@ -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) + } + }) + } +} + +// 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) + } +}