diff --git a/internal/api/client.go b/internal/api/client.go index ec0aeba..0d6771d 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -382,6 +382,36 @@ func parseIncidentData(d incidentResponseData) Incident { return incident } +// labelEntry represents a single key-value label. +type labelEntry struct { + Key string `json:"key"` + Value interface{} `json:"value"` +} + +// flexibleLabels handles the API returning labels as either an array of {key,value} +// objects or as a plain object/map (e.g. empty {} or {"key":"value"}). +type flexibleLabels []labelEntry + +func (f *flexibleLabels) UnmarshalJSON(data []byte) error { + // Try array first (normal case) + var arr []labelEntry + if err := json.Unmarshal(data, &arr); err == nil { + *f = arr + return nil + } + // Fall back to object/map + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + return err + } + result := make(flexibleLabels, 0, len(m)) + for k, v := range m { + result = append(result, labelEntry{Key: k, Value: v}) + } + *f = result + return nil +} + // incidentDetailResponse is the full JSON:API response for a single incident with includes. type incidentDetailResponse struct { Data struct { @@ -427,10 +457,7 @@ type incidentDetailAttributes struct { RetrospectiveProgressStatus *string `json:"retrospective_progress_status"` SlackChannelName *string `json:"slack_channel_name"` // Labels - Labels []struct { - Key string `json:"key"` - Value interface{} `json:"value"` - } `json:"labels"` + Labels flexibleLabels `json:"labels"` // Integration links SlackChannelURL *string `json:"slack_channel_url"` JiraIssueURL *string `json:"jira_issue_url"` @@ -1195,10 +1222,7 @@ type alertResponseData struct { Groups []struct { Name string `json:"name"` } `json:"groups"` - Labels []struct { - Key string `json:"key"` - Value interface{} `json:"value"` - } `json:"labels"` + Labels flexibleLabels `json:"labels"` } `json:"attributes"` } @@ -1401,21 +1425,18 @@ func (c *Client) GetAlertByID(ctx context.Context, id string) (*Alert, error) { Data struct { ID string `json:"id"` Attributes struct { - ShortID *string `json:"short_id"` - Summary string `json:"summary"` - Description *string `json:"description"` - Status string `json:"status"` - Source *string `json:"source"` - ExternalURL *string `json:"external_url"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - StartedAt *string `json:"started_at"` - EndedAt *string `json:"ended_at"` - Labels []struct { - Key string `json:"key"` - Value interface{} `json:"value"` - } `json:"labels"` - Services []struct { + ShortID *string `json:"short_id"` + Summary string `json:"summary"` + Description *string `json:"description"` + Status string `json:"status"` + Source *string `json:"source"` + ExternalURL *string `json:"external_url"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + StartedAt *string `json:"started_at"` + EndedAt *string `json:"ended_at"` + Labels flexibleLabels `json:"labels"` + Services []struct { Name string `json:"name"` } `json:"services"` Environments []struct { diff --git a/internal/api/client_resources_test.go b/internal/api/client_resources_test.go index afc5aa2..2621d95 100644 --- a/internal/api/client_resources_test.go +++ b/internal/api/client_resources_test.go @@ -1316,10 +1316,7 @@ func TestParseAlertData(t *testing.T) { d.Attributes.Groups = []struct { Name string `json:"name"` }{{Name: "SRE"}} - d.Attributes.Labels = []struct { - Key string `json:"key"` - Value interface{} `json:"value"` - }{{Key: "host", Value: "web-1"}} + d.Attributes.Labels = flexibleLabels{{Key: "host", Value: "web-1"}} alert := parseAlertData(d) @@ -1365,3 +1362,49 @@ func TestParseAlertDataMinimal(t *testing.T) { t.Errorf("Status = %q, want empty", alert.Status) } } + +func TestFlexibleLabels_Array(t *testing.T) { + input := `[{"key":"host","value":"web-1"},{"key":"count","value":42}]` + var labels flexibleLabels + if err := json.Unmarshal([]byte(input), &labels); err != nil { + t.Fatalf("Unmarshal array: %v", err) + } + if len(labels) != 2 { + t.Fatalf("got %d labels, want 2", len(labels)) + } + if labels[0].Key != "host" || labels[0].Value != "web-1" { + t.Errorf("labels[0] = %+v, want {host web-1}", labels[0]) + } +} + +func TestFlexibleLabels_EmptyObject(t *testing.T) { + input := `{}` + var labels flexibleLabels + if err := json.Unmarshal([]byte(input), &labels); err != nil { + t.Fatalf("Unmarshal empty object: %v", err) + } + if len(labels) != 0 { + t.Errorf("got %d labels, want 0", len(labels)) + } +} + +func TestFlexibleLabels_ObjectWithKeys(t *testing.T) { + input := `{"env":"prod","region":"us-east-1"}` + var labels flexibleLabels + if err := json.Unmarshal([]byte(input), &labels); err != nil { + t.Fatalf("Unmarshal object: %v", err) + } + if len(labels) != 2 { + t.Fatalf("got %d labels, want 2", len(labels)) + } + m := make(map[string]interface{}) + for _, l := range labels { + m[l.Key] = l.Value + } + if m["env"] != "prod" { + t.Errorf("env = %v, want prod", m["env"]) + } + if m["region"] != "us-east-1" { + t.Errorf("region = %v, want us-east-1", m["region"]) + } +}