-
Notifications
You must be signed in to change notification settings - Fork 1
Fix: add ComputePool.Status custom UnmarshalJSON for flat API response #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bf2b67c
798cf9b
407de3d
62c1966
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||
| } | ||||||||||||||
| 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 { | ||||||||||||||
|
||||||||||||||
| for k, v := range flat { | |
| for k, v := range flat { | |
| if obj, ok := v.(map[string]interface{}); ok { | |
| wrapped[k] = obj | |
| continue | |
| } |
| 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) | ||
| } | ||
| }) | ||
| } | ||
|
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) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.